Бывает что мы хотим добавить новую функциональность в сервис и всегда хочется это сделать быстро. И иногда приходит мысль написать рабочий вариант, а после этого исправлять баги. Может показаться, что если мы разрабатываем новую функциональность, мы не можем затронуть существующий функционал - у нас же 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 есть довольно хорошие инструменты для написания тестов.

Комментарии (5)


  1. Kekmefek
    21.09.2022 07:10

    Для ошибок можно сделать отдельный канал.


  1. 12rbah
    21.09.2022 08:31

    panic("sending email is not implemented")

    В целом конечно пример с паникой понятен, но в реальности это уже редко встречается, многие считают это bad practice/антипаттерном, т.к. по сути почти всегда вместо паники можно вернуть ошибку, т.к. не все хотят, чтобы приложение падало из-за внешней либы, например.


    1. AndreyBerenda Автор
      21.09.2022 08:39
      +1

      Я хотел показать, что если что в функции SendEmail есть паника, не обязательно ее явно вызывать, может быть обращение к nil интерфейсу или что-нибудь такое, то это приводит к падению сервера. Я вызвал явно панику для того, что бы было проще понять почему здесь паника, а не потому что это считается хорошей практикой, может быть нужно это было пояснить в статье.
      Но я с тобой согласен, вызывать явно паники считается bad practice в Golang, правильнее возвращать явные ошибки из функции.


      1. 12rbah
        21.09.2022 12:36
        +1

        Я понял, что это имитация, просто некоторые кто недавно знаком с языком так любят делать, т.к. привыкли к механизму исключений и поэтому оставил тот коммент.


  1. toxic_air
    23.09.2022 14:40

    Таких ошибок в Golang еще много,  ...

    Продолжайте, расширяйте, дополняйте списочек. Весьма полезно.