[Golang] Ошибки, которые нельзя обработать

Бывает что мы хотим добавить новую функциональность в сервис и всегда хочется это сделать быстро. И иногда приходит мысль написать рабочий вариант, а после этого исправлять баги. Может показаться, что если мы разрабатываем новую функциональность, мы не можем затронуть существующий функционал — у нас же http или grpc фреймворк ловит все паники и обрабатывает их. Но это не всегда так и в этой статье я хочу рассказать о некоторых ошибках, за которыми нужно всегда внимательно смотреть, потому что они могут привести к падению сервиса

85556d09fe8bb0f792eeddee21b2facd.png

Паника в горутине

Если мы в обработчике будем использовать горутину, которая по каким-то причинам вызвала панику, 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 есть довольно хорошие инструменты для написания тестов.

© Habrahabr.ru