Apostila completa de Go (Golang) para Backend e Cloud-Native
Go foi desenhado por engenharia da Google para escrever serviços de rede confiáveis, compiláveis rápido e fáceis de operar — e virou a língua da infraestrutura moderna: Docker, Kubernetes, Terraform, Prometheus são Go. Esta apostila vai da sintaxe minimalista ao modelo de concorrência CSP, à construção de serviços HTTP e gRPC de produção, ao runtime e ao profiling — com o olho no que empresas pedem em vagas de back-end, plataforma e SRE.
Por que Go existe e onde ele domina
Objetivo: entender a filosofia de design de Go, os problemas que ele resolve bem, e quando não é a ferramenta certa.
1.1 O problema que Go ataca
Go nasceu na Google (2009) para uma dor específica: escrever serviços de rede concorrentes, em times grandes, com builds lentos (C++), deploy complicado (dependências de runtime) e curva de aprendizado longa. A resposta foi uma linguagem deliberadamente pequena: poucos conceitos, uma forma canônica de fazer cada coisa, compilação quase instantânea, e um binário estático único sem dependências.
"Menos é exponencialmente mais." Go troca expressividade e recursos por legibilidade e uniformidade: qualquer programador Go lê código Go de qualquer outro sem esforço, porque quase não há dialetos. Não tem herança, generics foram adicionados só em 2022 e com parcimônia, e o gofmt impõe um estilo único para toda a comunidade. Isso frustra quem vem de linguagens ricas — e é exatamente o ponto: Go otimiza para o time e para o código que dura anos.
1.2 Os pontos fortes
| Característica | O que significa na prática |
|---|---|
| Compilação rápida | Build de um serviço grande em segundos; ciclo de feedback curto |
| Binário estático único | Deploy = copiar um arquivo; imagem Docker FROM scratch de poucos MB; sem "instalar runtime" |
| Concorrência de primeira classe | Goroutines (baratas, milhares) + channels; modelo CSP em vez de callback hell (Módulos 5–6) |
| Garbage collector de baixa latência | Pausas de sub-milissegundo; sem gerência manual de memória |
| Biblioteca padrão forte | net/http, encoding/json, crypto, testing — dá para ir longe sem framework |
| Ferramental oficial | go build/test/fmt/vet, race detector, pprof, módulos — tudo na caixa |
| Compilação cruzada trivial | GOOS=linux GOARCH=arm64 go build de qualquer máquina |
1.3 Onde Go domina
- Infraestrutura cloud-native: Docker, Kubernetes, Terraform, Prometheus, etcd, Consul, Vault, Grafana Loki, containerd — todos escritos em Go. Se você trabalha com plataforma/SRE, vai ler e escrever Go.
- APIs e microserviços de alta vazão e baixa latência.
- CLIs e ferramentas de linha de comando (binário único, cross-compile).
- Sistemas de rede: proxies, gateways, workers, daemons.
- Data plumbing: agentes, coletores, ETL leve.
1.4 Quando NÃO Go
- UI / front-end (WASM em Go existe mas é nicho).
- Ciência de dados / ML — o ecossistema é Python.
- Domínios que pedem forte modelagem de tipos (ADTs, pattern matching exaustivo) — Rust/Kotlin/F# encaixam melhor.
- Computação numérica pesada de baixíssimo nível — C/C++/Rust.
- Projetos onde a expressividade e o metaprogramming são o valor central.
Go é linha de vaga constante em plataforma, infra, SRE, DevOps tooling e em back-end de empresas cloud-native (fintechs, streaming, adtech). A demanda cresceu junto com Kubernetes. Pergunta de abertura: "Por que escolher Go para um serviço?" — a resposta cita concorrência barata, deploy de binário único, compilação rápida, e o ecossistema cloud-native; e reconhece que para CRUD simples qualquer linguagem serve — o diferencial aparece em concorrência e operação.
✏️ Exercício 1 — Go ou não?
Para cada projeto, Go é boa escolha? (a) um operador de Kubernetes que reconcilia recursos custom; (b) um pipeline de treino de modelo de visão computacional; (c) uma CLI multiplataforma para desenvolvedores; (d) um dashboard web interativo; (e) um proxy reverso de alta vazão com rate limiting.
Gabarito: (a) Sim — é o caso canônico; controller-runtime e client-go são Go. (b) Não — ecossistema é Python (PyTorch). (c) Sim — binário único + cross-compile é a killer feature de CLI em Go. (d) Não para o front (é JS/TS); Go pode servir a API por trás. (e) Sim — concorrência, latência baixa, e há blocos prontos (net/http/httputil, golang.org/x/time/rate).
A linguagem essencial
Objetivo: os blocos fundamentais — tipos, structs, slices e maps, funções com múltiplos retornos, ponteiros sem susto, zero values, e módulos.
2.1 O primeiro programa e o módulo
// go.mod define o módulo e as dependências // go mod init github.com/voce/servico package main import ( "fmt" "os" ) func main() { if len(os.Args) < 2 { fmt.Fprintln(os.Stderr, "uso: saudar <nome>") os.Exit(1) } fmt.Printf("Olá, %s\n", os.Args[1]) }
package main+func main()= executável. Outros pacotes são bibliotecas.- Exportação por maiúscula:
Nomeé público (visível fora do pacote);nomeé privado ao pacote. Não hápublic/private. - Import não usado = erro de compilação. Go é rígido de propósito.
gofmt(ougo fmt) formata; não há debate de estilo.
2.2 Tipos, structs e zero values
type Usuario struct { ID string Nome string Idade int Ativo bool } u := Usuario{ID: "u1", Nome: "Ana"} // Idade=0, Ativo=false (zero values) var v Usuario // tudo zero: "", "", 0, false
Go garante que toda variável começa "zerada" de forma útil: 0, "", false, nil, e uma struct com todos os campos zerados. Bibliotecas Go bem feitas fazem o zero value já ser um estado válido (var b bytes.Buffer já funciona; var mu sync.Mutex já está pronto). Não há "undefined" nem "null pointer" surpresa em valores — só em ponteiros, slices, maps, channels e interfaces explicitamente nil.
2.3 Slices e maps
// slice: janela dinâmica sobre um array; a estrutura de coleção que você usa 95% do tempo nums := []int{1, 2, 3} nums = append(nums, 4) // append pode realocar; sempre reatribua sub := nums[1:3] // [2 3] — compartilha o array subjacente! // map: tabela hash idade := map[string]int{"ana": 28} idade["beto"] = 31 v, ok := idade["carol"] // ok=false se ausente; v é o zero value delete(idade, "ana")
- Slices têm comprimento (
len) e capacidade (cap);appendalém da capacidade realoca — por isso sempres = append(s, x). - Um sub-slice compartilha memória com o original — cuidado ao mutar.
- Iterar map não tem ordem garantida (é aleatória de propósito).
- Acessar map
nilpara leitura é ok (retorna zero); escrever num mapnildá panic — inicialize commakeou literal.
2.4 Funções e múltiplos retornos
func dividir(a, b float64) (float64, error) { if b == 0 { return 0, fmt.Errorf("divisão por zero") } return a / b, nil } q, err := dividir(10, 2) if err != nil { // trate o erro AQUI, agora (Módulo 4) }
O idioma valor, err := f() seguido de if err != nil é onipresente em Go — a linguagem não tem exceções para fluxo normal (Módulo 4).
2.5 Ponteiros (sem aritmética, sem medo)
func envelhecer(u *Usuario) { // recebe ponteiro → altera o original u.Idade++ } u := Usuario{Nome: "Ana", Idade: 28} envelhecer(&u) // & pega o endereço
- Go passa tudo por valor (cópia). Para mutar o original, passe
*T. - Sem aritmética de ponteiro (segurança).
nilpointer deref dá panic (recuperável). - Regra prática de receiver: use ponteiro (
func (u *Usuario) M()) se o método muta o struct ou se o struct é grande; senão valor. Seja consistente dentro do mesmo tipo.
Perguntas de screening: "O que é um zero value?", "Diferença entre array e slice; o que append pode fazer?" (realocar → sempre reatribuir; sub-slices compartilham memória), "Value ou pointer receiver?", "Por que iterar um map dá ordem diferente a cada vez?" (intencional, para não criar dependência de ordem).
✏️ Exercício 2 — O bug do slice
Este código imprime o quê e por quê? a := []int{1,2,3,4,5}; b := a[1:3]; b = append(b, 99); fmt.Println(a, b)
Gabarito: Imprime [1 2 3 99 5] [2 3 99]. b tem len=2, cap=4 (compartilha o array de a a partir do índice 1). O append ainda cabe na capacidade, então não realoca — escreve 99 na posição do array subjacente que corresponde a a[3], sobrescrevendo o 4. Lição: sub-slices compartilham memória; se você precisa isolar, use append([]int{}, a[1:3]...) ou o "full slice expression" a[1:3:3] para limitar a capacidade.
Interfaces e composição
Objetivo: entender as interfaces implícitas de Go, por que "interfaces pequenas" é a regra, e como composição por embedding substitui herança.
3.1 Interfaces implícitas
Em Go, um tipo satisfaz uma interface só por ter os métodos certos — não há implements. Isso desacopla: a interface pode ser definida onde é consumida, mesmo sem o autor do tipo saber que ela existe.
type Notificador interface { Notificar(msg string) error } type EmailSender struct{ /* ... */ } func (e EmailSender) Notificar(msg string) error { /* ... */ return nil } // EmailSender satisfaz Notificador automaticamente — nenhuma declaração func Alertar(n Notificador, msg string) error { return n.Notificar(msg) // funciona com qualquer coisa que tenha Notificar }
3.2 Interfaces pequenas
As interfaces mais poderosas da stdlib têm um método: io.Reader (Read), io.Writer (Write), fmt.Stringer (String), error (Error). Isso permite compor: um gzip.Writer envolve um io.Writer, que pode ser um arquivo, uma resposta HTTP, um buffer... Prefira várias interfaces pequenas a uma "God interface" com 15 métodos.
3.3 Aceite interfaces, retorne structs
Idioma clássico: uma função deve aceitar a interface mais estreita que precisa (flexibilidade para quem chama) e retornar o tipo concreto (o chamador vê tudo que o tipo oferece e você não trava a API numa interface). Exceção comum: retornar error (interface) e construtores que retornam interface por design de plugin.
3.4 Composição por embedding
type Logger struct{ /* ... */ } func (l *Logger) Info(msg string) { /* ... */ } type Servico struct { *Logger // embedding: Servico "ganha" o método Info db *sql.DB } s := &Servico{Logger: &Logger{}, db: db} s.Info("iniciado") // promovido do Logger embutido
- Embedding promove os campos e métodos do tipo embutido — parece herança, mas é has-a, não is-a. Você pode "sobrescrever" definindo um método de mesmo nome no tipo externo.
- Não há hierarquia de classes, nem
super, nem polimorfismo por herança. Polimorfismo é só via interface. - Padrão útil: embutir uma interface num struct para satisfazê-la parcialmente e implementar só o que muda.
3.5 Type assertion e type switch
func descrever(v any) string { // any == interface{} switch x := v.(type) { case int: return fmt.Sprintf("int %d", x) case string: return "string " + x case Notificador: return "sabe notificar" default: return "desconhecido" } }
Use com parcimônia — any + type switch abandona a segurança de tipos. Boa para desserialização, plugins, e casos genuinamente dinâmicos.
Perguntas: "Como funcionam interfaces em Go?" (implícitas, satisfeitas por estrutura, definidas onde consumidas), "Por que interfaces pequenas?", "Go tem herança?" (não — composição por embedding; polimorfismo só via interface), "O que é 'aceite interfaces, retorne structs'?", "Quando um valor de interface é nil?" (a pegadinha: uma interface com tipo concreto mas ponteiro nil não é == nil).
✏️ Exercício 3 — A pegadinha do nil
Por que este código imprime não-nil? type E struct{}; func (e *E) Error() string { return "x" }; func faz() error { var e *E = nil; return e }; func main() { if faz() != nil { fmt.Println("não-nil") } }
Gabarito: Um valor de interface tem dois componentes: o tipo dinâmico e o valor. faz() retorna uma interface error cujo tipo dinâmico é *E e cujo valor é nil. A interface só é == nil quando ambos são nil. Aqui o tipo é *E (não-nil), então a interface não é nil, mesmo o ponteiro sendo nil. Lição: nunca retorne uma variável de ponteiro concreto tipada como error; retorne nil literal quando não há erro.
Erros como valores
Objetivo: o modelo de erros de Go — sem exceções para fluxo normal — com wrapping, inspeção (Is/As), quando usar panic, e defer/recover.
4.1 A filosofia
Em Go, erros são valores comuns retornados pela função, não um mecanismo de fluxo separado. Você lida com eles explicitamente, ali, na hora. Isso é verboso (if err != nil por toda parte) mas torna o caminho de falha visível e local — você vê exatamente o que pode dar errado e o que é feito a respeito.
type error interface { // a interface embutida — só isso Error() string }
4.2 Criar e enriquecer erros
// erro simples return errors.New("conexão recusada") return fmt.Errorf("usuário %s não encontrado", id) // wrapping: adiciona contexto SEM perder o erro original (o %w) if err != nil { return fmt.Errorf("buscar pedido %s: %w", id, err) } // resultado: "buscar pedido o1: consultar db: connection reset"
4.3 Inspecionar erros: errors.Is e errors.As
| Estratégia | Como | Quando |
|---|---|---|
| Sentinel error | Um valor exportado (var ErrNotFound = errors.New("not found")); comparar com errors.Is(err, ErrNotFound) | Condições bem conhecidas do domínio (não encontrado, sem permissão) |
| Tipo de erro | Um struct que implementa error; extrair com errors.As(err, &target) para ler campos | O erro carrega dados úteis (campo inválido, código HTTP, retryable?) |
| Opaco | Só err != nil; a mensagem é para humano/log | O chamador não precisa distinguir — a maioria dos casos |
if errors.Is(err, sql.ErrNoRows) { return nil, ErrPedidoNaoEncontrado // traduz para o erro do seu domínio } var ve *ValidationError if errors.As(err, &ve) { return respostaHTTP(422, ve.Campos) // ve agora aponta para o erro tipado }
4.4 Panic e recover
- panic é para o irrecuperável e o "nunca deveria acontecer": bug de programação, invariante violada, erro na inicialização de um programa. Não é o
throwde outras linguagens. - Não use panic para erro esperado (arquivo não existe, input inválido, timeout) — isso é
error. - recover (dentro de um
defer) captura um panic e evita derrubar o processo. Uso legítimo: o topo de um handler HTTP / worker, para que o panic de uma requisição não mate o servidor — logando e devolvendo 500.
func comRecover(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if p := recover(); p != nil { slog.Error("panic no handler", "panic", p) http.Error(w, "erro interno", 500) } }() next.ServeHTTP(w, r) }) }
4.5 defer
defer agenda uma chamada para quando a função retornar (em ordem LIFO). Uso canônico: liberar recursos junto de onde são adquiridos, para não esquecer em nenhum caminho de saída.
f, err := os.Open(path) if err != nil { return err } defer f.Close() // roda ao sair da função, sempre // ... use f livremente; todo return já fecha
Perguntas: "Como Go trata erros e por que não tem exceções?" (erros são valores; caminho de falha explícito e local), "Diferença entre errors.Is e errors.As" (Is compara com um sentinel; As extrai um tipo), "Quando usar panic?" (irrecuperável / bug; nunca para erro esperado; recover no topo de handler), "O que %w faz?" (embrulha o erro preservando a cadeia para Is/As).
✏️ Exercício 4 — Projete os erros
Um repositório de pedidos: GetByID pode não encontrar o pedido, ou falhar por erro de banco. Como você desenha os erros para que o handler HTTP consiga responder 404 vs 500 sem acoplar-se ao banco?
Gabarito: No repositório: definir var ErrNotFound = errors.New("pedido não encontrado") no pacote de domínio; em GetByID, quando errors.Is(err, sql.ErrNoRows), retornar fmt.Errorf("get pedido %s: %w", id, ErrNotFound); para outros erros de banco, fmt.Errorf("get pedido %s: %w", id, err) (opaco). No handler: if errors.Is(err, domain.ErrNotFound) { 404 } else if err != nil { log; 500 }. O handler nunca importa database/sql — o repositório traduz.
Concorrência I — goroutines e channels
Objetivo: o modelo CSP de Go — goroutines baratas, channels para comunicação, select — e as primitivas de sincronização quando comunicar não basta.
5.1 Goroutines
Uma goroutine é uma função executando concorrentemente, gerida pelo runtime de Go (não pelo SO). Custa ~2 KB de stack inicial (que cresce sob demanda), então você pode ter centenas de milhares. O runtime as multiplexa sobre poucos threads do SO (Módulo 9).
go processar(pedido) // dispara; main NÃO espera // problema: se main() retorna, o programa acaba e a goroutine morre. // você precisa de um mecanismo de sincronização.
5.2 Channels
Um channel é um tubo tipado por onde goroutines enviam e recebem valores, com sincronização embutida. É o mecanismo de comunicação idiomático.
ch := make(chan int) // sem buffer: send bloqueia até alguém receber buf := make(chan int, 10) // com buffer 10: send bloqueia só quando cheio go func() { ch <- 42 // envia }() v := <-ch // recebe (bloqueia até ter valor) close(ch) // fecha; receber de canal fechado retorna zero, ok=false for v := range ch { // itera até o canal ser fechado _ = v }
Em vez de várias goroutines acessando a mesma variável com locks, passe a propriedade do dado por um channel: quem tem o valor no channel é quem pode mexer nele. Isso elimina classes inteiras de bugs de concorrência. Nem sempre é possível/prático — daí as primitivas de sync — mas é o instinto padrão em Go.
5.3 select
select { case v := <-entrada: processar(v) case resultado <- calcular(): // enviou case <-time.After(2 * time.Second): return errTimeout // timeout idiomático case <-ctx.Done(): return ctx.Err() // cancelamento (Módulo 6) }
select espera em vários channels ao mesmo tempo e age no primeiro que estiver pronto (aleatório se vários). É o coração de timeouts, cancelamento e multiplexação.
5.4 Quando channels não bastam: o pacote sync
| Primitiva | Uso |
|---|---|
sync.Mutex / RWMutex | Proteger uma seção crítica / estrutura compartilhada. RWMutex permite múltiplos leitores. |
sync.WaitGroup | Esperar N goroutines terminarem (Add, Done, Wait) |
sync.Once | Executar uma inicialização exatamente uma vez (lazy singleton) |
sync/atomic | Contadores e flags lock-free (atomic.Int64, CompareAndSwap) |
sync.Map | Map concorrente para padrões específicos (muita leitura, chaves estáveis) — raro; geralmente map + RWMutex |
5.5 O race detector
Duas goroutines acessando a mesma memória sem sincronização, com pelo menos uma escrevendo = data race — comportamento indefinido. Go tem um detector embutido: go test -race, go run -race. Rode-o no CI. Ele não pega tudo (só o que executou), mas pega muito.
Concorrência é o tema de entrevista de Go: "O que é uma goroutine e como difere de um thread?" (leve, gerida pelo runtime, multiplexada sobre threads), "Channel com e sem buffer", "Explique select", "Quando Mutex em vez de channel?" (proteger estado compartilhado simples; channels para transferir propriedade/coordenar), "O que é um data race e como você o encontra?" (-race).
✏️ Exercício 5 — Some em paralelo com segurança
Você tem 1000 URLs e quer baixar todas, somando o total de bytes. Esboce a solução com goroutines. Onde está o risco de race e como evitar?
Gabarito (esboço): Um sync.WaitGroup para esperar todas; limitar concorrência com um channel-semáforo sem := make(chan struct{}, 20) (adquire antes, libera com defer). Para somar: não incrementar uma variável total compartilhada direto (race) — use atomic.Int64.Add, OU cada goroutine envia seu resultado num channel e uma goroutine coletora soma, OU use errgroup (Módulo 6) e some no retorno. Rodar os testes com -race para confirmar.
Concorrência II — context e padrões
Objetivo: propagar cancelamento e deadlines com context.Context, aplicar os padrões de concorrência estruturada, e evitar goroutine leaks.
6.1 context.Context
Context carrega cancelamento, deadline/timeout e valores de escopo de requisição através da árvore de chamadas. Convenção: é o primeiro parâmetro de qualquer função que faz I/O, chamada de rede, ou trabalho longo — func Buscar(ctx context.Context, id string) (...).
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second) defer cancel() // SEMPRE — libera recursos do context row := db.QueryRowContext(ctx, query, id) // a query é abortada se ctx expira // numa goroutine que faz trabalho longo: select { case <-ctx.Done(): return ctx.Err() // context.Canceled ou context.DeadlineExceeded case res := <-worker: return res }
- O
ctxde uma requisição HTTP (r.Context()) é cancelado quando o cliente desiste — propague-o para não desperdiçar trabalho. context.WithValuesó para dados verdadeiramente de escopo de requisição (trace id, usuário autenticado) — não para passar parâmetros opcionais (anti-padrão).- Sempre chame o
cancel()retornado (viadefer), mesmo que o timeout não dispare — senão vaza.
6.2 Padrões de concorrência
| Padrão | Ideia |
|---|---|
| Worker pool | N goroutines consomem tarefas de um channel; limita a concorrência e o uso de recursos |
| Fan-out / fan-in | Distribuir trabalho para várias goroutines (fan-out) e juntar os resultados num channel (fan-in) |
| Pipeline | Estágios ligados por channels: gerar → transformar → filtrar → gravar, cada um uma goroutine |
| errgroup | golang.org/x/sync/errgroup: roda N funções concorrentes, cancela todas se uma falhar, e retorna o primeiro erro. É o jeito idiomático moderno de "faça essas 5 coisas em paralelo e me diga se alguma quebrou" |
import "golang.org/x/sync/errgroup" g, ctx := errgroup.WithContext(ctx) g.SetLimit(10) // no máximo 10 concorrentes for _, u := range urls { u := u g.Go(func() error { return baixar(ctx, u) // se retornar erro, ctx é cancelado p/ as outras }) } if err := g.Wait(); err != nil { // espera todas; primeiro erro return err }
6.3 Goroutine leaks
Uma goroutine que bloqueia para sempre (num channel que ninguém vai fechar, num send sem receptor) nunca é coletada — vaza memória e recursos. Causas e prevenções:
- Sempre haja um caminho de saída: toda goroutine deve terminar quando o
ctxé cancelado ou o channel de entrada é fechado. - Quem escreve fecha o channel (nunca o leitor); um channel só é fechado uma vez.
- Cuidado com
selectsemcase <-ctx.Done()num loop infinito. - Ferramentas:
goleak(Uber) nos testes;pprofmostra a contagem de goroutines em produção — se cresce sem parar, é leak.
6.4 Graceful shutdown
srv := &http.Server{Addr: ":8080", Handler: mux}
go func() { srv.ListenAndServe() }()
// espera SIGINT/SIGTERM
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
<-ctx.Done()
// dá 15s para as requisições em andamento terminarem
shutCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
srv.Shutdown(shutCtx)
Perguntas sénior: "Para que serve context.Context e por que é o primeiro parâmetro?", "O que é um goroutine leak e como preveni-lo?" (sempre um caminho de saída via ctx/close), "Como você limita concorrência ao chamar uma API externa?" (worker pool / semáforo / errgroup.SetLimit), "Como fazer graceful shutdown de um servidor HTTP?".
✏️ Exercício 6 — Ache o leak
Este código vaza uma goroutine em algum caso. Qual e por quê? func first(ctx context.Context, chs ...<-chan int) int { out := make(chan int); for _, c := range chs { go func(c <-chan int) { out <- <-c }(c) }; return <-out }
Gabarito: A função retorna assim que a primeira goroutine consegue enviar em out. As demais goroutines ficam bloqueadas para sempre tentando out <- ... num channel sem buffer que ninguém mais lê — leak (uma por channel restante). Correções: dar buffer suficiente a out (make(chan int, len(chs))) para que os sends não bloqueiem; e/ou usar select com case <-ctx.Done() nas goroutines para que o cancelamento as libere. O idioma correto costuma combinar buffer + context.
Construindo serviços HTTP
Objetivo: montar um serviço de produção com a stdlib — roteamento, middleware, logging estruturado, banco de dados e testes — sem depender de um framework pesado.
7.1 net/http e o roteador da stdlib
Desde o Go 1.22, o http.ServeMux da biblioteca padrão faz roteamento por método e path params — muitos serviços não precisam mais de um roteador externo (chi, gorilla/mux, gin) para o básico.
mux := http.NewServeMux() mux.HandleFunc("GET /pedidos/{id}", getPedido) mux.HandleFunc("POST /pedidos", criarPedido) func getPedido(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") p, err := svc.Get(r.Context(), id) if errors.Is(err, domain.ErrNotFound) { problema(w, 404, "pedido não encontrado"); return } if err != nil { slog.Error("getPedido", "err", err); problema(w, 500, "erro interno"); return } writeJSON(w, 200, p) }
7.2 Middleware
Middleware em Go é uma função func(http.Handler) http.Handler — você encadeia: logging, recover (Módulo 4), autenticação, rate limit, CORS, tracing.
func comLog(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { início := time.Now() rec := &statusRecorder{ResponseWriter: w, status: 200} next.ServeHTTP(rec, r) slog.Info("http", "method", r.Method, "path", r.URL.Path, "status", rec.status, "dur_ms", time.Since(início).Milliseconds()) }) } handler := comLog(comRecover(comTrace(mux)))
7.3 Logging estruturado com slog
Desde o Go 1.21, log/slog é o logger estruturado da stdlib: níveis, atributos chave-valor, saída JSON para produção, e context-aware.
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
slog.SetDefault(logger)
slog.Info("pedido criado", "pedido_id", id, "valor_cents", 1990, "trace_id", tid)
// {"time":"...","level":"INFO","msg":"pedido criado","pedido_id":"o1",...}
7.4 Banco de dados
database/sql(stdlib) + um driver (pgxpara PostgreSQL, o mais recomendado;pgxtambém tem API nativa mais rica).- Sempre passe
ctx:QueryContext,ExecContext— cancelamento propaga ao banco. - Configure o pool:
SetMaxOpenConns,SetMaxIdleConns,SetConnMaxLifetime— o default é ilimitado e derruba o banco sob carga. - Geradores de código para SQL type-safe: sqlc (escreve SQL, gera Go tipado) é muito popular;
sqlxpara menos boilerplate; ORMs (GORM, Ent) existem mas a comunidade tende a SQL explícito. - Migrations:
golang-migrate,goose,atlas.
7.5 Testes
// table-driven test: o idioma de Go para cobrir muitos casos func TestValidarCPF(t *testing.T) { casos := []struct { nome string in string want bool }{ {"válido", "11144477735", true}, {"dígito errado", "11144477730", false}, {"curto", "123", false}, } for _, c := range casos { t.Run(c.nome, func(t *testing.T) { if got := ValidarCPF(c.in); got != c.want { t.Errorf("ValidarCPF(%q) = %v, quer %v", c.in, got, c.want) } }) } }
testingna stdlib;testify(assert/require/mock) é adição comum.net/http/httptestpara testar handlers sem subir servidor.- testcontainers-go para testes de integração com Postgres/Redis reais em Docker.
go test -race -cover ./...no CI; benchmarks comfunc BenchmarkX(b *testing.B).
Perguntas e teste prático: "Monte um endpoint com a stdlib", "O que é middleware em Go?" (func(http.Handler) http.Handler), "Como você estrutura logging?" (slog JSON + trace id no context), "Como configurar o pool de conexões e por quê?", "O que é um table-driven test?". Ter um serviço Go idiomático no portfólio (stdlib, sqlc, testcontainers) impressiona.
✏️ Exercício 7 — Ordene os middlewares
Você tem middlewares: recover, log, auth, rateLimit, trace. Em que ordem você os encadeia em volta do mux, e por quê?
Gabarito (uma boa ordem): trace(recover(log(rateLimit(auth(mux))))) — do mais externo ao mais interno: (1) trace primeiro para todo o resto ter o trace id; (2) recover logo em seguida para capturar panics de qualquer middleware interno e do handler; (3) log para registrar toda requisição com status final (inclusive as barradas depois); (4) rateLimit antes de gastar trabalho com auth; (5) auth por último antes do handler. Variações são defensáveis (alguns põem rateLimit por usuário depois do auth) — o importante é justificar.
Cloud-native com Go
Objetivo: empacotar e operar serviços Go como cidadãos de primeira classe da nuvem — Docker mínimo, gRPC, o ecossistema Kubernetes, e observabilidade.
8.1 Docker: imagem minúscula
# build multi-stage: compila num estágio, copia só o binário para outro FROM golang:1.23 AS build WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app ./cmd/api # imagem final: distroless (só libc + certs) ou scratch (nada) FROM gcr.io/distroless/static-debian12:nonroot COPY --from=build /app /app USER nonroot ENTRYPOINT ["/app"]
CGO_ENABLED=0⇒ binário estático puro ⇒ roda emscratch/distroless(imagem de poucos MB, superfície de ataque mínima, sem shell).-ldflags="-s -w"tira símbolos de debug (binário menor).- Rodar como
nonroot; sem shell = mais difícil de explorar.
8.2 gRPC em Go
Go é cidadão de primeira classe do gRPC. Fluxo: escrever o .proto → protoc (ou buf) gera o código Go → implementar a interface do servidor → registrar no grpc.Server. Interceptors são o "middleware" do gRPC. Ver apostila de API Design para o design do contrato; aqui, a mecânica:
lis, _ := net.Listen("tcp", ":50051") s := grpc.NewServer( grpc.ChainUnaryInterceptor(logInterceptor, recoverInterceptor), ) pb.RegisterOrderServiceServer(s, &orderServer{svc: svc}) s.Serve(lis)
Para expor a mesma API em REST sem reescrever: grpc-gateway (gera um proxy HTTP/JSON a partir do proto) ou Connect (protocolo que fala gRPC, gRPC-Web e HTTP/JSON no mesmo endpoint).
8.3 O ecossistema Kubernetes
client-go: o cliente oficial da API do Kubernetes — listar/criar/observar (watch) recursos.controller-runtime+ Kubebuilder / Operator SDK: o framework para escrever operators e controllers — o loop de reconciliação ("estado desejado vs atual") que é o coração do Kubernetes. Se você vai estender o K8s, é Go.- CRD + reconciler: você define um recurso custom (ex.:
Database) e um controller que o materializa em Deployments/Services/Secrets reais. - Bibliotecas de operação:
cobra(CLIs),viper(config),kustomize,helm(SDK).
8.4 Observabilidade
- Métricas:
prometheus/client_golang— expor/metrics; contadores/histogramas RED (rate, errors, duration). - Tracing: OpenTelemetry Go SDK — instrumentação automática de
net/http,database/sql, gRPC; propagatraceparent; exporta para Jaeger/Tempo/vendor. - Logs:
slogJSON (Módulo 7) com trace id, coletados por Loki/ELK. - Health: endpoints
/healthz(liveness) e/readyz(readiness — checa deps).
8.5 12-factor em Go
- Config por variável de ambiente (não arquivo commitado);
os.Getenv+ validação no boot, ouenvconfig. - Logs para stdout (o orquestrador coleta) — não escreva arquivos de log.
- Processo stateless; estado em Postgres/Redis/S3.
- Graceful shutdown em
SIGTERM(Módulo 6) — o K8s manda SIGTERM e espera oterminationGracePeriod. - Um binário, uma responsabilidade;
cmd/api,cmd/worker,cmd/migrate.
Para vagas de plataforma/SRE: "Como você faz uma imagem Docker mínima de um serviço Go?" (multi-stage + CGO_ENABLED=0 + distroless/scratch + nonroot), "O que é um operator do Kubernetes e por que se escreve em Go?" (controller-runtime, loop de reconciliação, client-go), "Como instrumentar um serviço Go?" (prometheus/client_golang + OTel + slog), "liveness vs readiness".
✏️ Exercício 8 — Diagnóstico de imagem
Um colega reclama que a imagem do serviço Go tem 900 MB e o scanner de segurança acusa dezenas de CVEs. O Dockerfile é FROM golang:1.23, COPY . ., RUN go build -o app, CMD ["./app"]. O que muda?
Gabarito: A imagem inclui o toolchain Go inteiro, o código-fonte, e um Debian completo com shell e utilitários (daí as CVEs). Solução: build multi-stage — estágio golang:1.23 AS build compila com CGO_ENABLED=0 -ldflags="-s -w"; estágio final FROM gcr.io/distroless/static-debian12:nonroot (ou scratch + copiar certs CA) recebe só o binário. Resultado: imagem de ~10–20 MB, sem shell, sem toolchain, superfície de CVE drasticamente menor, rodando como nonroot. Adicionar .dockerignore e cache de go mod download antes do COPY . ..
Runtime, performance e produção
Objetivo: entender o scheduler e o GC de Go, medir com pprof e benchmarks, usar generics com critério, e estruturar um repositório de produção.
9.1 O runtime: scheduler G-M-P
- G = goroutine, M = thread do SO, P = processador lógico (por padrão
GOMAXPROCS= nº de CPUs). O scheduler multiplexa muitos G sobre poucos M, cada M ligado a um P com sua fila de goroutines prontas. - Preempção: o runtime pode interromper uma goroutine (desde 1.14, até em loops apertados) para dar vez a outra — sem isso, uma goroutine CPU-bound travava o P.
- Work stealing: um P ocioso "rouba" goroutines da fila de outro — balanceamento automático.
- Chamada bloqueante do SO (I/O de arquivo, syscall): o M se desprende do P e outro M assume, então goroutines continuam rodando. I/O de rede usa o netpoller (epoll/kqueue) e não bloqueia thread.
9.2 Garbage collector e escape analysis
- GC concorrente, tricolor, mark-and-sweep, otimizado para latência (pausas < 1 ms típicas), não para throughput máximo.
GOGC(default 100) controla o trade-off: menor = GC mais frequente, menos memória; maior = menos GC, mais memória.GOMEMLIMIT(1.19+) põe um teto suave de memória — útil em contêiner.- Escape analysis: o compilador decide se um valor fica na stack (barato, sem GC) ou "escapa" para a heap (custa GC).
go build -gcflags='-m'mostra. Retornar ponteiro de uma variável local, guardar em interface, ou capturar em closure tende a fazer escapar. - Reduzir alocação (quando o profiler apontar): pré-alocar slices com
make([]T, 0, n), reusar buffers comsync.Pool, evitar conversões[]byte↔stringdesnecessárias, cuidar defmtem caminho quente.
9.3 Profiling e benchmarks
// benchmark func BenchmarkParse(b *testing.B) { for i := 0; i < b.N; i++ { _ = Parse(entrada) } } // go test -bench=. -benchmem → ns/op, B/op, allocs/op // pprof em produção: expor via net/http/pprof (rota protegida) import _ "net/http/pprof" // go tool pprof http://host/debug/pprof/profile?seconds=30 (CPU) // go tool pprof http://host/debug/pprof/heap (memória) // go tool pprof http://host/debug/pprof/goroutine (leaks!)
Fluxo: meça antes de otimizar. Benchmark do trecho suspeito com -benchmem; pprof de CPU e heap em carga real; o flame graph mostra onde o tempo/memória vai. Otimização sem profiling é chute.
9.4 Generics (1.18+) — com critério
func Map[T, U any](s []T, f func(T) U) []U { r := make([]U, len(s)) for i, v := range s { r[i] = f(v) } return r }
- Use para estruturas de dados genéricas (árvores, sets, filas) e utilitários de coleção type-safe (
slices,mapsna stdlib desde 1.21). - Não substitua interfaces por generics onde a interface expressa melhor a intenção (polimorfismo de comportamento). "Se dúvida, não use generics" ainda é a orientação da comunidade.
9.5 Estrutura de repositório e supply chain
- Layout:
cmd/<binário>/main.go(entrypoints finos);internal/(código privado ao módulo — o compilador impede import externo);pkg/só se você quer exportar bibliotecas; domínio no centro, adapters na borda (hexagonal — ver apostila de Arquitetura). - Dependências: Go Modules;
go.sumfixa hashes;GOFLAGS=-mod=readonlyno CI; revisargo.modem PR. - Segurança:
govulncheck(oficial) no CI — reporta só vulnerabilidades em código que você realmente chama;go vet,staticcheck,golangci-lint. - Reprodutibilidade: build com
-trimpath; versão embutida via-ldflags "-X main.version=..."ouruntime/debug.ReadBuildInfo.
Perguntas sénior: "Como o scheduler de Go funciona?" (G-M-P, work stealing, preempção, netpoller), "O GC de Go otimiza para quê?" (latência; GOGC, GOMEMLIMIT), "O que é escape analysis e como reduzir alocação?", "Como você investiga um serviço Go lento / com memória crescente?" (pprof CPU/heap/goroutine, benchmark com -benchmem), "Generics: quando usar?".
✏️ Exercício 9 — Memória crescendo
Um serviço Go em produção tem RSS que sobe continuamente e nunca cai, mesmo com tráfego constante. Como você investiga e quais as duas causas mais prováveis?
Gabarito: Investigar: go tool pprof /debug/pprof/heap (com -inuse_space e -alloc_space) para ver o que retém memória; /debug/pprof/goroutine para contar goroutines ao longo do tempo. Causas prováveis: (1) goroutine leak — a contagem de goroutines cresce sem parar; alguma goroutine bloqueia para sempre segurando referências (channel sem saída, sem ctx.Done()). (2) Estrutura que só cresce — um map/slice global usado como cache sem expiração/limite, ou um sync.Pool mal usado, ou um slice grande fatiado que mantém o array subjacente vivo. Menos comum: GOMEMLIMIT alto demais fazendo o GC adiar; RSS que não devolve ao SO (comportamento normal do runtime até certo ponto — olhar heap "in use", não só RSS).
Mercado de trabalho: roadmap, entrevistas e portfólio
Objetivo: converter o conteúdo dos módulos anteriores em aprovação — perfis, roadmap, banco de perguntas com respostas e projetos que geram entrevista.
10.1 Onde Go abre portas
| Perfil | Foco |
|---|---|
| Backend Engineer (Go) | APIs e serviços de alta vazão; fintech, adtech, streaming, marketplaces |
| Platform / Infra Engineer | Ferramentas internas, operators, controllers, CLIs; extensões do Kubernetes |
| SRE / DevOps | Automação, exporters, agentes; ler e corrigir o ecossistema CNCF (que é Go) |
| Cloud / Distributed Systems | Bancos, filas, proxies, sistemas de coordenação |
10.2 Roadmap de estudo (7–9 semanas)
| Semanas | Foco | Prática |
|---|---|---|
| 1 | Sintaxe, tipos, slices/maps, ponteiros, módulos (Módulos 1–2) | "A Tour of Go" + uma CLI pequena (parser de argumentos, faz algo útil) |
| 2 | Interfaces, composição, erros como valores (Módulos 3–4) | Refatorar a CLI com interfaces pequenas e um modelo de erros claro |
| 3–4 | Concorrência: goroutines, channels, select, context, errgroup, leaks (Módulos 5–6) | Um baixador concorrente com limite de concorrência, timeout e graceful shutdown |
| 5 | Serviço HTTP com a stdlib: mux 1.22, middleware, slog, pgx/sqlc, testes (Módulo 7) | API REST com Postgres, table-driven tests, httptest, testcontainers, CI com -race |
| 6 | Cloud-native: Docker distroless, gRPC, OTel, health checks, 12-factor (Módulo 8) | Empacotar o serviço; adicionar gRPC e métricas Prometheus + tracing |
| 7 | Runtime, pprof, benchmarks, generics, layout de repo, govulncheck (Módulo 9) | Profilar um trecho, otimizar alocação, medir o "antes/depois" |
| 8–9 | Consolidação, ler código de projeto grande (ex.: um controller), portfólio | Contribuição pequena num projeto Go OSS + estudo de caso publicado |
10.3 Banco de perguntas de entrevista (com a resposta que aprova)
Júnior/pleno — "O que é uma goroutine? Como difere de um thread?"
Uma função executando concorrentemente, gerida pelo runtime de Go (não pelo SO). Começa com ~2 KB de stack (cresce sob demanda), então dá para ter centenas de milhares; o runtime as multiplexa sobre poucos threads do SO (modelo G-M-P). Threads do SO custam ~1 MB e trocar de contexto é caro. A comunicação idiomática entre goroutines é por channels, não por memória compartilhada.
Pleno — "Channel com buffer vs sem buffer"
Sem buffer: send bloqueia até que alguém faça receive (rendez-vous, sincroniza as duas goroutines). Com buffer N: send só bloqueia quando o buffer está cheio; desacopla produtor e consumidor até N itens. Use sem buffer para sincronização/handoff; com buffer para absorver rajadas ou quando você sabe o limite. Buffer não "resolve" backpressure — só adia.
Pleno — "Para que serve context.Context?"
Propagar cancelamento, deadline/timeout e valores de escopo de requisição pela árvore de chamadas. Convenção: primeiro parâmetro de toda função com I/O. Quando o cliente HTTP desiste, r.Context() é cancelado e o trabalho a jusante (queries, chamadas de rede) é abortado — não se desperdiça CPU. Sempre chamar o cancel() retornado (defer). Não usar WithValue para parâmetros normais.
Pleno/sénior — "O que é um goroutine leak e como evitar?"
Uma goroutine que bloqueia para sempre (channel que ninguém fecha, send sem receptor, select sem ctx.Done()) — nunca é coletada, retém memória e recursos. Evitar: toda goroutine precisa de um caminho de saída (fechar o channel de entrada, ou case <-ctx.Done()); quem escreve fecha o channel; testar com goleak; monitorar a contagem de goroutines no pprof em produção.
Sénior — "Como o GC de Go funciona e o que você ajusta?"
Concorrente, mark-and-sweep, tricolor, otimizado para latência (pausas sub-ms). GOGC controla a frequência (trade-off memória × CPU de GC); GOMEMLIMIT (1.19+) põe um teto suave — importante em contêiner para o GC ser mais agressivo antes do OOM kill. Para reduzir pressão de GC: escape analysis (-gcflags=-m), pré-alocar slices, sync.Pool para buffers, evitar alocações no caminho quente — sempre guiado por pprof/benchmark.
Sénior — "Quando Mutex e quando channel?"
Mutex: proteger uma seção crítica curta ou uma estrutura compartilhada (um cache, um contador) — mais simples e rápido para "só me deixe atualizar isto com segurança". Channel: transferir propriedade de um dado, coordenar etapas de um pipeline, distribuir trabalho, sinalizar eventos/cancelamento. O lema é "compartilhe memória comunicando", mas Mutex não é derrota — o próprio time de Go usa os dois. atomic para contadores/flags simples.
Armadilha — "Go é lento porque tem GC / não tem generics de verdade"
Go troca alguns picos de performance e expressividade por tempo de compilação, simplicidade operacional e concorrência barata — e o GC de baixa latência raramente é o gargalo de um serviço de rede (o gargalo é I/O). Generics existem desde 1.18 e cobrem bem estruturas de dados; a filosofia é usá-los com parcimônia. Para o domínio de Go (serviços cloud-native), esses trade-offs são features, não defeitos.
10.4 Projetos de portfólio que geram entrevista
- Serviço idiomático completo (âncora): API REST + gRPC, Postgres via sqlc,
slog, middleware, graceful shutdown, table-driven tests + testcontainers, CI com-race/govulncheck, Dockerfile distroless, métricas Prometheus + tracing OTel. README explicando cada escolha. - Ferramenta de concorrência: um crawler/baixador com worker pool configurável,
errgroup, limite, timeout, retry, e um teste que prova ausência de leak (goleak). - Operator/controller de Kubernetes: um CRD simples (ex.: provisiona um recurso) com Kubebuilder — mostra domínio do padrão de reconciliação.
- CLI multiplataforma: com
cobra, cross-compile no CI, releases com goreleaser. - Estudo de performance: um trecho profilado e otimizado, com flame graphs e números de
-benchmemantes/depois.
10.5 Fontes para continuar
- Oficial: "A Tour of Go", "Effective Go", "Go Code Review Comments", o blog do Go, os release notes de cada versão.
- Livros: The Go Programming Language (Donovan & Kernighan); Learning Go (Jon Bodner) — atualizado, ótimo para idiomas modernos; 100 Go Mistakes and How to Avoid Them (Teiva Harsanyi).
- Concorrência: Concurrency in Go (Katherine Cox-Buday); os talks "Concurrency is not Parallelism" e "Go Concurrency Patterns" (Rob Pike).
- Prática: ler o código de um projeto CNCF de porte médio; Gophercises; os padrões de
golang-standards/project-layout(com senso crítico — não é oficial).
Quatro ideias sustentam Go: (1) simplicidade é uma feature — poucos conceitos, uma forma de fazer cada coisa, código que qualquer um lê; (2) erros são valores — o caminho de falha é explícito e local, não escondido em exceções; (3) concorrência por comunicação — goroutines baratas + channels + context, com sync quando comunicar não basta, e sempre um caminho de saída; (4) opere fácil — binário único, imagem mínima, 12-factor, pprof na caixa. É por isso que a nuvem moderna é escrita em Go.