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

5.10. Синтаксис

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

Синтаксис

Go (или Golang) — компилируемый статически типизированный язык системного уровня, созданный в Google в 2007–2009 годах Робертом Гризмером, Робом Пайком и Кеном Томпсоном. Язык проектировался как инструмент для решения практических задач в условиях масштабной разработки: команды десятков и сотен инженеров, длительных жизненных циклов, требований к производительности, надёжности и сопровождаемости.

Синтаксис Go — это один из ключевых носителей его философии. Он минимально декоративен, лексически близок к C, но семантически — к более современным языкам. При этом он намеренно лишён многих «удобств», привычных разработчикам из других экосистем, — как средство снижения когнитивной нагрузки и предотвращения распространённых классов ошибок.

Ниже рассматривается синтаксис Go через призму двух углов зрения:

  1. Техническое описание — как устроен код, какие конструкции допустимы, какие правила соблюдаются.
  2. Контекстальный анализ — что в этом синтаксисе бросается в глаза разработчику с опытом в Java или Python, какие ожидания ломаются, какие компенсации предлагают вместо «потерянных» возможностей.

1. Общая структура исходного файла

Каждый файл на Go начинается с объявления пакета:

package main

Это обязательное требование. Даже если файл состоит из одного выражения — без package компилятор выдаст ошибку. package здесь — единица компиляции и видимости. Пакет — это совокупность исходных файлов, объявляющих один и тот же package name. Все идентификаторы (переменные, типы, функции), объявленные в любом файле пакета, доступны в других файлах этого же пакета без дополнительных импортов.

Для Java-разработчика первое отличие — отсутствие прямого соответствия между именем пакета и путём в файловой системе. В Java имя пакета обычно повторяет иерархию каталогов (com.example.servicesrc/com/example/service/). В Go имена пакетов короткие, идентификатороподобные (main, http, json) и обычно совпадают с именем каталога, в котором лежат файлы, но только на одном уровне:

project/
├── cmd/
│ └── myapp/
│ └── main.go // package main
├── internal/
│ └── storage/ // каталог
│ └── postgres.go // package storage

Здесь storage — имя пакета и одновременно имя каталога, но не полный путь (internal.storage — недопустимо). Имя пакета не влияет на путь импорта; путь импорта — это путь к каталогу от корня модуля, а имя, под которым к нему обращаются в коде — это имя, указанное в package.

Для Python-разработчика: да, это похоже на __init__.py, но без динамической загрузки и без иерархических имён. Python оперирует модулями, Go — пакетами. Пакет в Go компилируется в один объектный файл; нет понятия «импортировать конкретную функцию», только весь пакет целиком.

После объявления пакета идут блоки импорта. Синтаксис импорта минималистичен:

import (
"fmt"
"os"

"github.com/some/external"

"myproject/internal/storage"
)

Ключевые особенности:

  • Импорты всегда строковые литералы, заключённые в двойные кавычки.
  • Стандартные пакеты импортируются без префикса (в отличие от Python, где os, sys — встроенные модули, но в Go они — часть стандартной библиотеки, и путь os — это физический путь внутри $GOROOT/src).
  • Нет звёздочных импортов (import *), нет from ... import .... Всё, что нужно, обращается через префикс имени пакета: fmt.Println(), os.Exit().
  • Импорты группируются: стандартная библиотека → сторонние зависимости → внутренние пакеты. Это не требование языка, но требование стиля (см. goimports). Инструменты автоматически сортируют и группируют их.

Импорт — это не копирование кода, не макроподстановка. Это объявление зависимости: компилятор проверяет, что пакет существует и экспортирует нужные идентификаторы, а линковщик включает соответствующие символы в исполняемый файл. Нет runtime-импорта, как в Python или Java.


2. Объявление переменных и констант

Go поддерживает как явное, так и неявное объявление переменных. Оба механизма — часть одного и того же семантического поля, но с разной степенью контроля.

Явное объявление (var и const)

var name string
var age int = 30
var (
host = "localhost"
port = 8080
)
  • var — ключевое слово для объявления переменной.
  • Можно указать тип явно (string, int) — тогда инициализатор может отсутствовать; переменная получит нулевое значение типа ("", 0, nil).
  • Можно опустить тип — тогда он выводится из выражения инициализации.
  • Группировка нескольких объявлений в var (...) — полноценная конструкция, позволяющая ссылаться в инициализаторах на объявленные выше в той же группе переменные.

Аналогично для констант:

const Pi = 3.14159
const (
Read = 1 << iota
Write
Execute
)

iota — это встроенная константа-счётчик, равная 0 на первой строке внутри const (...), 1 — на второй и т.д. Это компиляционный механизм, не runtime-переменная. Так реализуются перечисления (enum), хотя в Go нет встроенного типа enum.

Для Java-разработчика: const в Go — не то же, что final static. Это настоящая константа: значение подставляется на этапе компиляции, и обращение к ней не порождает чтения памяти. Поэтому const может быть только примитивного типа (число, строка, bool) или составным из константных значений (например, const mask = Read | Write). Объекты, слайсы, мапы — только через var.

Для Python-разработчика: в Python нет констант. Всё — переопределяемо. В Go константы гарантированно неизменяемы, и компилятор проверяет попытки их присвоения. Это важный инструмент для снижения побочных эффектов.

Короткое объявление (:=)

name := "Timur"
age := 25
conn, err := net.Dial("tcp", "example.com:80")

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

  • Можно использовать только там, где хотя бы одна из переменных ещё не объявлена. Это объявление новых переменных в текущей области видимости.
    x := 1
    x, y := 2, 3 // OK: y новая, x переобъявляется (shadowing)
    x := 4 // ОШИБКА: x уже объявлена, а новых переменных нет
  • Тип выводится из инициализатора.
  • Широко используется в идиоматическом коде, особенно при обработке ошибок (см. ниже).

Это — ответ Go на запрос «почему так много var?». := — инструмент для локального контекста, где инициализация очевидна. Вместе с тем, в полях структур, глобальных переменных, сигнатурах функций — только var.


3. Чистота кода и стиль

Go — один из немногих языков, где форматирование кода жёстко регламентировано и автоматизировано. Инструмент gofmt определяет стиль. Это часть языкового контракта.

Что это значит практически:

  • Отступы — только табы (да, именно табы, 8 пробелов по умолчанию в визуализации).
  • Открывающая фигурная скобка { всегда на той же строке, что и объявление (func, if, for, struct).
    if x > 0 {   // правильно
    ...
    }

    if x > 0 // НЕПРАВИЛЬНО — gofmt перенесёт {
    {
    ...
    }
  • Пробелы вокруг операторов, после запятых, перед : в :=, но не перед ( в вызовах функций: f(a, b), не f (a, b).
  • Пустые строки группируют логически связанные блоки, но не разделяют каждую строчку.
  • Длина строки не ограничена жёстко, но принято укладываться в 80–100 символов — из удобства просмотра в терминале и diff’ах.

Ключевой посыл: форматирование не предмет обсуждения. Команда не тратит время на «tabs vs spaces» или «K&R vs Allman». gofmt применяется перед каждым коммитом (часто автоматически через pre-commit hook). Это снижает когнитивную нагрузку при чтении чужого кода: структура видна сразу, без привыкания к стилю автора.

defer — синтаксис управления жизненным циклом

Одна из самых сильных конструкций Go — ключевое слово defer. Оно откладывает выполнение вызова функции до выхода из текущей функции (не блока!).

f, err := os.Open("file.txt")
if err != nil {
return err
}
defer f.Close() // будет вызвано перед return, panic или концом функции

// ... работа с f
return process(f)

Семантика:

  • Выражение после defer вычисляется немедленно (аргументы фиксируются), а вызов — откладывается.
  • Несколько defer выполняются в порядке LIFO (последний объявленный — первый выполнен).
  • defer работает даже при panic — это основа для безопасного управления ресурсами.

Для Java-разработчика: похоже на try-with-resources, но без специального интерфейса (AutoCloseable). Любая функция может быть defer’ена. Нет необходимости в обёртках.

Для Python-разработчика: напоминает __exit__ в контекстных менеджерах, но без with. При этом deferглобальный для функции, а не локальный для блока — это требует внимания, но даёт предсказуемость: ресурс освобождается после всей логики, включая обработку ошибок.

Запрет глобальных переменных (как практика, а не правило)

Глобальные переменные в Go разрешены — синтаксис позволяет:

var globalCounter int

Однако идиоматический код стремится их избегать. Почему?

  • Go — язык конкурентности. Глобальные переменные без синхронизации — источник data races.
  • Тестирование кода с глобальными состояниями сложно: тесты непредсказуемо влияют друг на друга.
  • Вместо глобальных переменных предпочтительно передавать зависимости явно (инъекция) или использовать конфигурационные структуры.

Это культурный договор. В стандартной библиотеке глобальные переменные — исключения (http.DefaultClient, log.Logger по умолчанию), и их использование считается «удобством по умолчанию».


4. Функции

Функция в Go — именованная последовательность инструкций, имеющая входные параметры, возвращаемые значения и область видимости. Синтаксис объявления:

func имя(параметр1 тип1, параметр2 тип2) типВозврата {
// тело
return значение
}

Ключевые особенности:

Порядок типов

func Sum(a int, b int) int
func Connect(host string, port int) (net.Conn, error)

Для Java-разработчика: тип стоит после имени параметра (a int, а не int a). Это сделано для удобства чтения при длинных именах и составных типах:

func Process(items []map[string]interface{}) ([]Result, error)

— здесь items сразу виден как параметр, а его тип — сложная структура — следует за ним. В C-подобных языках (int (*fp)(int)) тип может «оборачивать» имя, усложняя парсинг. Go выбирает линейный, слева-направо порядок.

Для Python-разработчика: типы обязательны (статическая типизация), но при вызове они не указываются — только значения. Аннотации в Python (def f(x: int) -> str) — документация и подсказка для инструментов; в Go типы — часть контракта, проверяемого компилятором.

Множественный возврат

Go позволяет функции возвращать несколько значений:

func Divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero")
}
return a / b, nil
}

Это — фундаментальный паттерн. Особенно: возврат значения и ошибки. Вместо исключений (как в Java/Python) Go предпочитает явную передачу ошибок как обычного значения. Это делает поток управления линейным: ошибки обрабатываются рядом с местом возникновения, а не через стек вызовов через catch.

Множественный возврат также используется для:

  • Возврата результата и флага (value, ok := m[key] — идиома проверки наличия ключа в мапе).
  • Возврата координат (x, y := position()).
  • Именованных возвращаемых параметров (см. ниже).

Именованные возвращаемые параметры

func Parse(s string) (n int, err error) {
// n и err — локальные переменные, инициализированы нулевыми значениями
if s == "" {
err = fmt.Errorf("empty string")
return // эквивалентно return n, err
}
n = len(s)
return // неявный возврат всех именованных параметров
}

Здесь (n int, err error) — объявление локальных переменных в области видимости функции. Они инициализируются нулями (0, nil) и могут быть использованы в теле. return без аргументов возвращает текущие значения этих переменных.

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

Анонимные функции и замыкания

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

adder := func(x int) func(int) int {
return func(y int) int {
return x + y
}
}
f := adder(10)
fmt.Println(f(5)) // 15

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

funcs := make([]func(), 3)
for i := 0; i < 3; i++ {
i := i // копия! иначе все замыкания увидят i = 3
funcs[i] = func() { fmt.Println(i) }
}

Без i := i все три функции напечатают 3, потому что захватится одна и та же переменная i, значение которой к моменту вызова функций будет равно 3. Это поведение идентично Java (effectively final capture), но в Python по умолчанию захватывается связь с именем, а не значением — разница в семантике требует внимания.


5. Составные типы

Go не имеет наследования, дженериков (до версии 1.18) и большинства объектно-ориентированных «украшений». Вместо этого — композиция через структуры и интерфейсы.

Структуры (struct)

type Person struct {
Name string
Age int
}
  • Объявление типа Person как структуры.
  • Поля идут в порядке объявления; порядок влияет на выравнивание памяти.
  • Нет модификаторов доступа (public/private). Видимость определяется регистром первого символа имени: Name — экспортируемое (public), age — неэкспортируемое (package-private). Это правило распространяется на всё: функции, переменные, типы.

Для Java-разработчика: нет class, нет конструкторов. Инициализация — через литералы или функции-фабрики:

p := Person{Name: "Timur", Age: 30} // именованный литерал
q := Person{"Anna", 25} // позиционный (небезопасен при изменении порядка)
r := NewPerson("Bob", 40)

Для Python-разработчика: похоже на dataclass или NamedTuple, но с компиляционной проверкой типов и без динамических атрибутов (p.Email = "x@y.z" — ошибка компиляции, если поля нет).

Встраивание (embedding) — замена наследованию

type Address struct {
City, Street string
}

type Employee struct {
Person // анонимное встраивание
Address // ещё одно
Salary int
}

Объект Employee «встраивает» Person и Address. Это означает:

  • Все поля Person и Address становятся прямо доступны в Employee: e.Name, e.City.
  • Можно вызывать методы, определённые на Person или Address, как будто они объявлены на Employee.
  • При конфликте имён (например, Person и Address оба имеют Name) компилятор требует явного уточнения: e.Person.Name.

Это — композиция с делегированием по умолчанию, а не наследование. Нет super, нет виртуальных вызовов. Поведение предсказуемо: метод вызывается на типе, на котором он объявлен, с получателем, приведённым к этому типу.

Слайсы (slice) — динамические массивы

var s []int              // nil-слайс
s = make([]int, 0, 10) // слайс длины 0, ёмкости 10
s = append(s, 1, 2, 3) // добавление

Слайс — описание окна в массиве:

  • указатель на начало данных,
  • длина (len),
  • ёмкость (cap — сколько элементов можно добавить до перераспределения).

Оператор append возвращает новый слайс (возможно, с тем же подлежащим массивом, возможно — с новым, если не хватило cap). Это требует переприсвоения:

s = append(s, x)

Если забыть s =, изменения потеряются. Это частая ошибка новичков.

Для Java-разработчика: []T в Go — это не T[], а ArrayList<T>, но без методов, только функции (len, append, copy). Нет null-слайсов в смысле NullPointerExceptionnil-слайс ведёт себя как пустой: len(nilSlice) == 0, append работает.

Для Python-разработчика: list, но с фиксированным типом элементов и явным управлением ростом. Нет list.append() как метода — только append(list, item), что подчёркивает: это операция над данными, а не поведение объекта.

Мапы (map) — ассоциативные массивы

m := make(map[string]int)
m["key"] = 42
value, ok := m["key"] // ok == false, если ключа нет
  • Мапа должна быть создана через make (или литерал map[string]int{"a":1}), иначе nil и запись вызовет панику.
  • Чтение несуществующего ключа возвращает нулевое значение типа значения (0, "", nil), но не ошибку. Для проверки используйте идиому value, ok := m[key].
  • Порядок итерации неопределён — даже в одном и том же запуске. Это намеренно: чтобы предотвратить завязку на порядок.

Нет методов get(), put(), containsKey() — только операторы [ ] и встроенная функция delete(m, key).


6. Интерфейсы

Интерфейс в Go — набор методов:

type Reader interface {
Read(p []byte) (n int, err error)
}

type Writer interface {
Write(p []byte) (n int, err error)
}

Ключевые принципы:

Неявная реализация

Тип удовлетворяет интерфейсу, если имеет все его методы. Никакого implements или class X implements Y не требуется.

type MyReader struct{ /* ... */ }

func (m MyReader) Read(p []byte) (int, error) {
// реализация
return 0, nil
}

var r io.Reader = MyReader{} // OK: MyReader имеет Read()

Для Java-разработчика: это похоже на «структуру, реализующую интерфейс», но без явного объявления. Преимущество — гибкость: вы можете реализовать интерфейс из стандартной библиотеки (io.Reader) в своём типе после того, как интерфейс был объявлен. Нет необходимости предвидеть иерархию.

Для Python-разработчика: это «утинная типизация», но на уровне компиляции. Если функция принимает io.Reader, компилятор проверит, что переданный аргумент имеет метод Read([]byte) (int, error). Ошибки выявляются до запуска.

Пустой интерфейс и any

var x interface{}   // до Go 1.18
var x any // синоним interface{} с 1.18

interface{} — интерфейс без методов. Ему удовлетворяет любой тип. Используется для:

  • Универсальных контейнеров ([]interface{}).
  • Сериализации (json.Marshal принимает any).
  • Reflection.

Но — избегается в идиоматическом коде. Передача any размывает контракт, заставляет использовать type assertion или reflection, что менее эффективно и менее безопасно. Предпочтительно — конкретные интерфейсы (io.Reader, fmt.Stringer), даже если они состоят из одного метода.

Проверка типов и type assertion

var r io.Reader = MyReader{}
if mr, ok := r.(MyReader); ok {
// mr — MyReader, можно использовать его специфичные методы
}

Это проверка: «является ли значение под r конкретно MyReader?». Если ok == false, mr — нулевое значение MyReader, а не nil.


7. Обработка ошибок

Go не использует исключения. Вместо этого:

  • Функции, которые могут завершиться неуспешно, возвращают error как последнее значение.
  • Ошибки — это значения: error — встроенный интерфейс с одним методом Error() string.
  • Программист обязан проверить ошибку: игнорирование err — нарушение стиля (и часто — баг).

Идиоматическая проверка:

f, err := os.Open("file.txt")
if err != nil {
return fmt.Errorf("failed to open file: %w", err)
}
defer f.Close()

Обратите внимание:

  • := объявляет f и err.
  • Проверка err != nil — сразу после вызова.
  • Используется fmt.Errorf с %w для оборачивания ошибки — это позволяет сохранять стек вызовов при логировании.

Для Java/Python-разработчика: это кажется многословным. Но преимущество — в явности: каждая точка, где может произойти сбой, отмечена в коде. Нет «невидимого» исключения, перескакивающего через несколько уровней. Это упрощает рассуждение о потоке управления, особенно в конкурентном коде.

Go 1.24+ экспериментально поддерживает try-подобную конструкцию через errors.Join и идиомы, но основной подход остаётся неизменным: ошибки — часть API, а не исключение из правил.


8. Управление потоком выполнения

Go использует традиционные управляющие конструкции (if, for, switch), но с важными синтаксическими и семантическими отличиями.

if

if n, err := strconv.Atoi(s); err == nil {
fmt.Printf("Parsed: %d\n", n)
} else {
log.Printf("Invalid number: %v", err)
}

Особенности:

  • Скобки вокруг условия не требуются. Это не опция форматирования — синтаксис запрещает if (x > 0).
  • Перед ; допускается инициализирующее выражение — объявление переменных, видимых только внутри if и else. Это устраняет необходимость внешнего объявления и загрязнения области видимости.
  • Неявные приведения к bool запрещены. Условие должно иметь тип bool. Нельзя написать if ptr { … }, если ptr — указатель; нужно if ptr != nil. Компилятор не пытается интерпретировать «истинность» — он требует явного сравнения.

Для C/Java-разработчика: отсутствие скобок снижает визуальный шум, но требует точного соблюдения структуры — фигурная скобка { обязательно на той же строке. Компилятор вставляет точку с запятой после ) автоматически, и перенос { на новую строку приведёт к синтаксической ошибке («unexpected newline»).

Для Python-разработчика: это похоже на if (n := int(s)) > 0:, но с ключевым различием — err == nil явно проверяется, а не подразумевается из «ложности» err. В Go нет понятия «ложного значения»: 0, "", nil, false — это просто значения, и их «истинность» не определена.

for

Go имеет только одну циклическую конструкцию — for. Она покрывает все случаи:

  • Традиционный счётчик:

    for i := 0; i < 10; i++ {}
  • Бесконечный цикл (аналог while true):

    for {}  // эквивалентно for true { … }
  • Условный цикл (аналог while):

    for condition {}
  • Итерация по составным типам (range):

    for i, v := range slice {}      // i — индекс, v — копия элемента
    for k, v := range map {} // k — ключ, v — копия значения
    for v := range channel {} // v — значение, полученное из канала

Особенности range:

  • Возвращает копии значений, а не ссылки. Изменение v внутри цикла не влияет на исходный срез/мапу.
  • Для мапы порядок итерации неопределён (намеренно).
  • Можно игнорировать один из возвращаемых параметров через _:
    for _, v := range slice {}  // индекс не нужен
    for k := range set {} // значение не нужно (идиома множества)

Отсутствие while и do-whileограничение в пользу единообразия. Разработчик не тратит время на выбор между тремя синтаксическими формами; читатель не гадает, что именно имелось в виду.

switch

switch os := runtime.GOOS; os {
case "darwin":
fmt.Println("macOS")
case "linux":
fmt.Println("Linux")
default:
fmt.Printf("Unknown: %s", os)
}

Особенности:

  • Как и if, поддерживает инициализирующее выражение перед ;.
  • Нет проваливания (fallthrough) по умолчанию. Каждый case завершается неявным break. Это исключает классические ошибки из C/Java, когда забытый break приводит к выполнению следующего блока.
  • При необходимости провалиться — явно указывается fallthrough.
  • Условие не обязано быть константой:
    switch {
    case x < 0:
    case x == 0:
    case x > 0:
    }
    Это «switch без выражения» — эквивалент цепочки if-else if. Компилятор проверяет исчерпаемость только для перечислений (через const + iota), но не требует default — его отсутствие — не ошибка.

select

select — специализированная конструкция для работы с каналами (chan). Синтаксически похож на switch, но семантически уникален:

select {
case msg := <-ch:
fmt.Println("Received:", msg)
case ch <- "ping":
fmt.Println("Sent ping")
case <-time.After(1 * time.Second):
fmt.Println("Timeout")
}

Правила:

  • Каждый case содержит одну операцию с каналом: отправку (ch <- v) или приём (v := <-ch).
  • select блокирует выполнение до тех пор, пока одна из операций не станет возможна.
  • Если подходят несколько — выбирается случайно (для предотвращения голодания).
  • default делает select неблокирующим: если ни один канал не готов — выполняется default.

select — единственный способ одновременно ожидать нескольких асинхронных событий без блокировки. Это — синтаксическая реализация мультиплексирования событий на уровне языка.


9. Конкурентность

Go вводит конкурентность через встроенные синтаксические конструкции и правила выполнения.

Горутины

go func() {
result := heavyComputation()
ch <- result
}()
  • Префикс go перед вызовом функции порождает горутину — легковесный поток выполнения, управляемый шедулером Go (не ОС).
  • Горутины изолированы стеком (начальный размер ~2 КБ, растёт по мере необходимости), но разделяют кучу.
  • Нет синтаксиса «ожидания завершения» — это достигается через каналы или sync.WaitGroup.

goоператор языка, как if или return. Его наличие сразу сигнализирует: здесь начинается параллельное выполнение.

Каналы

ch := make(chan int, 10)   // буферизованный канал ёмкости 10
ch := make(chan int) // небуферизованный (синхронный)

ch <- 42 // отправка — блокирует, если нет получателя (для небуферизованного)
x := <-ch // приём — блокирует, если нет отправителя
  • Канал — тип первого класса: может быть параметром, возвращаемым значением, полем структуры.
  • Направление канала можно уточнить в сигнатуре:
    func producer(out chan<- int)   // только отправка
    func consumer(in <-chan int) // только приём
    Это не проверка в runtime — компилятор запретит попытку прочитать из chan<-.
  • Закрытие канала: close(ch). После этого приём возвращает нулевое значение и false в идиоме v, ok := <-ch, что используется для сигнализации завершения.

Для Java-разработчика: каналы заменяют BlockingQueue, CountDownLatch, CompletableFuture — но без callback-ада. Потоки синхронизируются через сообщения. Это реализует принцип: Do not communicate by sharing memory; instead, share memory by communicating.

Для Python-разработчика: похоже на asyncio.Queue, но без async/await. Горутины — реальные параллельные потоки (на уровне M:N шедулера), и select работает без цикла событий.

select

Как уже упоминалось, select — единственный способ эффективно работать с несколькими каналами. Пример идиоматического шаблона завершения:

done := make(chan struct{})
go func() {
defer close(done)
// ... работа
}()

select {
case <-done:
fmt.Println("Work completed")
case <-time.After(5 * time.Second):
fmt.Println("Timed out")
}

struct{} здесь — «пустая структура», chan struct{} — канал, передающий события, а не данные (размер сообщения — 0 байт). Это стандартная идиома для сигнализации.


10. Инструментарий

Синтаксические соглашения Go не остаются на уровне рекомендаций. Они жёстко поддерживаются инструментами, встроенными в go CLI:

ИнструментФункцияСвязь с синтаксисом
gofmt / goimportsАвтоматическое форматирование и управление импортамиОпределяет единственно верный стиль отступов, скобок, пробелов.
go vetСтатический анализ: выявляет подозрительные конструкции (fmt.Print без \n, неиспользуемые defer, небезопасные преобразования)Проверяет семантическую корректность синтаксических паттернов.
staticcheck / golangci-lintРасширенный линтер: обнаруживает data races, неэффективные аллокации, избыточные приведенияДополняет vet, фокусируясь на идиоматичности.
go doc / godocГенерация документации из комментариевТребует комментариев над объявлением, начинающихся с имени (// Func does X), — синтаксис документации строго регламентирован.

Пример: go vet выдаст предупреждение на:

if err := doSomething(); err != nil {
log.Println("Error:", err)
return err
}
fmt.Println("Success") // <- unreachable code после return

— и это будет ошибка компиляции в CI, если настроен go vet. Таким образом, синтаксис не просто «красив» — он проверяется, и нарушение стиля может остановить сборку.