Go после Python: первые две недели
Несколько лет пишу бэкенд на Python и решил наконец всерьёз взяться за Go. Ниже то, что удивило в первые две недели. Это не сравнение «что лучше», а список мест, где питонистская интуиция подводит.
Ошибки — это просто значения
Первое, что бросается в глаза, — бесконечное if err != nil. Сначала раздражает, потом понимаешь, что это честно: по сигнатуре функции сразу видно, может ли она сломаться, а путь ошибки читается так же линейно, как основной. В Python исключение может прилететь из любой строки, и об этом узнаёшь на проде.
Полезная привычка — сразу оборачивать ошибку контекстом: fmt.Errorf("load config: %w", err). Тогда в логах получается цепочка вроде трейсбэка, только короче.
Нулевые значения вместо None
Объявленная, но не заполненная структура — это не None, а структура с нулями, пустыми строками и nil-слайсами. В слайс nil можно спокойно делать append, а в nil-мапу писать нельзя — паника. Это стоило мне одного вечера.
Интерфейсы реализуются неявно
Типу не нужно объявлять, что он реализует интерфейс: достаточно иметь нужные методы. По ощущениям это утиная типизация, но проверенная компилятором. Хорошее правило из книжек: интерфейс объявляет тот, кто его потребляет, и делает его маленьким — на один-два метода.
Горутины без «цветных» функций
После asyncio приятно, что нет деления на async и обычные функции. Любую функцию можно запустить в горутине, а блокирующий вызов не стопорит весь цикл событий. Отменять работу помогает context.Context, а собирать результаты — errgroup:
func fetchAll(ctx context.Context, urls []string) error {
g, ctx := errgroup.WithContext(ctx)
for _, u := range urls {
g.Go(func() error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
if err != nil {
return err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
return resp.Body.Close()
})
}
return g.Wait()
}
Первая ошибка отменяет контекст, и остальные запросы обрываются. В Python того же поведения я добивался через TaskGroup, о нём — в заметке про asyncio.gather.
Что дальше
Следующий шаг — переписать на Go небольшой внутренний сервис, который сейчас крутится на FastAPI, и честно сравнить потребление памяти и удобство деплоя. Один статический бинарник в Docker-образе на несколько мегабайт уже выглядит заманчиво.