Топ ошибок начинающих Go разработчиков: продвинутый уровень

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

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

Начать курс

Введение

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

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

Ошибка 1: Утечки горутин (Goroutine Leaks)

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

Проблема: Забытый канал без получателя

func process(data []int) chan int {
    out := make(chan int)
    go func() {
        for _, v := range data {
            out <- v * 2
        }
        close(out)
    }()
    return out
}

func main() {
    ch := process([]int{1, 2, 3})
    // Читаем только одно значение, канал закрыт не будет
    fmt.Println(<-ch)
    // Горутина зависнет, пытаясь отправить 4 и 6 в канал
    // time.Sleep(time.Second) — утечка памяти
}

Решение: Всегда контролируйте время жизни горутины. Используйте context.Context для отмены или буферизированные каналы, если вы не уверены в количестве читателей.

func process(ctx context.Context, data []int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for _, v := range data {
            select {
            case out <- v * 2:
            case <-ctx.Done():
                return
            }
        }
    }()
    return out
}

Ошибка 2: Неправильное использование select с default

Конструкция select с default — это мощный инструмент для неблокирующих операций. Но новички часто используют её как «костыль», что приводит к busy-loop (бесконечному циклу с высокой загрузкой CPU).

Проблема: Бесконечный цикл без блокировки

for {
    select {
    case msg := <-ch:
        fmt.Println(msg)
    default:
        // Ничего не делаем, но цикл не блокируется
    }
}

Такой код будет молотить процессор, потребляя 100% одного ядра. default делает select неблокирующим, и цикл превращается в бесконечный опрос.

Решение: Либо уберите default (тогда select будет ждать), либо добавьте time.Sleep или time.Tick для снижения нагрузки.

// Правильный вариант: без default
for msg := range ch {
    fmt.Println(msg)
}

// Или с таймаутом
for {
    select {
    case msg := <-ch:
        fmt.Println(msg)
    case <-time.After(100 * time.Millisecond):
        // Периодическая проверка
    }
}

Ошибка 3: nil интерфейс — это не nil

Это классическая ловушка Go. Переменная типа interface{} равна nil только в том случае, если и её тип, и значение равны nil. Если вы присвоите интерфейсу указатель на nil, интерфейс перестанет быть nil.

Проблема: Проверка на nil через интерфейс

type MyError struct{}

func (e *MyError) Error() string {
    return "ошибка"
}

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

func main() {
    err := getError()
    if err != nil {
        fmt.Println("Ошибка есть!") // Выведется! Хотя e == nil
    }
}

Решение: Всегда возвращайте из функций явный nil, а не переменную конкретного типа-указателя.

func getError() error {
    if someCondition {
        return &MyError{}
    }
    return nil // Явный nil
}

Ошибка 4: Копирование sync.Mutex или sync.WaitGroup

Мьютексы, WaitGroup и другие примитивы синхронизации из пакета sync не должны копироваться. После копирования они теряют свою внутреннюю блокировку, что приводит к data race.

Проблема: Передача структуры по значению

type Counter struct {
    mu  sync.Mutex
    val int
}

func (c Counter) Increment() { // Ошибка! Копирование по значению
    c.mu.Lock()
    c.val++
    c.mu.Unlock()
}

Метод Increment получает копию Counter, а значит — копию мьютекса. Гонки данных гарантированы.

Решение: Используйте указатели для методов и структур, содержащих sync-примитивы.

func (c *Counter) Increment() { // Правильно: указатель
    c.mu.Lock()
    c.val++
    c.mu.Unlock()
}

Ошибка 5: Закрытие канала несколько раз или запись в закрытый канал

Каналы в Go — это не просто очереди. Они имеют строгие правила: закрыть канал может только отправитель, а запись в закрытый канал вызывает панику. Новички часто пытаются закрыть канал из нескольких горутин или пишут в него после close().

Проблема: Двойное закрытие

ch := make(chan int)
close(ch)
close(ch) // panic: close of closed channel

Решение: Используйте sync.Once для гарантии однократного закрытия, или делегируйте закрытие одной «ответственной» горутине.

var once sync                

Блоги

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