Skip to content

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:

go version

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.

GOOS=windows GOARCH=amd64 go build -o app.exe
GOOS=linux   GOARCH=arm64 go build -o app-linux-arm64

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

package main

import "fmt"

func main() {
    fmt.Println("Olá, Go!")
}

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 pacote main com uma função main() (sem argumentos e sem retorno) como ponto de partida.
  • Argumentos de linha de comando não chegam em main(): use os.Args ou o pacote flag.
  • Fontes são UTF-8; strings e caracteres Unicode funcionam nativamente. Um caractere literal ('A') é do tipo rune (alias de int32, 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ão fmt.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 de main() 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 rotulo sai de um laço externo (útil quando há switch dentro do for, pois break simples só sai do switch).
  • range percorre 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, use criar(dir, lista...). fmt.Printf e append são variádicas.
  • defer adia 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.

var a [3]int                     // [0 0 0]
primos := [...]int{2, 3, 5, 7}
var m [2][2]int                  // matriz 2x2

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{} (hoje map[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

type ListaDeCompras []string

func (l ListaDeCompras) Contar() int { return len(l) }

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() string implementa a interface fmt.Stringer: o fmt o 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.Reader funciona 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.Errorf com %w embrulha (wrap) adicionando contexto.
  • Complemento: errors.Is(err, alvo) e errors.As(err, &tipo) inspecionam a cadeia de erros embrulhados.
  • Evite ignorar erros com _ (o livro o faz em alguns exemplos por brevidade).
  • panic existe para falhas irrecuperáveis (erro de programação); recover dentro de um defer a captura. No fluxo normal, prefira devolver error.
  • 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 por v, ok := <-c (ok == false) ou, melhor, com for 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<- int só envia; <-chan int só 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 -race ou go run -race.
  • sync.Mutex / sync.RWMutex protegem estruturas compartilhadas (um map comum trava o programa com concurrent map writes se acessado por várias goroutines).
  • context.Context carrega cancelamento, prazo e valores entre goroutines e chamadas de rede (context.WithTimeout, ctx.Done()); é o padrão para encerrar trabalho em cascata.
  • sync/atomic para 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). ResponseWriter satisfaz io.Writer, por isso fmt.Fprintf(w, ...) escreve a resposta.
  • Format usa 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 de yyyy-MM-dd.
  • Para registrar um handler que precisa de estado (um tipo seu), implemente a interface http.Handler (ServeHTTP(w, r)) e use http.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 defina Content-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

  1. Eliminar variável global: o canal stats virou um campo de uma struct Redirecionador que implementa http.Handler, injetado em main ao registrar a rota — em vez de http.HandleFunc (que fixa a assinatura), usa-se http.Handle("/r/", &Redirecionador{stats}).
  2. Reduzir duplicação: extraiu-se buscarUrlEExecutar(w, r, func(*url.Url)), que lê o id do caminho, busca e, se achar, executa a função recebida (senão responde 404) — uso de função de ordem superior para remover repetição.
  3. 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 fmt decidir 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).
  • defer logo 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 com context e 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).