Топ ошибок начинающих Go разработчиков: как их избежать

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Топ ошибок начинающих Go разработчиков: как их избежать

Go (или Golang) — это компилируемый, статически типизированный язык программирования, созданный в недрах Google. Он завоевал популярность благодаря своей простоте, производительности и встроенной поддержке конкурентности. Однако даже с таким лаконичным синтаксисом новички часто совершают одни и те же ошибки. В этой статье мы разберем топ-4 типичных ошибок начинающих Go-разработчиков и покажем, как их избежать.

1. Игнорирование возвращаемых ошибок

Одна из самых распространенных ошибок — игнорирование ошибок, возвращаемых функциями. В Go нет исключений (try-catch), и ошибки — это обычные значения, которые нужно явно обрабатывать. Многие новички, привыкшие к другим языкам, просто присваивают результат переменной, забывая проверить err.

Неправильно:

package main

import (
    "fmt"
    "os"
)

func main() {
    file, _ := os.Open("config.txt") // Игнорируем ошибку
    fmt.Println(file.Name())
}

Если файл не существует, программа запаникует (вызовет panic) или отработает с nil указателем, что приведет к неожиданному поведению.

Правильно:

package main

import (
    "fmt"
    "log"
    "os"
)

func main() {
    file, err := os.Open("config.txt")
    if err != nil {
        log.Fatalf("Не удалось открыть файл: %v", err)
    }
    defer file.Close()
    fmt.Println(file.Name())
}

Совет: Всегда проверяйте err сразу после вызова функции. Используйте log.Fatal или возвращайте ошибку выше по стеку, но никогда не игнорируйте её.

2. Неправильная работа с горутинами и гонками данных

Горутины — это легковесные потоки в Go. Однако новички часто запускают горутины, не заботясь о синхронизации доступа к общим данным. Это приводит к гонкам данных (data race), когда несколько горутин одновременно читают и пишут в одну переменную.

Неправильно:

package main

import (
    "fmt"
    "sync"
)

func main() {
    counter := 0
    var wg sync.WaitGroup

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter++ // Гонка данных!
        }()
    }
    wg.Wait()
    fmt.Println(counter) // Результат может быть любым (не 1000)
}

Правильно (с использованием мьютекса):

package main

import (
    "fmt"
    "sync"
)

type Counter struct {
    mu sync.Mutex
    value int
}

func (c *Counter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.value++
}

func (c *Counter) Value() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.value
}

func main() {
    counter := &Counter{}
    var wg sync.WaitGroup

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter.Increment()
        }()
    }
    wg.Wait()
    fmt.Println(counter.Value()) // Всегда 1000
}

Совет: Используйте мьютексы (sync.Mutex) или каналы для синхронизации. Запускайте тесты с флагом -race: go test -race.

3. Путаница с nil-срезом и пустым срезом

В Go срезы (slices) могут быть nil или пустыми. Новички часто не понимают разницы, что приводит к ошибкам при сериализации (JSON) или передаче срезов в функции.

  • Nil-срез: объявлен как var s []int. Он не имеет базового массива.
  • Пустой срез: создан как s := []int{} или make([]int, 0). У него есть базовый массив, но длина равна 0.

Пример путаницы:

package main

import (
    "encoding/json"
    "fmt"
)

func main() {
    var nilSlice []int
    emptySlice := []int{}

    // JSON маршалинг
    nilJSON, _ := json.Marshal(nilSlice)
    emptyJSON, _ := json.Marshal(emptySlice)

    fmt.Println(string(nilJSON))   // Вывод: null
    fmt.Println(string(emptyJSON)) // Вывод: []
}

Если вы ожидаете [], но передаете nil-срез, клиентский код может сломаться при парсинге JSON.

Совет: Если вам нужен пустой срез для передачи в JSON или итерации, явно инициализируйте его: s := make([]int, 0). Используйте len(s) == 0 для проверки на пустоту, а не s == nil.

4. Неправильное использование defer и panic/recover

Оператор defer откладывает выполнение функции до возврата из внешней функции. Новички часто забывают, что defer выполняется в порядке LIFO (последним добавлен — первым выполнен) и аргументы вычисляются сразу.

Ошибка с аргументами:

package main

import "fmt"

func main() {
    i := 0
    defer fmt.Println(i) // Выведет 0, а не 1
    i++
    fmt.Println(i) // Выведет 1
}

Здесь fmt.Println(i) "запоминает" значение i на момент вызова defer, а не на момент его выполнения.

Ошибка с panic/recover: Восстановление после паники сработает только внутри одной горутины.

package main

import "fmt"

func main() {
    go func() {
        defer func() {
            if r := recover(); r != nil {
                fmt.Println("Восстановлено:", r)
            }
        }()
        panic("Ошибка в горутине")
    }()
    // Паника в main не будет восстановлена
    panic("Ошибка в main")
}

Совет: Всегда помните, что defer вычисляет аргументы сразу. Используйте замыкания, если нужно отложить вычисление:

Блоги

Рекомендаций по книгам