Расскажите о распространенных ошибках и подводных камнях в 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()

Ответ 18+ 🔞

А, вот ты решил на Go писать, да? Ну что ж, добро пожаловать в мир, где тебя ждёт не только статическая типизация и горутины, но и целый овердохуища подводных граблей, на которые можно наступить с таким треском, что в рот меня чих-пых!

Слушай сюда, я тебе сейчас про основные косяки расскажу, чтобы ты не наступал на те же грабли, что и мы все в своё время.

Первое — это, блядь, указатели. Ну, точнее, их отсутствие. Вот представь: объявляешь ты переменную, а она nil. А потом пытаешься к ней обратиться, как будто там что-то есть. Пиздец, а не паника.

var p *int
fmt.Println(*p) // panic: runtime error: invalid memory address or nil pointer dereference

Вот так вот, на ровном месте. Проверяй, не nil ли твой указатель, прежде чем лезть в него, как в свою жизнь.

Второе — выход за границы. Это классика, ёпта. Массив или слайс на три элемента, а ты лезешь за четвёртый. Ну и что ты там надеешься найти? Пиздюлину от рантайма.

arr := []int{1, 2, 3}
fmt.Println(arr[3]) // panic: runtime error: index out of range [3] with length 3

Третье — гонки данных. Вот это, блядь, настоящая головная боль. Две горутины одновременно пишут в одну переменную. Кто победит? А хуй его знает! Результат каждый раз разный, как погода в Питере. Для этого есть флаг -race, он как детектор лжи, только для твоего кода. Используй мьютексы (sync.Mutex) или атомарные операции, не будь распиздяем.

Четвёртое — взаимная блокировка, deadlock. Ситуация, когда две горутины сидят и смотрят друг на друга, как два идиота, и ждут, пока другая что-то сделает. А они обе ждут. Вечность. Например, пишешь в канал, а читать его некому. Или два мьютекса взялись в разном порядке — и привет, вечный сон.

ch := make(chan int)
ch <- 42 // Сиди и жди, пока кто-то прочитает. А никто не придёт.

Пятое — утечка горутин. Создал горутину, она пошла в канал за данными, а данных-то и нет. И не будет. А она сидит, ждёт, память жрёт. Как та собака из анекдота, которая ждёт хозяина, а он на курорте. Закрывай каналы, продумывай логику завершения, не плоди сущности без надобности.

Шестое — defer в циклах. Вот тут многие обжигаются, я сам чуть не обосрался. Переменная в 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 выполняется ВСЕГДА, даже если в функции случилась паника. Он как верный пёс, который приберётся за тобой в любом случае.

Седьмое — переопределение переменных (shadowing). Объявляешь переменную val снаружи, а внутри блока, по привычке, через := создаёшь новую с тем же именем. И потом удивляешься: "Почему моя внешняя val не изменилась, ёбта?!"

val := 10
if true {
    val, err := someFunc() // Создалась НОВАЯ val, локальная!
    // Внешняя val так и осталась 10
}
// Правильно: val, err = someFunc()

Вот такие пироги, дружище. Go — язык мощный, но требует внимательности, а то так и до "панки-дринки-встаньки" недалеко. Держи ухо востро и не повторяй чужих ошибок, их и своих хватит.