Uma das razões pelas quais Go se tornou a linguagem preferida para infraestrutura e backends é a qualidade excepcional de sua biblioteca padrão. Ela cobre HTTP, JSON, criptografia, compressão, templates, testes, banco de dados, manipulação de strings, tempo, sistema de arquivos e muito mais — tudo isso sem dependências externas, com APIs consistentes e documentação exemplar.
Em muitos casos, o que em outras linguagens exigiria três ou quatro bibliotecas externas é resolvido puramente com a biblioteca padrão do Go. Conhecê-la bem reduz dependências, simplifica projetos e evita a fragmentação de ecossistema que afeta outras comunidades.
Este artigo faz um tour pelos pacotes mais importantes, com exemplos práticos de cada um.
fmt — Formatação e I/O
Já presente em todos os artigos anteriores (O tipo `error` e tratamento idiomático de falhas, Pacotes: visibilidade, nomenclatura e organização de projeto, Go Modules: go.mod, go.sum e gerenciamento de dependências), fmt merece um olhar mais atento. Além de Println e Printf, oferece funções para escrever em qualquer io.Writer e para escanear entrada:
package main
import (
"fmt"
"os"
"strings"
)
func main() {
// Escrevendo em qualquer Writer
fmt.Fprintln(os.Stderr, "erro: arquivo não encontrado")
fmt.Fprintf(os.Stdout, "processados: %d itens\n", 42)
// Formatando para string
s := fmt.Sprintf("coordenadas: (%.4f, %.4f)", -23.5505, -46.6333)
fmt.Println(s)
// Lendo de qualquer Reader
r := strings.NewReader("42 true Go")
var n int
var b bool
var lang string
fmt.Fscan(r, &n, &b, &lang)
fmt.Println(n, b, lang) // 42 true Go
// Verificação de erros com Errorf
err := fmt.Errorf("operação falhou: código %d", 500)
fmt.Println(err)
}
strings — Manipulação de strings
O pacote strings oferece tudo que se precisa para trabalhar com texto:
package main
import (
"fmt"
"strings"
)
func main() {
s := " Olá, Mundo em Go! "
fmt.Println(strings.TrimSpace(s)) // "Olá, Mundo em Go!"
fmt.Println(strings.ToUpper(s)) // " OLÁ, MUNDO EM GO! "
fmt.Println(strings.ToLower(s)) // " olá, mundo em go! "
fmt.Println(strings.Contains(s, "Mundo")) // true
fmt.Println(strings.HasPrefix(s, " Olá")) // true
fmt.Println(strings.HasSuffix(s, "Go! ")) // true
fmt.Println(strings.Count(s, "o")) // 2
fmt.Println(strings.Replace(s, "Mundo", "Go", 1))
fmt.Println(strings.Index(s, "Mundo")) // 8 — posição em bytes; o "á" ocupa dois
// Split e Join
partes := strings.Split("a,b,c,d", ",")
fmt.Println(partes) // [a b c d]
fmt.Println(strings.Join(partes, " | ")) // a | b | c | d
// Fields divide por qualquer espaço em branco
palavras := strings.Fields(" Go é rápido ")
fmt.Println(palavras) // [Go é rápido]
// Builder para concatenação eficiente
var sb strings.Builder
for i := 0; i < 5; i++ {
fmt.Fprintf(&sb, "item%d ", i)
}
fmt.Println(sb.String())
}
strconv — Conversões entre string e tipos
package main
import (
"fmt"
"strconv"
)
func main() {
// int ↔ string
fmt.Println(strconv.Itoa(42)) // "42"
n, _ := strconv.Atoi("123")
fmt.Println(n + 1) // 124
// float ↔ string
f, _ := strconv.ParseFloat("3.14", 64)
fmt.Println(f * 2) // 6.28
fmt.Println(strconv.FormatFloat(f, 'f', 3, 64)) // "3.140"
// bool ↔ string
b, _ := strconv.ParseBool("true")
fmt.Println(b) // true
fmt.Println(strconv.FormatBool(false)) // "false"
// int com base
fmt.Println(strconv.FormatInt(255, 16)) // "ff"
fmt.Println(strconv.FormatInt(255, 2)) // "11111111"
v, _ := strconv.ParseInt("ff", 16, 64)
fmt.Println(v) // 255
}
time — Data, hora e duração
package main
import (
"fmt"
"time"
)
func main() {
agora := time.Now()
fmt.Println(agora)
// Formatação — Go usa uma data de referência específica: Mon Jan 2 15:04:05 MST 2006
fmt.Println(agora.Format("02/01/2006 15:04:05"))
fmt.Println(agora.Format(time.RFC3339))
// Parsing
t, _ := time.Parse("02/01/2006", "15/03/2024")
fmt.Println(t.Year(), t.Month(), t.Day())
// Aritmética com tempo
amanha := agora.Add(24 * time.Hour)
semanaPassada := agora.AddDate(0, 0, -7)
fmt.Println(amanha.Format("02/01/2006"))
fmt.Println(semanaPassada.Format("02/01/2006"))
// Diferença entre datas
diferenca := amanha.Sub(agora)
fmt.Println(diferenca) // 24h0m0s
fmt.Println(diferenca.Hours()) // 24
// Comparação
fmt.Println(agora.Before(amanha)) // true
fmt.Println(agora.After(amanha)) // false
// Timer e Ticker
timer := time.NewTimer(100 * time.Millisecond)
<-timer.C
fmt.Println("timer disparou")
// Sleep
time.Sleep(10 * time.Millisecond)
fmt.Println("após sleep")
}
A data de referência Mon Jan 2 15:04:05 MST 2006 é um mnemônico: 1-2-3-4-5-6-7 (mês-dia-hora-minuto-segundo-ano-fuso). É única no Go e confunde iniciantes — mas uma vez memorizada, a formatação de datas torna-se intuitiva.
os — Sistema operacional e arquivos
package main
import (
"fmt"
"os"
)
func main() {
// Variáveis de ambiente
home := os.Getenv("HOME")
fmt.Println("HOME:", home)
os.Setenv("APP_ENV", "producao")
// Argumentos da linha de comando
fmt.Println("Argumentos:", os.Args)
// Escrevendo arquivo
err := os.WriteFile("saida.txt", []byte("conteúdo do arquivo\n"), 0644)
if err != nil {
fmt.Println("Erro ao escrever:", err)
return
}
// Lendo arquivo
dados, err := os.ReadFile("saida.txt")
if err != nil {
fmt.Println("Erro ao ler:", err)
return
}
fmt.Print(string(dados))
// Informações sobre arquivo
info, err := os.Stat("saida.txt")
if err == nil {
fmt.Println("Tamanho:", info.Size(), "bytes")
fmt.Println("Modificado:", info.ModTime().Format("02/01/2006"))
}
// Criando diretórios
os.MkdirAll("dados/temp", 0755)
// Removendo
os.Remove("saida.txt")
os.RemoveAll("dados")
// Saindo com código de status
// os.Exit(1) — encerra o programa imediatamente
}
io e bufio — I/O eficiente
package main
import (
"bufio"
"fmt"
"io"
"strings"
)
func main() {
// io.Reader → io.Writer com Copy
origem := strings.NewReader("dados a copiar\n")
destino := &strings.Builder{}
n, _ := io.Copy(destino, origem)
fmt.Printf("copiados %d bytes: %s", n, destino.String())
// bufio.Scanner para leitura linha por linha
texto := "linha 1\nlinha 2\nlinha 3\n"
scanner := bufio.NewScanner(strings.NewReader(texto))
for scanner.Scan() {
fmt.Println("→", scanner.Text())
}
// bufio.Writer para escrita com buffer
bw := bufio.NewWriter(io.Discard) // io.Discard descarta tudo
fmt.Fprintln(bw, "linha com buffer")
bw.Flush() // importante: descarregar o buffer
// io.LimitReader — limita quantidade de bytes lidos
r := io.LimitReader(strings.NewReader("texto longo demais"), 5)
dados, _ := io.ReadAll(r)
fmt.Println(string(dados)) // "texto"
// io.MultiWriter — escreve em múltiplos destinos simultaneamente
var b1, b2 strings.Builder
mw := io.MultiWriter(&b1, &b2)
fmt.Fprintln(mw, "enviado para dois lugares")
fmt.Println(b1.String() == b2.String()) // true
}
encoding/json — Serialização JSON
package main
import (
"encoding/json"
"fmt"
"strings"
)
type Endereco struct {
Rua string `json:"rua"`
Cidade string `json:"cidade"`
CEP string `json:"cep"`
}
type Usuario struct {
ID int `json:"id"`
Nome string `json:"nome"`
Email string `json:"email"`
Endereco Endereco `json:"endereco"`
Ativo bool `json:"ativo"`
Senha string `json:"-"`
}
func main() {
// Struct → JSON (Marshal)
u := Usuario{
ID: 1,
Nome: "Ricardo",
Email: "ricardo@exemplo.com",
Endereco: Endereco{
Rua: "Av. Paulista, 1000",
Cidade: "São Paulo",
CEP: "01310-100",
},
Ativo: true,
Senha: "secreta",
}
dados, err := json.MarshalIndent(u, "", " ")
if err != nil {
fmt.Println("Erro:", err)
return
}
fmt.Println(string(dados))
// JSON → Struct (Unmarshal)
jsonStr := `{"id":2,"nome":"Ana","email":"ana@exemplo.com","ativo":false}`
var u2 Usuario
if err := json.Unmarshal([]byte(jsonStr), &u2); err != nil {
fmt.Println("Erro:", err)
return
}
fmt.Printf("%+v\n", u2)
// Decodificação incremental com Decoder
// útil para streams e arquivos grandes
decoder := json.NewDecoder(strings.NewReader(jsonStr))
var u3 map[string]any
decoder.Decode(&u3)
fmt.Println(u3["nome"]) // Ana
}
net/http — Cliente e servidor HTTP
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
// Servidor HTTP simples
func servidorExemplo() {
mux := http.NewServeMux()
mux.HandleFunc("GET /saude", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
})
mux.HandleFunc("GET /usuarios/{id}", func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
fmt.Fprintf(w, "usuário: %s", id)
})
servidor := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
fmt.Println("Servidor em :8080")
servidor.ListenAndServe()
}
// Cliente HTTP com timeout e tratamento de erros
func clienteExemplo() {
cliente := &http.Client{Timeout: 10 * time.Second}
resp, err := cliente.Get("https://httpbin.org/json")
if err != nil {
fmt.Println("Erro:", err)
return
}
defer resp.Body.Close()
fmt.Println("Status:", resp.Status)
corpo, _ := io.ReadAll(resp.Body)
fmt.Println(string(corpo[:100]))
}
O roteamento com padrões como GET /usuarios/{id} foi introduzido no Go 1.22, tornando o roteamento básico possível sem bibliotecas externas.
math e math/rand — Operações matemáticas
package main
import (
"fmt"
"math"
"math/rand/v2"
)
func main() {
// math
fmt.Println(math.Sqrt(144)) // 12
fmt.Println(math.Pow(2, 10)) // 1024
fmt.Println(math.Abs(-42.5)) // 42.5
fmt.Println(math.Round(3.7)) // 4
fmt.Println(math.Floor(3.9)) // 3
fmt.Println(math.Ceil(3.1)) // 4
fmt.Println(math.Log(math.E)) // 1
fmt.Println(math.Log2(1024)) // 10
fmt.Println(math.MaxInt64) // 9223372036854775807
fmt.Println(math.Pi) // 3.141592653589793
// math/rand/v2 — Go 1.22+
fmt.Println(rand.IntN(100)) // inteiro aleatório [0, 100)
fmt.Println(rand.Float64()) // float [0.0, 1.0)
fmt.Println(rand.N(50)) // genérico — inteiro [0, 50)
}
sort — Ordenação
package main
import (
"fmt"
"sort"
)
type Produto struct {
Nome string
Preco float64
}
func main() {
// Tipos básicos
nums := []int{5, 2, 8, 1, 9, 3}
sort.Ints(nums)
fmt.Println(nums) // [1 2 3 5 8 9]
palavras := []string{"banana", "maçã", "abacaxi", "uva"}
sort.Strings(palavras)
fmt.Println(palavras) // [abacaxi banana maçã uva]
// Verificando se está ordenado
fmt.Println(sort.IntsAreSorted(nums)) // true
// Tipos personalizados com sort.Slice
produtos := []Produto{
{"Monitor", 1500.0},
{"Teclado", 250.0},
{"Mouse", 120.0},
{"Headset", 380.0},
}
// Ordenar por preço crescente
sort.Slice(produtos, func(i, j int) bool {
return produtos[i].Preco < produtos[j].Preco
})
for _, p := range produtos {
fmt.Printf("%-10s R$ %.2f\n", p.Nome, p.Preco)
}
// Busca binária em slice ordenado
idx := sort.SearchInts(nums, 5)
fmt.Println("índice do 5:", idx) // 3
}
Desde o Go 1.21, o pacote slices oferece o mesmo com genéricos e menos cerimônia: slices.Sort(nums), slices.SortFunc(produtos, func(a, b Produto) int { return cmp.Compare(a.Preco, b.Preco) }) e slices.BinarySearch(nums, 5), que devolve também se o valor foi encontrado. Em código novo, ele costuma substituir o sort.
log e log/slog — Logging
package main
import (
"log"
"log/slog"
"os"
)
func main() {
// log padrão
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
log.Println("aplicação iniciada")
log.Printf("porta: %d", 8080)
// log.Fatal chama os.Exit(1) após logar
// log.Panic chama panic() após logar
// slog — logging estruturado (Go 1.21+)
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))
logger.Info("servidor iniciado",
"porta", 8080,
"ambiente", "producao",
)
logger.Warn("memória alta",
"uso_mb", 512,
"limite_mb", 1024,
)
logger.Error("falha na conexão",
"host", "localhost",
"erro", "connection refused",
)
}
path/filepath — Caminhos de arquivo
package main
import (
"fmt"
"os"
"path/filepath"
)
func main() {
// Construindo caminhos portáveis
p := filepath.Join("dados", "usuarios", "perfil.json")
fmt.Println(p) // dados/usuarios/perfil.json (Linux/Mac)
fmt.Println(filepath.Dir(p)) // dados/usuarios
fmt.Println(filepath.Base(p)) // perfil.json
fmt.Println(filepath.Ext(p)) // .json
// Caminho absoluto
abs, _ := filepath.Abs("main.go")
fmt.Println(abs)
// Percorrendo diretórios — em código novo, prefira filepath.WalkDir (Go 1.16),
// que não chama Stat em cada entrada e é mais rápido
filepath.Walk(".", func(caminho string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
fmt.Println(caminho)
}
return nil
})
}
A biblioteca padrão do Go dá conta de boa parte do que um serviço precisa — texto, conversões, tempo, arquivos, JSON, HTTP, logging — sem dependência externa nenhuma, e a documentação em pkg.go.dev é a primeira parada antes de procurar uma biblioteca. Os tropeços mais comuns não estão na falta de recursos, mas nos padrões de cada pacote: o layout de data com 15 e não 03, o Parse que assume UTC, o Scanner que para em silêncio se ninguém olhar o Err(), o cliente HTTP padrão sem timeout.
Fontes e leituras recomendadas
-
pkg.go.dev — Biblioteca padrão completa — Documentação oficial de todos os pacotes. https://pkg.go.dev/std
-
Go by Example — Exemplos práticos para cada pacote da biblioteca padrão. https://gobyexample.com
-
Documentação net/http — Referência completa do servidor e cliente HTTP. https://pkg.go.dev/net/http
-
Go Blog: Structured Logging with slog — Introdução ao novo pacote de logging estruturado. https://go.dev/blog/slog
-
Go Blog: Working with JSON — Guia completo de encoding/json. https://go.dev/blog/json
-
Documentação time — Format e Parse — Referência dos layouts de formatação de datas. https://pkg.go.dev/time#Time.Format
Exercícios
Exercício 1
Um relatório formata horários com t.Format("02/01/2006 03:04"). Os lançamentos da madrugada aparecem como "12:00" e os das 15h como "03:00". Outro sistema recebe a data "31/02/2024" num formulário e o time.Parse devolve um erro que ninguém confere. Explique os dois comportamentos com base na data de referência do Go.
Ver resposta
✓ Resposta: No layout do Go, cada componente é escrito com o valor que ele tem na data de referência, Mon Jan 2 15:04:05 MST 2006. A hora de referência é 15, ou 3 da tarde, e por isso existem duas grafias: 15 pede a hora de 0 a 23, e 03 pede a hora de 1 a 12, sem indicar manhã ou tarde. Com 03, meia-noite vira 12:00 e 15h vira 03:00; num teste com 31/01/2024 à meia-noite, o layout com 15 dá 31/01/2024 00:00 e o com 03 dá 31/01/2024 12:00. Para o formato brasileiro, o certo é "02/01/2006 15:04". Quanto ao segundo caso, o time.Parse valida a data de verdade: 31 de fevereiro produz day out of range, e o valor devolvido é a data zero, 1º de janeiro do ano 1. Sem conferir o erro, esse ano 1 vai para o banco como se fosse uma data válida. O erro do Parse precisa ser tratado como qualquer outro, e o formulário deve devolver a mensagem ao usuário.
Exercício 2
Um sistema brasileiro lê "15/03/2024 10:00" digitado pelo usuário com time.Parse, grava no banco e depois mostra na tela convertido para o horário local. O horário aparece como 07:00. De onde saem as três horas, e como ler corretamente?
Ver resposta
✓ Resposta: Quando o texto não traz fuso, o time.Parse assume UTC. As 10:00 digitadas por alguém em São Paulo viram 10:00 UTC, que em horário de Brasília (UTC−3) são 07:00. Num teste, o mesmo texto lido com time.Parse deu 10:00:00 +0000 UTC, e lido com time.ParseInLocation e a localização America/Sao_Paulo deu 10:00:00 -0300. A diferença entre os dois instantes foi exatamente 3h0m0s. A correção é ParseInLocation, com a localização carregada uma vez por time.LoadLocation("America/Sao_Paulo"), cujo erro também deve ser conferido. LoadLocation depende da base de fusos do sistema; em imagens de contêiner mínimas ela pode não existir, e o import em branco _ "time/tzdata" embute a base no binário. A regra que evita a classe inteira de erros é ler no fuso de quem digitou, guardar em UTC e converter para o fuso de quem lê só na hora de mostrar.
Exercício 3
Um job de cobrança calcula o vencimento seguinte com venc.AddDate(0, 1, 0). Para um contrato que vence em 31 de janeiro, o próximo vencimento sai em 2 de março, e o cliente reclama que fevereiro foi pulado. O que o AddDate fez, e como tratar esse caso?
Ver resposta
✓ Resposta: O AddDate soma os campos e depois normaliza a data resultante. Somar um mês a 31/01/2024 produz "31 de fevereiro de 2024", que não existe; como 2024 é bissexto e fevereiro tem 29 dias, os dois dias excedentes transbordam para março, e o resultado medido é 02/03/2024. Não é defeito, é a regra documentada, a mesma que faz time.Date aceitar valores fora da faixa. Para vencimentos, porém, a regra de negócio quase sempre é "o mesmo dia do mês seguinte, ou o último dia desse mês se ele for mais curto", e ela precisa ser escrita. Uma forma é calcular o último dia do mês de destino com time.Date(ano, mes+1, 0, ...), em que o dia 0 recua para o último dia do mês anterior, e usar o menor entre esse valor e o dia original. Outra, comum em sistemas financeiros, é guardar o dia de vencimento do contrato separado da data e recalcular cada vencimento a partir dele, em vez de encadear somas, que acumulam o desvio mês a mês.
Exercício 4
Um importador lê um arquivo CSV linha a linha com bufio.Scanner. Num arquivo com uma linha de 70 mil caracteres, o laço termina sem processar nada, e o programa encerra como se o arquivo estivesse vazio. O que aconteceu, e que duas linhas de código faltaram?
Ver resposta
✓ Resposta: O Scanner tem um limite de tamanho por linha, que por padrão é de 64 KB (bufio.MaxScanTokenSize). Ao encontrar uma linha maior, Scan() devolve false e o laço termina, exatamente como no fim do arquivo. A diferença só aparece em scanner.Err(): num teste com uma linha de 70 mil caracteres seguida de outra linha curta, o laço rodou 0 vezes e o erro foi bufio.Scanner: token too long. A primeira linha que faltou é a checagem de if err := scanner.Err(); err != nil depois do laço, que o exemplo do artigo também não faz e que é obrigatória em qualquer uso real, porque é assim que o Scanner reporta falhas de leitura. A segunda é aumentar o limite antes do laço, com scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024), que permite linhas de até 1 MB. Para CSV, aliás, o encoding/csv da biblioteca padrão é a ferramenta certa, porque também lida com aspas e campos que contêm quebras de linha.
Exercício 5
O artigo cria o cliente HTTP com &http.Client{Timeout: 10 * time.Second}. Um colega acha exagero e usa http.Get(url) direto, "que é igual e mais curto". Numa madrugada, um serviço parceiro para de responder sem fechar a conexão. O que acontece com o serviço do colega?
Ver resposta
✓ Resposta: As requisições ficam esperando para sempre. http.Get usa o http.DefaultClient, cujo Timeout é zero, e zero significa "sem limite"; o valor medido no Go 1.27 é 0s. Se o parceiro aceita a conexão e não responde, cada chamada prende uma goroutine, uma conexão e a memória associada. Como os handlers do serviço esperam por essas chamadas, as requisições dos próprios clientes se acumulam, até que o serviço esgote memória ou descritores de arquivo, derrubado por um problema que nem era dele. O Timeout do http.Client limita a requisição inteira, da conexão à leitura do corpo, e é a defesa mínima. Em código que já trabalha com context, http.NewRequestWithContext com um contexto de prazo permite limites diferentes por chamada e cancelamento quando o cliente original desiste. O mesmo cuidado vale para o servidor: o exemplo do artigo define ReadTimeout e WriteTimeout exatamente porque o ListenAndServe padrão também não tem limites.