Go (frequentemente chamada de Golang) não foi criada por acaso. Nascida dentro dos laboratórios do Google em 2007 pelas mãos de lendas da ciência da computação — Robert Griesemer, Rob Pike e Ken Thompson (este último, um dos pais do Unix e da linguagem C, além de co-criador da codificação UTF-8) —, a linguagem surgiu para resolver problemas reais de engenharia de software em hiperescala.
Naquela época, o desenvolvimento de infraestrutura e serviços de rede enfrentava um dilema crônico. De um lado, o C++ compilava tão devagar que times inteiros perdiam horas só esperando o código terminar de compilar antes de conseguir testar qualquer coisa. Do outro, o Java consumia memória sem dó e empilhava abstrações sobre abstrações, tornando sistemas simples desnecessariamente complicados. E linguagens dinâmicas como Python, embora ágeis para escrever, não ofereciam nem a segurança de checar os tipos antes de rodar, nem a velocidade de execução exigida em produção. Você não precisa conhecer C++, Java ou Python a fundo para entender o ponto: cada uma resolvia uma parte do problema e criava outra no lugar — e foi exatamente essa lacuna que Go veio preencher.
Go foi desenhada como uma resposta pragmática: simplicidade sintática, ortogonalidade, compilação estática ultrarrápida e suporte nativo de primeira classe à concorrência.
Neste guia definitivo, mergulharemos nos fundamentos e mecanismos internos que tornam Go uma das linguagens mais eficientes do mundo para backend e infraestrutura moderna (sistemas como Docker, Kubernetes, Terraform e Prometheus são todos construídos em Go).
A premissa central de Go é: “Menos é exponencialmente mais”. Go possui apenas 25 palavras-chave reservadas. Não há herança de classes, não há sobrecarga de métodos, não há exceções não checadas (try/catch) e não há operadores ternários. A linguagem força o desenvolvedor a escrever código legível, explícito e previsível.
Diferente de linguagens baseadas em máquina virtual (JVM, CLR) ou interpretadores (Node.js, Python) — que precisam de um programa externo instalado na máquina para traduzir e rodar seu código —, Go compila diretamente para código de máquina nativo (machine code), o formato que o processador executa direto, sem intermediários. O binário final embute o seu próprio Runtime — um subsistema enxuto (geralmente de poucos megabytes) responsável por gerenciar a memória automaticamente através do Garbage Collector (um processo que roda em segundo plano e recicla a memória que seu programa já não usa mais, poupando você de liberá-la manualmente como se faz em C) e por escalonar as Goroutines (a unidade de concorrência leve de Go, que vamos explorar em detalhe mais adiante) através do Scheduler M.
Isso significa que você não precisa instalar nenhuma dependência no servidor de produção; basta copiar o binário executável e rodá-lo, tornando Go a escolha ideal para contêineres Docker mínimos (scratch ou alpine).
Dockerfile
1
FROM golang:1.22-alpine AS builder
2
WORKDIR /app
3
COPY . .
4
RUN CGO_ENABLED=0 GOOS=linux go build -o server ./cmd/api
Graças ao compilador de Go, compilar para outro sistema operacional ou arquitetura não exige ferramentas externas ou toolchains complexas de C. Basta definir as variáveis de ambiente GOOS (Target OS) e GOARCH (Target Architecture):
Terminal
1
# Compilar binário para Linux 64-bit a partir do Windows ou macOS
Ao desativar CGO com CGO_ENABLED=0, o compilador de Go gera um binário estático puro sem vínculos dinâmicos com a libc do sistema hospedeiro, garantindo que o executável funcione em qualquer distribuição Linux sem conflitos de versão de biblioteca.
Go introduziu no compilador uma regra estrita de visibilidade através de diretórios nomeados internal/. Qualquer pacote dentro de uma pasta internal/ só pode ser importado por pacotes que estejam na mesma árvore de diretórios pai.
1
meu-projeto/
2
├── cmd/
3
│ └── api/
4
│ └── main.go
5
├── internal/ <-- Pacotes privados do projeto
6
│ ├── auth/ <-- NINGUÉM de fora do repositório pode importar
7
│ └── database/
8
└── pkg/ <-- Código que bibliotecas externas podem importar
9
└── logger/
Se um projeto externo tentar importar github.com/empresa/meu-projeto/internal/auth, o compilador de Go recusará o build com o erro: use of internal package not allowed.
Antes de entender COMO isso funciona, entenda POR QUE isso importa: em várias linguagens, uma variável declarada mas não inicializada guarda “lixo” — o que sobrou daquele pedaço de memória antes de ser reutilizado — e ler esse valor por engano é fonte clássica de bugs difíceis de rastrear (e, historicamente, de falhas de segurança). Go elimina essa categoria inteira de problema por decreto: não existe valor indefinido (undefined) ou lixo de memória residual em variáveis não inicializadas. Ao declarar uma variável sem valor inicial, o compilador garante a atribuição do seu respectivo Zero Value — ou seja, um valor padrão previsível e seguro para aquele tipo:
Tipo
Zero Value
int, int64, float64, byte, rune
0 / 0.0
bool
false
string
"" (string vazia, nunca nil)
Ponteiros (*T), interface, slice, map, chan, func
nil
struct
Struct com todos os campos zerados
zerovalues.go
1
packagemain
2
3
import"fmt"
4
5
typeMetricsstruct {
6
RequestsCount int64
7
LatencyMs float64
8
IsActive bool
9
}
10
11
funcmain() {
12
var m Metrics
13
// m já está pronta e segura para uso imediato:
14
// {RequestsCount:0, LatencyMs:0, IsActive:false}
15
fmt.Printf("%+v\n", m)
16
}
Design Guideline
Em Go, uma das maiores diretrizes de design de bibliotecas é: “Faça o Zero Value ser imediatamente útil”. Por exemplo, sync.Mutex e bytes.Buffer não exigem construtores NewMutex(); basta declarar var mu sync.Mutex e chamar mu.Lock().
Constantes declaradas com const possuem precisão arbitrária e não recebem um tipo rígido até que sejam usadas em um contexto específico, permitindo máxima flexibilidade matemática:
constants.go
1
constMaxTimeout=5000// Constante inteira não tipada
2
3
funcmain() {
4
var f float64= MaxTimeout // Aceito diretamente sem cast
5
var i int64= MaxTimeout // Aceito diretamente sem cast
6
var d = MaxTimeout *2// Mantém precisão sem overflow prematuro
Funções em Go são cidadãs de primeira classe (first-class citizens): podem ser passadas como argumentos, retornadas por outras funções e atribuídas a variáveis.
Uma closure é simplesmente uma função que “lembra” das variáveis do lugar onde foi criada, mesmo depois que esse lugar (a função que a criou) já terminou de executar. É útil sempre que você quer uma função com um pedacinho de estado privado grudado nela — no exemplo abaixo, um contador que só aquela função gerada consegue enxergar e incrementar.
closures.go
1
packagemain
2
3
import"fmt"
4
5
// Função Variádica: aceita zero ou mais inteiros
6
funcSum(numbers...int) int {
7
total :=0
8
for _, n :=range numbers {
9
total += n
10
}
11
return total
12
}
13
14
// Closure: fábrica de geradores de ID com estado encapsulado
15
funcIDGenerator(prefixstring) func() string {
16
counter :=0
17
returnfunc() string {
18
counter++
19
return fmt.Sprintf("%s-%04d", prefix, counter)
20
}
21
}
22
23
funcmain() {
24
gen :=IDGenerator("USR")
25
fmt.Println(gen()) // USR-0001
26
fmt.Println(gen()) // USR-0002
27
28
vals := []int{10, 20, 30}
29
fmt.Println(Sum(vals...)) // Desempacotamento com '...' -> 60
30
}
6. Controle de Fluxo: if, switch e for no Go 1.22#
Go simplificou o controle de fluxo: existe apenas uma estrutura de repetição (for) e condicionais expressivas.
Em Go, os casos de um switch não continuam a execução para o próximo bloco (no fallthrough by default), eliminando a necessidade manual de break. O switch sem expressão (tagless switch) substitui longas cadeias de if/else if:
Go tem apenas a palavra-chave for, servindo como for tradicional, while e loop infinito:
loops.go
1
// 1. Estilo C tradicional
2
for i :=0; i <5; i++ {}
3
4
// 2. Estilo While
5
for active {}
6
7
// 3. Loop Infinito
8
for {
9
break
10
}
11
12
// 4. Go 1.22+: Range sobre inteiros
13
for i :=range5 {
14
fmt.Print(i, " ") // Imprime: 0 1 2 3 4
15
}
A Revolução do Go 1.22 no Escopo do For Range
Antes do Go 1.22, as variáveis do for range eram reutilizadas em todas as iterações (alocadas uma única vez no início do loop), o que causava o clássico bug de captura de referência em Goroutines e Closures. A partir do Go 1.22, cada iteração cria uma nova instância de variável, eliminando a necessidade de truques manuais como item := item!
go122_scoping.go
1
for _, item :=range items {
2
// Pré Go 1.22: era OBRIGATÓRIO fazer cópia local:
3
item := item
4
gofunc() {
5
process(item) // 100% seguro nativamente a partir do Go 1.22+!
Compreender a diferença entre Arrays e Slices é o divisor de águas entre um programador iniciante e um engenheiro de software sênior em Go.
Arrays: Tamanho Fixo e Passagem por Cópia de Valor#
Em Go, o tamanho faz parte do tipo do array. [3]int é um tipo completamente diferente de [5]int. Ao atribuir ou passar um array para uma função, todo o conteúdo é copiado na memória.
arrays.go
1
var a [3]int= [3]int{1, 2, 3}
2
b := a // Copia os 3 elementos inteiros
3
b[0] =999
4
fmt.Println(a[0]) // Imprime 1 (o array original permaneceu intacto)
Antes de entender COMO um slice é montado por dentro, entenda POR QUE isso importa na prática: essa estrutura interna é o que explica por que passar um slice gigante para uma função é praticamente grátis (você não está copiando os elementos, só um pequeno “controle remoto”), e também por que, às vezes, modificar um slice pode misteriosamente afetar outra variável que parecia não ter relação nenhuma com ele. Um Slice não armazena dados diretamente — quem guarda os dados de verdade é o Backing Array (o array real, escondido por trás do slice, na memória). O slice em si é apenas um Slice Header de 24 bytes (em sistemas 64-bit) composto por:
Data unsafe.Pointer: Ponteiro para o primeiro elemento no array subjacente (Backing Array).
Len int (8 bytes): Número atual de elementos acessíveis no slice.
Cap int (8 bytes): Capacidade total a partir do ponteiro até o fim do Backing Array.
Quando append() adiciona elementos além do Cap atual, o runtime de Go aloca um novo Backing Array maior (geralmente dobrando até certo limiar), copia os dados antigos e atualiza o ponteiro do Slice Header.
slices.go
1
// Ineficiente: causa múltiplas realocações no Heap e trabalho extra ao GC
2
var slow []int
3
for i :=0; i <10000; i++ {
4
slow =append(slow, i)
5
}
6
7
// Alta Performance: Pré-alocação evita alocações intermediárias
Ao fatiar um slice existente (sub := orig[0:2]), o novo slice compartilha o mesmo Backing Array. Se você der append() no sub, poderá sobrescrever dados do slice original! Para evitar isso, use a Full Slice Expression que limita a capacidade do slice derivado:
// sub compartilha o array e sua capacidade vai até o final (cap: 5)
4
// sub := original[0:2]
5
6
// Full Slice Expression: [low:high:max] -> len = high-low (2), cap = max-low (2)
7
subSafe := original[0:2:2]
8
9
// Agora, qualquer append em subSafe forçará a criação de um novo Backing Array,
10
// blindando 'original' contra mutações acidentais!
11
subSafe =append(subSafe, 99)
12
fmt.Println(original) // [1 2 3 4 5] - Intacto!
Repare que a única mudança de código foi acrescentar :2 no fatiamento ([0:2:2] em vez de [0:2]). Esse terceiro número trava o Cap do slice derivado no mesmo valor do Len, então, na próxima vez que alguém der append() nele, não sobra espaço livre no Backing Array compartilhado — Go é obrigado a criar um array novo, e o original fica protegido.
Toda vez que você acessa slice[i], Go verifica em tempo de execução se i realmente existe dentro do slice — é essa checagem que evita os bugs de “buffer overflow” (ler memória além do que deveria) tão comuns em linguagens como C. Essa verificação tem um custo, ainda que pequeno. O compilador de Go insere essas verificações de limite automaticamente para evitar leitura fora do array (index out of range). Escrever loops que permitam ao compilador provar matematicamente que os limites são sempre seguros resulta em eliminação de verificações (BCE, de Bounds Check Elimination) — ou seja, o compilador remove a checagem por saber, com certeza, que ela nunca falharia —, gerando loops em assembly com performance comparável a C puro, sem abrir mão da segurança do dia a dia.
Go possui ponteiros, mas não possui aritmética de ponteiros (diferente de C), o que previne vulnerabilidades de corrupção de memória.
&: Operador de referência (obtém o endereço de memória de uma variável).
*: Operador de desreferenciação (acessa ou modifica o valor contido no endereço) ou declaração de tipo ponteiro.
pointers.go
1
funcincrement(val*int) {
2
if val ==nil {
3
return// Previne Nil Pointer Dereference (Panic)
4
}
5
*val++// Altera diretamente o valor na memória original
6
}
7
8
funcmain() {
9
x :=10
10
increment(&x)
11
fmt.Println(x) // 11
12
}
Perigo do Nil Pointer Dereference
Tentar desreferenciar um ponteiro que aponta para nil (var p *int; *p = 5) causa um runtime panic: runtime error: invalid memory address or nil pointer dereference. Sempre verifique se o ponteiro é diferente de nil antes de utilizá-lo quando ele vier de parâmetros externos.
Entender como Go gerencia a memória no nível de máquina é fundamental para construir sistemas de alta performance. Se você vem de linguagens como Python, JavaScript ou Java, provavelmente nunca precisou pensar em onde uma variável mora fisicamente na memória — o Garbage Collector cuida disso nos bastidores e você nem percebe. Em Go essa distinção fica visível e é ela que separa código que aloca memória à toa (e sobrecarrega o GC sem necessidade) de código genuinamente rápido.
Stack (Pilha): Cada Goroutine possui sua própria pilha que começa com míseros 2KB a 4KB e cresce dinamicamente sob demanda. A alocação e liberação na Stack são praticamente instantâneas (apenas um decremento/incremento de ponteiro de pilha na CPU). Não gera trabalho para o Garbage Collector.
Heap: Área de memória compartilhada para dados cuja vida útil ultrapassa o retorno da função ou cujo tamanho não pôde ser determinado em tempo de compilação. Gerenciada pelo Garbage Collector (GC) concorrente Tri-color Mark-and-Sweep de Go.
Na prática: Stack é rápida e descartada automaticamente quando a função acaba; Heap é mais flexível, mas exige que o Garbage Collector fique de olho nela para saber quando pode liberá-la — e esse trabalho extra tem custo de CPU.
Antes de entender COMO o compilador decide isso, entenda POR QUE você deveria se importar: toda variável que acaba indo parar no Heap vira responsabilidade do Garbage Collector, que gasta tempo de CPU rastreando-a e, eventualmente, liberando-a. Código com poucas alocações no Heap tende a ser bem mais rápido — por isso ferramentas de profiling em Go sempre reportam quantas alocações um trecho de código está fazendo.
Em C ou C++, retornar o endereço de uma variável local (&x) resulta em comportamento indefinido (Dangling Pointer / Segfault, ou seja, um ponteiro “pendurado” apontando para memória que já foi destruída), pois o frame da função é destruído na saída.
Em Go, retornar &x é 100% seguro! O compilador executa a Escape Analysis durante o build — uma análise que rastreia se o endereço de uma variável “escapa” do escopo onde ela nasceu: se ele detectar que a variável sobrevive após o retorno da função, ele automaticamente aloca essa variável no Heap em vez da Stack, sem que você precise fazer nada manualmente.
escape.go
1
packagemain
2
3
typeUserstruct {
4
ID int
5
Name string
6
}
7
8
// u 'escapa' para o Heap porque seu ponteiro é retornado para fora
9
funcNewUser(idint, namestring) *User {
10
u :=User{ID: id, Name: name}
11
return&u // Seguro em Go! Compilador move para o Heap
12
}
13
14
// v NÃO escapa, permanece 100% na Stack (Zero overhead de GC)
15
funcCalculateSum(a, bint) int {
16
v := a + b
17
return v
18
}
Compare as duas funções: NewUser devolve &u, o endereço de uma variável local — como esse endereço “escapa” da função, u é promovida para o Heap. Já CalculateSum só devolve um valor simples (v, um int comum, não um endereço), então v nunca precisa sair da Stack e some sem custo algum assim que a função retorna.
Podemos inspecionar o que o compilador faz com a flag -gcflags="-m", que imprime exatamente quais variáveis foram promovidas ao Heap e por quê:
A instrução defer agenda a execução de uma função para o momento imediatamente posterior ao encerramento da função envolvente (seja por return normal ou por um panic). Pense nela como um post-it colado na porta: “antes de sair da sala, apague a luz” — não importa por qual caminho você saia, aquele lembrete sempre roda no final.
// CORRETO: Usar closure anônima para avaliar na saída:
15
deferfunc() {
16
fmt.Println("Duração real:", time.Since(start))
17
}()
18
19
time.Sleep(100* time.Millisecond)
20
}
Armadilha do Defer dentro de Loops Longos
O defer só executa quando a função termina, e não quando o bloco do loop termina! Colocar defer f.Close() dentro de um loop de 1 milhão de iterações manterá 1 milhão de arquivos abertos simultaneamente até a função retornar, estourando os descritores de arquivo do sistema operacional (Too many open files).
defer_loop_fix.go
1
// Solução: Envolver o corpo em uma função anônima que executa a cada ciclo
2
for _, filename :=range files {
3
func() {
4
f, err := os.Open(filename)
5
if err !=nil { return }
6
defer f.Close() // Fecha com segurança a cada iteração!
7
process(f)
8
}()
9
}
11. Maps: Hash Tables, Concorrência e o Idioma Comma-Ok#
Maps em Go são tabelas de dispersão (Hash Tables) altamente otimizadas que associam chaves a valores: map[KeyType]ValueType.
Isso cria um problema prático: se você buscar scores["Bob"] e receber 0, como saber se “Bob” realmente tem nota zero ou se ele simplesmente não existe no map? Como consultar uma chave ausente retorna o zero value, usamos o padrão comma-ok (pedir dois valores de volta, onde o segundo é um booleano “ok, achei” ou “não, não achei”) para distinguir entre uma chave inexistente e uma chave presente com valor zero:
// Go 1.21+: Função nativa clear() esvazia todos os elementos mantendo a alocação
10
clear(scores)
Maps e Não-Determinismo
A iteração em maps com for k, v := range myMap produz uma ordem aleatória intencional por design no runtime de Go para evitar que desenvolvedores criem dependências de ordenação de chaves. Além disso, maps não são seguros para leitura e escrita concorrente (use sync.RWMutex ou sync.Map).
Um receiver é o parâmetro especial escrito entre func e o nome do método — pense nele como o this/self de outras linguagens, a variável que dá acesso à própria struct dentro do método. Ao associar métodos a uma struct, podemos escolher entre dois tipos de receiver:
receivers.go
1
typePointstruct {
2
X, Y float64
3
}
4
5
// 1. Value Receiver: Recebe uma CÓPIA da struct.
6
// Seguro para concorrência e objetos imutáveis pequenos.
7
func (p Point) DistanceFromOrigin() float64 {
8
return math.Sqrt(p.X*p.X + p.Y*p.Y)
9
}
10
11
// 2. Pointer Receiver: Recebe uma REFERÊNCIA.
12
// Obrigatório para mutar estado interno ou evitar cópias de structs grandes.
13
func (p *Point) Move(dx, dyfloat64) {
14
p.X += dx
15
p.Y += dy
16
}
Convenção de Consistência
Se ao menos um método de uma struct exigir Pointer Receiver (por mutabilidade ou tamanho), todos os outros métodos dessa mesma struct devem adotar Pointer Receiver por consistência.
Go não possui a palavra-chave extends. A reutilização de código e polimorfismo são alcançados através do Embedding (campos anônimos): você coloca uma struct dentro de outra sem dar um nome ao campo, e o compilador passa a “promover” automaticamente os campos e métodos da struct interna, como se pertencessem à struct externa.
// Campos e métodos de BaseEntity são promovidos diretamente para Customer!
24
fmt.Println(c.ID) // uuid-123
25
fmt.Println(c.IsPersisted()) // true
26
}
13. Interfaces, Duck Typing & A Armadilha do Typed Nil#
As interfaces em Go são o ápice da elegância da linguagem. Elas são satisfeitas de forma 100% implícita (Duck Typing: “Se algo anda como um pato e grasna como um pato, então é um pato”).
Você não escreve implements MyInterface. Se um tipo implementa o conjunto exato de métodos requeridos pela interface, ele automaticamente satisfaz a interface.
Antes de mais nada, uma imagem mental simples: pense numa variável de interface como um envelope com duas informações coladas nele — uma etiqueta dizendo “o que tem dentro” (o tipo) e o conteúdo em si (o valor). Uma variável de interface em Go não é um ponteiro simples; ela é uma estrutura de dois ponteiros na memória:
_type / tab (*itab): a etiqueta — informações sobre o tipo concreto armazenado e a interface table (tabela de despacho que mapeia os métodos do tipo concreto para a interface).
data (unsafe.Pointer): o conteúdo — ponteiro para o valor concreto na memória.
1
Interface Header (16 bytes)
2
+------------------+------------------+
3
| itab / Type Info | Data Pointer |
4
+------------------+------------------+
Uma interface só é considerada nil quando ambos os ponteiros (type e data) são nulos — ou seja, o envelope inteiro (etiqueta E conteúdo) precisa estar vazio.
Este é um dos bugs mais comuns e sutis na jornada de um desenvolvedor Go — e ele confunde até programadores experientes na primeira vez que o encontram. A ideia central: é perfeitamente possível ter um envelope cuja etiqueta está preenchida (diz “sou um *CustomError”), mas cujo conteúdo está vazio (nil). Como a etiqueta não está vazia, o Go considera esse envelope diferente de nil, mesmo que, na prática, o valor dentro dele seja nulo. É exatamente isso que acontece no exemplo abaixo:
typed_nil_trap.go
1
packagemain
2
3
import"fmt"
4
5
typeCustomErrorstruct{}
6
7
func (e *CustomError) Error() string {
8
return"algo deu errado"
9
}
10
11
funcdoWork() error {
12
var err *CustomError=nil// err é um ponteiro tipado (*CustomError) com valor nil
13
14
// RETORNAR err DIRETAMENTE É UMA ARMADILHA!
15
// A interface 'error' de retorno receberá:
16
// (Type: *CustomError, Data: nil) -> Portanto, a interface NÃO É NIL!
17
return err
18
}
19
20
funcmain() {
21
err :=doWork()
22
if err !=nil {
23
fmt.Println("ERRO DETECTADO:", err) // ISTO SERÁ EXECUTADO!
24
} else {
25
fmt.Println("Sucesso!")
26
}
27
}
Repare no que acontece passo a passo: dentro de doWork, err é um ponteiro *CustomError com valor nil — até aí, nada de errado. O problema surge na linha return err: como a função retorna o tipo error (uma interface), Go copia a etiqueta (*CustomError) e o conteúdo (nil) para dentro do envelope da interface. O envelope resultante tem etiqueta preenchida, então err != nil é verdadeiro em main, e o bloco if err != nil executa — mesmo sem nenhum erro real ter acontecido!
Como Evitar a Armadilha do Typed Nil
Ao retornar error, sempre retorne o literal explícito nil em vez de variáveis ponteiro não inicializadas:
1
if!hasError {
2
returnnil// Correto e Seguro!
3
}
14. Type Assertions, Type Switches & A Convenção -er#
errors.Is(err, target): Percorre a cadeia de erros comparando se algum erro na árvore coincide com o erro sentinela target (substitui com maestria o frágil err == ErrNotFound).
errors.As(err, &targetStruct): Percorre a cadeia e extrai o erro para uma struct customizada caso ele exista na árvore.
errors_inspection.go
1
packagemain
2
3
import (
4
"errors"
5
"fmt"
6
)
7
8
typeDatabaseTimeoutErrorstruct {
9
Host string
10
Port int
11
}
12
13
func (d *DatabaseTimeoutError) Error() string {
14
return fmt.Sprintf("timeout conectando a %s:%d", d.Host, d.Port)
15
}
16
17
funcquery() error {
18
return fmt.Errorf("camada de serviço: %w", &DatabaseTimeoutError{Host: "10.0.0.1", Port: 5432})
19
}
20
21
funcmain() {
22
err :=query()
23
24
// 1. Extração tipada com errors.As (obrigatório passar ponteiro para o ponteiro)
25
var dbErr *DatabaseTimeoutError
26
if errors.As(err, &dbErr) {
27
fmt.Printf("Falha na porta do banco: %d\n", dbErr.Port)
Permite agrupar múltiplos erros simultâneos (muito comum em rotinas de validação ou encerramento de múltiplos recursos concorrentes):
errors_join.go
1
funcValidate(uUser) error {
2
var errs []error
3
if u.Name =="" {
4
errs =append(errs, errors.New("nome é obrigatório"))
5
}
6
if u.ID <=0 {
7
errs =append(errs, errors.New("id inválido"))
8
}
9
return errors.Join(errs...)
10
}
16. Streams, E/S de Alta Performance & Abstrações io#
As interfaces io.Reader e io.Writer são as espinhas dorsais de todo o ecossistema de entrada e saída (I/O) em Go.
Antes do código, pense numa analogia simples: encher um balde usando um copo pequeno, em vez de tentar despejar uma piscina inteira de uma vez. Você entrega um copo (o buffer p) ao Read, e ele te diz de volta quantos bytes ele efetivamente colocou no seu copo (n) — pode ser o copo cheio, pode ser só um pouco, dependendo do que tinha disponível na fonte no momento. Esse é o “contrato”: quem chama fornece o espaço, quem implementa a interface preenche o que conseguir e reporta quanto preencheu. É esse design simples que permite processar arquivos de qualquer tamanho — até gigabytes — sem nunca carregar tudo de uma vez na memória.
Ao ler de um io.Reader, você fornece o buffer p e a função retorna quantos bytes n foram lidos. O fim do fluxo é sinalizado pela sentinela io.EOF.
stream_reader.go
1
funcProcessStream(rio.Reader) error {
2
buf :=make([]byte, 4096) // Buffer reutilizável de 4KB
3
for {
4
n, err := r.Read(buf)
5
if n >0 {
6
chunk := buf[:n] // Sempre fatie de 0 até n!
7
fmt.Printf("Processados %d bytes\n", len(chunk))
8
}
9
if err == io.EOF {
10
break// Fim normal do stream
11
}
12
if err !=nil {
13
return err // Erro real de E/S
14
}
15
}
16
returnnil
17
}
Note o for sem condição: ele roda indefinidamente, “enchendo o copo” a cada chamada de r.Read(buf), até que o próprio Read sinalize io.EOF — o fim natural do stream, tratado como saída normal do laço, não como erro.
// Copia do descompressor direto para stdout com zero alocação intermediária
9
io.Copy(os.Stdout, gzReader)
Evite io.ReadAll em Produção sem Limites
Chamar io.ReadAll(resp.Body) em endpoints públicos é uma vulnerabilidade clássica de negação de serviço (DoS / OOM Kill), pois um payload malicioso de 5GB consumirá 5GB de RAM instantaneamente. Prefira io.Copy ou limite o leitor com io.LimitReader(resp.Body, 10<<20) (10MB max).
Lançado no Go 1.18, o suporte a Generics trouxe reutilização de código com segurança estática de tipos, sem recorrer a any ou reflection.
Você pode se perguntar: “por que não simplesmente usar any (interface vazia) e resolver tudo em runtime?” O problema é que any joga fora toda a informação de tipo — o compilador não sabe mais o que tem lá dentro, então você perde o autocomplete, perde erros de compilação úteis (trocando isso por panics em produção) e precisa fazer type assertions manuais o tempo todo para recuperar o tipo original. Generics resolvem isso: você escreve a função uma única vez, mas o compilador ainda sabe, em cada chamada, exatamente qual tipo concreto está sendo usado — juntando a reutilização de código do any com a segurança de tipos de escrever uma função separada para cada tipo.
Uma constraint (restrição) é simplesmente uma regra que diz quais tipos são aceitos no lugar de T — por exemplo, “só tipos que suportam ==” ou “só tipos numéricos”. Ela aparece entre colchetes, logo após o nome da função ou struct:
generics.go
1
packagemain
2
3
import"fmt"
4
5
// Constraint 'comparable' permite operadores == e !=
Vale entender essa Stack[T] com calma, pois é um bom exemplo de generics “no mundo real”. Stack[T any] diz: “essa struct funciona para qualquer tipo T, seja ele int, string ou uma struct sua” — e uma vez que você escreve Stack[int]{}, todos os métodos passam a operar exclusivamente com int, com checagem em tempo de compilação. Repare também no Pop: quando a pilha está vazia, não há um elemento real para devolver, então a linha var zero T cria o zero value do tipo T (0 para int, "" para string, etc.) — o mesmo padrão comma-ok que já vimos em maps, agora generalizado para qualquer tipo.
Uma constraint como comparable ou a Number que veremos a seguir é, tecnicamente, um Type Set: o conjunto de todos os tipos que satisfazem aquela restrição. Pense nela como uma “lista de tipos permitidos” que o compilador consulta antes de aceitar sua chamada.
O operador til (~) permite que uma restrição inclua todos os tipos definidos cujo tipo subjacente (underlying type) seja aquele especificado:
type_sets.go
1
// Aceita int, int32, int64, float32, float64 E tipos customizados como 'type UserID int64'
2
typeNumberinterface {
3
~int|~int32|~int64|~float32|~float64
4
}
5
6
funcMin[TNumber](a, bT) T {
7
if a < b {
8
return a
9
}
10
return b
11
}
12
13
typePortNumberint
14
15
funcmain() {
16
var p1, p2 PortNumber=8080, 9000
17
fmt.Println(Min(p1, p2)) // Funciona perfeitamente graças ao ~int!
Uma goroutine é, na prática, uma função rodando de forma independente das demais — você a inicia colocando a palavra-chave go na frente de uma chamada de função, e o runtime de Go cuida de executá-la “ao fundo”, sem bloquear o resto do programa.
Vale distinguir dois conceitos que soam parecidos: concorrência não é paralelismo. Concorrência é sobre estruturar seu programa como a composição de processos autônomos e independentes (dividir o trabalho em partes que podem progredir de forma intercalada); paralelismo é a execução simultânea real dessas partes em múltiplos núcleos de CPU. Você pode ter concorrência mesmo numa máquina de um núcleo só — o Go apenas alterna entre as goroutines rapidamente.
O runtime de Go utiliza o modelo M Scheduler com técnica de Work Stealing:
G (Goroutine): Representa a Goroutine com sua stack e instruction pointer.
M (Machine): Representa a Thread real do Sistema Operacional criada pelo kernel.
P (Processor): Contexto de execução lógica que detém a fila local de Goroutines prontas para rodar.
1
+---------------+ +---------------+
2
| Processor P0 | | Processor P1 |
3
| Local Queue: | | Local Queue: |
4
| [G1] [G2] [G3]| | [G4] [G5] |
5
+-------+-------+ +-------+-------+
6
| |
7
v v
8
+---------------+ +---------------+
9
| OS Thread M0 | | OS Thread M1 |
10
| Executando G0| | Executando G6|
11
+---------------+ +---------------+
Se o processador P1 esgotar suas tarefas, ele “rouba” (Work Stealing) metade das Goroutines da fila de P0, mantendo todos os núcleos da CPU constantemente ocupados sem gargalos de sincronização global.
gocheckURL(u, &wg) // Passar ponteiro do WaitGroup!
34
}
35
36
wg.Wait() // Bloqueia a main até que todos os wg.Done() sejam chamados
37
fmt.Println("Todas as checagens foram concluídas!")
38
}
19. Contextos: Cancelamento, Timeouts e Deadlines#
O pacote context é a espinha dorsal para gerenciar o ciclo de vida, sinais de cancelamento e prazos de requisições concorrentes em Go. Na prática, um context.Context é um valor que você passa como primeiro argumento por toda a cadeia de chamadas de uma requisição — como um “bilhete” carregado por todas as funções envolvidas, que pode ser “carimbado” com um prazo ou cancelado a qualquer momento, avisando todas as goroutines dependentes para pararem o que estão fazendo.
Se o cliente fechar a conexão ou o tempo limite expirar, o contexto notifica a Goroutine através do canal ctx.Done(), cancelando imediatamente o processamento de banco de dados ou chamadas de API externas:
Memória e Slices: Sempre inicialize slices com make([]T, 0, cap) quando souber o tamanho final. Use a Full Slice Expressions[a:b:b] ao passar recortes de slices para funções externas.
Defers: Cuidado com defer dentro de loops longos. Agrupe chamadas em closures para liberar handles rapidamente.
Ponteiros & Nil: Nunca retorne um ponteiro tipado nil em uma variável de retorno do tipo error.
I/O Streams: Nunca utilize io.ReadAll em fluxos sem limite de tamanho. Fatie sempre buffers lidos como p[:n].
Concorrência: Concorrência sem controle de ciclo de vida é vazamento de memória (Goroutine leak). Propague context.Context em todas as operações de E/S.
Dominando esses pilares, você não apenas escreve código Go que compila — você constrói softwares de infraestrutura robustos, elegantes e prontos para suportar milhões de requisições por segundo.