Introdução
Bem-vindo!! Nesse workshop vamos aprender os fundamentos da linguagem e construir alguns exemplos, a idéia é partir do Hello, World e ir incrementalmente evoluindo até um servidor lidando com requests concorrentemente, logging e rastreabilidade! No final ainda vamos ter uma breve discussão sobre diferentes trade-offs entre linguagens (Tudo em menos de 2 horas 😅)
Vamos dividir em 3 arcos:
-
Arco 1: A linguagem
-
Configurar o ambiente
-
Escrever o primeiro código GO
-
Entender como fazer em GO o que você faz nas linguagens que já conhece
-
Entender o que são structs e como GO trata funções
-
-
Arco 2: O Servidor
-
Conseguir receber as primeiras requests
-
Complicando: integrar com um serviço externo
-
Descobrir o que está acontecendo e rastreando requests
-
Retrospectiva: O que aprendemos até aqui? 🤔
-
-
Arco 3: Os Números
-
Analisar velocidade e consumo de memória entre diferentes tecnologias para servidores web
-
Sendo crítico: Onde Go resolve e onde é um problema
-
💡 Dica: Você pode usar o Go Playground para executar pequenos trechos diretamente no navegador, embora para alguns exemplos vai ser necessário ter o ambiente local configurado.
Arco 1
Ambiente
O que precisamos?
Precisamos basicamente do GO instalado na máquina e de um cliente HTTP para fazermos requisições. Vamos lá?
Instalando o GO
A instalação do GO é bem simples, você pode instalar diretamente pelo binário seguindo a documentação oficial. Ou pelo gerenciador de pacotes da sua distribuição, por exemplo:
sudo apt install golang-go # Debian based
sudo pacman -S go # Arch based
Para testar se a instalação foi bem sucedida, rode o comando:
go version
Caso seja imprimido algum texto com a versão do GO, está tudo certo!
Clonando o repositório
Vamos clonar o repositório do workshop para termos acesso ao código fonte dos exemplos.
git clone https://github.com/Gustavo-maia-gst/goworkshop.git
Hello World
Seu primeiro programa em Go
Uma vez com o GO devidamente instalado, podemos partir para o nosso primeiro programa! Você pode criar um arquivo com o código abaixo:
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
e executá-lo com o comando:
go run <seu_arquivo>
Se tudo estiver correto, você verá a mensagem Hello, World! impressa no terminal!!
Parabéns, você acabou de executar o seu primeiro programa em Go! (Embora talvez não tenha entendido ainda 😅)
Curiosidade: A tradição do "rito de passagem" do hello world começou em 1978 com o livro 'The C Programming Language', livro escrito pelos criadores do C que introduzia a linguagem, desde então, surgiu a superstição.
Entendendo o código linha a linha
Linha: package main
Todos os programas GO começam com a declaração de um pacote. Pacotes são a forma de modularização da linguagem (parecido com os packages de java, aos familiarizados).
Não vamos nos aprofundar em pacotes aqui, mas eles são centrais para a organização semântica, gerenciamento de dependências e visibilidade (public, protected e private para os que já conhecem, aqui é diferente, não existem essas keywords, tipos e valores começando com letras maiúsculas são assumidos públicos e são acessíveis fora do pacote, aqueles começando com letras minúsculas são assumidos privados e são acessíveis por todos os arquivos do pacote, mas não fora dele).
Linha: import "fmt"
Aqui estamos apenas importando o pacote fmt, apesar do nome esquisito, fmt significa apenas format (a comunidade golang gosta de abreviações), é o pacote que tem funções de entrada e saída formatadas.
Entre essas funções está o Println, que usamos para imprimir a mensagem no terminal, ele seria o equivalente ao print do python ou o System.out.println do java, etc.
Linha: func main() {
Para a surpresa de ninguém, essa linha declara uma função chamada main, que o GO usa como entrypoint do binário.
Linha: fmt.Println("Hello, World!")
Alguns detalhes aqui:
- Não estamos chamando um método de um objeto, mas chamando uma função definida em um pacote, por isso o formato
pacote.Função(), note o nome começa com letra maiúscula, indicando que é pública. - Em GO não é possível fazer imports parciais, ou seja, não existe o conceito de importar apenas um método ou variável específica de um pacote, você sempre importa o pacote inteiro e usa o formato
pacote.Função(). - Strings são sempre entre aspas duplas, aspas simples são usadas para caracteres.
- As linhas são terminadas em
;, mas elas são opcionais 🥳, normalmente só são colocadas quando existem múltiplas instruções na mesma linha.
Linha: }
Supreendentemente, essa linha fecha o bloco da função main.
Go — Mini Guia Rápido
Variáveis
- Declaração explícita:
var x int = 10
- Declaração com inferência de tipo:
var x = 10 // tipo de x inferido como int
- Declaração curta (mais comum):
y := 42 // y declarado como int e inicializado com 42
- Múltiplas variáveis:
a, b := 1, "texto" // a é int = 1, b é string = "texto"
- Constantes:
const pi = 3.14 // constantes conhecidas em compile time
Tipos básicos
- Tipos primitivos:
int,float64,string,bool,rune,byte - Arrays e slices:
array := [3]int{1,2,3} // array com tamanho estático 3
slice_inicializado := []int{1,2,3} // slice dinâmico (listas do python, ou arrays do javascript)
slice_declarado := make([]int, 5) // slice pre-alocado com 5 elementos, mas vazio
var slice_nao_inicializado []int // slice vazio, referência para nil
- Maps:
m := map[string]int{"a":1, "b":2}
- nil: É o null ou None do GO, usado para ponteiro sem valor
💡 GO é mais estrito com a possibilidade de um valor ser nulo do que outras linguagens, por exemplo, uma variável que aponta pra struct, string, int, etc NUNCA pode ser nil, apenas tipos de referência como ponteiros, mapas e slices podem ser nil.
Funções
- Declaração simples:
func soma(a, b int) int {
return a + b
}
- Funções em GO são valores de primeira classe, podem ser atribuídas a variáveis:
f := func(x int) int { return x*2 }
fmt.Println(f(3)) // 6
- E passadas como parâmetros
func aplica(f func(int) int, x int) int {
return f(x)
}
Structs
- Definem tipos compostos:
type Pessoa struct {
Nome string
Idade int
}
- Instanciando e acessando campos:
p := Pessoa{Nome:"Gustavo", Idade:19}
fmt.Println(p.Nome) // Gustavo
- Métodos
func (p Pessoa) Saudacao() string {
return "Olá, " + p.Nome
}
func (p Pessoa) SaudacaoPara(outro string) string { // note que o nome do argumento vem sempre antes do tipo
return "Olá, " + outro + ", eu sou " + p.Nome
}
fmt.Println(p.Saudacao()) // Olá, Gustavo
fmt.Println(p.SaudacaoPara("Ana")) // Olá, Ana, eu sou Gustavo
💡 Embora chamados de métodos, eles são bem diferentes dos métodos de objetos em linguagens orientadas a objeto (GO não é orientado a objeto), você pode imaginar esses métodos apenas como sintax sugar para funções que atuam sobre uma struct específica. Aqui não existe overload de método ou os outros problemas trazidos por espalhar o código entre as definições dos tipos como acontece em linguagens OO tradicionais.
Expressões e operadores
- Aritméticos:
+ - * / % - Comparação:
== != < <= > >= - Lógicos:
&& || !
if x > 0 && x < 10 { ... } // sem parenteses desnecessários
Controle de fluxo
if,else,switch,for(não existewhile)- Loop clássico:
for i := 0; i < 5; i++ {
fmt.Println(i)
}
- Loop estilo “while”:
i := 0
for i < 5 {
fmt.Println(i)
i++
}
- loop estilo range do python
for i, item := range lista {
fmt.Println("Item " + string(item) + " na posição " + string(i))
}
Interfaces e polimorfismo
- Define comportamento que structs podem implementar:
type Saudavel interface {
Saudacao() string
}
func cumprimenta(s Saudavel) {
fmt.Println(s.Saudacao())
}
- Structs implementam interface implicitamente se tiverem os métodos exigidos
💡 As interfaces em GO são apenas contratos, não exigem declaração explícita, basta que a struct tenha os métodos com o mesmo nome e assinatura, isso torna a linguagem muito flexível e deixa tudo menos verboso, embora adicione um pouco de complexidade, são os trade-offs típicos de design de linguagens.
Arco 2
- Primeiras Requests
- Virando o jogo
- Integrando com um serviço externo
- Logging e rastreabilidade
- Restrospectiva
Sua primeira request
Noções básicas de redes
Interpretação dummy para comunicação HTTP
De forma muito, mas muito, simplificada, pode-se pensar em requisições HTTP como uma forma de um computador (client) executar um procedimento remotamente por meio de um formato pré-definido (request) em outro computador (host) e obter um resultado (response).
No nosso caso, o procedimento será executar função em GO e a resposta seu retorno, mas poderia ser retornar o conteúdo de um arquivo ou realizar qualquer operação que você possa imaginar...
DNS e portas: Nomes na internet
Toda vez que você acessa um site como www.google.com ou aquele que não deve ser nomeado (sigaa.ufcg.edu.br), por exemplo, o que acontece é que o seu computador, por meio do DNS (outra sigla que vamos tratar como caixa preta) realiza a tradução desse nome para um endereço IP (192.0.2.0 ou algo assim) que pode ser usado para encontrar um computador acessível em algum lugar do mundo.
Então o seu computador envia uma requisição para o computador do endereço encontrado direcionada a uma porta, sim, precisa estar direcionado a uma porta.
No nosso caso, o servidor estará rodando na nossa própria máquina, o nome será localhost. A porta você pode escolher seu número da sorte (desde que ele seja maior que 1024, essas costumam ter uma semântica pré-definida - por exemplo, porta 80 é a porta HTTP- e requerem níveis maiores de privilégio para serem usadas).
Para acessarmos o serviço rodando na máquina local na porta 8000, usamos localhost:8000.
Não vamos se aprofundar mais que isso em HTTP ou redes, mas caso você se interesse em conhecer mais, recomendo os capítulos sobre HTTP e TCP do livro do Kurose (Computer Networking: A Top-Down Approach)
"Watch and learn before you do." - Se comunicando com outros servidores
Nesta pequena seção vamos realizar algumas chamadas para APIs externas para tentar visualizar o que acontece e ter uma noção do que vamos construir.
Vamos escolher alguma API pública para fazer algumas chamadas, eu selecionei algumas legais, você pode escolher a que te chamar mais atenção, ou qualquer outra:
| API | Descrição |
|---|---|
| https://api.chucknorris.io/jokes/random | Retorna uma piada aleatória sobre o Chuck Norris. |
| https://rickandmortyapi.com/api/character/1 | Retorna dados sobre o Rick do Rick and Morty. |
| https://pokeapi.co/api/v2/pokemon/pikachu | Retorna dados sobre o Pikachu. |
Para testar no terminal, podemos usar o comando
curl, ficaria:curl <url>, se você não gostar muito do que ver e tiver ojqinstalado, pode rodarcurl <url> | jqe ver um resultado um pouco mais amigável.
gurl: Vamos fazer uma pequena cópia do curl, recebendo uma url e imprimindo o resultado da chamada na tela!
O código ficaria assim:
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
url := obterUrl() // obtém a url do parâmetro do terminal
resposta, err := http.Get(url) // A boa notícia é que GO já tem toda a parte complicada pronta, então podemos só usar!!
// Esse é o padrão de tratamento de erro em GO, as funções retornam um objeto de erro e você precisa checar ele!
// Pode parecer um pouco verboso no começo mais deixa tudo mais explicíto e previsível, qualidades muito subestimadas em um programa.
if err != nil {
fmt.Println("nao foi possível se comunicar com o servidor: ", err)
os.Exit(1)
}
// Só fazemos ler a reposta e imprimir na tela
bytesDaResposta := lerResposta(resposta)
fmt.Println(bytesDaResposta)
}
func obterUrl() string {
// os.Args tem todos os parâmetros que passamos mais o nome do arquivo na primeira posição
if len(os.Args) < 2 {
fmt.Println("Usagem inválida, passe uma url como parâmetro.")
os.Exit(1)
}
return os.Args[1] // os.Args[0] é o nome do arquivo, então estamos interessados no segundo elemento
}
func lerResposta(resp *http.Response) string {
body, err := io.ReadAll(resp.Body) // Aqui lemos a resposta do servidor para um array de bytes
resp.Body.Close() // Precisamos sempre fechar as "streams" em GO (isso seria discussão para um outro workshop)
if err != nil {
fmt.Println("Não foi possível ler a resposta: ", err)
os.Exit(1)
}
return string(body) // o que é uma string se não um array de bytes?
}
Você pode brincar um pouco com diferentes URLs, experimente o que acontece se passar uma url que não existe, ou tente trocar a URI (parte dps do .com/), o que será que acontecerá se você trocar o /pikachu por /squirtle? Ou o /1 do rick and morty por /2?
Repare no início da url
https://, esse é o indicador do protocolo que deve ser usado, é recomendado sempre prefixar com o protocolo. O s após http significa safe, indica que é uma variante com criptografia do http, nos nossos exemplos em localhost, vamos usarhttp://
Virando o jogo
Até então aprendemos uma noção sobre como a comunicação funciona, e fizemos nossas primeiras requisições para outros servidores. Chegou a hora de começar a ser o servidor!
Idéia geral
Assim como no caso do gurl que fizemos, o GO vai tratar de todos os detalhes de implementação para nós, o que vamos precisar fazer é basicamente registrar um callback para lidar com a requisição, ou seja, uma função que vai ser chamada todas as vezes que uma nova requisição chegar.
O código ficaria assim:
package main
import (
"fmt"
"net/http"
"os"
)
func main() {
http.HandleFunc("/", funcaoHandler) // Aqui dizemos que para todas as requisições que chegarem, funcaoHandler deve ser executada
// o primeiro parâmetro é o padrão de match, ou seja, a URI que a request deve ter para cair nesse handler, "/" significa todas
err := http.ListenAndServe("localhost:8000", nil) // Aqui é onde explicitamos inciamos o servidor, indicando em que porta ele deve inciar, o segundo parâmetro foge um pouco do escopo, mas seria um router
// O nosso stub de tratamento de erro
if err != nil {
fmt.Println("erro inciando o servidor: ", err)
os.Exit(1)
}
}
// Essa é a função que será executada em todas as requisições
func funcaoHandler(
writer http.ResponseWriter, // Aqui é onde escrevemos a resposta, é uma daquelas "streams"
request *http.Request, // Esse é o objeto da request, existem muitas informações aqui, mas vamos usar poucas
) {
helloEmBytes := []byte("Hello!\n")
writer.Write(helloEmBytes) // Por hora escrevemos um 'Hello!'
}
Tente alterar o código e ver o que acontece, e tente entender porquê, por exemplo, se você remover a linha do listenAndServe, o que vai acontecer? Tente entender o que aconteceu
Tente alterar também o código de funcaoHandler.
PRONTO já temos um servidor rodando e recebendo requisições! Você pode testar com o curl ou com o nosso gurl ou ainda abrir o navegador e verá a mensagem sendo impressa, lembrando que para acessar o servidor você usa localhost:
Agora os próximos passos vão ser adicionar comportamento no handler, os próximos exemplos vão usar esse server como base, é bom que você entenda todas as linhas presentes
Integrando com um serviço externo
Por hora o nosso servidor apenas retorna um singelo Hello!, vamos estender ele para retornar a resposta de uma das três APIs que vimos quando estávamos construindo o client a partir da URI.
A idéia vai ser:
- Caso a URI seja /chuck: retornar a piada aleatória
- Caso a URI seja /pokemon: retornar um pokemon aleatório (note que a API dos pokemons não tem essa funcionalidade)
- Caso a URI seja /rickandmorty: retornar um personagem do rick and morty aleatório (mesmo caso do de cima)
Antes de continuarmos, vamos isolar a lógica de fazer a requisição em um arquivo separado, esse é o mesmo código do client que estávamos usando encapsulado numa função, você pode chamar do que quiser:
package main
import (
"fmt"
"io"
"net/http"
"os"
)
// Esse é o mesmo código do GURL que fizemos encapsulado numa função retornando os bytes ao invés de imprimir eles
// copiado e colado de arco1/gurl.go
func enviarRequisicao(url string) []byte {
resposta, err := http.Get(url) // A boa notícia é que GO já tem toda a parte complicada pronta, então podemos só usar!!
if err != nil {
fmt.Println("nao foi possível se comunicar com o servidor: ", err)
os.Exit(1)
}
// Só fazemos ler a reposta e imprimir na tela
bytesDaResposta := lerResposta(resposta)
return bytesDaResposta
}
func lerResposta(resp *http.Response) []byte {
body, err := io.ReadAll(resp.Body) // Aqui lemos a resposta do servidor para um array de bytes
resp.Body.Close() // Precisamos sempre fechar as "streams" em GO (isso seria discussão para um outro workshop)
if err != nil {
fmt.Println("Não foi possível ler a resposta: ", err)
os.Exit(1)
}
return body // o que é uma string se não um array de bytes?
}
Integrando
Você talvez já deva imaginar como ficariam os handlers para cada uma das APIs agora. Ficaria algo assim:
// server v1
package main
import (
"fmt"
"math/rand"
"net/http"
"os"
"strconv"
)
func main() {
// Cadastramos os 4 patterns, ao receber uma requisição com /pokemon na URI, somente o handlerPokemon será chamado
http.HandleFunc("/", handlerDefault)
http.HandleFunc("/chuck", handlerChuck)
http.HandleFunc("/pokemon", handlerPokemon)
http.HandleFunc("/rickandmorty", handlerRickAndMorty)
err := http.ListenAndServe("localhost:8000", nil)
// Novamente o nosso stub de tratamento de erro
if err != nil {
fmt.Println("erro inciando o servidor: ", err)
os.Exit(1)
}
}
// Os 3 handlers fazem a mesma coisa, fazem a requisição para a API externa e retornam os resultados!
func handlerChuck(
writer http.ResponseWriter,
request *http.Request,
) {
// Podemos gerar um número aleatório para buscar a cada request
url := "https://api.chucknorris.io/jokes/random"
handleAPI(writer, url)
}
func handlerPokemon(
writer http.ResponseWriter,
request *http.Request,
) {
// Podemos gerar um número aleatório para buscar a cada request
randomNumber := rand.Intn(100) + 1
url := "hthttps://pokeapi.co/api/v2/pokemon/" + strconv.Itoa(randomNumber) // converte o num pra string e concatena
handleAPI(writer, url)
}
func handlerRickAndMorty(
writer http.ResponseWriter,
request *http.Request,
) {
// Podemos gerar um número aleatório para buscar a cada request
randomNumber := rand.Intn(100) + 1
url := "https://rickandmortyapi.com/api/character/" + string(randomNumber)
handleAPI(writer, url)
}
// Chamado por todos
func handleAPI(writer http.ResponseWriter, url string) {
resposta := enviarRequisicao(url)
writer.Write(resposta)
}
// Essa é a função que será chamada caso a URI não seja nenhuma das outras
func handlerDefault(
writer http.ResponseWriter,
request *http.Request,
) {
helloEmBytes := []byte("Oii, para acessar alguma API, use /chuck, /pokemon ou /rickandmorty!\n")
writer.Write(helloEmBytes)
}
Se você preferir separar o arquivo do client, você precisará passar esse arquivo como parâmetro para o go run também! Caso contrário ocorrerá erro
Simples, né? Essa é a nossa V1, mas ela tem um problema grave! Você consegue imaginar o que vai acontecer se ocorrer algum erro ao enviar a requisição para alguma API? Tente simular colocando a string errada e veja o que vai acontecer rsrs
Tratando erros
O problema é que o client está finalizando o processo ao encontrar o menor erro, isso não é muito interessante...,
Para isso, vamos seguir o padrão idiomático do GO, a função enviarRequisicao passa a retornar duas coisas, um []byte e um err indicando um possível problema que possa ter ocorrido.
O handlers agora fariam algo parecido como:
// server v2
handleAPI(writer http.ResponseWriter, url string) {
resposta, err := enviarRequisicao(url)
if err != nil {
writer.Write([]byte(err.Error()))
return
}
writer.Write(resposta)
}
(os arquivos em arco2/server_v2 tem as implementações atualizadas)
Lendo a resposta e montando payload
Podemos retornar por exemplo o tamanho do resultado em um outro campo.
Vamos retornar um JSON com dois campos, { tamanho: number; resultado: any }, mas como podemos fazer isso?
Para o tamanho, podemos apenas chamar o length
Para retornar o dado estruturado em json, fazemos um Marshal (ou serialização) de uma struct anonima combinando essas duas variáveis, o handleAPI ficaria assim:
// server v3
// Chamado por todos
func handleAPI(writer http.ResponseWriter, url string) {
respostaDaApi, err := enviarRequisicao(url)
if err != nil {
writer.Write([]byte(err.Error()))
}
// Aqui transformamos os bytes retornados em objeto estruturado para serializarmos depois
var respostaDaApiEmJson any
json.Unmarshal(respostaDaApi, &respostaDaApiEmJson) // o unmarshal vai montar uma struct com os campos do json
// Aqui tem um conceito novo, structs anonimas, parecidas com os types do typescript
// IMPORTANTE: os campos precisam começar com letra maiúscula, caso contrário
// seriam tratados como campos privados e não seriam serializados!
resposta := struct {
Tamanho int
Resposta any // isso significa que a tipagem desse campo é desconhecida
}{
Tamanho: len(respostaDaApi),
Resposta: respostaDaApiEmJson,
}
respostaJson, err := json.Marshal(resposta)
if err != nil {
writer.Write([]byte(err.Error()))
return
}
writer.Write(respostaJson)
Com isso nós temos um pequeno servidor com diversas coisas já funcionando! Temos roteamento de requests, integramos com serviços externos e trabalhamos serialização/desserialização.
Se você quiser testar como seu server lida com várias requests ao mesmo tempo, pode usar o script em arco2/requests.sh passando a quantidade de requests como parâmetro
Introduzindo o conceito de logging
Uma coisa que talvez tenha te incomodado, é que não temos informação sobre o que está acontecendo no servidor... Não sabemos dizer onde está a maior demora no processamento e esse tipo de coisa.
Esse problema é resolvido por meio de logging, isso é tão importante que o GO traz na sua stdlib um package para logging, que vamos usar para deixar o nosso servidor com uma cara um pouco mais profissional.
Para isso, importamos o package log, e loggamos informações relevantes com log.Default().. Uma coisa muito interessante do log, é que podemos criar objetos de logging com propriedades definidas, por exemplo, o snippet abaixo vai criar uma nova instância de logger e setar nela um prefixo, todas as chamadas subsequentes de log será prefixada com o texto passado!
// server v4
logger := log.New(os.Stdout, request.RequestURI, log.LstdFlags)
logger.SetPrefix(request.RequestURI + "\t")
Adicione alguns logs ao seu código para rastrear o que está sendo executado!. Tem um exemplo no arco2/server_v4/server_v4.go
No arquivo arco2/server_v4/server_gurl_v4.go foi adicionado um sleep de 2s para analisarmos melhor como o servidor está se comportando, você pode adicionar a linha: time.Sleep(2 * time.Second).
Um resultado que talvez você não estivesse esperando, é que todas as requests são tratadas juntas, isso se dá por conta do listenAndServe, ele vai criar uma nova goroutine para cada uma das requests, as nossas funções de handler são executadas dentro de goroutines! Pense em goroutines como threads virtuais muito leves.
Introduzindo rastreabilidade de requests
Um problema que vocês podem ter imaginado, é o volume de dados nos logs, isso cresce muito com a quantidade de usuários... Tornando quase impossível rastrear uma request, hoje tudo que temos no log é a mensagem e a rota que ela veio.
Uma possível solução para isso é o que chamamos de requestId, é uma string, normalmente um UUID, que serve para identificar unicamente uma request, desse modo, conseguimos rastrear toda a request a partir de uma filtragem automática.
Esse poder de rastreabilidade é muitíssimo importante no mundo real, imagine tentar descobrir o que causou um problema enfrentado por um cliente se seu servidor não tem nenhum tipo de log. Você estaria completamente de mãos atadas... Com os logs e a rastreabilidade com um requestId, nós temos o superpoder de descobrir exatamente o que aconteceu com uma request do passado.
Uma possível implementação seria algo assim:
// server_v4
// Chamado por todos
func handleAPI(writer http.ResponseWriter, request *http.Request, url string) {
requestId := smallID(12) // gera um ID aleatório
logger := log.New(os.Stdout, request.RequestURI, log.LstdFlags)
logger.SetPrefix("[" + requestId + "] " + request.RequestURI + "\t")
logger.Println("Requisição recebida")
logger.Println("Enviando requisição para API externa")
respostaDaApi, err := enviarRequisicao(url)
if err != nil {
writer.Write([]byte(err.Error()))
}
logger.Println("Resposta recebida")
// Aqui transformamos os bytes retornados em objeto estruturado para serializarmos depois
var respostaDaApiEmJson any
json.Unmarshal(respostaDaApi, &respostaDaApiEmJson) // o unmarshal vai montar uma struct com os campos do json
// Aqui tem um conceito novo, structs anonimas, parecidas com os types do typescript
// IMPORTANTE: os campos precisam começar com letra maiúscula, caso contrário
// seriam tratados como campos privados e não seriam serializados!
resposta := struct {
RequestId string
Tamanho int
Resposta any // isso significa que a tipagem desse campo é desconhecida
}{
RequestId: requestId,
Tamanho: len(respostaDaApi),
Resposta: respostaDaApiEmJson,
}
logger.Println("Montando resposta")
respostaJson, err := json.Marshal(resposta)
if err != nil {
writer.Write([]byte(err.Error()))
return
}
writer.Write(respostaJson)
logger.Println("Resposta enviada")
}
smallID é uma função que retorna uma string aleatória, a implementação dela também está no server_v4.
Com essas pequenas alterações, agora somos capazes de rastrear todo o lifecycle de uma request.
Restrospectiva
Até o momento nós já cobrimos muito chão:
- Aprendemos o básico da linguagem
- Aprendemos a enviar requisições pela internet
- Aprendemos a receber requisições pela internet
- Aprendemos a integrar diferentes serviços
- Aprendemos sobre roteamento de requests e um pouco sobre HTTP
- Aprendemos que GO consegue executar múltiplas coisas ao mesmo tempo de forma out of the box
- Aprendemos sobre logging e rastreabilidade
No final de tudo, temos um pequeno servidor com processamento concorrente de requests, integrado com outros serviços externos, com serialização e desserialização de objeto, com logging e rastreabilidade de requests. Isso é bastante coisa ein!
Isso encerra o nosso arco 2, no arco 3, vamos falar um pouco sobre trade offs
Arco 3
Benchmarks
Existem diversos benchmarks interessantes sobre diferentes linguagens na internet, vamos analisar alguns:
GO x NodeJs
Aqui vemos uma vantagem enorme sobre o nodejs, tanto em latência, quanto em consumo, as vantagens em questão de perfomance são bem claras!
GO x Java
Nos dois benchmarks temos uma vantagem de performance sobre o java também, porém aqui as diferenças são bem mais brandas, elas iriam se manifestar mais profundamente em testes com códigos reais, as aplicações java tem alguns problemas com localidade dos dados, alocações em heap o tempo inteiro e uma cadeia de chamadas que é naturalmente maior do que em GO por design, a orientação a objeto, especialmente no java, em sua forma mais raiz, adiciona muita complexidade de execução.
GO x Bunjs
Esse serve para mostrar como os benchmarks podem passar informações que podem levar ao engano as vezes, é sempre importante pensar criticamente e saber interpretar benchmarks
Sendo crítico
É importante sermos críticos sobre as tecnologias que usamos!!
Essa parte é reservada para uma conversa