Недавно я рефакторил сервис, изначально написанный на Node, вместо to Go. Этот сервис работает на AWS Lambda, и при написании сервиса на Go я заметил несколько отличий, которые хотел записать и выделить.
Начав с различий, характерных для AWS, я использовал слои Lambda для совместного использования кода между Lambdas в Node, но быстро понял, что слои не нужны для использования с Go. Неиспользование слоев на самом деле было результатом явных различий в самих языках, поскольку Go компилируется, а этот двоичный файл отправляется для запуска Lambda. Это означает, что общий код объединяется во время компиляции, и ему не нужно прокладывать свой путь в файловую систему Lambda.
В результате желания написать о различиях в использовании Go по сравнению с другими распространенными языками для Lambda, я создал шаблон CDK для начала. Это часть моего справочного репозитория CDK, если кто-то захочет его использовать.
Пройдя через этот процесс, я должен сказать, что мне нравится система набора текста гораздо больше, чем я думал изначально. Моим первым языком программирования был C++. После C++ следующим языком, который я изучил, был Javascript, и после того, как я выучил Javascript, я почувствовал себя быстрым и свободным без системы набора текста. То же самое касается Python. До сих пор я не оглядывался назад на строгую типизацию. Между системой набора текста и необходимостью компилировать код перед развертыванием в Lambda я чувствую, что сэкономил огромное количество времени на отладке кода, потому что так много было сделано до развертывания. Хотя ошибки компиляции из-за несоответствия типов раздражают, общее преимущество перевешивает любые недостатки.
Я хотел бы рассказать о том, как я организую код для своих лямбда-выражений, а затем я обсужу еще несколько технических деталей, на которые мне нужно было обратить внимание при написании самого кода. С организационной точки зрения все мои обработчики Lambda находятся в папке cmd/
. (Ну, на самом деле на корневом уровне репозитория у меня есть две папки. В папке infra/
находится весь мой код приложения CDK, а в папке src/
находится весь мой исходный код. В папке src/
находится папка cmd/
.) Каждая Lambda получает свою собственную папку в папке cmd/
, соответствующую имени и назначению Lambda. Например, у меня есть папки cmd/createApplication/
и cmd/createUser/
. В каждой папке находятся три файла: init.go
со всем моим кодом перед выполнением в init
func (это общеязыковая конструкция, которую я не реализовал), lambdaHandler.go
со всем моим входящим кодом адаптера, специфичным для Lambda, и logic.go
со всей моей бизнес-логикой.
Файл init.go
обрабатывает в основном две вещи. Инициализация логгера для моего пакета и получение config. Я использую Uber Zap в своих проектах, потому что мне нравится структурированное ведение журнала. У меня также есть внутренний пакет конфигурации (подробнее об этом позже), который считывает переменные среды, устанавливает константы и организует их в структуру, чтобы упростить жизнь.
Файл lambdaHandler.go
становится немного сложнее. Моя функция main
находится в этом файле, поэтому обязательная функция lambda.Start()
вызывается в main
. Аргумент, который я даю lambda.Start()
, является частью шаблонного кода, частью кода для каждой конечной точки. Шаблон исходит из функции-оболочки, которая следует шаблону декоратора. Эта функция-оболочка принимает код, специфичный для конечной точки, как функцию, запускает эту функцию, затем обрабатывает ответ, если произошла ошибка, и добавляет обязательные заголовки к каждому ответу. Специфическая для конечной точки функция находится в функции lambdaHandler.go
и также оформляется там, прежде чем будет передана в lambda.Start()
. (Если это кажется запутанным, я предлагаю заглянуть в репозиторий службы аутентификации, где это реализовано.)
Наконец, файл logic.go
содержит всю бизнес-логику, которую Lambda должна запустить, включая обращение к исходящим адаптерам. Поскольку мне нравится писать свои проекты в соответствии с гексагональной архитектурой, именно здесь реализуется domain
. Исходящие адаптеры, о которых я упоминал, находятся в папке internal/
.
Мои внутренние папки (src/internal/
) также имеют некоторое сходство между проектами, и я уже упомянул довольно много функций, которые здесь присутствуют. У меня есть папки internal/common/
, internal/adapters/
и internal/types/
, каждая из которых является собственным пакетом Go.
Моя папка internal/common/
содержит конфигурацию, о которой я уже упоминал, декоратор обработчика Lambda, о котором я также упоминал, и несколько других служебных функций, которые используются во всем проекте.
Моя папка internal/adapters/
содержит код всех исходящих адаптеров. Всякий раз, когда мне нужно вызвать исходящий адаптер в моей бизнес-логике, хранение их всех в этом пакете делает код интуитивно понятным, adapters.CreateSomeRecord()
. Исходящие адаптеры включают в себя операции с базой данных, отправку электронных писем, отправку событий и т.п. Мои соединения и клиенты также инициализируются в этом пакете.
Моя папка internal/types/
содержит определения структур для таких вещей, как полезные нагрузки API и модели баз данных. Я нашел, что это особенно легко хранить все это в центральном месте. Я также храню здесь такие вещи, как пользовательские ошибки, которые используются в декораторе Lambda для определения того, какой код состояния должен быть возвращен пользователю.
Вот визуальное представление того, как может выглядеть структура папок проекта, прежде чем я начну заполнять бизнес-логику.
<repo> |_ src/ |_ cmd/ |_ <lambda operation folders> |_ internal/ |_ adapters/ |_ common/ |_ types/ |_ infra/ |_ <all CDK code>
Теперь перейдем к более техническому обсуждению.
Один немного случайный момент, который мне запомнился, — это синглтоны. Node экспортирует значения из модуля в виде синглетонов, поэтому мне никогда не приходилось беспокоиться о создании слишком большого количества сеансов DynamoDB при импорте или о создании нового экземпляра регистратора каждый раз, когда я импортировал свой модуль журналирования. Go этого не делает. Чтобы избежать подобных ситуаций, я научился использовать удобную функцию стандартной библиотеки под названием Once
. Инициализация значения внутри Once
означает, что значение создается только один раз, как вы уже догадались. Go отлично справляется с параллелизмом, и это лишь один из примеров этого. Вот фрагмент, который показывает, о чем я говорю.
package clients
import (
"context"
"sync"
"github.com/aws/aws-sdk-go-v2/aws"
awsConfigMod "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
)
var awsConfig aws.Config
var onceAwsConfig sync.Once
var dynamodbClient *dynamodb.Client
var onceDdbClient sync.Once
func getAwsConfig() aws.Config {
onceAwsConfig.Do(func() {
var err error
awsConfig, err = awsConfigMod.LoadDefaultConfig(context.TODO())
if err != nil {
panic(err)
}
})
return awsConfig
}
func GetDynamodbClient() *dynamodb.Client {
onceDdbClient.Do(func() {
awsConfig = getAwsConfig()
region := config.Region
dynamodbClient = dynamodb.NewFromConfig(awsConfig, func(opt *dynamodb.Options) {
opt.Region = region
})
})
return dynamodbClient
}
Одна последняя вещь. Я только что упомянул, что Go отлично справляется с параллелизмом. Запуск Go в Lambda может не полностью использовать весь набор преимуществ, которыми может похвастаться Go. Поскольку код работает в недолговечной среде, реализация таких вещей, как рабочие пулы и долговременные горутины, может быть неправильным выбором. Тем не менее, писать функции Go для Lambda по-прежнему стоит, и я немного расстроен собой, что не сделал этого раньше. Лямбды работают быстрее с Go по сравнению с другими распространенными языками, такими как Node или Python. Когда я провел рефакторинг своего сервиса с Node на Go, я увидел, что холодные запуски сократились более чем на 1 секунду, а теплые запросы иногда занимали менее половины времени их аналогов Node. Вот некоторые из преимуществ использования скомпилированного языка по сравнению с интерпретируемым. И я начинаю наслаждаться строгой типизацией. Спасибо, иди.
Первоначально опубликовано на https://thomasstep.com 31 августа 2022 г.