Go¶
Go (ou Golang) é uma linguagem compilada, de tipagem estática e coletor de lixo, feita para escrever servidores e ferramentas de infraestrutura que sejam simples de ler, rápidos de compilar e fáceis de manter por muitas pessoas. É a linguagem por trás de Docker, Kubernetes e Terraform, e aparece com frequência em vagas de backend, nuvem e DevOps.
Sobre a versão
O livro-fonte foi escrito para o Go 1.4 (2014). Os conceitos continuam valendo, mas esta
página usa o Go atual (módulos, any, genéricos, for range sobre inteiros).
Onde algo mudou, há uma nota "Hoje". Os trechos marcados como complemento não estão
no livro.
História e filosofia¶
Go nasceu dentro do Google em 2007, criada por Rob Pike, Ken Thompson e Robert Griesemer, e foi aberta ao público em novembro de 2009. A motivação era prática: compilações lentas de C++ e a dificuldade de escalar o desenvolvimento de sistemas enormes, em grande parte por causa de como essas linguagens tratam as dependências entre arquivos.
Em vez de acumular recursos, Go aposta em ter poucos recursos e todos previsíveis:
- Sintaxe enxuta e uma só forma idiomática de resolver a maioria dos problemas.
- Tipagem forte e estática, com inferência de tipos para evitar repetição.
- Compilação muito rápida e um binário único, sem máquina virtual nem dependências em tempo de execução (fácil de distribuir e de colocar em contêiner).
- Coletor de lixo (garbage collector), mas com ponteiros (sem aritmética de ponteiros).
- Biblioteca padrão extensa: HTTP, JSON, expressões regulares, arquivos, criptografia, testes.
- Concorrência como parte da linguagem, inspirada no modelo CSP de C. A. R. Hoare: goroutines que se comunicam por channels.
- Ferramentas oficiais que padronizam o dia a dia:
go fmt(formata o código),go vet,go test,go build,go doc.
Definição: Duck typing em Go
Um tipo satisfaz uma interface automaticamente, só por ter os métodos exigidos, sem
declarar implements. O nome vem de "se anda e grasna como um pato, é um pato". Em Go o
compilador verifica isso em tempo de compilação, ao contrário de Python ou Ruby.
Go não tem herança de classes, exceções, sobrecarga de métodos nem classes: a reutilização vem de composição, interfaces e funções. Foi uma decisão deliberada — menos discussão de estilo, mais foco em resolver o problema.
Instalação, módulos e ferramentas¶
Baixe o instalador em go.dev/dl e confira com:
Hoje: módulos substituíram o GOPATH
O livro organiza o código em um workspace (GOPATH) com src, pkg e bin. Desde o
Go 1.11/1.16, o padrão são os módulos: cada projeto vive em qualquer pasta e tem um
arquivo go.mod.
mkdir encurtador && cd encurtador
go mod init github.com/usuario/encurtador # cria o go.mod (nome do módulo)
go run . # compila e executa o pacote main
go build # gera o executável (fica no diretório atual)
go install # instala o executável em $GOPATH/bin (ou $GOBIN)
go test ./... # roda todos os testes
go fmt ./... # formata o código no padrão oficial
go vet ./... # procura erros comuns
| Comando | O que faz |
|---|---|
go run |
Compila, gera o binário em pasta temporária, executa e descarta |
go build |
Compila e deixa o executável no disco |
go install |
Compila e instala o binário no diretório de binários do Go |
go mod tidy |
Ajusta o go.mod/go.sum às importações reais |
go get pacote@versão |
Adiciona ou atualiza uma dependência |
Compilação cruzada (cross-compile) é trivial, pois não há VM: o mesmo código gera executáveis para outros sistemas só com variáveis de ambiente.
Definição: Compilação direta para código de máquina
Go compila para código nativo do sistema alvo (não há bytecode nem VM como em Java).
O resultado é um binário estaticamente ligado, compatível apenas com o sistema e a
arquitetura de destino. Por isso a inicialização é rápida e a imagem de contêiner pode ser
minúscula (até FROM scratch).
Primeiro programa e estrutura de um arquivo¶
Todo arquivo Go tem três partes, nesta ordem: declaração do pacote, importações e o código. Regras importantes:
- Todo código vive dentro de um pacote (
package). Um programa executável tem o pacotemaincom uma funçãomain()(sem argumentos e sem retorno) como ponto de partida. - Argumentos de linha de comando não chegam em
main(): useos.Argsou o pacoteflag. - Fontes são UTF-8; strings e caracteres Unicode funcionam nativamente. Um caractere literal
(
'A') é do tiporune(alias deint32, um ponto de código Unicode). - Visibilidade por maiúscula: identificador que começa com letra maiúscula é
exportado (visível fora do pacote); minúscula é privado ao pacote. É por isso que se
chama
fmt.Println, e nãofmt.println. - Variável declarada e não usada é erro de compilação, assim como importação não usada.
Para descartar um valor, use o identificador vazio
_. - Pode haver funções
init()(várias por pacote) que rodam antes demain()para preparar o estado do pacote.
Variáveis, tipos e fluxo de controle¶
var nome string // zero value: ""
var idade int = 30
cidade := "Goiânia" // declaração curta com inferência (só dentro de funções)
x, y := 1, 2.5 // x int, y float64
const pi = 3.14159
A forma var nome tipo lê-se naturalmente ("variável nome do tipo string"), ao contrário
do tipo nome de C e Java.
Definição: Zero value
Toda variável declarada sem valor recebe o valor zero do seu tipo: 0 para números,
false para bool, "" para string e nil para ponteiros, funções, interfaces,
slices, maps e channels. Não existe variável "não inicializada" em Go.
Tipos básicos: bool, string, int (e int8…int64, uint…), float32/float64,
byte (alias de uint8) e rune. A conversão entre tipos é sempre explícita:
float64(total) / float64(qtd) (dividir dois int descarta a parte decimal).
// if: a condição precisa ser bool (nada de "truthy" como em outras linguagens)
if n := len(args); n < 3 { // variável com escopo limitado ao if/else
fmt.Println("argumentos insuficientes")
os.Exit(1) // código != 0 indica erro para o sistema operacional
}
// for é a ÚNICA estrutura de repetição, em várias formas
for i := 0; i < 10; i++ { } // clássica
for n < 100 { n *= 2 } // estilo "while"
for { /* laço infinito */ break } // infinito, sai com break
for i, v := range slice { } // percorre índice e valor (valor é uma CÓPIA)
for range 3 { } // Hoje (Go 1.22): repete 3 vezes; também: for i := range 3
switch dia { // não precisa de break; casa só um case
case "sab", "dom": // vários valores por case
fmt.Println("fim de semana")
default:
fmt.Println("dia útil")
}
- Laços nomeados:
break rotulosai de um laço externo (útil quando háswitchdentro dofor, poisbreaksimples só sai doswitch). rangepercorre slices, arrays, strings, maps e channels. Omita o índice com_e omita o valor escrevendo sófor i := range s.
Funções¶
func soma(a, b int) int { return a + b } // mesmo tipo pode ser declarado uma vez
func precoFinal(custo float64) (dolar, real float64) { // retornos nomeados documentam a função
dolar = custo * 1.33
real = dolar * 2.34
return dolar, real // prefira retornar explicitamente
}
- Múltiplos retornos são o idioma central do tratamento de erros (veja adiante).
- Retornos nomeados viram variáveis dentro da função. Servem como documentação, mas um
return"pelado" prejudica a leitura — prefira listar os valores. - Funções variádicas:
func criar(dir string, nomes ...string)aceita zero ou mais argumentos (dentro da função,nomesé um[]string); para passar um slice existente, usecriar(dir, lista...).fmt.Printfeappendsão variádicas. deferadia uma chamada para o instante em que a função retorna; é a forma idiomática de liberar recursos.
func criar(caminho string) error {
f, err := os.Create(caminho)
if err != nil {
return err
}
defer f.Close() // sempre executado, mesmo se a função terminar mais cedo
_, err = f.WriteString("conteúdo")
return err
}
Cuidado com a ordem do defer
No livro, o defer arq.Close() aparece antes de verificar o erro de os.Create. Se
o arquivo não puder ser criado, arq é nil e o Close falha. Faça o defer depois
de confirmar que não houve erro, como acima. Vários defer executam em ordem inversa (pilha).
Funções como valores¶
Em Go funções são cidadãs de primeira classe: podem ser guardadas em variáveis, passadas como argumento, devolvidas por outras funções e armazenadas em structs e coleções.
transforma := func(s string) string { return strings.ToUpper(s) } // função anônima
fmt.Println(transforma("go"))
// Closure: a função "lembra" das variáveis do contexto onde nasceu
fib := func() func() int {
a, b := 0, 1
return func() int { a, b = b, a+b; return a }
}()
fmt.Println(fib(), fib(), fib(), fib()) // 1 1 2 3
Definição: Closure e função de ordem superior
Closure é uma função que captura e pode alterar as variáveis do escopo em que foi
definida. Função de ordem superior (higher-order function) recebe ou devolve outras
funções — como Cronometrar(f func()), que mede o tempo de qualquer função.
Para dar nome a uma assinatura de função, defina um tipo de função; qualquer função com a mesma assinatura o satisfaz implicitamente:
type Agregadora func(n, m int) int
func Agregar(valores []int, inicial int, fn Agregadora) int {
acc := inicial
for _, v := range valores {
acc = fn(v, acc)
}
return acc
}
soma := Agregar([]int{3, -2, 5}, 0, func(n, m int) int { return n + m }) // 6
O exemplo clássico da biblioteca padrão é http.HandlerFunc, um tipo de função que permite
usar uma função comum como handler HTTP.
Coleções: arrays, slices e maps¶
Arrays¶
Lista de tamanho fixo, que faz parte do tipo: [3]int e [5]int são tipos
diferentes. É preenchida com zero values; [...]int{2, 3, 5} deixa o compilador contar os
elementos. São copiados por valor ao serem passados para funções, por isso na prática quase
sempre se usa slice.
Slices¶
Um slice é uma janela sobre um array com três informações: ponteiro, tamanho (len)
e capacidade (cap). Cresce dinamicamente e é o tipo de lista padrão da biblioteca.
primos := []int{2, 3, 5, 7} // literal (sem tamanho)
b := make([]int, 10) // len 10, cap 10, preenchido com 0
c := make([]int, 0, 20) // len 0, cap 20 (reserva espaço para crescer)
| Operação | Código |
|---|---|
| Fatiar | s[inicio:fim] (fim exclusivo; ambos opcionais: s[:3], s[2:], s[:]) |
| Acrescentar no fim | s = append(s, 10) |
| Acrescentar vários / outro slice | s = append(s, outro...) |
| Inserir no início | s = append([]int{0}, s...) |
| Inserir no meio (posição i) | s = append(s[:i], append([]int{x}, s[i:]...)...) |
| Remover do início / fim | s = s[1:] / s = s[:len(s)-1] |
| Remover do meio | s = append(s[:i], s[i+1:]...) |
| Copiar | dst := make([]int, len(s)); copy(dst, s) |
Fatias compartilham o mesmo array
Fatiar não copia os dados: o novo slice aponta para o mesmo array. Alterar um elemento em um slice altera o outro.
original := []int{1, 2, 3, 4, 5}
novo := original[1:3] // [2 3]
original[2] = 13
fmt.Println(novo) // [2 13]
Complemento: quando append ultrapassa a capacidade, o Go aloca um array maior e o
slice passa a apontar para ele — a partir daí os dois deixam de se afetar. Por isso a
regra é sempre reatribuir o resultado (s = append(s, x)) e usar copy quando se quer
uma cópia independente.
Slices, maps e channels são tipos de referência (o valor copiado ao passar para uma
função aponta para os mesmos dados), o que torna as chamadas baratas. Hoje, o pacote
slices da biblioteca padrão (slices.Sort, slices.Contains, slices.Delete,
slices.Insert) cobre boa parte dessas receitas manuais.
Maps¶
Coleção de pares chave-valor sem ordem (tabela hash). A chave precisa ser comparável
(==), como string, int, bool ou uma struct de campos comparáveis.
capitais := map[string]string{"GO": "Goiânia", "PR": "Curitiba"}
populacao := make(map[string]int, 6) // capacidade inicial opcional (evita realocações)
capitais["RN"] = "Natal" // inserir ou atualizar
delete(capitais, "PR") // remover
fmt.Println(len(capitais))
cidade, existe := capitais["SP"] // idioma "comma ok": existe é false se a chave não está
if !existe { fmt.Println("não cadastrado") }
- Ler uma chave inexistente devolve o zero value (não um erro). Use o segundo retorno
para distinguir "ausente" de "zero". Por isso
contagem[id]++funciona mesmo na primeira vez. - A ordem de iteração é aleatória por design. Para imprimir ordenado, extraia as chaves, ordene e percorra:
chaves := make([]int, 0, len(quadrados))
for k := range quadrados { chaves = append(chaves, k) }
sort.Ints(chaves) // Hoje: slices.Sort(chaves), ou maps.Keys + slices.Sorted
for _, k := range chaves { fmt.Println(k, quadrados[k]) }
- Um
map[string]interface{}(hojemap[string]any) aceita valores de qualquer tipo, mas exige type assertion na leitura — use com parcimônia. - Um map não é seguro para uso concorrente (veja a seção de concorrência).
Criando tipos: structs, métodos e ponteiros¶
Novos nomes para tipos existentes¶
Você não pode adicionar métodos ao []string da linguagem, mas pode criar um tipo
derivado e dar métodos a ele. Os dois tipos são distintos: a conversão é explícita,
[]string(lista) e ListaDeCompras(slice).
Structs¶
Uma struct agrupa campos em um novo tipo (o equivalente Go de uma "classe" sem herança).
type Arquivo struct {
Nome string
Tamanho float64
Palavras int
Linhas int
}
a := Arquivo{"artigo.txt", 12.68, 1862, 220} // pela ordem dos campos
b := Arquivo{Nome: "programa.go", Tamanho: 1.12} // por nome (o resto fica com zero value)
p := &Arquivo{Nome: "notas.txt"} // ponteiro para a struct
fmt.Println(p.Nome) // com ponteiro não precisa de (*p).Nome
Structs são mutáveis e copiadas por valor. Comparação com == funciona quando todos os
campos são comparáveis.
Métodos e receptores¶
Um método é uma função com um receptor declarado antes do nome. Não existe this
implícito: o receptor tem nome explícito.
func (a Arquivo) MediaPalavrasPorLinha() float64 { // receptor por valor (cópia)
return float64(a.Palavras) / float64(a.Linhas)
}
func (a *Arquivo) Renomear(novo string) { a.Nome = novo } // receptor por ponteiro (altera o original)
Definição: Receptor por valor x por ponteiro
Em Go, argumentos e receptores são passados por cópia. Se o método precisa alterar
o objeto (ou o objeto é grande), declare o receptor como ponteiro (*T). Se só lê, o
receptor por valor basta. Regra prática: se algum método de um tipo usa receptor por
ponteiro, use ponteiro em todos, por consistência.
Exemplo clássico — uma pilha com tipo customizado:
type Pilha struct{ valores []any }
func (p Pilha) Tamanho() int { return len(p.valores) }
func (p Pilha) Vazia() bool { return p.Tamanho() == 0 }
func (p *Pilha) Empilhar(v any) { p.valores = append(p.valores, v) }
func (p *Pilha) Desempilhar() (any, error) {
if p.Vazia() {
return nil, errors.New("pilha vazia")
}
topo := p.valores[p.Tamanho()-1]
p.valores = p.valores[:p.Tamanho()-1]
return topo, nil
}
Hoje: any e genéricos
interface{} (a interface vazia, satisfeita por qualquer tipo) ganhou o alias any
no Go 1.18. Desde essa versão também existem genéricos, que permitem uma pilha
com tipo seguro (complemento):
type Pilha[T any] struct{ valores []T }
func (p *Pilha[T]) Empilhar(v T) { p.valores = append(p.valores, v) }
func (p *Pilha[T]) Desempilhar() (T, bool) {
var zero T
if len(p.valores) == 0 {
return zero, false
}
topo := p.valores[len(p.valores)-1]
p.valores = p.valores[:len(p.valores)-1]
return topo, true
}
Composição em vez de herança¶
Go não tem herança. Para reaproveitar comportamento, embuta um tipo em outro (embedding); os campos e métodos do tipo embutido são promovidos:
type Animal struct{ Nome string }
func (a Animal) Descrever() string { return "Animal: " + a.Nome }
type Cachorro struct {
Animal // embutido (composição)
Raca string
}
c := Cachorro{Animal{"Rex"}, "Labrador"}
fmt.Println(c.Descrever()) // método promovido de Animal
Veja o princípio "favoreça a composição" em Boas práticas.
Interfaces e polimorfismo¶
Uma interface é um conjunto de métodos. Um tipo a satisfaz implicitamente, apenas tendo esses métodos:
type Operacao interface {
Calcular() int
}
type Soma struct{ a, b int }
func (s Soma) Calcular() int { return s.a + s.b }
func (s Soma) String() string { return fmt.Sprintf("%d + %d", s.a, s.b) } // usado pelo fmt
type Subtracao struct{ a, b int }
func (s Subtracao) Calcular() int { return s.a - s.b }
func acumular(ops []Operacao) int {
total := 0
for _, op := range ops {
total += op.Calcular() // polimorfismo: não importa o tipo concreto
}
return total
}
total := acumular([]Operacao{Soma{10, 20}, Subtracao{30, 15}}) // 45
- O método
String() stringimplementa a interfacefmt.Stringer: ofmto chama sozinho ao imprimir o valor com%v. - Interfaces pequenas (um ou dois métodos) são o estilo idiomático. A biblioteca padrão
é um bom modelo:
io.Reader(Read(p []byte) (n int, err error)),io.Writer,fmt.Stringer,error,http.Handler. - O poder está em depender de interfaces, não de tipos concretos: uma função que recebe
io.Readerfunciona com arquivo, conexão de rede, buffer em memória ou um tipo seu. - A interface vazia (
any) aceita qualquer valor; para recuperar o tipo concreto use type assertion (v, ok := x.(string)) ou type switch. - Complemento: "aceite interfaces, devolva structs" — declare a interface no pacote que a consome, não no que a implementa.
Tratamento de erros¶
Go não usa exceções. Erros são valores do tipo error (uma interface com o método
Error() string), devolvidos como último retorno e verificados explicitamente:
valor, err := strconv.ParseFloat(texto, 64)
if err != nil {
return fmt.Errorf("valor inválido %q: %w", texto, err) // %w embrulha o erro original
}
errors.New("mensagem")cria um erro simples;fmt.Errorfcom%wembrulha (wrap) adicionando contexto.- Complemento:
errors.Is(err, alvo)eerrors.As(err, &tipo)inspecionam a cadeia de erros embrulhados. - Evite ignorar erros com
_(o livro o faz em alguns exemplos por brevidade). panicexiste para falhas irrecuperáveis (erro de programação);recoverdentro de umdefera captura. No fluxo normal, prefira devolvererror.log.Fatal(err)registra e encerra o programa com código de erro;os.Exit(1)só encerra.
Concorrência: goroutines e channels¶
"Não comunique compartilhando memória; compartilhe memória comunicando." — Effective Go
Definição: Goroutine
Unidade de execução extremamente leve, gerenciada pelo runtime do Go (que as
distribui sobre as threads do sistema). Cria-se uma com a palavra go antes de uma
chamada de função. Pode haver milhares ou milhões delas.
func imprimir(n int) {
for i := 0; i < 3; i++ {
fmt.Printf("%d ", n)
time.Sleep(200 * time.Millisecond)
}
}
func main() {
go imprimir(2) // roda concorrentemente
imprimir(3) // roda na goroutine principal
}
As goroutines morrem quando main termina: o programa não espera por elas.
Channels¶
Um channel é um canal tipado por onde goroutines trocam valores; a seta <- indica a
direção.
c := make(chan int) // sem buffer
go func() { c <- 33 }() // envia
valor := <-c // recebe (bloqueia até chegar um valor)
- Em um canal sem buffer, enviar e receber bloqueiam até o outro lado estar pronto — a própria comunicação sincroniza as goroutines, sem travas.
- Canal com buffer (
make(chan int, 3)): o envio só bloqueia quando o buffer enche; o recebimento, quando esvazia. - Fechar o canal (
close(c)) é responsabilidade de quem envia, ao terminar. Quem recebe detecta o fechamento porv, ok := <-c(ok == false) ou, melhor, comfor v := range c, que termina sozinho. - Deadlock: se todas as goroutines ficam bloqueadas (ex.: receber de um canal que nunca
mais receberá dados), o runtime aborta com
fatal error: all goroutines are asleep - deadlock!. - Direção no tipo:
chan<- intsó envia;<-chan intsó recebe — o compilador impede o uso errado.
func produzir(c chan<- int) {
defer close(c)
for i := 1; i <= 3; i++ { c <- i }
}
func main() {
c := make(chan int, 3)
go produzir(c)
for v := range c { fmt.Println(v) } // 1 2 3
}
select, timeout e WaitGroup¶
O select é um switch para canais: espera por vários ao mesmo tempo e executa o
caso que ficar pronto primeiro (com default ele não bloqueia).
for !fim {
select {
case n := <-impares: /* ... */
case n := <-pares: /* ... */
case fim = <-pronto: // canal usado só para sinalizar o término
}
}
select {
case resultado := <-c:
fmt.Println("ok:", resultado)
case <-time.After(2 * time.Second): // timeout: After devolve um canal
fmt.Println("timeout")
}
Para esperar várias goroutines, use sync.WaitGroup:
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1) // antes de iniciar a goroutine
go func() {
defer wg.Done() // avisa que terminou
trabalhar()
}()
}
wg.Wait() // bloqueia até todas terminarem
(Se precisar passar o WaitGroup a uma função, passe ponteiro, senão a função recebe uma cópia.)
Concorrência não é paralelismo¶
Concorrência é estruturar o programa em tarefas independentes; paralelismo é
executá-las ao mesmo tempo em vários núcleos. O livro mostra que, na época, o padrão era
usar um só núcleo e ensina a ajustar GOMAXPROCS (variável de ambiente ou
runtime.GOMAXPROCS(runtime.NumCPU())).
Hoje
Desde o Go 1.5, GOMAXPROCS já vale o número de CPUs disponíveis por padrão;
normalmente não é preciso mexer. Meça antes de ajustar: o ganho depende da natureza do
programa (tarefas de CPU intensivo escalam; tarefas de I/O quase não precisam).
Complementos importantes (não estão no livro)¶
- Condição de corrida (race condition): acesso concorrente a dados compartilhados sem
sincronização. Detecte com
go test -raceougo run -race. sync.Mutex/sync.RWMutexprotegem estruturas compartilhadas (ummapcomum trava o programa com concurrent map writes se acessado por várias goroutines).context.Contextcarrega cancelamento, prazo e valores entre goroutines e chamadas de rede (context.WithTimeout,ctx.Done()); é o padrão para encerrar trabalho em cascata.sync/atomicpara contadores simples;errgroup(golang.org/x/sync) para grupos de goroutines com propagação de erro.- Compare com Java: goroutines têm função parecida com as threads virtuais do Java 21, mas são nativas desde o início.
Servidor HTTP e JSON¶
A biblioteca padrão traz um servidor HTTP pronto para produção em net/http.
func main() {
http.HandleFunc("/tempo", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, time.Now().Format("2006-01-02 15:04:05"))
})
log.Fatal(http.ListenAndServe(":8080", nil)) // bloqueia; devolve erro se não conseguir subir
}
- O handler tem a assinatura
func(http.ResponseWriter, *http.Request).ResponseWritersatisfazio.Writer, por issofmt.Fprintf(w, ...)escreve a resposta. Formatusa uma data de referência (2006-01-02 15:04:05, ou seja, 2 de janeiro de 2006, 15:04:05) como "máscara" do formato desejado — em vez deyyyy-MM-dd.- Para registrar um handler que precisa de estado (um tipo seu), implemente a interface
http.Handler(ServeHTTP(w, r)) e usehttp.Handle. - Funções úteis:
http.Redirect,http.NotFound,http.Error,w.Header().Set(...),w.WriteHeader(status).
JSON com encoding/json¶
type Url struct {
ID string `json:"id"` // struct tags definem o nome no JSON
Criacao time.Time `json:"criacao"`
Destino string `json:"destino"`
}
dados, err := json.Marshal(url) // struct -> []byte (JSON)
var u Url
err = json.Unmarshal(dados, &u) // JSON -> struct (passe ponteiro)
- Só campos exportados (maiúscula) são serializados; as struct tags trocam o nome
(
json:"id") e aceitam opções (json:"obs,omitempty"). - Canais, funções e números complexos não são serializáveis.
- Complemento: para enviar JSON direto na resposta, use
json.NewEncoder(w).Encode(v)e definaContent-Type: application/json.
Projeto: encurtador de URLs¶
O livro consolida tudo em um serviço real: recebe uma URL, devolve uma URL curta e redireciona. O que vale levar do desenho (independente de linguagem):
flowchart LR
C["Cliente"] -->|"POST /api/encurtar"| E["Encurtador (handler)"]
C -->|"GET /r/id"| R["Redirecionador (handler)"]
C -->|"GET /api/stats/id"| V["Visualizador (handler)"]
E --> U["pacote url"]
R --> U
V --> U
U --> P["interface Repositorio"]
P --> M["repositório em memória (map)"]
R -. "id" .-> CH(("canal stats"))
CH --> G["goroutine que registra cliques"]
G --> U
| Decisão | Como foi resolvida |
|---|---|
| Rotas | /api/encurtar (POST → 201 Created com Location, ou 200 OK se a URL já existia; 405 com cabeçalho Allow para outros métodos; 400 para URL inválida), /r/<id> (301 Moved Permanently ou 404), /api/stats/<id> (JSON) |
| Pacotes | main (servidor HTTP) e url (regras de negócio); só o que começa com maiúscula é exposto |
| Persistência trocável | Interface Repositorio com IdExiste, BuscarPorId, BuscarPorUrl, Salvar, RegistrarClick, BuscarClicks; a implementação concreta (map em memória) fica privada (repositorioMemoria) e é injetada. Trocar por um banco relacional só exige outra implementação |
| Identificador curto | 5 caracteres sorteados de um alfabeto; se já existir no repositório, sorteia de novo |
| Validação | url.ParseRequestURI rejeita URLs malformadas |
| Resposta rápida | O redirecionamento só envia o id para um canal; uma goroutine separada registra a estatística, para não atrasar o usuário |
| Estatísticas | Struct Stats{*Url, Clicks} serializada em JSON com struct tags |
| Configuração | Pacote flag (-p porta, -l liga/desliga logs); -h gera a ajuda automaticamente |
| Logs | Pacote log; log.Fatal registra e encerra se o servidor não subir |
Refatorações aplicadas¶
- Eliminar variável global: o canal
statsvirou um campo de uma structRedirecionadorque implementahttp.Handler, injetado emmainao registrar a rota — em vez dehttp.HandleFunc(que fixa a assinatura), usa-sehttp.Handle("/r/", &Redirecionador{stats}). - Reduzir duplicação: extraiu-se
buscarUrlEExecutar(w, r, func(*url.Url)), que lê oiddo caminho, busca e, se achar, executa a função recebida (senão responde404) — uso de função de ordem superior para remover repetição. - Logs e flags para operar o serviço sem recompilar.
O que o livro não trata: segurança de concorrência
O repositório em memória é um map acessado por várias goroutines (cada requisição
HTTP roda em uma). Sem proteção isso causa race condition e pode derrubar o processo.
A correção é guardar um sync.RWMutex na struct e fazer Lock/RLock em cada método
(complemento):
type repositorioMemoria struct {
mu sync.RWMutex
urls map[string]*Url
clicks map[string]int
}
func (r *repositorioMemoria) Salvar(u Url) error {
r.mu.Lock()
defer r.mu.Unlock()
r.urls[u.ID] = &u
return nil
}
func (r *repositorioMemoria) BuscarPorID(id string) *Url {
r.mu.RLock()
defer r.mu.RUnlock()
return r.urls[id]
}
Outras melhorias de produção: rand.Seed está obsoleto (o gerador já é aleatório por
padrão desde o Go 1.20), ler o corpo com io.ReadAll em vez de Body.Read direto, e
usar crypto/rand se os identificadores precisarem ser imprevisíveis. A mesma ideia de
"dados acessados por vários clientes ao mesmo tempo" aparece em
Threads e concorrência (Java).
Boas práticas e idiomas de Go¶
- Deixe o
go fmtdecidir o estilo — não existe debate de formatação. - Verifique erros sempre e acrescente contexto ao propagá-los (
%w). - Interfaces pequenas, definidas onde são usadas; devolva tipos concretos.
- Pacotes coesos e com nomes curtos; só exporte o necessário (maiúscula = API pública).
deferlogo após adquirir um recurso (e depois de checar o erro).- Prefira composição (embedding e interfaces) à hierarquia de tipos.
- Evite estado global; injete dependências (como o canal e o repositório no projeto).
- Concorrência: comece simples, use
-race, cancele comcontexte feche canais no lado que envia. - Teste com o pacote
testing(go test), usando testes em tabela (table-driven tests) — complemento próprio, e a mesma lógica de Qualidade.
Perguntas comuns de entrevista (resumo)¶
| Pergunta | Resposta curta |
|---|---|
| Por que Go para backend? | Compila rápido, gera um binário único, tem concorrência nativa e biblioteca padrão forte para HTTP/JSON; ótima para serviços e ferramentas de nuvem |
| Qual a diferença entre array e slice? | Array tem tamanho fixo (parte do tipo) e é copiado por valor; slice é uma visão dinâmica sobre um array, com len e cap, que compartilha dados ao ser fatiado |
| Como o Go trata erros? | Como valores: funções devolvem error e o chamador verifica (if err != nil); não há exceções |
| O que é uma goroutine? | Uma tarefa leve gerenciada pelo runtime, criada com go; comunica-se por channels |
| Concorrência x paralelismo? | Concorrência é estruturar tarefas independentes; paralelismo é rodá-las ao mesmo tempo em vários núcleos |
| Como uma interface é implementada? | Implicitamente: basta o tipo ter os métodos; não há implements |
| O que é o zero value? | O valor padrão de um tipo não inicializado (0, "", false, nil) |
| Quando usar receptor por ponteiro? | Quando o método altera o objeto, ou o objeto é grande, ou por consistência com os outros métodos do tipo |
Um map é seguro para várias goroutines? |
Não; proteja com sync.Mutex/RWMutex ou use sync.Map |
Veja também: Java e Java Avançado (para comparar modelos de concorrência), Estrutura de Dados (pilha, mapas e a complexidade de cada uma) e Backend (HTTP, REST e JSON).