Go-грабли новичков: 5 ошибок, которые превращают код в ад

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему Go кажется простым, но ломает новичков?

Go (или Golang) — это язык, который создавался с одной целью: чтобы программисты меньше страдали. Минималистичный синтаксис, встроенная конкурентность, быстрая компиляция — кажется, что выучить его можно за неделю. И да, выучить можно. Но «написать хороший код» и «написать код на Go» — это две разные истории.

Новички часто приносят привычки из Python, JavaScript или Java и пытаются натянуть их на Go. Результат — баги, которые трудно отловить, паника в рантайме и ощущение, что «Go какой-то сломанный». Спойлер: Go не сломан. Просто он думает иначе.

В этой статье мы разберём 5 самых частых ошибок при изучении Go. Если вы только начинаете — прочитайте внимательно. Если вы уже пишете на Go — проверьте, не делаете ли вы их до сих пор.

Ошибка №1: Игнорирование ошибок (и привычка из Python)

В Python или JavaScript вы привыкли, что функция может вернуть результат или выбросить исключение. Вы оборачиваете вызов в try/catch и живёте спокойно. В Go всё иначе: здесь нет исключений. Есть только возвращаемые значения, и одно из них — ошибка.

Самая популярная ошибка новичков — написать result, _ := someFunc() и забыть про ошибку. Или, что ещё хуже, просто не проверить её:

// ПЛОХО: ошибка игнорируется
file, _ := os.Open("config.yaml")
// Если файла нет — file == nil, и следующая строка упадёт с паникой
defer file.Close()

В Go ошибка — это не что-то страшное. Это просто значение. Его нужно проверять. Всегда. Без исключений.

Как правильно?

// ХОРОШО: проверяем ошибку сразу
file, err := os.Open("config.yaml")
if err != nil {
    log.Fatalf("не удалось открыть файл: %v", err)
}
defer file.Close()

Это не паранойя. Это дисциплина. Go учит вас быть аккуратным. Если вы проигнорируете ошибку в production — готовьтесь к тому, что в 3 часа ночи упадёт сервис, а в логах будет тишина.

Совет: никогда не используйте _ для ошибки, если только вы не уверены на 100%, что она никогда не возникнет. А таких ситуаций в реальном коде почти нет.

Ошибка №2: Паника там, где нужна ошибка

В Go есть механизм panic и recover. Новички часто путают их с исключениями и начинают кидать панику при любой проблеме. Это ошибка.

panic — это аварийная остановка программы. Она предназначена для ситуаций, когда продолжение работы невозможно: например, разыменование нулевого указателя или выход за границы массива. Использовать panic для обработки бизнес-логики — моветон.

// ПЛОХО: паника вместо ошибки
func divide(a, b int) int {
    if b == 0 {
        panic("деление на ноль!")
    }
    return a / b
}

Правильный подход — вернуть ошибку:

// ХОРОШО: возвращаем ошибку
func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, fmt.Errorf("деление на ноль")
    }
    return a / b, nil
}

Используйте panic только для действительно критических ситуаций. Всё остальное — это ошибки. Go даёт вам error не просто так.

Ошибка №3: Неправильная работа с горутинами и отсутствие синхронизации

Горутины — это, пожалуй, самая крутая фишка Go. Но она же и самая опасная для новичков. Типичная картина: запускаете 1000 горутин, пишете в общую переменную — и получаете гонку данных (data race).

// ПЛОХО: гонка данных
var counter int
for i := 0; i < 1000; i++ {
    go func() {
        counter++ // небезопасно!
    }()
}
time.Sleep(time.Second)
fmt.Println(counter) // результат непредсказуем

Почему это проблема? Потому что counter++ — это не атомарная операция. Она состоит из чтения, увеличения и записи. Две горутины могут прочитать одно и то же значение, увеличить его и записать — и вы потеряете один инкремент.

Как исправить?

Вариантов несколько, но самый простой — использовать мьютекс:

// ХОРОШО: используем sync.Mutex
var (
    counter int
    mu      sync.Mutex
)
for i := 0; i < 1000; i++ {
    go func() {
        mu.Lock()
        counter++
        mu.Unlock()
    }()
}
// Ждём завершения всех горутин (лучше использовать sync.WaitGroup)

Ещё лучше — использовать каналы. Каналы — это идиоматичный способ общения горутин в Go. Если вы пишете код с горутинами, но не используете каналы — скорее всего, вы что-то делаете не так.

// ЕЩЁ ЛУЧШЕ: используем каналы
func main() {
    ch := make(chan int)
    go func() {
        ch <- 42
    }()
    result := <-ch
    fmt.Println(result)
}

Важно: никогда не используйте time.Sleep для синхронизации. Это костыль. Используйте sync.WaitGroup или каналы.

Ошибка №4: Путаница с nil — почему ваша программа падает

В Go nil — это не просто «пусто». Это нулевое значение для указателей, срезов, мап, каналов и интерфейсов. И с ним связана куча подводных камней.

Самая частая проблема — попытка записать данные в nil-мапу:

// ПЛОХО: паника!
var m map[string]int
m["key"] = 42 // panic: assignment to entry in nil map

Мапы в Go нужно инициализировать перед использованием. Или через make, или через литерал:

// ХОРОШО
m := make(map[string]int)
m["key"] = 42

// ИЛИ
m := map[string]int{}
m["key"] = 42

То же самое касается срезов. С nil-срезом можно работать (читать, вызывать range), но нельзя записывать по индексу, если он не выделен через make или append.

Ещё одна ловушка — nil-интерфейс. Интерфейс в Go — это пара (тип, значение). Если вы присвоите в интерфейс указатель, который равен nil, то сам интерфейс НЕ будет равен nil:

var p *int = nil
var i interface{} = p
fmt.Println(i == nil) // false!

Это одна из самых коварных ошибок.

Блоги

Рекомендаций по книгам