Перейти к основному содержимому

Операторы и управляющие конструкции в Go

Разработчику Архитектору

Перед чтением: Операторы — общие понятия оператора, операнда, приоритетов и типов операций без привязки к языку.

Сначала: Циклы в коде — общая идея повторений, виды циклов и типичные ошибки без привязки к синтаксису языка.


Go — if, for, switch

Условия — if с обязательными {} (даже для одной строки). Цикл for — единственный: в нём и счётчик (for i := 0; i < n; i++), и for range slice, и "бесконечный" for { }. Отдельного while нет. switch без падения в следующую ветку, если не писать fallthrough.

Порядок в статье: операторы → условия → циклы.


Операторы в Go

Play ITЗагрузка интерактивного демо…

В языке Go операторы делятся на группы по выполняемой функции:
арифметические, сравнения, логические, побитовые, присваивания, адресные, а также специальные операторы, такие как :=, ..., *, &. Ниже рассматривается каждая группа с учётом семантических особенностей Go.


Арифметические операторы

Арифметические операторы применяются к числовым типам — целочисленным (int, int8int64, uint, uint8uint64, uintptr) и вещественным (float32, float64). Поддержка комплексных чисел (complex64, complex128) также включена, но с рядом ограничений — например, деление и умножение определены, а операции сравнения — запрещены.

Доступны следующие операторы:

  • + — сложение;
  • - — вычитание или унарный минус;
  • * — умножение;
  • / — деление;
  • % — остаток от деления (только для целочисленных типов);
  • ++ и -- — постфиксные инкремент и декремент.

Важно отметить, что в Go инкремент и декремент реализованы только как постфиксные операторы и только в виде отдельного выражения. Например, запись i++ допустима, а ++i, j = i++ или a[i++] — недопустимы. Это сделано сознательно, чтобы исключить неоднозначности, связанные с порядком вычисления и побочными эффектами. Инкремент и декремент не возвращают значение и не могут быть частью других выражений.

Оператор деления / ведёт себя по-разному в зависимости от типа операндов. При делении целых чисел результат округляется к нулю (так называемое усечение), например: 7 / 3 == 2, -7 / 3 == -2. При делении вещественных чисел результат точный в пределах разрядности типа.

Оператор % определяется так, что для любых целых a и b (где b != 0) выполняется тождество:
a == b * (a / b) + a % b.
Это гарантирует, что знак остатка совпадает со знаком делимого. Например:
7 % 3 == 1, -7 % 3 == -1, 7 % -3 == 1, -7 % -3 == -1.

a, b := 7, 3
fmt.Println(a/b) // 2
fmt.Println(a%b) // 1

Разбор:

  • При делении целых a/b дробная часть отбрасывается, поэтому результат равен 2.
  • Оператор % возвращает остаток от целочисленного деления, в примере это 1.
  • Обе операции работают только с целочисленными типами в таком виде.
  • Этот базовый паттерн часто используют в пагинации, циклических индексах и проверке кратности.

Операторы сравнения

Операторы сравнения возвращают булево значение true или false. Поддерживаются следующие операторы:

  • == — равенство;
  • != — неравенство;
  • <, <=, >, >= — строгие и нестрогие отношения порядка.

Сравниваемые операнды должны быть сравнимыми. Сравнимость — это свойство типа, определяемое спецификацией языка. В частности:

  • Все базовые скалярные типы (числа, строки, булевы значения) — сравнимы;
  • Указатели сравнимы, если они указывают на объекты одного типа;
  • Интерфейсы сравнимы, если динамические типы и значения их содержимого сравнимы;
  • Структуры сравнимы, если все их поля сравнимы;
  • Срезы, карты и функции — несравнимы, за исключением сравнения с nil.

Сравнение nil определено только для указателей, срезов, карт, функций, интерфейсов и каналов. Попытка сравнить, например, структуру с nil вызывает ошибку компиляции.

Особое внимание заслуживает сравнение строк: оно выполняется лексикографически на основе значений байтов в UTF-8-представлении. Это означает, что сравнение не учитывает локаль и не является лингвистически корректным — только байтовым.


Логические операторы

Логические операторы применяются к булевым операндам и возвращают булево значение. Язык Go поддерживает:

  • && — логическое И (сокращённое вычисление: если левый операнд false, правый не вычисляется);
  • || — логическое ИЛИ (сокращённое вычисление: если левый операнд true, правый не вычисляется);
  • ! — логическое НЕ (унарный оператор).

Go не допускает неявного приведения чисел или других типов к bool. Это исключает классические ошибки вроде if (x = 5) вместо if (x == 5), поскольку присваивание не возвращает значение и не может быть использовано в условии.

Сокращённое вычисление гарантируется спецификацией. Это позволяет безопасно писать цепочки вида:
if ptr != nil && ptr.IsValid() — здесь ptr.IsValid() не будет вызвано, если ptr равен nil.

var ptr *User
if ptr != nil && ptr.Active {
fmt.Println("active user")
}

Разбор:

  • Левое условие ptr != nil защищает доступ к полю структуры через указатель.
  • Благодаря short-circuit правая часть ptr.Active вычисляется только если указатель не nil.
  • Такой шаблон предотвращает панику nil pointer dereference.
  • Это канонический способ писать безопасные составные условия в Go.

Побитовые операторы

Побитовые операторы работают с битами целочисленных типов, а не с логикой true/false. Поддерживаются:

  • & — побитовое И;
  • | — побитовое ИЛИ;
  • ^ — побитовое исключающее ИЛИ;
  • &^ — побитовое И-НЕ (операция "очистки битов");
  • &lt;&lt; — сдвиг влево;
  • &gt;&gt; — сдвиг вправо.

Оператор &^ уникален: x &^ y эквивалентен x & (^y), но записан компактнее. Он часто используется для сброса определённых битов — например, flags &^ FlagDebug удаляет флаг FlagDebug, не затрагивая остальные.

Сдвиги в Go определены только для целочисленных операндов. Количество разрядов сдвига должно быть неотрицательным и меньше, чем разрядность типа результата. В противном случае поведение не определено, и компилятор может выдать ошибку или предупреждение.


Операторы присваивания

Go поддерживает обычные операторы присваивания и составные формы:

  • = — простое присваивание;
  • +=, -=, *=, /=, %=;
  • &=, |=, ^=, &^=;
  • &lt;&lt;=, &gt;&gt;=.

Все составные операторы эквивалентны соответствующей бинарной операции с последующим присваиванием, например:
x += yx = x + y.

Важной особенностью Go является отсутствие неявного приведения типов при присваивании. Если тип правого выражения не совпадает с типом левой переменной, требуется явное приведение, даже если типы совместимы в математическом смысле. Например:
var i int = 5; var f float64 = i — ошибка компиляции.
Корректно: var f float64 = float64(i).


Операторы работы с адресами и указателями

Операторы * и & выполняют противоположные функции:

  • &x — получение адреса переменной x; результат имеет тип *T, где T — тип x;
  • *p — разыменование указателя p; применяется только к значениям указательного типа.

Указатели в Go не поддерживают арифметику (в отличие от C), что повышает безопасность и предсказуемость. Единственное исключение — пакет unsafe, который предоставляет ограниченные и небезопасные возможности, но его использование считается крайней мерой.

Оператор new(T) выделяет память под значение типа T, инициализирует его нулевым значением и возвращает указатель *T. Эквивалентен var t T; p := &t, но более лаконичен.


Оператор := — короткое объявление

Оператор := сочетает в себе объявление и инициализацию переменной. Его можно использовать только внутри функций. Левая часть может содержать одно или несколько имён, правая — выражения. Типы выводятся из инициализирующих выражений.

Пример:
x, y := 10, "hello"
эквивалентен
var x int = 10; var y string = "hello".

Если часть имён уже объявлена в текущей области видимости, они переиспользуются, но только при условии, что хотя бы одно новое имя присутствует. Это позволяет, например, обновлять переменные цикла в range, не нарушая область видимости.

Важно: := не является синтаксическим сахаром для var; это отдельная языковая конструкция с собственными правилами области видимости и переиспользования.


Оператор ... — распаковка среза

Оператор ... используется при вызове функции с переменным числом аргументов (variadic function). Он "распаковывает" элементы среза в отдельные аргументы.

Пример:

func sum(nums ...int) int {}
values := []int{1, 2, 3}
total := sum(values...) // передаётся три отдельных аргумента

Разбор:

  • sum(nums ...int) объявляет variadic-параметр, который внутри функции обрабатывается как слайс []int.
  • values := []int{1, 2, 3} формирует набор аргументов в коллекции.
  • values... разворачивает элементы слайса в отдельные аргументы при вызове функции.
  • Такой формат полезен, когда набор значений динамический и заранее хранится в срезе.

Тип среза должен точно соответствовать типу параметра-вариадика (после ...), включая именованные типы. Это требование обеспечивает сохранение типобезопасности.


Условные операторы

Оператор if

Синтаксис if в Go состоит из трёх частей, из которых обязательна только первая:

if инициализация; условие {
// тело, выполняемое при истинности условия
} else if инициализация; условие {
// альтернативное тело
} else {
// ветка по умолчанию
}

Разбор:

  • Заголовок if может содержать инициализацию перед ;, чтобы ограничить жизнь временных переменных текущим блоком.

  • Условие обязано вычисляться в bool, поэтому неявные truthy/falsy-преобразования исключены.

  • Все ветки оформляются в фигурных скобках, что делает структуру кода однозначной при расширении.

  • Этот шаблон удобен для локальной проверки результатов вызовов и ранних возвратов.

  • Инициализация — необязательное простое выражение, обычно объявление переменной с :=. Оно выполняется один раз перед проверкой условия и создаёт переменные, область видимости которых ограничена всем if-блоком (включая else-ветки). Это позволяет локализовать вспомогательные переменные, избегая загрязнения внешней области видимости.

  • Условие — выражение, результат которого должен иметь тип bool. В Go нет неявного преобразования в логический тип, поэтому конструкции вроде if x (где x — число или указатель) недопустимы. Требуется явное сравнение: if x != 0, if ptr != nil.

  • Фигурные скобки — обязательны, даже если тело состоит из одной инструкции. Это устраняет классическую ошибку "висячего else" и делает структуру кода однозначной.

  • Скобки вокруг условия — отсутствуют. Это требование синтаксиса: условие не окружается скобками. Причина — избежать визуального шума и усилить читаемость. Конструкция if (x > 0) содержит синтаксическую ошибку.

Пример идиоматичного использования:

f, err := os.Open("file.txt")
if err != nil {
log.Fatal(err)
}
defer f.Close()
// работа с файлом

Разбор:

  • os.Open возвращает ресурс и ошибку, поэтому проверка err идёт сразу после вызова.
  • Ветка if err != nil останавливает поток при неуспехе, чтобы не работать с невалидным дескриптором.
  • defer f.Close() гарантирует освобождение ресурса перед выходом из функции.
  • Это базовый шаблон безопасной работы с файлами и другими закрываемыми объектами.

Здесь f и err видны в теле функции после короткого объявления; defer f.Close() выполнится при выходе из функции. После завершения блока они уничтожаются. Это способствует локализации обработки ошибок и предотвращает случайное использование неинициализированных или уже закрытых ресурсов.

Важно — инициализация может содержать любое простое выражение — присваивание, короткое объявление, вызов функции. Но не может включать составные операторы (if, for, блоки). Цель — сохранить компактность и читаемость заголовка условия.


Оператор switch

switch в Go — мощная и безопасная альтернатива цепочкам if-else. Он поддерживает несколько форм и обладает рядом особенностей, повышающих надёжность.


Базовый синтаксис

switch инициализация; выражение {
case значение1, значение2:
// выполняется, если выражение == значение1 или значение2
case значение3:
// ...
default:
// выполняется, если ни один case не совпал
}

Разбор:

  • Выражение в switch вычисляется один раз и дальше сравнивается со значениями в case.

  • В одном case можно перечислять несколько совпадающих значений через запятую.

  • Ветка default задаёт сценарий по умолчанию, когда ни одно сравнение не сработало.

  • Из-за отсутствия автоматического fallthrough поведение ветвления остаётся предсказуемым.

  • Как и в if, поддерживается необязательная инициализация, видимая во всём switch-блоке.

  • Выражение в заголовке вычисляется один раз перед сопоставлением с case-ами.

  • В каждом case перечисляются точные значения (или списки значений), с которыми сравнивается результат выражения. Сравнение выполняется оператором ==, поэтому типы должны быть совместимы и сравнимы.

  • Автоматического проваливания (fallthrough) нет. После выполнения тела case управление переходит за пределы switch; явное fallthrough требуется только при необходимости продолжить выполнение в следующем case.

  • Ветка default может находиться в любом месте, но выполняется только если ни один case не подошёл.


switch без выражения (tagless switch)

Особенно ценная форма — switch без выражения в заголовке:

switch {
case x < 0:
// ...
case x == 0:
// ...
case x > 0 && x < 10:
// ...
default:
// ...
}

Разбор:

  • Это форма switch без выражения, где каждое условие в case вычисляется как bool.
  • Ветви идут сверху вниз, и выполняется первая истинная проверка.
  • Такой стиль снижает вложенность по сравнению с длинной цепочкой if/else if.
  • Конструкция удобна для диапазонов, состояний и правил приоритетной маршрутизации.

Здесь каждый case содержит булево выражение. Конструкция эквивалентна цепочке if-else if, но читается линейнее, избегает дублирования условий и визуально группирует взаимоисключающие альтернативы. Компилятор часто оптимизирует такой switch эффективнее, чем длинную цепочку if.

Эта форма рекомендуется вместо if-else, когда число условий превышает два–три, особенно если условия сложные или требуют проверки нескольких взаимоисключающих состояний.


Сравнение с интерфейсами и типами (type switch)

Отдельная форма — switch по типу интерфейсного значения:

switch v := x.(type) {
case int:
fmt.Println("целое:", v)
case string:
fmt.Println("строка длиной", len(v))
default:
fmt.Println("неизвестный тип")
}

Разбор:

  • x.(type) в заголовке доступен только в type-switch и извлекает динамический тип значения интерфейса.
  • Ветка case int получает v уже как int, а case string — как string, без ручных привидений.
  • Такой механизм заменяет несколько последовательных type assertion и делает код безопаснее.
  • default покрывает все остальные типы, которые не были перечислены явно.

Здесь v получает значение x, приведённое к конкретному типу внутри соответствующего case. Это безопасная и эффективная замена последовательным if x, ok := x.(T); ok { … }.

Важно — в default-ветке v имеет тип исходного интерфейса, а не interface{} — это позволяет работать с динамическим значением, даже если тип неизвестен.


Отсутствие дублирования значений в case

Go требует, чтобы значения в case не повторялись. Попытка написать case 1, 2, 1: вызовет ошибку компиляции. Это исключает неочевидные логические ошибки.


Циклы

Интерактивное демо — пошаговый цикл на примере JavaScript (for, while). В Go один оператор for на все случаи, но порядок шагов тот же. Обобщённо: циклы в коде.

Play ITЗагрузка интерактивного демо…

Go имеет единственный циклический оператор — for. Все виды циклов реализуются через его синтаксические варианты. Это упрощает язык, устраняет дублирование и делает структуру циклов предсказуемой.

Синтаксис for состоит из трёх компонентов, из которых обязательны только скобки и фигурные скобки:

for инициализация; условие; поститерационное_выражение {
// тело цикла
}

Разбор:

  • Инициализация выполняется один раз перед входом в цикл и обычно создаёт счётчик.

  • Условие проверяется перед каждой итерацией и определяет момент остановки.

  • Поститерационное выражение обновляет состояние после тела цикла.

  • Эта форма покрывает классический счётный цикл и даёт полный контроль над шагом.

  • Инициализация — выполняется один раз перед началом цикла. Обычно — объявление счётчика (i := 0).

  • Условие — проверяется перед каждой итерацией. Если true — тело выполняется; если false — цикл завершается.

  • Поститерационное выражение — выполняется после каждой итерации, перед повторной проверкой условия. Чаще всего — i++.

Пример классического цикла:

for i := 0; i < 10; i++ {
fmt.Println(i)
}

Разбор:

  • Цикл стартует с i = 0 и повторяется, пока значение меньше 10.
  • После каждой итерации i++ увеличивает счётчик на один.
  • fmt.Println(i) выводит текущий шаг выполнения в стандартный вывод.
  • Итогом становится последовательность значений от 0 до 9.

Бесконечный цикл

Если опустить все три компонента, получится бесконечный цикл:

for {
// выполняется вечно, пока не встретится break, return или panic
}

Разбор:

  • Пустой заголовок for создаёт бесконечный цикл без встроенного условия выхода.
  • Прерывание должно быть реализовано в теле через break, return, panic или внешний сигнал.
  • Такой паттерн часто используют в воркерах и event-loop, где поток живёт длительно.
  • Для контролируемого завершения обычно добавляют select с context.Done().

Это идиоматичный способ написать "вечный" цикл. Явное for true { … } считается избыточным и не рекомендуется.


Цикл с предусловием (аналог while)

Опустив инициализацию и поститерационное выражение, получаем цикл с предусловием:

for условие {
// тело
}

Эквивалентно while (условие) в других языках. Например:

for scanner.Scan() {
line := scanner.Text()
process(line)
}

Итерация по коллекциям — for range

Оператор range — встроенная форма for, предназначенная для перебора элементов итерируемых структур — массивов, срезов, строк, карт, каналов.

Общий вид:

for индекс, значение := range коллекция {
// тело
}
  • Для срезов и массивов:

    • индекс — порядковый номер элемента (int);
    • значение — копия элемента (T, где T — тип элемента);
    • можно опустить индекс: for _, v := range slice;
    • можно опустить значение: for i := range slice (часто используется для создания нового среза той же длины).
  • Для строк:

    • индекс — позиция начала символа в байтах (не номер символа!);
    • значениеrune (Unicode-кодовая точка);
    • range корректно обходит UTF-8-последовательность, пропуская байты продолжения.

Пример:

s := "Привет"
for i, r := range s {
fmt.Printf("%d: %c (%U)\n", i, r, r)
}
// Вывод:
// 0: П (U+041F)
// 2: р (U+0440)
// 4: и (U+0438)
// 6: в (U+0432)
// 8: е (U+0435)
// 10: т (U+0442)

Обратите внимание: индексы идут с шагом 2 — потому что кириллические символы в UTF-8 занимают по 2 байта.

  • Для карт:

    • индекс — ключ (K);
    • значение — копия значения (V);
    • порядок итерации не определён и варьируется от запуска к запуску (это мера защиты от неявной зависимости от порядка).
  • Для каналов:

    • range читает из канала, пока он не закрыт;
    • возвращает только значение (v), индекс не предусмотрен;
    • блокируется при пустом незакрытом канале.

Важные нюансы range:

  • значение — это копия элемента. Изменение v внутри цикла не влияет на исходную коллекцию. Чтобы изменить элемент среза, нужно использовать индекс: slice[i] = newValue.
  • При итерации по карте, модификация карты внутри цикла запрещена и вызывает панику — это защита от неопределённого поведения.

Управление выполнением цикла — break и continue

  • break — немедленно завершает выполнение ближайшего for, switch или select.
  • continue — завершает текущую итерацию цикла и переходит к проверке условия (или к следующей итерации range).

Оба оператора могут иметь метку (label), если требуется управление вложенным циклом:

outer:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if i == j {
break outer // выйти из внешнего цикла
}
fmt.Println(i, j)
}
}

Метки — это идентификаторы, ставящиеся перед оператором, за которыми следует двоеточие. Используются редко, только при необходимости выйти из нескольких уровней вложенности.


Отсутствие do-while и foreach

Go сознательно не включает цикл do-while, так как его можно легко выразить через for:

for {
// тело
if !условие { break }
}

Аналогично, отдельного foreach нет, потому что for range покрывает все сценарии итерации по коллекциям — более обобщённо и безопасно.


Идиомы и лучшие практики

Предпочтение switch перед цепочками if-else

Когда условное ветвление содержит более двух взаимоисключающих вариантов, и особенно если условия — это проверки на равенство констант, switch предпочтительнее if-else. Причины:

  • Читаемость: структура switch визуально группирует альтернативы, делая логику выбора прозрачной.
  • Безопасность: отсутствие автоматического fallthrough исключает случайные проваливания, характерные для C-подобных языков.
  • Поддерживаемость: добавление нового case не требует вставки else if в середину длинной цепочки и не нарушает баланс скобок.

Даже при неточном совпадении (switch без выражения), когда case содержат произвольные булевы условия, switch остаётся выразительным и компактным инструментом — особенно в обработке ошибок, состояний конечных автоматов или маршрутизации запросов.


Избегание избыточных скобок

Go требует фигурных скобок вокруг тел if, for, switch, но запрещает круглые скобки вокруг условий и управляющих выражений. Это часть грамматики. Конструкции вида:

if (x > 0) {} // ошибка
for (i := 0; i < n; i++) {} // ошибка
switch (value) {} // ошибка

не компилируются. Форма без скобок — обязательное правило. Это устраняет визуальный шум, снижает вероятность ошибок при редактировании (например, добавлении else к многострочному условию) и делает структуру кода более регулярной.


Локализация переменных через инициализацию в if и switch

Идиома объявления переменной непосредственно в заголовке if или switch — один из краеугольных камней идиоматичного Go-кода. Она позволяет:

  • Ограничивать область видимости переменной строго тем блоком, где она необходима.
  • Гарантировать, что переменная инициализирована перед использованием (в отличие от var x T; if … &#123; x = … &#125;, где x может остаться в нулевом состоянии).
  • Компактно связывать проверку и использование результата (особенно при обработке ошибок).

Пример:

if file, err := os.Open(path); err != nil {
return fmt.Errorf("не удалось открыть %s: %w", path, err)
} else {
defer file.Close()
return process(file)
}

Здесь file и err недоступны вне этого блока — это исключает случайное использование file после его закрытия или в ветке ошибки.


Эффективное использование range

При итерации по срезам и массивам часто требуется только индекс или только значение. Go позволяет опускать ненужную часть, используя "пустой идентификатор" _:

  • for _, v := range slice — когда индекс не нужен.
  • for i := range slice — когда нужно только количество итераций (например, для инициализации нового среза: dest := make([]T, len(src)); for i := range src &#123; dest[i] = transform(src[i]) &#125;).

Важно помнить: v в for _, v := range slice — это копия элемента. Прямое присваивание v = newValue не изменит исходный срез. Для модификации требуется доступ по индексу:

for i := range slice {
slice[i] = transform(slice[i])
}

При итерации по карте порядок не гарантирован. Если требуется детерминированная обработка, следует предварительно извлечь ключи, отсортировать их и пройтись по отсортированному срезу.


Избегание изменения коллекции внутри range

Модификация среза (например, добавление через append) во время итерации по нему допустима, но требует осторожности: длина может измениться, и цикл for i := 0; i < len(slice); i++ приведёт к пропуску или повторной обработке элементов. Цикл for range фиксирует длину среза на момент входа в цикл, поэтому новые элементы не будут пройдены — это часто желаемое поведение.

Напротив, модификация карты (вставка или удаление) внутри range по той же карте вызывает панику времени выполнения. Это намеренное ограничение, защищающее от неопределённого поведения.


Типичные ошибки и их причины

Попытка использовать ++ и -- внутри выражений

Как уже отмечалось, i++ — это самостоятельное выражение, не возвращающее значение. Попытки написать a[i++] = x или j = i++ приводят к ошибке компиляции. Это мера защиты: такие конструкции часто становятся источником неочевидных побочных эффектов, особенно при параллельном выполнении или сложных зависимостях.

Обход: выделить инкремент в отдельную строку:

a[i] = x
i++

Сравнение несравнимых типов с nil

Ошибка:

type Config struct { Host string }
var c Config
if c == nil {} // ошибка компиляции

Структуры, срезы, карты и функции без явного объявления как указателей или интерфейсов — не являются указателями и не могут сравниваться с nil. Только *Config, []T, map[K]V, func(…), interface{}, chan T могут быть nil.

Правильно:

var c *Config // указатель
if c == nil {}

или

var m map[string]int
if m == nil {} // допустимо, так как m имеет тип map

Неправильное понимание nil в интерфейсах

Интерфейсное значение состоит из типа и значения. Оно считается nil только если оба компонента nil. Если переменная интерфейсного типа содержит ненулевой указатель на nil (например, *Config(nil)), сравнение i == nil вернёт false, хотя i "кажется пустым". Это частый источник ошибок при возврате ошибок из функций.

Иллюстрация:

func returnsNilPtr() error {
var err *os.PathError = nil
return err // возвращает интерфейс error с типом *os.PathError и значением nil
}

e := returnsNilPtr()
fmt.Println(e == nil) // false!

Решение — никогда не возвращать nil, завёрнутый в именованный тип указателя, из функции, возвращающей интерфейс. Используйте литерал nil напрямую.


Переполнение при арифметике

Go не проверяет переполнение целочисленных операций. Результат "оборачивается" по модулю разрядности типа. Например, при int8 значение 127 + 1 даёт -128.

Это соответствует поведению большинства современных процессоров и обеспечивает максимальную производительность. Проверка переполнения возлагается на программиста. Для критичных к безопасности вычислений рекомендуется использовать сторонние библиотеки (например, math/bits для проверки флагов, или пакеты вроде github.com/ericlagergren/decimal для точной арифметики).


Неправильная обработка range по строкам

Начинающие часто полагают, что for i, _ := range s перебирает символы строки по порядку, и что i — это номер символа. На самом деле i — это байтовый смещение начала символа в UTF-8. Для ASCII-строк это совпадает, но для Unicode — нет.

Пример ошибки:

s := "café"
for i := 0; i < len(s); i++ {
fmt.Printf("%c ", s[i]) // неправильно: s[i] — байт, не rune
}
// Вывод: c a f

Правильно использовать range или явно конвертировать в []rune:

runes := []rune(s)
for i := 0; i < len(runes); i++ {
fmt.Printf("%c ", runes[i])
}
// или
for _, r := range s {
fmt.Printf("%c ", r)
}

Сравнение с другими языками — что убрано и зачем

Go сознательно отказывается от ряда привычных конструкций, чтобы снизить когнитивную нагрузку и повысить надёжность:

  • Нет тернарного оператора ?: — потому что он поощряет написание сложных однострочных выражений в ущерб читаемости. if-else с инициализацией достигает той же цели явно и безопасно.
  • Нет неявного приведения к bool — исключает ошибки вроде if (x = 5).
  • Нет арифметики указателей — повышает безопасность и упрощает сборку мусора.
  • Один цикл for вместо while, do-while, foreach — снижает число понятий, упрощает обучение и инструменты анализа.
  • Обязательные фигурные скобки — устраняют классические баги стиля ("висячий else", забытые блоки при рефакторинге).

Эти решения отражают философию Go: "меньше — лучше, если это делает поведение предсказуемым".


Практика управления потоком

Чтобы раздел закреплялся быстрее, используйте небольшой рабочий шаблон для сложных условий и циклов.


Шаблон "if + guard clause"

func handle(user *User) error {
if user == nil {
return errors.New("user is nil")
}
if user.ID == 0 {
return errors.New("user id is empty")
}
return nil
}

Шаблон "switch состояний"

switch state {
case StateDraft:
return processDraft()
case StatePublished:
return processPublished()
default:
return fmt.Errorf("unknown state: %v", state)
}

Разбор:

  • switch делает маршрутизацию по состоянию линейной и легко расширяемой.
  • Каждая ветка отвечает за отдельный бизнес-сценарий (draft, published).
  • default защищает от неожиданных новых значений и не даёт функции молча продолжить работу.
  • Такой каркас удобно покрывать таблицей тестов по значениям state.

Шаблон "for + context"

for {
select {
case <-ctx.Done():
return ctx.Err()
case job := <-jobs:
process(job)
}
}
Связанные темы

Логику условий удобно читать вместе с Синтаксические конструкции Go, а обработку ошибок и дизайн API — с Особенности языка Go.


Разбор кейса — "ветвления выросли и стали нечитаемыми"

Когда в обработчике растёт цепочка if-else if-else, обычно появляется:

  • повторение одних и тех же проверок;
  • ошибки в приоритетах условий;
  • сложность добавления нового состояния.

Рабочая стратегия:

  • для состояний использовать switch;
  • для early-fail проверок использовать guard clauses в начале функции;
  • для конкурентных событий выносить логику в select с тайм-аутом через context.

Так код легче ревьюить, проще тестировать и безопаснее менять.

Содержание