Go e o ecossistema OpenAPI

Introdução
Ao desenvolver um servidor backend de produção com a linguagem Go, quase todos os desenvolvedores se deparam, logo no início, com um dos desafios mais difíceis, que é o seguinte.
Documentação de API, como fazer...?
Ao pesquisar um pouco sobre o assunto, percebe-se que é vantajoso escrever documentação em conformidade com a especificação OpenAPI e, naturalmente, procura-se uma biblioteca que seja integrada ao OpenAPI. No entanto, mesmo após tomar essa decisão, surge o próximo problema.
Há muitas bibliotecas relacionadas ao OpenAPI... qual devo usar...?
Este documento é uma breve introdução às bibliotecas, escrita para iniciantes em Go que vivenciam essa situação. Ele foi elaborado com base no final de 2024, e, como o ecossistema de linguagens está em constante mudança, recomenda-se consultá-lo e acompanhar sempre as novidades mais recentes.
Estratégias das bibliotecas para lidar com o OpenAPI
Talvez você já saiba, mas o OpenAPI é uma especificação para definir e documentar de forma clara APIs REST. Ao definir os endpoints, as requisições e os formatos de resposta da API no formato YAML ou JSON, ela contribui significativamente para automatizar a geração de código para desenvolvedores, tanto no front-end quanto no back-end, reduzindo repetições desnecessárias e pequenos erros humanos.
Para integrar naturalmente o OpenAPI aos projetos, as bibliotecas do ecossistema Go, em linhas gerais, adotam três estratégias principais.
1. Composição da especificação OpenAPI a partir de comentários em Go
Uma dificuldade ao desenvolver APIs seguindo o OpenAPI é que o documento real e o código que implementa esse documento ficam em arquivos separados, em locais completamente diferentes. Por isso, é bastante frequente acontecer de o código ser atualizado sem que o documento seja atualizado, ou de o documento ser atualizado sem que o código acompanhe essa mudança.
Como exemplo simples:
- Você altera a lógica da API no arquivo
./internal/server/user.go. - O documento real está em
./openapi3.yaml, e você pode esquecer de fazer a alteração correspondente nele. - Sem perceber esse problema, você envia um Pull Request e os colegas revisam o código.
- Como os revisores também não veem as alterações em
./openapi3.yaml, pode ocorrer um incidente em que a especificação da API permanece igual, mas a implementação real da API é modificada.
Escrever a documentação da API na forma de comentários em Go pode resolver, em certa medida, esse problema. Como código e documentação ficam reunidos em um único lugar, é possível atualizar os comentários junto com o código ao modificá-lo. Existem ferramentas que geram automaticamente a especificação OpenAPI com base nesses comentários.
Um projeto representativo é o Swag. O Swag analisa os comentários do código Go e gera documentação no formato OpenAPI 2. O uso é simples: basta escrever comentários acima da função handler, no formato definido por essa biblioteca.
1// @Summary 유저 생성
2// @Description 새로운 유저를 생성합니다.
3// @Tags Users
4// @Accept json
5// @Produce json
6// @Param user body models.User true "유저 정보"
7// @Success 200 {object} models.User
8// @Failure 400 {object} models.ErrorResponse
9// @Router /users [post]
10func CreateUser(c *gin.Context) {
11 // ...
12}
Ao escrever os comentários dessa forma, a ferramenta CLI Swag analisa esses comentários e gera a documentação OpenAPI 2. Geralmente, esse processo é executado no CI, e a especificação OpenAPI gerada é publicada no Git Repository, no artefato final de build ou em um sistema externo de gerenciamento de documentação de API, sendo utilizada na colaboração com outros projetos.
Vantagens:
- Como os comentários estão junto ao código, há menor probabilidade de a forma real do código e a documentação divergirem.
- Sem ferramentas adicionais ou configurações complexas, é possível documentar de forma simples e flexível apenas com comentários.
- Como os comentários não afetam a lógica real da API, é fácil adicionar funcionalidades temporárias que seria inconveniente expor na documentação.
Desvantagens:
- À medida que o número de linhas de comentário aumenta, a legibilidade de um único arquivo de código pode diminuir.
- Pode ser difícil representar todas as especificações da API apenas na forma de comentários.
- Como a documentação não obriga o código a seguir suas definições, não há garantia de que a documentação OpenAPI e a lógica real sejam consistentes.
2. Geração de código Go a partir da especificação OpenAPI
Também existe a abordagem de manter o Single Source of Truth (SSOT) no documento, e não no código Go. Trata-se de definir primeiro a especificação OpenAPI e, com base no conteúdo definido, gerar o código Go. Como a especificação da API gera o código, é possível impor, na cultura de desenvolvimento, que o design da API seja feito primeiro. Além disso, como a definição da especificação da API é a primeira etapa do desenvolvimento, há a vantagem de evitar cedo que, somente após a conclusão do desenvolvimento, sejam percebidas partes omitidas e todo o código tenha de ser modificado junto com a alteração da especificação da API.
Projetos representativos que adotam essa abordagem são o oapi-codegen e o OpenAPI Generator. O uso também é simples.
- Escreva um documento YAML ou JSON de acordo com a especificação OpenAPI.
- Execute a CLI.
- O código stub Go correspondente é gerado.
- Agora, basta implementar diretamente apenas a lógica detalhada de cada API para que esse stub possa ser utilizado.
A seguir, um exemplo do código gerado pelo oapi-codegen.
1// StrictServerInterface represents all server handlers.
2type StrictServerInterface interface {
3 // ...
4 // Returns all pets
5 // (GET /pets)
6 FindPets(ctx context.Context, request FindPetsRequestObject) (FindPetsResponseObject, error)
7 // ...
8}
O código gerado pelo oapi-codegen a partir dessa interface realiza o processamento de query parameters, header e body, além da validação, e chama o método apropriado declarado na interface. O usuário só precisa implementar a estrutura que realiza a interface para concluir o trabalho necessário na implementação da API.
Vantagens:
- Como a especificação é definida primeiro e depois o desenvolvimento avança, é vantajoso executar as tarefas em paralelo quando várias equipes colaboram.
- O código das partes repetitivas e trabalhosas é gerado automaticamente, o que aumenta a eficiência do trabalho e continua favorável à depuração.
- É fácil garantir que a forma do documento e a do código permaneçam sempre consistentes.
Desvantagens:
- Caso não se conheça a especificação OpenAPI, existe certa curva de aprendizado inicial.
- Como a forma do código que manipula a API é gerada automaticamente de acordo com o projeto, pode ser difícil lidar com casos que exigem personalização.
Comentário do autor. Em outubro de 2024, o código Go gerado pelo OpenAPI Generator impõe não apenas a lógica da API, mas toda a estrutura do projeto, e gera código com uma estrutura rígida, inadequada para adicionar os diversos recursos necessários em um ambiente real de produção. Para quem optar por essa abordagem, recomenda-se fortemente o uso do oapi-codegen. O autor utiliza oapi-codegen + echo + StrictServerInterface.
3. Geração de documentação OpenAPI a partir de código Go
Quando dezenas ou centenas de pessoas desenvolvem em um mesmo servidor, surge inevitavelmente o problema de que a uniformidade entre as APIs individuais pode ser comprometida. Como exemplo intuitivo, se a especificação de mais de 100 endpoints de API for declarada em um único arquivo OpenAPI YAML, esse arquivo se tornará um monstro com mais de 10 mil linhas. Ao declarar novos endpoints, passa-se a duplicar modelos, omitir alguns campos ou criar nomes de path que não seguem as convenções, o que começa a comprometer a uniformidade de toda a API.
Para resolver esse problema, é possível designar um responsável específico pelo YAML do OpenAPI ou desenvolver um Linter que detecte automaticamente as falhas durante o CI. No entanto, também é possível definir um Domain-specific language (DSL) em Go, obrigando todas as APIs a manterem uma uniformidade consistente.
Um projeto representativo que utiliza essa técnica é o Kubernetes (que a implementa de forma própria, sem bibliotecas separadas). Também é possível experimentar projetos como o go-restful e o goa. A seguir, um exemplo de uso do goa.
1var _ = Service("user", func() {
2 Method("create", func() {
3 Payload(UserPayload)
4 Result(User)
5 HTTP(func() {
6 POST("/users")
7 Response(StatusOK)
8 })
9 })
10})
Ao escrever código Go compilável como esse, obtém-se a vantagem de concluir simultaneamente a implementação e a definição da documentação da API POST /users.
Vantagens:
- Como tudo é gerado a partir do código, é fácil manter a consistência da API em todo o projeto.
- Ao utilizar o sistema de tipos forte do Go, é possível obter uma especificação mais precisa e sem controvérsias do que quando se utilizam todos os recursos do OpenAPI3.
Desvantagens:
- É necessário aprender o DSL definido por cada framework, e pode ser difícil aplicá-lo a código existente.
- Como é preciso seguir obrigatoriamente as regras propostas pelo framework, a liberdade e a flexibilidade podem diminuir.
Considerações finais
Cada método tem seus prós e contras, e é importante escolher o mais adequado de acordo com os requisitos do projeto e a preferência da equipe. O mais importante não é qual método é o melhor, mas sim realizar um julgamento de valor sobre qual é a solução mais adequada para a situação atual, elevando a produtividade no desenvolvimento para que você possa desfrutar de saídas mais cedo do trabalho e de um equilíbrio satisfatório entre vida pessoal e profissional.
Embora este texto tenha sido escrito com base em outubro de 2024, os ecossistemas de Go e OpenAPI continuam evoluindo. Considerando o intervalo entre o momento da leitura e a data de redação, recomenda-se acompanhar continuamente as novidades das bibliotecas e projetos, bem como as mudanças em suas vantagens e desvantagens.
Tenha uma ótima experiência com Go~ 😘