
[ad_1]
O Go 1.18 finalmente chegou e com ele vem seu próprio sabor de genéricos. Em um post anterior, analisamos a proposta aceita e mergulhamos na nova sintaxe. Para este post, peguei o último exemplo do primeiro post e o transformei em uma biblioteca de trabalho que usa genéricos para projetar uma API mais segura, dando uma boa olhada em como usar esse novo recurso em uma configuração de produção. Então, pegue uma atualização para o Go 1.18 e prepare-se para começar a usar nossos novos genéricos para realizar coisas que a linguagem não conseguia antes.
Antes de discutirmos como estamos usando genéricos na biblioteca, gostaria de fazer uma observação: os genéricos são apenas uma ferramenta que foi adicionada à linguagem. Como muitas ferramentas na linguagem, não é recomendado usar todas elas o tempo todo. Por exemplo, você deve tentar lidar com erros antes de usar panic
já que o último acabará saindo do seu programa. No entanto, se você não conseguir recuperar o programa após um erro, panic
pode ser uma opção perfeitamente bem. Da mesma forma, um sentimento tem circulado com o lançamento do Go 1.18 sobre quando usar genéricos. Ian Lance Taylor, cujo nome você pode reconhecer da proposta de genéricos aceitos, tem uma ótima citação em uma palestra dele:
Write Go escrevendo código, não projetando tipos.
Essa ideia se encaixa perfeitamente na filosofia “simples” do Go: fazer o menor trabalho para atingir nosso objetivo antes de evoluir a solução para ser mais complexa. Por exemplo, se você já se pegou escrevendo funções semelhantes a:
func InSlice(s string, ss []string) bool {
for _, c := range ss {
if s != c {
continue
}
return true
}
return false
}
E então você duplica esta função para outros tipos, como int
talvez seja hora de começar a pensar em codificar o comportamento mais abstrato que o código está tentando nos mostrar:
func InSlice[T constraints.Ordered](t T, ts []T) bool {
for _, c := range ss {
if s != c {
continue
}
return true
}
return false
}
Geral: não otimize para os problemas que você ainda não resolveu. Espere para começar a projetar tipos genéricos, pois seu projeto tornará as abstrações visíveis para você quanto mais você trabalhar com ele. Uma boa regra aqui é mantê-lo simples até que você não consiga.
Embora nós apenas discutimos como não devemos tentar projetar tipos antes de codificar e aprender as abstrações escondidas em nosso projeto, há uma área em que acredito que não podemos e não devemos deixar de projetar os tipos primeiro: design de API. Afinal, uma vez que nosso servidor começa a responder e aceitar corpos de solicitação de clientes, alterações descuidadas em qualquer um deles podem resultar em um aplicativo que não funciona mais. No entanto, a maneira como atualmente escrevemos manipuladores HTTP em Go tem um pouco de falta de tipos. Vamos analisar todas as maneiras pelas quais isso pode quebrar ou introduzir problemas sutilmente em nosso servidor, começando com um belo exemplo de baunilha:
func ExampleHandler(w http.RepsonseWriter, r *http.Request) {
var reqBody Body
if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
resp, err := MyDomainFunction(reqBody)
if err != nil {
// Write out an error to the client...
}
byts, err := json.Marshal(resp)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(byts)
w.WriteHeader(http.StatusCreated)
}
Apenas para esclarecer o que esse manipulador HTTP faz: ele ingere um corpo e o decodifica de JSON, o que pode retornar um erro. Em seguida, ele passa essa estrutura decodificada para MyDomainFunction
, que nos dá uma resposta ou um erro. Por fim, empacotamos a resposta de volta ao JSON, definimos nossos cabeçalhos e escrevemos a resposta para o cliente.
Separando a função: Alterando os tipos de retorno
Imagine uma pequena mudança no tipo de retorno do MyDomainFunction
função. Digamos que estava retornando esta estrutura:
type Response struct {
Name string
Age int
}
E agora ele retorna isso:
type Response struct {
FirstName string
LastName string
Age int
}
Assumindo que MyDomainFunction
compila, assim também nossa função de exemplo. É ótimo que ainda compile, mas isso pode não ser uma grande coisa, pois a resposta mudará e um cliente pode depender de uma determinada estrutura, por exemplo, não há mais um Name
campo na nova resposta. Talvez o desenvolvedor quisesse massagear a resposta para que parecesse a mesma, apesar da mudança para MyDomainFunction
. Pior ainda é que, uma vez que isso compila, não saberemos que isso quebrou algo até implantarmos e obtermos o relatório do bug.
Separando a função: Esquecer de retornar
O que acontece se esquecermos de retornar depois de escrevermos nosso erro ao desempacotar o corpo da solicitação?
var reqBody RequestBody
if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
Porque http.Error
faz parte de uma interface imperativa para lidar com respostas de volta para clientes HTTP, não faz com que o manipulador saia. Em vez disso, o cliente receberá sua resposta e seguirá seu caminho alegremente, enquanto a função do manipulador continua a alimentar um valor zero RequestBody
estrutura para MyDomainFunction
. Isso pode não ser um erro completo, dependendo do que seu servidor faz, mas provavelmente é um comportamento indesejado que nosso compilador não detectará.
Separando a função: Ordenando os cabeçalhos
Finalmente, o erro mais silencioso é escrever um código de cabeçalho na hora errada ou na ordem errada. Por exemplo, aposto que muitos leitores não notaram que a função de exemplo escreverá de volta um 200
código de estado em vez do 201
que a última linha do exemplo queria retornar. o http.ResponseWriter
A API tem uma ordem implícita que exige que você escreva o código do cabeçalho antes de chamar Write
e embora você possa ler alguma documentação para saber disso, não é algo que é imediatamente chamado quando fazemos push ou compilamos nosso código.
Dado que todos esses problemas (embora menores) existam, como os genéricos podem nos ajudar a nos afastar das falhas silenciosas ou atrasadas para evitar esses problemas em tempo de compilação? Para responder a isso, escrevi uma pequena biblioteca chamada Upfront. É apenas uma coleção de funções e assinaturas de tipo para aplicar genéricos a essas APIs fracamente tipadas no código do manipulador HTTP. Primeiro, os consumidores da biblioteca implementam esta função:
type BodyHandler[In, Out, E any] func(i BodyRequest[In]) Result[Out, E]
Como uma pequena revisão da sintaxe, esta função aceita três tipos para seus parâmetros: In
para o tipo que é a saída da decodificação do corpo, Out
para o tipo que você deseja retornar e E
o possível tipo de erro que você deseja retornar ao seu cliente quando algo der errado. Em seguida, sua função aceitará um upfront.BodyRequest
type, que atualmente é apenas um wrapper para a solicitação e o corpo da solicitação decodificado em JSON:
// BodyRequest is the decoded request with the associated body type BodyRequest[T any] struct { Request *http.Request Body T }
E por fim, o Result
tipo fica assim:
// Result holds the necessary fields that will be output for a response type Result[T, E any] struct { StatusCode int // If not set, this will be a 200: http.StatusOK value T err *E }
A estrutura acima faz a maior parte da mágica quando se trata de corrigir as partes sutis e inesperadas dos manipuladores HTTP vanilla. Reescrevendo um pouco nossa função, podemos ver o resultado final e trabalhar para trás:
func ExampleHandler[Body, DomainResp, error](in upfront.BodyRequest[Body]) Result[DomainResp, error] { resp, err := MyDomainFunction(in.Body) if err != nil { return upfront.ErrResult( fmt.Errorf("error from MyDomainFunction: %w"), http.StatusInternalServerError, ) } return upfront.OKResult( resp, http.StatusCreated, ) }
Eliminamos muito código, mas, esperamos, também eliminamos alguns dos “problemas” da função de exemplo original. Você notará primeiro que a decodificação e a codificação JSON são tratadas pelo upfront
pacote, então há alguns lugares a menos para esquecer return
‘s. Também usamos nosso novo Result
type para sair da função e recebe um código de status. o Result
type que estamos retornando tem um parâmetro de tipo para o que queremos enviar de volta do nosso manipulador. Isso significa se MyDomainFunction
alterar seu tipo de retorno, o manipulador falhará na compilação, informando que quebramos nosso contrato com nossos chamadores muito antes de git push
. finalmente, o Result
type também recebe um código de status, portanto, ele lidará com a ordenação de defini-lo no momento certo (antes de escrever a resposta).
E o que há com os dois construtores, upfront.ErrResult
e upfront.OKResult
? Estes são usados para definir os campos privados do pacote value
e err
dentro de Result
estrutura. Como eles são privados, podemos impor que qualquer construtor do tipo não possa definir ambos value
e err
ao mesmo tempo. Em outras linguagens, isso seria semelhante (definitivamente não o mesmo) a um tipo de ambos.
Este é um pequeno exemplo, mas com esta biblioteca, podemos obter feedback sobre problemas silenciosos em tempo de compilação, em vez de quando reimplantamos o servidor e recebemos relatórios de bugs dos clientes. E embora essa biblioteca seja para manipuladores HTTP, esse tipo de pensamento pode ser aplicado a muitas áreas da ciência da computação e áreas em que temos sido bastante negligentes com nossos tipos em Go. Com este blog e biblioteca, meio que reimplementamos a ideia de tipos de dados algébricos, que não vejo sendo adicionados ao Go em um futuro próximo. Mas ainda assim, é um bom conceito para entender: pode abrir sua mente para pensar sobre seu código atual de forma diferente.
Tendo trabalhado com esta biblioteca em um projeto de amostra, existem algumas áreas para melhorias que espero ver em patches futuros. A primeira é que não podemos usar parâmetros de tipo em aliases de tipo. Isso economizaria um monte de escrita e permitiria que os consumidores da biblioteca criassem seus próprios Result
type com um tipo de erro implícito em vez de ter que repeti-lo em todos os lugares. Em segundo lugar, a inferência de tipo é um pouco sem brilho. Isso fez com que o código resultante fosse muito detalhado sobre os parâmetros de tipo. Por outro lado, Go nunca abraçou a ideia de ser conciso. Se você estiver interessado no código-fonte da biblioteca, você pode encontrá-lo aqui.
Dito tudo isso, os genéricos são, em última análise, uma ferramenta muito legal. Eles nos permitem adicionar alguma segurança de tipo a uma API realmente popular na biblioteca padrão sem atrapalhar muito. Mas, como em qualquer outro, use-os com moderação e quando se aplicarem. Como sempre: mantenha as coisas simples até não poder mais.
[ad_2]
Source link