1. Приоритет и порядок вычислений
Приоритет операторов — это про то, в каком порядке Go вычисляет выражение, когда вы написали несколько операций подряд. На первых шагах кажется: «Ну, оно же очевидно». А потом внезапно скидка считается “не так”, проценты превращаются в магию, а итоговая сумма “плавает” — и виноват не компьютер, а выражение без скобок.
В программировании (и в жизни) неприятнее всего ошибки, которые не падают с паникой, а дают неправильный, но “похожий на правду” результат. Как раз такие баги часто рождаются из-за приоритета и целочисленного деления. Поэтому сейчас мы сделаем две вещи: разберёмся с порядком вычислений и научимся предсказывать результат.
Арифметические операторы
С арифметикой вы уже сталкивались: + - * / %. Сейчас полезно чуть “допротереть стекло” и заметить важную деталь: один и тот же символ может вести себя предсказуемо, но контекст решает, что именно происходит. Например, -5 — это не “вычитание”, а унарный минус (оператор у одного операнда). А 10 - 5 — это уже вычитание.
И ещё важнее: в Go арифметика почти всегда строго типизирована. То есть выражения на int дают результаты типа int. Это особенно заметно на делении.
Небольшая диагностика “на всякий случай”:
package main
import "fmt"
func main() {
a := 10
b := 3
fmt.Printf("a=%d, b=%d\n", a, b) // a=10, b=3
}
Приоритет операторов
Когда мы пишем выражение вроде 2 + 3*4, Go не читает его слева направо “как человек, который устал”. Он читает по правилам: сначала умножение, потом сложение.
Здесь удобно держать в голове очень короткую “школьную” версию, которая нам сейчас полностью подходит: скобки сильнее всего, затем идут * / %, затем + -. И этого уже хватает, чтобы не попасть в 90% типичных ловушек.
Мини-таблица приоритетов (то, что нужно прямо сейчас):
| Группа | Операторы | Как запомнить |
|---|---|---|
| 1 (самое сильное) | |
“Скобки — начальник” |
| 2 | |
“Умножение-деление-остаток — одна команда” |
| 3 (слабее) | |
“Сложение/вычитание — потом” |
Важно: оператор % (остаток) живёт на одном уровне с * и /. То есть a + b%10 — это a + (b%10), а не (a+b)%10.
Левоассоциативность
Когда у операторов одинаковый приоритет (например, * и /), возникает второй вопрос: “Окей, но кто первый?” Ответ: в обычных случаях Go вычисляет слева направо.
Это называется (не пугайтесь) ассоциативность. Для нас сейчас достаточно помнить: 8 / 2 * 2 считается как ((8 / 2) * 2), а не 8 / (2 * 2).
Посмотрим на конкретных числах:
package main
import "fmt"
func main() {
fmt.Println(8/2*2) // 8
fmt.Println(8/(2*2)) // 2
}
И это не “особенность Go”, это нормальная математика выражений в большинстве языков. Но именно поэтому, если вы хотите явно другой порядок — ставьте скобки, даже если “и так понятно”.
Скобки как способ сделать код очевиднее
В реальности приоритет — это не то, что вы должны постоянно держать в голове как таблицу умножения. В реальности приоритет — это то, что вы должны уметь проверять, а код писать так, чтобы приоритет был очевиден.
Скобки — это не признак слабости (хотя иногда коллеги делают вид, что это так). Скобки — это как подписи на проводах: “красный — это плюс, не трогай”.
Сравним две версии:
package main
import "fmt"
func main() {
price := 200
qty := 3
total1 := price*qty + 50
total2 := (price * qty) + 50
fmt.Println(total1, total2) // 650 650
}
Обе работают одинаково, но total2 читается быстрее. Ваш будущий “я” скажет спасибо вашему сегодняшнему “я”. (Редкий момент в жизни, когда это реально происходит.)
2. Целочисленное деление и остаток
Целочисленное деление
Теперь — про деление. И здесь будет тот самый момент, когда многие говорят: “В смысле?!”
Если оба операнда — целые числа (int, int64 и т.д.), то / делает целочисленное деление, то есть дробная часть просто выкидывается.
Не округляется “по правилам математики”, не превращается в float64 автоматически, не пытается угадать, что вы “имели в виду”. Просто берёт целую часть.
package main
import "fmt"
func main() {
fmt.Println(7 / 2) // 3
fmt.Println(9 / 3) // 3
fmt.Println(1 / 2) // 0
}
1/2 == 0 — это нормально для целочисленной арифметики. Именно поэтому она очень удобна для счётчиков, индексов, “сколько полных коробок” и денежных сумм в копейках/центах.
Деление отрицательных чисел
Отдельный нюанс: что происходит с отрицательными числами. В Go целочисленное деление идёт в сторону нуля. Это означает, что -7/2 будет -3, а не -4.
package main
import "fmt"
func main() {
fmt.Println(-7 / 2) // -3
fmt.Println(-1 / 2) // 0
}
Почему это важно? Потому что остаток % связан с делением. И если вы будете делать логику с минусами (например, баланс, долг, разница), лучше заранее знать, что язык делает именно так, а не “как вам хотелось бы в пятницу вечером”.
Остаток от деления
Оператор % — это остаток от деления. Он работает только с целыми типами и отлично помогает “разобрать число” на части: секунды и минуты, доллары и центы, страницы и элементы.
Очень полезная формула, которую стоит запомнить как факт жизни:
Если a и b — целые, то выполняется: a == (a/b)*b + (a%b)
Проверим на примере:
package main
import "fmt"
func main() {
a := 17
b := 5
fmt.Println(a/b, a%b) // 3 2
fmt.Println((a/b)*b + (a%b)) // 17
}
Практика: Евро и центы
Самый “жизненный” пример для целочисленного деления — деньги. С float64 деньги часто дают “хвосты” вида 10.0000000002, а вот с целыми копейками/центами всё строго.
Допустим, у нас есть сумма в копейках (или центах), и мы хотим вывести “евро и центы”.
package main
import "fmt"
func main() {
totalCents := 12345
euros := totalCents / 100
cents := totalCents % 100
fmt.Printf("%d евро %02d центов\n", euros, cents) // 123 евро 45 центов
}
Обратите внимание на %02d: это просто формат вывода, чтобы копейки всегда печатались двумя цифрами (например, 05, а не 5). Мы сейчас не углубляемся в форматирование как в отдельную тему, но этот маленький трюк делает вывод “человеческим”.
Проценты и порядок операций
Проценты — классика багов. Причина проста: проценты почти всегда выглядят как “простая формула”, но в целых числах вы внезапно упираетесь в деление и порядок операций.
Представим: цена price = 199 евро, скидка 10% . Как посчитать скидку в евро?
Наивная попытка часто выглядит так: discount := price * 10 / 100. Это уже неплохо: умножение раньше деления, всё ок.
Но ошибка появляется, когда человек пишет так: discount := price * (10 / 100). Здесь (10/100) — это целочисленное деление и даёт 0. Поэтому скидка будет price*0, то есть 0.
Покажем рядом:
package main
import "fmt"
func main() {
price := 199
d1 := price * 10 / 100
d2 := price * (10 / 100)
fmt.Println(d1) // 19
fmt.Println(d2) // 0
}
Это отличный пример того, почему скобки — мощный инструмент, но ими тоже можно “сделать хуже”, если не понимать деление.
3. Практика: заказ, скидка и разбор суммы
Схема расчёта заказа
Продолжим наше мини-приложение. Пусть оно называется условно “Кофейня”: мы считаем стоимость заказа, скидку и итог. Мы будем хранить всё в копейках (или центах) как int64, чтобы не спорить с плавающей точкой.
Ниже — схема вычислений, чтобы вы не утонули в одной строке из 200 символов.
flowchart TD
A[Цена за штуку, коп] --> C[Подсчёт подитога]
B[Количество] --> C
C --> D[Скидка, коп]
D --> E[Итог, коп]
E --> F[доллары и центы]
Схема простая: сначала подитог, потом скидка, потом итог, потом “разбор” на доллары и центы для печати.
Код: подитог, скидка и итог
Сделаем маленький, но связный фрагмент: вводим цену (в копейках), количество, процент скидки. Считаем аккуратно, не смешивая всё в одну строку.
package main
import (
"fmt"
)
func main() {
var priceK int64
var qty int64
var discountPercent int64
fmt.Scan(&priceK, &qty, &discountPercent)
subtotal := priceK * qty
discount := subtotal * discountPercent / 100
total := subtotal - discount
fmt.Println(total)
}
Здесь самое важное — строчка discount := subtotal * discountPercent / 100. За счёт приоритета это считается как (subtotal * discountPercent) / 100, то есть “сначала умножили, потом поделили”. Это обычно то, что и нужно для процентов в целых числах.
Почему лучше умножать до деления
Это не только про “приоритет”. Это ещё и про то, что целочисленное деление отбрасывает дробную часть, а значит вы теряете точность.
Если вы делите слишком рано, вы можете потерять информацию ещё до умножения. Сумма потерь может быть неожиданной.
Сравним две формулы скидки в копейках:
1) subtotal * percent / 100
2) subtotal / 100 * percent
Они не равны из-за усечения.
package main
import "fmt"
func main() {
subtotal := int64(199) // центы для простоты примера
percent := int64(10)
a := subtotal * percent / 100
b := subtotal / 100 * percent
fmt.Println(a) // 19
fmt.Println(b) // 10
}
Во втором варианте subtotal/100 сначала превратился в 1, потому что 199/100 == 1. И только потом умножился на 10.
Куда деваются копейки при скидке
Если вы считаете скидку в копейках, вы почти всегда сталкиваетесь с тем, что “по математике” выходит дробно, а в целых копейках дроби нет. Например, скидка 15% от 99 копеек — это 14.85 копейки. Что делать с 0.85?
В этой лекции мы не вводим отдельные функции округления (это отдельная тема), но важно понять базовый эффект: в целых числах дробь просто теряется. Это значит, что вы всегда округляете вниз по модулю (в сторону нуля) при делении.
Посмотрим:
package main
import "fmt"
func main() {
subtotal := int64(99)
percent := int64(15)
discount := subtotal * percent / 100
fmt.Println(discount) // 14
}
Это не “ошибка Go”. Это просто выбранный тип данных: целые числа.
Делим сумму на людей и считаем остаток
Очень типовая ситуация: нужно разделить счёт поровну между N людьми. В целых копейках вы получите “остаток”, который кому-то придётся доплатить (или вы решите, что кофейня дарит эти центы миру).
package main
import "fmt"
func main() {
totalC := 1000 // 10 евро 00 центов
people := 3
perPerson := totalC / people
remainder := totalC % people
fmt.Println(perPerson, remainder) // 333 1
}
Тут мы получили: каждый платит по 333 цента, и остаётся 1 цент “хвоста”.
Почему одна строка не делает код умнее
Когда вы освоили приоритет, появляется соблазн писать так:
total := priceC*qty - priceC*qty*discountPercent/100
Компилятор это съест. Но человек (включая вас через неделю) начнёт медленно грустить.
Лучше чуть длиннее, но понятно. Мы уже делали так, и это действительно хороший стиль для новичка:
package main
import "fmt"
func main() {
priceC := int64(15000)
qty := int64(2)
discountPercent := int64(10)
subtotal := priceC * qty
discount := subtotal * discountPercent / 100
total := subtotal - discount
fmt.Println(total) // 27000
}
Числовые литералы и деление 7/2
Иногда путаница усиливается тем, что литералы вроде 7 и 2 выглядят “просто числами”. В Go числовые константы сначала живут как “идеальные числа (без типа)”, но когда вы используете их в обычном выражении, они получают тип по умолчанию. Для целых констант это int, для вещественных — float64.
Именно поэтому 7/2 — это деление int на int, то есть целочисленное деление. А вот 7.0/2.0 — уже вещественное.
Покажем разницу без преобразований типов (просто за счёт разных литералов):
package main
import "fmt"
func main() {
fmt.Println(7 / 2) // 3
fmt.Println(7.0 / 2.0) // 3.5
}
4. Типичные ошибки
Ошибка №1: ожидать “дробь” от int / int.
Когда вы делите int на int, вы получаете int, и дробная часть исчезает. Самый обидный вариант — когда вы делите маленькие числа и получаете 0, а дальше умножаете на что-то серьёзное и удивляетесь “почему всё ноль”. Это лечится не “магией”, а пониманием: какая арифметика выполняется — целочисленная или вещественная.
Ошибка №2: ставить скобки, которые меняют смысл процентов.
Выражение subtotal * percent / 100 обычно корректно. А вот subtotal * (percent / 100) почти всегда неверно в целых числах, потому что (percent/100) часто превращается в 0. Скобки — не просто украшение, они меняют порядок вычислений, поэтому их нужно ставить осознанно.
Ошибка №3: забыть, что % имеет высокий приоритет.
Новички иногда читают a + b%10 как “сначала сложили, потом взяли остаток”. Но на самом деле берётся остаток b%10, и только потом он прибавляется к a. Если вы хотели другое — нужны скобки: (a + b) % 10.
Ошибка №4: пытаться “округлять” через int(x/100*percent) в целых числах.
Если x — целое, то x/100 уже потеряло дробную часть, и никакое последующее умножение её не вернёт. Часто правильнее сначала умножать, потом делить, а не наоборот. Это не правило “всегда так”, но это хороший дефолт для процентов и пропорций в целочисленной арифметике.
Ошибка №5: превращать расчёт в одну строку и терять контроль над смыслом.
Технически Go позволяет писать длинные выражения, и приоритет действительно можно “держать в голове”. Но в реальном коде важнее, чтобы выражение было читаемым: промежуточные переменные (subtotal, discount, total) — это не “лишний код”, а способ сделать формулу проверяемой глазами, а значит — более надёжной.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ