selfdev

Go после Python: первые две недели

, 5 минут чтения

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

Все заметки