Расскажите о распространенных ошибках и подводных камнях в Go

Ответ

При разработке на Go можно столкнуться с рядом типичных ошибок:

  1. Разыменовывание nil указателя Это приводит к панике во время выполнения. Классический пример:

    var p *int
    fmt.Println(*p) // panic: runtime error: invalid memory address or nil pointer dereference
  2. Выход за границы слайса или массива Попытка доступа к элементу по индексу, которого не существует.

    arr := []int{1, 2, 3}
    fmt.Println(arr[3]) // panic: runtime error: index out of range [3] with length 3
  3. Гонка данных (Data Race) Возникает, когда две или более горутины одновременно обращаются к одной и той же области памяти, и хотя бы одна из них выполняет запись. Для обнаружения используется флаг -race (go run -race main.go).

    var counter int
    go func() { counter++ }() // Запись
    go func() { counter++ }() // Конкурирующая запись
    // Для безопасной работы используйте мьютексы (sync.Mutex) или атомарные операции (sync/atomic)
  4. Взаимная блокировка (Deadlock) Ситуация, когда горутины бесконечно ожидают друг друга. Например, при записи в небуферизованный канал без читателя или при циклической блокировке мьютексов.

    // Пример 1: Блокировка канала
    ch := make(chan int)
    ch <- 42 // Deadlock: горутина заблокируется навсегда, если никто не читает из ch
    
    // Пример 2: Блокировка мьютексов
    var mu1, mu2 sync.Mutex
    go func() { 
        mu1.Lock()
        mu2.Lock() 
        // ... 
    }()
    go func() {
        mu2.Lock()
        mu1.Lock() // Потенциальный deadlock, если горутины выполнятся одновременно
        // ...
    }()
  5. Утечки горутин (Goroutine Leaks) Горутина может "зависнуть" в ожидании данных из канала, который никогда не будет закрыт или в который никто не будет писать. Такие горутины продолжают существовать и потреблять память до конца работы программы.

    func leak() {
        ch := make(chan int)
        go func() {
            val := <-ch // Эта горутина будет ждать вечно, если в канал ch никто не запишет
            fmt.Println(val)
        }()
    }
  6. Неправильная работа с defer в циклах Переменные, используемые в defer, фиксируются в момент выполнения defer, а не в момент его вызова. В циклах это приводит к тому, что все отложенные вызовы будут использовать значение переменной из последней итерации.

    for i := 0; i < 3; i++ {
        defer func() {
            fmt.Println(i) // Выведет "3" три раза
        }()
    }
    // Правильно передавать значение как аргумент:
    for i := 0; i < 3; i++ {
        defer func(val int) {
            fmt.Println(val) // Выведет "2", "1", "0"
        }(i)
    }

    Важно: Распространенное заблуждение, что defer не выполняется при панике. На самом деле, defer выполняется всегда перед тем, как функция завершит свою работу, в том числе и во время паники.

  7. Переопределение переменных (Variable Shadowing) Объявление новой переменной с тем же именем во внутреннем блоке, что и во внешнем. Это частый источник багов.

    val := 10
    if true {
        val, err := someFunc() // Ошибка: создана новая переменная `val`
        if err != nil { /* ... */ }
        // Внешняя переменная `val` осталась без изменений
    }
    // Правильно: val, err = someFunc()