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

Механика языка и гонки данных

Разработчику

См. также: Асинхронность и горутины · Синтаксические конструкции · Обработка ошибок.

Порядок инициализации

При запуске программы Go выполняет:

  1. Инициализацию пакетов в порядке импорта (зависимости раньше зависимых).
  2. В каждом пакете — присваивания переменным уровня пакета, затем все функции init() (их может быть несколько в одном файле).
  3. Функцию main в пакете main.
func init() {
log.SetFlags(log.LstdFlags | log.Lshortfile)
}

init нельзя вызвать вручную. Используют для регистрации драйверов БД (import _ "github.com/lib/pq"), глобальных настроек, проверок окружения. Избегайте тяжёлой логики и сетевых вызовов в init — усложняет тесты и скрывает зависимости.


Пустой идентификатор _

Игнорирует значение:

_ = riskyCall() // явно «результат не нужен»
for _, item := range list { }
f, _ := os.Open("x") // ошибку намеренно не обрабатывают — только если обосновано

В импорте _ означает импорт только ради побочного эффекта (вызов init пакета):

import _ "net/http/pprof"
import _ "image/png"

Имя пакета при этом не попадает в область видимости — нельзя писать pprof.StartCPUProfile без обычного импорта.


Направленные каналы

Тип канала может ограничивать операции:

func producer(out chan<- int) { out <- 42 }
func consumer(in <-chan int) { v := <-in }

chan<- T — только отправка; <-chan T — только приём. chan T — оба направления. Это контракт API: функция не сможет случайно читать из канала, в который только пишет.

Закрытие — только на стороне отправителя; получатель проверяет:

v, ok := <-ch
if !ok {
// канал закрыт
}

Подробнее о паттернах — асинхронность.


Типичные гонки данных

Гонка — два потока обращаются к одной памяти, хотя бы один пишет, без синхронизации.

Замыкание в цикле

for i := 0; i < 3; i++ {
go func() {
fmt.Println(i) // часто три раза «3»
}()
}

Исправление — передать копию:

for i := 0; i < 3; i++ {
i := i
go func() { fmt.Println(i) }()
}

Общая map без mutex

Карты не потокобезопасны. Варианты: sync.Mutex + map, sync.Map для read-heavy кэшей, или одна горутина-владелец map и канал команд.

Срез из нескольких горутин

append может гонять за внутренний массив. Либо mutex, либо каждая горутина пишет в свой кусок, либо агрегация через канал.


sync-примитивы (кратко)

ТипНазначение
sync.Mutex / RWMutexЗащита общих структур
sync.WaitGroupДождаться N горутин
sync.OnceОднократная инициализация
atomicСчётчики, флаги без mutex

Предпочитайте передачу данных каналами, когда владение однозначно; mutex — когда общее состояние сложное.


Детектор гонок

Включение при тестах и локальном запуске:

go test -race ./...
go run -race .

Runtime отслеживает небезопасный доступ и печатает стек. Замедляет выполнение — в production не включают, в CI — да.

Статический go vet дополняет (например, копирование lock в структуре). Вместе с -race это базовый минимум для конкурентного кода.


Чек-лист перед code review

  • Есть ли go внутри for с переменными цикла?
  • Разделяют ли горутины map/slice без синхронизации?
  • Закрывает ли канал только отправитель?
  • Запускали ли go test -race для затронутых пакетов?

См. чек-лист самопроверки.


См. также

Другие статьи этого же раздела в боковом меню (как на странице «О разделе»).