Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 existe while)
  • 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

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:

APIDescrição
https://api.chucknorris.io/jokes/randomRetorna uma piada aleatória sobre o Chuck Norris.
https://rickandmortyapi.com/api/character/1Retorna dados sobre o Rick do Rick and Morty.
https://pokeapi.co/api/v2/pokemon/pikachuRetorna 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 o jq instalado, pode rodar curl <url> | jq e 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 usar http://

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