A biblioteca padrão: um tour pelas principais ferramentas

A biblioteca padrão: um tour pelas principais ferramentas

Tour pela biblioteca padrão do Go: fmt, strings, strconv, time, os, io e bufio, JSON, net/http com o roteamento do Go 1.22, math e rand, sort e slices, slog e filepath, com as armadilhas do layout 03, do Parse em UTC, do AddDate, do Scanner e do cliente HTTP sem timeout.
Golang

• • 16 min de leitura

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

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.

Comentários

Mais em Golang

Go Modules: go.mod, go.sum e gerenciamento de dependências
Go Modules: go.mod, go.sum e gerenciamento de dependências

Go Modules: go.mod e go.sum, go get e go mod tidy, versionamento semântico e o…

Leitura e escrita de arquivos com os pacotes os e bufio
Leitura e escrita de arquivos com os pacotes os e bufio

Arquivos em Go: ReadFile e WriteFile, Open, Create e OpenFile, bufio.Scanner e…

Testes em Go: testing, tabelas de casos e benchmarks
Testes em Go: testing, tabelas de casos e benchmarks

Testes em Go: arquivos _test.go, Error e Fatal, tabelas de casos com t.Run…