
Olá, NickName! Se você é um programador e trabalha com uma arquitetura de microsserviço, imagine que precisa configurar a interação do seu serviço A com algum serviço novo e ainda desconhecido B. O que você fará primeiro?
Se você fizer esta pergunta a 100 programadores de empresas diferentes, provavelmente obteremos 100 respostas diferentes. Alguém descreve contratos com arrogância, alguém no gRPC simplesmente faz clientes para seus serviços sem descrever um contrato. E alguém até armazena JSON em um googleok: D. A maioria das empresas desenvolve sua própria abordagem de interação entre serviços com base em alguns fatores históricos, competências, pilha de tecnologia e assim por diante. Quero contar como os serviços do Delivery Club se comunicam e por que fizemos essa escolha. E o mais importante, como garantimos a relevância da documentação ao longo do tempo. Haverá muito código!
Oi de novo! Meu nome é Sergey Popov, sou o líder da equipe responsável pelos resultados de pesquisa de restaurantes nos aplicativos e no site do Delivery Club, e também um membro ativo de nossa guilda de desenvolvimento interno para Go (podemos falar sobre isso mais tarde, mas não agora).
Vou fazer uma reserva agora mesmo, vamos falar principalmente sobre serviços escritos em Go. Ainda não implementamos a geração de código para serviços PHP, embora alcancemos a uniformidade nas abordagens de uma maneira diferente.
O que queríamos terminar com:
- Certifique-se de que os contratos de serviço estejam atualizados. Isso deve acelerar a introdução de novos serviços e facilitar a comunicação entre as equipes.
- Venha para um método unificado de interação sobre HTTP entre serviços (não consideraremos interações por meio de filas e streaming de eventos por enquanto).
- Para padronizar a abordagem para trabalhar com contratos de serviço.
- Use um único repositório de contratos para não procurar docas para todos os tipos de confluências.
- Idealmente, gere clientes para diferentes plataformas.
De todos os itens acima, Protobuf vem à mente como uma forma unificada de descrever contratos. Possui boas ferramentas e pode gerar clientes para diferentes plataformas (nossa cláusula 5). Mas também existem desvantagens óbvias: para muitos, o gRPC continua sendo algo novo e desconhecido, e isso complicaria muito sua implementação. Outro fator importante era que a empresa havia adotado por muito tempo a abordagem de “especificação primeiro”, e a documentação já existia para todos os serviços na forma de um swagger ou descrição RAML.
Go-swagger
Coincidentemente, ao mesmo tempo, começamos a adaptar o Go na empresa. Portanto, nosso próximo candidato a ser considerado foi go-swagger - uma ferramenta que permite gerar clientes e código de servidor a partir da especificação swagger. A desvantagem óbvia é que ele só gera código para Go. Na verdade, ele usa nossa geração de código e go-swagger permite um trabalho flexível com modelos, então, em teoria, ele pode ser usado para gerar código PHP, mas ainda não experimentamos.
Go-swagger não é apenas sobre a geração da camada de transporte. Na verdade, ele gera o esqueleto do aplicativo, e aqui eu gostaria de mencionar um pouco sobre a cultura de desenvolvimento em DC. Temos um Inner Source, o que significa que qualquer desenvolvedor de qualquer equipe pode criar uma solicitação de pull para qualquer serviço que tenhamos. Para que esse esquema funcione, tentamos padronizar as abordagens no desenvolvimento: usamos uma terminologia comum, uma abordagem única para registro, métricas, trabalho com dependências e, é claro, para a estrutura do projeto.
Portanto, ao implementar go-swagger, estamos introduzindo um padrão para o desenvolvimento de nossos serviços em Go. Este é mais um passo em direção aos nossos objetivos, que inicialmente não esperávamos, mas que é importante para o desenvolvimento em geral.
Os primeiros passos
Portanto, go-swagger acabou sendo um candidato interessante que parece ser capaz de cobrir a maioria dos requisitos
Nota: todos os outros códigos são relevantes para a versão 0.24.0, as instruções de instalação podem ser vistas em nosso repositório com exemplos , e o site oficial tem instruções para instalar a versão atual.Vamos ver o que ele pode fazer. Vamos pegar uma especificação de swagger e gerar um serviço:
> goswagger generate server \
--with-context -f ./swagger-api/swagger.yml \
--name example1
Temos o seguinte:

Makefile e go.mod que já criei.
Na verdade, acabamos com um serviço que processa as solicitações descritas em swagger.
> go run cmd/example1-server/main.go
2020/02/17 11:04:24 Serving example service at http://127.0.0.1:54586
> curl http://localhost:54586/hello -i
HTTP/1.1 501 Not Implemented
Content-Type: application/json
Date: Sat, 15 Feb 2020 18:14:59 GMT
Content-Length: 58
Connection: close
"operation hello HelloWorld has not yet been implemented"
Passo dois. Compreendendo os modelos
Obviamente, o código que geramos está longe de ser o que queremos ver em operação.
O que queremos da estrutura de nosso aplicativo:
- Ser capaz de configurar o aplicativo: transferir as configurações para se conectar ao banco de dados, especificar a porta de conexões HTTP e assim por diante.
- Selecione um objeto de aplicativo que armazenará o estado do aplicativo, a conexão com o banco de dados e assim por diante.
- Faça as funções de manipuladores de nossa aplicação, isso deve simplificar o trabalho com o código.
- Inicialize as dependências no arquivo principal (em nosso exemplo isso não acontecerá, mas ainda queremos isso.
Para resolver novos problemas, podemos substituir alguns modelos. Para fazer isso, vamos descrever os seguintes arquivos, como eu fiz ( Github ):

Precisamos descrever os arquivos de modelo (
`*.gotmpl`
) e o arquivo para a configuração ( `*.yml`
) de geração de nosso serviço.
A seguir, na ordem, analisaremos os templates que fiz. Não vou mergulhar profundamente em como trabalhar com eles, porque a documentação go-swagger é bastante detalhada, por exemplo, aqui está a descrição do arquivo de configuração. Vou apenas observar que Go-template é usado, e se você já tem experiência com isso ou teve que descrever configurações de HELM, então não será difícil descobrir.
Configurando o aplicativo
config.gotmpl contém uma estrutura simples com um parâmetro - a porta que o aplicativo ouvirá para solicitações HTTP de entrada. Também fiz uma função
InitConfig
que vai ler as variáveis de ambiente e preencher essa estrutura. Vou chamá-lo de main.go, portanto, InitConfig
tornei-o uma função pública.
package config
import (
"github.com/pkg/errors"
"github.com/vrischmann/envconfig"
)
// Config struct
type Config struct {
HTTPBindPort int `envconfig:"default=8001"`
}
// InitConfig func
func InitConfig(prefix string) (*Config, error) {
config := &Config{}
if err := envconfig.InitWithPrefix(config, prefix); err != nil {
return nil, errors.Wrap(err, "init config failed")
}
return config, nil
}
Para que este modelo seja usado ao gerar código, ele deve ser especificado na configuração YML :
layout:
application:
- name: cfgPackage
source: serverConfig
target: "./internal/config/"
file_name: "config.go"
skip_exists: false
Vou falar um pouco sobre os parâmetros:
name
- tem função meramente informativa e não afeta a geração.source
- na verdade, o caminho para o arquivo de modelo em camelCase, ou seja, serverConfig é equivalente a ./server/config.gotmpl .target
- diretório onde o código gerado será salvo. Aqui você pode usar modelos para gerar dinamicamente um caminho ( exemplo ).file_name
- o nome do arquivo gerado, aqui você também pode usar modelos.skip_exists
- um sinal de que o arquivo será gerado apenas uma vez e não substituirá o existente. Isso é importante para nós, porque o arquivo de configuração mudará conforme o aplicativo cresce e não deve depender do código gerado.
Na configuração de geração de código, você precisa especificar todos os arquivos, e não apenas aqueles que queremos substituir. Para arquivos que não mudam, na acepção do
source
ponto de fora asset:< >
, por exemplo, aqui : asset:serverConfigureapi
. A propósito, se você estiver interessado em ver os modelos originais, eles estão aqui .
Objeto de aplicativo e manipuladores
Não vou descrever o objeto do aplicativo para armazenar o estado, conexões de banco de dados e outras coisas, tudo é semelhante à configuração recém-feita. Mas com manipuladores, tudo é um pouco mais interessante. Nosso principal objetivo é criarmos uma função stub em um arquivo separado quando adicionamos uma URL à especificação e, o mais importante, para que nosso servidor chame essa função para processar a solicitação.
Vamos descrever o modelo e os stubs da função:
package app
import (
api{{ pascalize .Package }} "{{.GenCommon.TargetImportPath}}/{{ .RootPackage }}/operations/{{ .Package }}"
"github.com/go-openapi/runtime/middleware"
)
func (srv *Service){{ pascalize .Name }}Handler(params api{{ pascalize .Package }}.{{ pascalize .Name }}Params{{ if .Authorized }}, principal api{{ .Package }}.{{ if not ( eq .Principal "interface{}" ) }}*{{ end }}{{ .Principal }}{{ end }}) middleware.Responder {
return middleware.NotImplemented("operation {{ .Package }} {{ pascalize .Name }} has not yet been implemented")
}
Vejamos um pouco um exemplo:
pascalize
- traz uma linha com CamelCase (descrição de outras funções aqui )..RootPackage
- pacote de servidor web gerado..Package
- o nome do pacote no código gerado, que descreve todas as estruturas necessárias para solicitações e respostas HTTP, ou seja, estruturas. Por exemplo, uma estrutura para o corpo da solicitação ou uma estrutura de resposta..Name
- o nome do manipulador. Ele é obtido do operationID na especificação, se especificado. Recomendo sempre especificaroperationID
para um resultado mais óbvio.
A configuração do manipulador é a seguinte:
layout:
operations:
- name: handlerFns
source: serverHandler
target: "./internal/app"
file_name: "{{ (snakize (pascalize .Name)) }}.go"
skip_exists: true
Como você pode ver, o código do manipulador não será sobrescrito (
skip_exists: true
) e o nome do arquivo será gerado a partir do nome do manipulador.
Ok, existe uma função stub, mas o servidor da web ainda não sabe que essas funções devem ser usadas para processar solicitações. Corrigi isso no main.go (não vou fornecer o código completo, a versão completa pode ser encontrada aqui ):
package main
{{ $name := .Name }}
{{ $operations := .Operations }}
import (
"fmt"
"log"
"github.com/delivery-club/go-swagger-example/{{ dasherize .Name }}/internal/generated/restapi"
"github.com/delivery-club/go-swagger-example/{{ dasherize .Name }}/internal/generated/restapi/operations"
{{range $index, $op := .Operations}}
{{ $found := false }}
{{ range $i, $sop := $operations }}
{{ if and (gt $i $index ) (eq $op.Package $sop.Package)}}
{{ $found = true }}
{{end}}
{{end}}
{{ if not $found }}
api{{ pascalize $op.Package }} "{{$op.GenCommon.TargetImportPath}}/{{ $op.RootPackage }}/operations/{{ $op.Package }}"
{{end}}
{{end}}
"github.com/go-openapi/loads"
"github.com/vrischmann/envconfig"
"github.com/delivery-club/go-swagger-example/{{ dasherize .Name }}/internal/app"
)
func main() {
...
api := operations.New{{ pascalize .Name }}API(swaggerSpec)
{{range .Operations}}
api.{{ pascalize .Package }}{{ pascalize .Name }}Handler = api{{ pascalize .Package }}.{{ pascalize .Name }}HandlerFunc(srv.{{ pascalize .Name }}Handler)
{{- end}}
...
}
O código na importação parece complicado, embora na realidade seja apenas Go-templates e estruturas do repositório go-swagger. E em uma função,
main
simplesmente atribuímos nossas funções geradas aos manipuladores.
Resta gerar o código indicando nossa configuração:
> goswagger generate server \
-f ./swagger-api/swagger.yml \
-t ./internal/generated -C ./swagger-templates/default-server.yml \
--template-dir ./swagger-templates/templates \
--name example2
O resultado final pode ser visualizado em nosso repositório .
O que temos:
- Podemos usar nossas estruturas para a aplicação, configurações e o que quisermos. Mais importante ainda, é bastante fácil incorporar ao código gerado.
- Podemos gerenciar com flexibilidade a estrutura do projeto, até os nomes de arquivos individuais.
- Fazer modelos parece complicado e leva algum tempo para se acostumar, mas no geral é uma ferramenta muito poderosa.
Passo três. Gerando clientes
Go-swagger também nos permite gerar um pacote de cliente para o nosso serviço que outros serviços Go podem usar. Aqui, não vou me alongar sobre a geração de código em detalhes, a abordagem é exatamente a mesma de gerar código do lado do servidor.
Para projetos Go, é comum colocar pacotes públicos
./pkg
, faremos o mesmo: colocar o cliente do nosso serviço no pacote e gerar o próprio código da seguinte maneira:
> goswagger generate client -f ./swagger-api/swagger.yml -t ./pkg/example3
Um exemplo do código gerado está aqui .
Agora todos os consumidores do nosso serviço podem importar este cliente para si, por exemplo, por tag (no meu exemplo, a tag será
example3/pkg/example3/v0.0.1
).
Os modelos de cliente podem ser personalizados para, por exemplo, fluir
open tracing id
do contexto para o cabeçalho.
conclusões
Naturalmente, nossa implementação interna difere do código mostrado aqui principalmente devido ao uso de pacotes internos e abordagens de CI (executando vários testes e linters). No código gerado pronto para uso, coleta de métricas técnicas, trabalho com configurações e registro são configurados. Padronizamos todas as ferramentas comuns. Com isso, simplificamos o desenvolvimento em geral e o lançamento de novos serviços em particular, garantimos uma passagem mais rápida do checklist de serviço antes de implantar no produto.
Vamos verificar se atingimos nossos objetivos iniciais:
- Assegurar a relevância dos contratos descritos para os serviços, isto deverá acelerar a introdução de novos serviços e simplificar a comunicação entre equipas - Sim .
- HTTP ( event streaming) — .
- , .. Inner Source — .
- , — ( — Bitbucket).
- , — ( , , ).
- Go — ( ).
O leitor atento provavelmente já fez a pergunta: como os arquivos de modelo entram em nosso projeto? Agora os armazenamos em cada um de nossos projetos. Isso simplifica o trabalho diário, permite que você personalize algo para um projeto específico. Mas existe o outro lado da moeda: não há mecanismo de atualização centralizada de templates e entrega de novos recursos, principalmente relacionados à CI.
PS Se você gosta deste material, no futuro prepararemos um artigo sobre a arquitetura padrão de nossos serviços, diremos quais princípios usamos ao desenvolver serviços em Go.