Можно ли использовать assert в Go? Или это зло, как говорят про C++?
Короткий ответ: 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 предлагает более явные и безопасные механизмы.