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

Почему привычные решения ломаются именно в Go
Функции в Go выглядят знакомо для тех, кто приходит из Python, JavaScript или Java. Именно поэтому первые грабли — самые болезненные: синтаксис не сопротивляется, компилятор не ругается, а баг проявляется в проде под нагрузкой. Ниже — не учебник по функциям, а разбор конкретных ошибок, которые встречаются в реальных code review. Каждая проиллюстрирована кодом на Go 1.22, с объяснением, чем именно грозит и как выглядит исправление.
Ошибка 1. Проглоченная ошибка при множественном возврате
Go возвращает ошибку вторым значением — и ничто не мешает его проигнорировать. Компилятор промолчит, если вы присвоите результат в _ или вовсе не примете второе значение через короткое объявление.
Как выглядит проблема
// Go 1.22
package main
import (
"fmt"
"strconv"
)
func main() {
// Ошибка проглочена: если input невалиден, val == 0 и программа продолжит работу
val, _ := strconv.Atoi("не-число")
fmt.Println("Результат:", val) // Результат: 0 — молча, без паники
}
В маленьком скрипте это терпимо. В обработчике HTTP-запроса ноль вместо реального значения приводит к записи мусора в базу.
Как исправить
val, err := strconv.Atoi(input)
if err != nil {
return fmt.Errorf("невалидный ввод %q: %w", input, err)
}
Правило простое: _ для ошибки допустим только тогда, когда вы осознанно решили, что провал этой операции не влияет на результат, и оставили комментарий, почему.
Линтер errcheck ловит проглоченные ошибки автоматически. Включите его в CI — это дешевле, чем искать баг в логах прода.
Ошибка 2. Замыкание захватывает переменную цикла по ссылке
Начиная с Go 1.22 поведение переменной цикла for изменилось: каждая итерация получает свою копию. Но если вы работаете с Go 1.21 и ниже или читаете старый код, классическая ловушка по-прежнему актуальна — и её важно узнавать.
Проблемный код (Go 1.21 и ниже)
package main
import (
"fmt"
"sync"
)
func main() {
names := []string{"Алиса", "Боб", "Ева"}
var wg sync.WaitGroup
for _, name := range names {
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println(name) // все горутины печатают "Ева"
}()
}
wg.Wait()
}
Все горутины ссылаются на одну и ту же переменную name, которая к моменту выполнения уже содержит последнее значение.
Два способа исправить
// Способ 1: передать аргументом (работает на любой версии Go)
go func(n string) {
defer wg.Done()
fmt.Println(n)
}(name)
// Способ 2: локальная копия внутри цикла
for _, name := range names {
name := name // затенение переменной — новая копия на каждую итерацию
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println(name)
}()
}
В Go 1.22+ переменная цикла и так копируется на каждой итерации, так что проблема уходит сама. Но если в проекте минимальная версия ниже — исправление обязательно.
Ошибка 3. Defer в цикле не освобождает ресурсы вовремя
defer выполняется при выходе из функции, а не из блока for. Если вы открываете файлы или соединения в цикле и ставите defer Close(), ресурсы копятся до конца функции.
Проблемный код
func processFiles(paths []string) error {
for _, p := range paths {
f, err := os.Open(p)
if err != nil {
return err
}
defer f.Close() // закроется только когда processFiles вернёт управление
// ... чтение f ...
}
return nil
}
При тысяче файлов функция удержит тысячу открытых дескрипторов. На Linux лимит по умолчанию — 1024, и вы получите too many open files.
Исправление: вынести тело цикла в отдельную функцию
func processFiles(paths []string) error {
for _, p := range paths {
if err := processOne(p); err != nil {
return err
}
}
return nil
}
func processOne(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // теперь закроется при выходе из processOne — сразу после обработки
// ... чтение f ...
return nil
}
Ошибка 4. Именованные возвращаемые значения и «голый» return
Именованные возвращаемые значения удобны для документирования, но return без аргументов (naked return) делает код хрупким: при рефакторинге легко забыть, какое значение уйдёт наружу.
// Трудно читать: что именно вернётся при ошибке?
func divide(a, b float64) (result float64, err error) {
if b == 0 {
err = fmt.Errorf("деление на ноль")
return // result == 0.0 — неявно
}
result = a / b
return // возвращает result и nil — но это видно только если прочитать всю функцию
}
Рекомендация из Go Code Review Comments: используйте именованные значения для godoc, но возвращайте явно.
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("деление на ноль")
}
return a / b, nil
}
Ошибка 5. Nil-функция и интерфейсное nil-значение
Функция в Go — значение первого класса, и оно может быть nil. Если вы передаёте callback и забываете проверить — паника в рантайме.
type Handler func(event string)
func process(events []string, handler Handler) {
for _, e := range events {
handler(e) // panic: если handler == nil
}
}
Проверка тривиальна, но о ней забывают, потому что компилятор не требует:
if handler != nil {
handler(e)
}
Отдельная ловушка — интерфейсное nil. Интерфейс в Go хранит пару (тип, значение). Если вы возвращаете типизированный nil-указатель как error, проверка err != nil даст true, потому что тип внутри интерфейса заполнен.
type MyError struct{ Msg string }
func (e *MyError) Error() string { return e.Msg }
func doWork() error {
var err *MyError // nil-указатель
// ... логика, err остаётся nil ...
return err // интерфейс error{*MyError, nil} — НЕ равен nil!
}
Решение: возвращайте nil напрямую, а не через типизированную переменную.
func doWork() error {
// ... логика ...
return nil // именно так, а не через переменную конкретного типа
}
Сводная таблица
| Ошибка | Последствие | Где ловить |
|---|---|---|
Проглоченная ошибка (_) | Тихая запись мусора в данные | Линтер errcheck |
| Замыкание + переменная цикла (до Go 1.22) | Все горутины работают с одним значением | Линтер loopclosure / go vet |
| Defer в цикле | Утечка дескрипторов, too many open files | Code review, линтер gocritic |
| Naked return | Хрупкий код, баги при рефакторинге | Линтер nakedret |
| Nil-функция / интерфейсное nil | Паника в рантайме или ложное срабатывание проверки | Линтер nilnil, тесты |
Термин: naked return — оператор return без явного перечисления возвращаемых значений. Допустим в Go только при именованных возвращаемых параметрах; возвращает их текущие значения.
Когда что выбирать: линтеры или дисциплина
Линтеры закрывают ошибки 1, 2 и 4 почти полностью. Проблемы с defer в цикле и интерфейсным nil ловятся хуже — здесь помогает привычка выносить тело цикла в отдельную функцию и никогда не возвращать типизированный nil через интерфейс.
Практический минимум для CI — golangci-lint со включёнными errcheck, govet, gocritic, nakedret. Настройка занимает пять минут и закрывает большинство описанных случаев до того, как код попадёт в основную ветку.
Что делать дальше
Настройте golangci-lint в CI, если ещё нет, — это первый и самый дешёвый шаг. Дальше стоит пройтись по существующему коду с включённым errcheck: количество проглоченных ошибок в среднем проекте удивляет. Отдельная тема — обработка ошибок через errors.Is и errors.As вместо сравнения строк: она заслуживает отдельного разбора и напрямую вытекает из ошибки номер один.
Проектная работа: От проблемы до защиты: продукт, этапы, вопросы комиссии
Спросить за 15 ₽Источники: Go Code Review Comments — именованные результаты и naked return, Go 1.22 Release Notes — изменение семантики переменной цикла
Частые вопросы
В Go 1.22 проблема с замыканиями в циклах действительно исправлена?
Если у меня другая версия Go, какие из этих ошибок неактуальны?
Как golangci-lint отличает осознанный пропуск ошибки от случайного?
Naked return запрещён стандартом или это только рекомендация?
Что будет в проде, если не чинить defer в цикле?
Как отловить ошибку с интерфейсным nil в тестах?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.