Топ ошибок начинающих Go разработчиков: от А до Я

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

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

Начать курс

Введение: Почему Go — это просто, но нелегко

Язык Go (Golang) часто хвалят за его простоту и лаконичность. Однако именно эта простота нередко подводит новичков, которые приходят из более «словесных» языков вроде Python или Java. В этой статье мы собрали топ ошибок начинающих Go разработчиков — от типичных проблем с обработкой ошибок до неочевидных подводных камней при работе с горутинами. Если вы только начинаете свой путь в мире Golang, эта подборка поможет вам не наступить на те же грабли, что и многие до вас.

1. Игнорирование ошибок и неправильная обработка

Самая распространенная ошибка в Go — это игнорирование возвращаемых ошибок. В отличие от исключений (exceptions) в Python или Java, Go использует явную проверку ошибок через возвращаемое значение. Начинающие часто пишут:

package main

import (
    "fmt"
    "os"
)

func main() {
    // ❌ ОШИБКА: игнорируем ошибку
    file, _ := os.Open("file.txt")
    fmt.Println(file.Name())
}

Здесь символ _ (пустой идентификатор) буквально говорит компилятору: «Мне все равно на ошибку». В реальном проекте это приведет к панике или некорректной работе. Правильный подход — всегда проверять ошибку:

package main

import (
    "fmt"
    "os"
)

func main() {
    file, err := os.Open("file.txt")
    if err != nil {
        // ✅ Обрабатываем ошибку
        fmt.Println("Ошибка открытия файла:", err)
        return
    }
    defer file.Close()
    fmt.Println("Файл успешно открыт:", file.Name())
}

Совет: Никогда не используйте _ для ошибок, если вы не уверены на 100%, что ошибка невозможна. Лучше написать лишнюю строчку if err != nil, чем искать баг ночью перед дедлайном.

2. Проблемы с горутинами: гонка данных и утечки

Go славится своей простотой в работе с конкурентностью, но это же и создает ловушки. Новички часто забывают про синхронизацию доступа к общим данным.

2.1. Гонка данных (Data Race)

Представьте, что несколько горутин одновременно пишут в одну переменную:

package main

import (
    "fmt"
    "sync"
)

func main() {
    counter := 0
    var wg sync.WaitGroup

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            // ❌ ОШИБКА: гонка данных
            counter++
            wg.Done()
        }()
    }
    wg.Wait()
    fmt.Println("Counter:", counter) // Результат непредсказуем
}

Результат будет разным при каждом запуске. Решение: использовать мьютекс или атомарные операции:

package main

import (
    "fmt"
    "sync"
    "sync/atomic"
)

func main() {
    var counter int64
    var wg sync.WaitGroup

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            // ✅ Используем атомарное сложение
            atomic.AddInt64(&counter, 1)
            wg.Done()
        }()
    }
    wg.Wait()
    fmt.Println("Counter:", counter) // Всегда 1000
}

2.2. Утечка горутин

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

package main

import (
    "fmt"
    "time"
)

func worker(ch chan int) {
    for msg := range ch {
        fmt.Println(msg)
    }
}

func main() {
    ch := make(chan int)
    go worker(ch) // ❌ ОШИБКА: горутина никогда не завершится
    ch <- 1
    ch <- 2
    // Канал не закрыт -> утечка
    time.Sleep(time.Second)
}

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

3. Неправильное использование nil срезов и карт

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

3.1. Запись в nil карту

package main

func main() {
    var m map[string]int // m == nil
    // ❌ ОШИБКА: panic: assignment to entry in nil map
    m["key"] = 42
}

Карту нужно инициализировать через make:

package main

import "fmt"

func main() {
    m := make(map[string]int) // ✅ Инициализирована
    m["key"] = 42
    fmt.Println(m["key"]) // 42
}

3.2. Сравнение nil срезов

Пустой срез ([]int{}) и nil срез — это разные вещи, хотя len() для обоих вернет 0. Это может вызвать путаницу при сериализации в JSON:

package main

import (
    "encoding/json"
    "fmt"
)

func main() {
    var s1 []int // nil
    s2 := []int{} // пустой, но не nil

    json1, _ := json.Marshal(s1)
    json2, _ := json.Marshal(s2)

    fmt.Println(string(json1)) // null
    fmt.Println(string(json2)) // []
}

Совет: если вы ожидаете, что клиент получит [], а не null, всегда инициализируйте срез через make или литерал.

4. Путаница с интерфейсами: nil != nil

Одна из самых неочевидных ошибок в Go связана с тем, что интерфейс равен nil только если и его тип, и его значение равны nil.

package main

import "fmt"

type MyError struct{}

func (e *MyError) Error() string {
    return "my error"
}

func getError() error {
    var e *MyError = nil
    return e // Возвращаем nil указатель, но тип не nil
}

func main() {
    err := getError()
    // ❌ ОШИБКА: err != nil, хотя внутри nil
    if err !=                

Блоги

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