Avatar ·

Можно ли использовать assert в Go? Или это зло, как говорят про C++?

Всем привет! Я новичок в Go, перешел с C++. Там я часто использовал assert() для проверки предусловий и инвариантов — например, что указатель не nil, что индекс в пределах массива, или что состояние объекта корректно. Это очень помогало при отладке. Начал писать на Go и заметил, что в стандартной библиотеке нет функции assert. Конечно, есть пакет testing, но он только для тестов. В продакшн-коде я начал писать свои проверки вроде: ```go if somePtr == nil { panic("somePtr is nil, это баг!") } ``` Но коллега сказал, что это ужасный стиль, и что в Go принято возвращать ошибки, а не паниковать. Он привел пример из C++, где assert часто критикуют за то, что он может быть отключен макросом NDEBUG, и это ведет к скрытым багам. Теперь я запутался. С одной стороны, assert в C++ — это удобный способ документировать и проверять предположения. С другой — в Go культура обработки ошибок совсем другая. Вопрос: стоит ли вообще использовать assert-подобные конструкции в Go? Если да, то как правильно? Если нет, то чем их заменять для проверки инвариантов? Интересует именно продакшн-код, не тесты. Пример ситуации: у меня есть функция, которая принимает слайс и ожидает, что он не пустой. Если он пустой — это баг в вызывающем коде, а не ошибка пользовательского ввода. Как правильно обработать такой случай в Go? <<>>

Короткий ответ: assert в Go — это чаще всего зло, но есть нюансы

Ваш коллега прав. В Go принят другой подход к обработке ошибок, и слепое перенесение практик из C++ может привести к хрупкому и трудноотлаживаемому коду. Давайте разберемся, почему и что делать вместо этого.

Почему assert в Go — плохая идея для продакшна?

  • Паника — это не обработка ошибок. В Go panic предназначен для действительно исключительных ситуаций, когда программа не может продолжать работу (например, разрыв соединения с БД, выход за границы слайса). Использовать panic для проверки аргументов функции — это злоупотребление.
  • Отсутствие NDEBUG. В C++ assert можно отключить на этапе релиза. В Go такой возможности нет — ваш if x == nil { panic(...) } будет выполняться всегда. Это значит, что программа может упасть в production там, где могла бы просто вернуть ошибку и продолжить работу.
  • Нарушение идиомы Go. Основной паттерн в Go — "return error, don't panic". Код, который паникует при любой некорректной передаче аргументов, становится неожиданным для других Go-разработчиков.

Что использовать вместо assert?

В Go есть три основных подхода, в зависимости от ситуации:

1. Возврат ошибки (для публичных API и пользовательского ввода)

Это стандартный способ. Если функция получает некорректные данные, она возвращает ошибку. Вызывающий код решает, что делать.

package main

import (
    "errors"
    "fmt"
)

func ProcessItems(items []string) ([]string, error) {
    if len(items) == 0 {
        return nil, errors.New("ProcessItems: items slice is empty")
    }
    // ... обработка
    return items, nil
}

func main() {
    result, err := ProcessItems(nil)
    if err != nil {
        fmt.Println("Ошибка:", err)
        // Можно gracefully завершиться или попросить пользователя ввести данные
    }
    _ = result
}

2. Использование panic для проверки инвариантов (только в package-internal коде)

Иногда внутри пакета есть функции, которые вызываются только из других функций этого же пакета, и вы абсолютно уверены, что аргументы корректны. Если это не так — это баг в вашем пакете, и программа должна упасть, чтобы вы его нашли. Это близко к идее assert в C++.

package internal

// mergeSortedLists ожидает, что оба слайса отсортированы по возрастанию.
// Если это не так — баг в вызывающем коде внутри пакета.
func mergeSortedLists(a, b []int) []int {
    // Проверка инварианта: слайсы должны быть отсортированы
    if !isSorted(a) || !isSorted(b) {
        panic("internal: mergeSortedLists: input slices must be sorted")
    }
    // ... реализация
    return nil
}

func isSorted(s []int) bool {
    for i := 1; i < len(s); i++ {
        if s[i-1] > s[i] {
            return false
        }
    }
    return true
}

3. Использование пакета testing (только в тестах)

В тестах можно и нужно использовать t.Fatal или require из testify. Это аналог assert для тестов.

func TestProcessItems(t *testing.T) {
    result, err := ProcessItems([]string{"a", "b"})
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    // ... проверки
}

Что делать с вашим примером (пустой слайс)?

Если пустой слайс — это баг в вызывающем коде (например, его должен был заполнить другой модуль), то:

  • Если функция публичная (экспортируется) — возвращайте ошибку.
  • Если функция приватная и вызывается только из вашего пакета — можно использовать panic, но лучше сначала попробовать вернуть ошибку или изменить дизайн так, чтобы пустой слайс был валидным состоянием (например, обрабатывать его как "нет данных").

Итог: В Go не нужно писать свой assert. Используйте error для внешних интерфейсов и panic только для внутренних проверок, когда вы уверены, что это баг, а не штатная ситуация. Код станет более надежным и понятным для других Go-разработчиков.

<<>>

Альтернативный подход: использование утилитарных функций

Можно пойти дальше и создать небольшую вспомогательную функцию для проверки предусловий внутри пакета. Это сделает код чище и централизует логику паники.

package internal

// requirePanic паникует с сообщением, если условие ложно.
// Используется для проверки инвариантов внутри пакета.
func requirePanic(condition bool, format string, args ...interface{}) {
    if !condition {
        panic(fmt.Sprintf("assertion failed: "+format, args...))
    }
}

func ProcessItems(items []string) []string {
    requirePanic(len(items) > 0, "ProcessItems: items must not be empty, got %d", len(items))
    // ...
    return items
}

Этот подход имеет свои плюсы:

  • Читаемость: сразу видно, что это проверка инварианта, а не обработка ошибки.
  • Единый стиль: все проверки выглядят одинаково.
  • Легко найти: можно grep'нуть по проекту все requirePanic.

Однако нужно помнить: это всё ещё panic, и использовать его стоит только для внутренних проверок, когда вы готовы уронить программу при нарушении контракта. Для внешнего API всегда используйте возврат ошибки. Некоторые команды вообще запрещают panic в production-коде, даже внутренний — это вопрос стиля и дисциплины.

В любом случае, избегайте слепого копирования C++ assert. Go предлагает более явные и безопасные механизмы.