Golang Top 15 ошибок
- воскресенье, 27 апреля 2025 г. в 00:00:08
Go – язык простой, но из-за кажущейся простоты многие разработчики совершают одни и те же ошибки, которые приводят к серьёзным последствиям в production.
Ниже собраны 15 самых распространённых ошибок при разработке на Golang и рекомендации по их исправлению.
Игнорирование ошибок приводит к скрытым багам, которые сложно найти.
Неправильно:
_, err := ioutil.ReadFile("config.json")
Правильно:
data, err := ioutil.ReadFile("config.json")
if err != nil {
log.Fatal(err)
}
Бесконтрольный запуск горутин приводит к утечкам памяти и проблемам с конкурентностью.
Неправильно:
for i := 0; i < 1000; i++ {
go doSomething()
}
Правильно:
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
doSomething()
}()
}
wg.Wait()
var wg sync.WaitGroup
Создаётся переменная wg
типа sync.WaitGroup
— это специальная структура из стандартной библиотеки Go, которая позволяет ждать завершения группы горутин.
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
doSomething()
}()
}
Цикл запускает 1000 горутин.
Для каждой итерации:
wg.Add(1)
— увеличивает счётчик WaitGroup
на 1: «Ожидается одна горутина».
go func() { ... }()
— запускается анонимная функция в новой горутине.
defer wg.Done()
— отложенный вызов, который уменьшит счётчик WaitGroup
на 1, когда горутина завершится.
doSomething()
— выполняется ваша бизнес-логика в каждой горутине.
wg.Wait()
Этот вызов блокирует выполнение до тех пор, пока все 1000 горутин не вызовут wg.Done()
, то есть до их завершения.
Чтобы дождаться завершения всех асинхронных задач, прежде чем продолжить выполнение основной программы. Иначе main()
может завершиться раньше, чем горутины успеют выполниться.
Представте, что вы поручили 1000 помощникам разложить документы, но хотите убедиться, что все закончили работу, прежде чем закрыть офис. WaitGroup
— это как список с галочками: каждый помощник отмечает себя выполненным, и ты ждёшь, пока все поставят галочки.
Без контроля приложение становится нестабильным и непредсказуемым.
Отсутствие context приводит к сложностям в отмене и таймаутах операций.
Неправильно:
req, _ := http.NewRequest("GET", url, nil)
client.Do(req)
Правильно:
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
client.Do(req)
Context необходим для контроля длительных операций.
Отсутствие context приводит к сложностям в отмене и таймаутах операций.
Использование context
особенно важно в сетевых запросах и длительных операциях. Без context вы не сможете прервать запросы при превышении времени ожидания или отменить запросы, когда они больше не нужны. Это может привести к зависаниям, длительному ожиданию ответа от удалённых сервисов и повышению нагрузки на сервер, так как ресурсы остаются занятыми на неопределённое время.
Использование пустого интерфейса ухудшает читаемость и поддержку.
Неправильно:
func doSomething(data interface{}) {}
Правильно:
func doSomething(data string) {}
Строгая типизация помогает избегать ошибок во время компиляции.
Это усложняет код и затрудняет его поддержку без реальной выгоды.
Неправильно:
buf := make([]byte, 0, 1024)
Правильно:
buf := []byte{}
Оптимизируйте только по необходимости и после профилирования.
Утечки памяти незаметно ухудшают производительность приложения.
Что такое утечка памяти в Go?
Хотя Go использует сборщик мусора (GC), это не гарантирует, что память не будет утекать.
Утечка — это не обязательно потерянная память в классическом понимании (как в C), а скорее данные, которые остаются в памяти, но больше не нужны, и GC их не может освободить, потому что на них всё ещё есть ссылки.
Горутины, которые никогда не завершаются (зависли, ждут по каналу).
Кэш или map, в который пишут, но никогда не очищают.
Срезы или структуры, которые ссылаются на большие блоки данных, даже если используют только их часть.
Открытые файлы или соединения без Close()
.
Со временем утечки накапливаются.
Увеличивается использование памяти и CPU (GC работает чаще).
Программа может начать тормозить или крашиться из-за OOM (Out of Memory).
Используйте:
import _ "net/http/pprof"
Регулярно проверяйте утечки через pprof.
pprof
— это мощный инструмент профилирования в Go, встроенный в стандартную библиотеку. Он позволяет вам:
Снимать heap-профили (использование памяти).
Смотреть goroutine dump — какие горутины висят и сколько их.
Анализировать CPU, блокировки, аллокации и др.
Использовать интерактивные визуализации через go tool pprof
.
Как подключить pprof
mport _ "net/http/pprof"
import "net/http"
func main() {
go func() {
http.ListenAndServe("localhost:6060", nil)
}()
// твой код
}
Теперь вы можете открыть в браузере:
http://localhost:6060/debug/pprof/
Снятие и анализ профиля
go tool pprof http://localhost:6060/debug/pprof/heap
После чего можете в интерактивном режиме:
top
— показать топ аллокаторов.
list SomeFunc
— посмотреть, где в SomeFunc
утечка.
web
— открыть SVG-граф утечки в браузере (если установлен Graphviz).
Пример утечки:
func leaky() {
ch := make(chan int)
go func() {
for {
ch <- 1 // никто не читает — горутина никогда не завершится
}
}()
}
Такая горутина зависает навсегда и удерживает память.
Всегда закрывай каналы, соединения и файлы.
Используй context
с таймаутом.
Анализируй pprof
в stress-тестах или при длительной работе.
Неправильное использование каналов вызывает deadlock или panic.
Что такое каналы в Go?
Каналы (chan
) в Go — это механизм синхронизации между горутинами. Они позволяют передавать данные от одной горутины к другой без явной блокировки, при этом встроено поведение блокировки/ожидания.
В чём проблема?
Неправильная работа с каналами приводит к:
Deadlock (взаимная блокировка).
Panic (при отправке в закрытый канал или чтении из него).
Утечкам горутин, если канал не обслуживается (горутине некуда писать/читать).
Неопределённому поведению, особенно при конкурентной записи без синхронизации.
Неправильно:
func main() {
ch := make(chan int) // небуферизированный канал
ch <- 1 // deadlock: main заблокирован, никто не читает
fmt.Println("unreachable")
}
Здесь main()
пытается отправить в канал, но никто не читает, и он навсегда повисает — это и есть deadlock.
Правильно:
func main() {
ch := make(chan int, 1) // буферизированный канал
ch <- 1 // нет блокировки, потому что буфер есть
fmt.Println("done")
}
Буфер позволяет сделать одну отправку без ожидания читателя.
Или исправление с получателем
func main() {
ch := make(chan int)
go func() {
ch <- 1 // эта горутина отправит, когда main будет читать
}()
value := <-ch
fmt.Println(value)
}
А здесь уже есть полный цикл: одна горутина пишет, другая читает — всё корректно и без deadlock.
Другие типичные ошибки с каналами:
Запись в закрытый канал
close(ch)
ch <- 1 // panic: send on closed channel
Чтение из закрытого канала — норма, но нужно проверить:
value, ok := <-ch
if !ok {
fmt.Println(\"канал закрыт\")
}
Почему важно?
Каналы — ключевая часть модели конкурентности в Go:
Они блокируют по умолчанию, и это фича, а не баг.
Они обеспечивают синхронизацию.
Их неправильное использование может убить всю систему.
Забытые defer приводят к утечкам ресурсов.
Правильно:
f, err := os.Open("file.txt")
if err != nil { log.Fatal(err) }
defer f.Close()
Используйте defer для автоматического освобождения ресурсов.
Отсутствие синхронизации вызывает непредсказуемое поведение.
Правильно:
var mu sync.Mutex
mu.Lock()
defer mu.Unlock()
Используйте sync.Mutex для безопасной работы с данными.
Отсутствие тестов ухудшает стабильность и усложняет поддержку.
Правильно:
func TestAdd(t *testing.T) {}
Регулярно пишите тесты для критических функций.
Reflection замедляет код и усложняет чтение.
Что такое reflection в Go?
reflection
— это механизм, позволяющий программе анализировать и изменять свои собственные структуры во время выполнения. В Go это реализуется через пакет reflect
.
Пример:
import "reflect"
func printType(x interface{}) {
v := reflect.ValueOf(x)
fmt.Println("Type:", v.Type())
}
Потеря производительности
Reflection работает медленнее, чем обычный статический вызов. Всё, что делается через reflect
, требует дополнительных проверок, аллокаций и обращений к типовой информации.
В критичных по скорости частях кода это может стать узким местом.
Усложнение понимания
Код с reflect
менее прозрачен. Вместо явных вызовов методов и доступа к полям — приходится разбираться, что делает reflect.ValueOf
, Elem()
, Field(i)
и т.д.
Для новичков или команды сопровождения такой код — ночной кошмар.
Потеря типовой безопасности
Один из плюсов Go — это строгая типизация. Reflection обходит эту систему, что может привести к runtime-ошибкам вместо compile-time.
Фреймворки и библиотеки
Например, encoding/json
использует reflect
, чтобы сериализовать произвольные структуры.
ORM-библиотеки вроде gorm
— чтобы работать с любыми структурами данных.
Универсальные инструменты
В случаях, когда нужно написать универсальную функцию для работы с множеством разных типов — и они неизвестны заранее.
Пример: сериализация, логгирование, динамическое создание UI, роутинг HTTP-запросов по методам.
Вспомогательные утилиты для отладки или генерации кода
Например, авто-документация API на основе структур.
Избегайте:
func setField(x interface{}, fieldName string, value interface{}) {
v := reflect.ValueOf(x).Elem()
f := v.FieldByName(fieldName)
if f.IsValid() && f.CanSet() {
f.Set(reflect.ValueOf(value))
}
}
Этот код трудно отлаживать и сопровождать. Лучше сделать это явно через интерфейсы или использовать generics (с Go 1.18+).
Лучше использовать generics (если можно)
func print[T any](value T) {
fmt.Printf("%v\n", value)
}
Generics позволяют избежать использования reflect
в большинстве случаев, особенно при написании универсальных функций.
Это приводит к разрозненному стилю и сложности поддержки.
Используйте:
go fmt ./...
Поддерживайте единый стиль кода.
Передача структур по значению ухудшает производительность.
Неправильно:
type User struct {
Name string
Email string
Age int
}
func PrintUser(u User) {
fmt.Println(u.Name, u.Email, u.Age)
}
func main() {
user := User{Name: "Alice", Email: "alice@example.com", Age: 30}
PrintUser(user) // структура копируется
}
Здесь User
передаётся по значению — создаётся копия всей структуры при каждом вызове функции.
Правильно:
func PrintUser(u *User) {
fmt.Println(u.Name, u.Email, u.Age)
}
func main() {
user := User{Name: "Alice", Email: "alice@example.com", Age: 30}
PrintUser(&user) // передаётся указатель, копирования нет
}
Передача по указателю *User
позволяет избежать копирования и эффективнее использовать память и ресурсы.
Игнорирование мониторинга усложняет выявление проблем.
Используйте логирование, мониторинг. Например Prometheus:
prometheus.MustRegister(myMetric)
Мониторинг необходим для оперативной реакции на проблемы.
Во многих Go-проектах логирование выглядит примерно так:
goCopyEditlog.Println("Something went wrong")
log.Printf("error: %v", err)
log.Println("Something went wrong") log.Printf("error: %v", err)
На первый взгляд — нормально. Ошибка логируется. Но если система масштабируется, появляются микросервисы, параллельные запросы и DevOps-обвязка, такие логи превращаются в болото:
Непонятно, откуда пришла ошибка
Не видно, что именно происходило
Нет возможности отследить ошибку в системах мониторинга (например, Loki, ELK, Datadog)
Нет request_id
— ключевого идентификатора запроса
Хорошее логирование — это:
Структурированный вывод (JSON или key-value формат)
Уровни логирования: debug, info, warning, error, fatal
Контекст: модуль, пользователь, ID запроса (request_id
), ошибка, действия
Легкая интеграция в Prometheus/Grafana/Cloud Logging
request_id
— это уникальный ID каждого запроса. Его можно:
Получать из заголовка X-Request-ID
Генерировать, если отсутствует
Прокидывать через context.Context
Использовать в каждом логе
Это упрощает трейсинг ошибок, особенно в микросервисной архитектуре.
goCopyEditimport (
log "github.com/sirupsen/logrus"
"github.com/google/uuid"
)
func logAuthError(userID string, err error, requestID string) {
log.WithFields(log.Fields{
"request_id": requestID,
"module": "auth",
"user_id": userID,
"action": "login_attempt",
"error": err,
}).Error("failed to authenticate user")
}
goCopyEditimport (
"go.uber.org/zap"
"github.com/google/uuid"
)
func logAuthError(userID string, err error, requestID string, logger *zap.Logger) {
logger.Error("failed to authenticate user",
zap.String("request_id", requestID),
zap.String("module", "auth"),
zap.String("user_id", userID),
zap.String("action", "login_attempt"),
zap.Error(err),
)
}
goCopyEditfunc RequestIDMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
reqID := c.GetHeader("X-Request-ID")
if reqID == "" {
reqID = uuid.New().String()
}
c.Set("request_id", reqID)
c.Writer.Header().Set("X-Request-ID", reqID)
c.Next()
}
}
Затем в хендлерах:
goCopyEditfunc LoginHandler(c *gin.Context) {
requestID, _ := c.Get("request_id")
userID := "123"
err := errors.New("invalid password")
log.WithFields(log.Fields{
"request_id": requestID,
"module": "auth",
"user_id": userID,
"action": "login_attempt",
"error": err,
}).Error("failed to authenticate user")
}
Теперь каждый лог содержит структурированную информацию:
jsonCopyEdit{
"level": "error",
"msg": "failed to authenticate user",
"request_id": "abcd-1234-efgh-5678",
"module": "auth",
"user_id": "123",
"action": "login_attempt",
"error": "invalid password"
}
И вы можете легко:
Искать логи по request_id
Собирать метрики по модулям
Интегрировать с Observability-платформами
Плохие логи — это логи без контекста, уровня и ID.
Хорошие логи:
Структурированы
Содержат request_id
Используют уровни (Info
, Error
, Debug
)
Помогают тебе и машинам находить проблемы
Избегая этих ошибок и следуя рекомендациям, вы сможете значительно улучшить стабильность, производительность и читаемость ваших Go-приложений, сократить время на отладку и сделать разработку более эффективной.