Введение: Почему Go кажется простым, но таит ловушки
Go (Golang) — это компилируемый, статически типизированный язык, созданный в недрах Google. Его часто хвалят за простоту синтаксиса, встроенную поддержку конкурентности и высокую производительность. Однако именно эта кажущаяся простота часто играет злую шутку с новичками, приходящими из динамических языков (Python, JavaScript) или даже из C++.
Многие разработчики, начав писать на Go, совершают одни и те же типичные ошибки. В этой статье мы разберем топ-5 самых частых проблем, с которыми сталкиваются начинающие Go-разработчики, и покажем, как их исправить с помощью наглядных примеров кода.
1. Игнорирование возвращаемых ошибок (Error Handling)
Проблема
В Go нет исключений (try-catch). Ошибка — это просто значение, которое функция возвращает как последний аргумент. Самая распространенная ошибка новичка — присвоить результат переменной, но проигнорировать ошибку с помощью подчеркивания _.
// ПЛОХО: ошибка проглочена
result, _ := doSomething()
fmt.Println(result)
Почему это плохо?
Программа продолжит работу с некорректными данными, что может привести к панике, утечке памяти или логическим багам, которые сложно отловить.
Как правильно
Всегда проверяйте ошибку. Используйте идиоматический паттерн:
// ХОРОШО: ошибка обработана
result, err := doSomething()
if err != nil {
log.Printf("Ошибка при выполнении doSomething: %v", err)
return // или panic, или другая логика
}
fmt.Println(result)
Совет: Пишите свой код так, чтобы if err != nil стало вашим рефлексом. Не бойтесь раннего возврата (early return) — это делает код чище.
2. Путаница с nil и пустыми срезами/картами
Проблема
В Go nil — это не "пустой объект". Это нулевое значение для указателей, срезов, карт, каналов и интерфейсов. Новички часто пытаются записать данные в nil карту или срез, получая панику.
// ПЛОХО: паника при записи в nil map
var m map[string]int
m["key"] = 42 // panic: assignment to entry in nil map
Как правильно
Всегда инициализируйте карты и срезы перед использованием:
// ХОРОШО: инициализация через make
m := make(map[string]int)
m["key"] = 42
// Или через литерал
m2 := map[string]int{}
m2["key"] = 42
Со срезами ситуация хитрее: в nil срез можно читать (он вернет пустой срез), но нельзя записывать по индексу. Используйте append:
var s []int
s = append(s, 1) // OK, создаст новый срез
// s[0] = 1 // panic: runtime error: index out of range [0] with length 0
3. Неправильное использование горутин и гонка данных (Data Race)
Проблема
Горутины — это легковесные потоки. Новички часто запускают горутины, забывая синхронизировать доступ к общим данным. Это приводит к гонке данных (data race) — неопределенному поведению программы.
// ПЛОХО: гонка данных
var counter int
for i := 0; i < 1000; i++ {
go func() {
counter++ // небезопасно!
}()
}
time.Sleep(time.Second)
fmt.Println(counter) // результат непредсказуем
Как правильно
Используйте мьютексы (sync.Mutex) или каналы для синхронизации:
// ХОРОШО: использование мьютекса
var (
counter int
mu sync.Mutex
)
for i := 0; i < 1000; i++ {
go func() {
mu.Lock()
counter++
mu.Unlock()
}()
}
// Ждем завершения (лучше использовать sync.WaitGroup)
time.Sleep(time.Second)
fmt.Println(counter) // 1000
Важно: Всегда проверяйте код на гонки с помощью флага -race: go run -race main.go.
4. Ошибки с замыканиями и переменными цикла
Проблема
Классическая ловушка для новичков. В Go переменная цикла i переиспользуется на каждой итерации. Если вы запускаете горутину внутри цикла, она захватывает ссылку на одну и ту же переменную.
// ПЛОХО: все горутины увидят последнее значение i
for i := 1; i <= 3; i++ {
go func() {
fmt.Println(i) // напечатает 3,3,3 (или 4,4,4)
}()
}
time.Sleep(time.Second)
Как правильно
Создавайте локальную копию переменной на каждой итерации (до Go 1.22) или используйте новое поведение цикла (Go 1.22+):
// ХОРОШО (Go < 1.22): копирование переменной
for i := 1; i <= 3; i++ {
i := i // создаем новую переменную в области видимости горутины
go func() {
fmt.Println(i)
}()
}
// ХОРОШО (Go 1.22+): переменная цикла теперь уникальна на каждой итерации
for i := 1; i <= 3; i++ {
go func() {
fmt.Println(i) // 1,2,3 (порядок не гарантирован)
}()
}
5. Злоупотребление интерфейсами и путаница с пустым интерфейсом
Проблема
Пустой интерфейс interface{} (или any в Go 1.18+) — это мощный, но опасный инструмент. Новички часто используют его как "универсальный тип", чтобы избежать дженериков (до Go