JavaRush /Курсы /Go SELF /Арифметика и приоритет операторов

Арифметика и приоритет операторов

Go SELF
7 уровень , 3 лекция
Открыта

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) — это не “лишний код”, а способ сделать формулу проверяемой глазами, а значит — более надёжной.

1
Задача
Go SELF, 7 уровень, 3 лекция
Недоступна
Упаковка склада
Упаковка склада
1
Задача
Go SELF, 7 уровень, 3 лекция
Недоступна
Две сметы
Две сметы
1
Задача
Go SELF, 7 уровень, 3 лекция
Недоступна
Скидка без дроби
Скидка без дроби
1
Задача
Go SELF, 7 уровень, 3 лекция
Недоступна
Звонок по минутам
Звонок по минутам
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ