[Golang] Ошибки, которые нельзя обработать
Бывает что мы хотим добавить новую функциональность в сервис и всегда хочется это сделать быстро. И иногда приходит мысль написать рабочий вариант, а после этого исправлять баги. Может показаться, что если мы разрабатываем новую функциональность, мы не можем затронуть существующий функционал — у нас же http или grpc фреймворк ловит все паники и обрабатывает их. Но это не всегда так и в этой статье я хочу рассказать о некоторых ошибках, за которыми нужно всегда внимательно смотреть, потому что они могут привести к падению сервиса
Паника в горутине
Если мы в обработчике будем использовать горутину, которая по каким-то причинам вызвала панику, Golang не сможет обработать эту панику и приложение упадет с ошибкой и все запросы, которые сейчас обрабатывает сервис оборвутся.
Eсли написать примерно вот такой код
type User struct {
Email string
}
func UpsertUser(r *http.Request) (User, error) {
return User{}, nil
}
func SendEmail(u User) {
panic("sending email is not implemented")
}
func CreateUser(w http.ResponseWriter, r *http.Request) {
user, err := UpsertUser(r)
if err != nil {
// handling error
}
go func() {
SendEmail(user)
// may be something else
}()
}
То при вызове функции CreateUser сервис упадет.
Для того, что бы исправить это, нужно обрабатывать паники в каждой горутине
func CreateUser(w http.ResponseWriter, r *http.Request) {
user, err := UpsertUser(r)
if err != nil {
// handling error
}
go func() {
defer func() {
if err := recover(); err != nil {
log.Printf("panic recovered: %v", err)
}
}()
SendEmail(user)
// may be something else
}()
}
и приложение не будет падать и все будут счастливы.
Бесконечная рекурсия
Если мы по какой-то причине используем рекурсию, то нужно обязательно смотреть что при любых входных параметрах не будет бесконечной рекурсии, так как обработать переполнение стека в Golang нельзя (бесконечная рекурсия приводит к переполнения стека) и приложение падает.
Допустим мы написали такую реализация вычисления числа Фибоначчи:
func Fib(n int64) int64 {
if n == 1 {
return 1
}
return Fib(n-1) + Fib(n-2)
}
И если у нас в коде будет вызов функции Fib с отрицательным числом, то приложение упадет с ошибкой fatal error: stack overflow.
Это может удивить людей, которые писали на скриптовых языках таких как Ruby или Python и начинают писать на Golang. Так как в скриптовых языках можно довольно просто поймать исключение, которое показывает что стек достиг максимального значения:
def fib(n):
if n == 1:
return 1
return fib(n- 1) + fib(n - 2)
try:
fib(0)
except RecursionError as e:
print(e)
Код на Python выше просто выведет в консоль maximum recursion depth exceeded in comparison и приложение продолжит работу.
Работа с unsafe
При работе с модулем unsafe довольно просто сделать так что бы приложение упало. Поэтому не стоит использовать этот модуль, если все тщательно не проверено и не протестировано.
Допустим мы решили не копировать строку, а преобразовать ее в слайс байтов и написали вот такую функцию:
func ToSlice(a string) []byte {
return *(*[]byte)(unsafe.Pointer(&a))
}
Теперь мы можем изменять строку:
a := string([]byte("Andrey Berenda"))
b := ToSlice(a)
b[5] = 'i'
fmt.Println(a) // Andrei Berenda
Но если случайно передать строку, которая известна на этапе компиляции и попробовать ее изменить, то будет ошибка, которая приведет к остановке сервера
a := "Andrey Berenda"
b := ToSlice(a)
b[5] = 'i'
fmt.Println(a) // fatal error: fault
Заключение
В данной статье я разобрал некоторые ошибки, которыми можно довольно просто положить сервер. Таких ошибок в Golang еще много, поэтому нужно стараться покрывать свой код тестами, которые проверяют работоспособность кода, благо в Golang есть довольно хорошие инструменты для написания тестов.