Введение
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