Недавно я рефакторил сервис, изначально написанный на 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 г.