Introdução ao Pacote fmt: Fundamentos e Importância
O pacote fmt é um dos pilares da linguagem Go, responsável pela formatação e impressão de dados. Seu nome vem de "format" e oferece funções que permitem controlar como valores são exibidos, sejam eles números, strings, estruturas ou tipos customizados. Diferentemente de linguagens como Python que usam f-strings ou C que utiliza printf, Go oferece uma abordagem elegante através de verbos de formato específicos que você precisa dominar.
A razão pela qual este pacote é tão importante está em sua ubiquidade. Praticamente todo programa Go que gera saída o utiliza, seja para debug, logs ou apresentação de dados ao usuário. Compreender profundamente seus mecanismos não apenas torna seu código mais legível, mas também permite tratamento de erros mais adequado e otimização de performance em operações de formatação intensivas. Este artigo guiará você desde os conceitos básicos até técnicas avançadas que poucos desenvolvedores Go exploram completamente.
Verbos de Formatação: O Coração do fmt
Os Verbos Básicos e Seus Usos
Um verbo de formato é um marcador que começa com % seguido de uma letra que especifica como um valor deve ser formatado. Go oferece uma variedade considerável, cada uma com um propósito específico. Os mais utilizados são %v para valor geral, %d para inteiros, %s para strings e %f para números de ponto flutuante. Compreender quando usar cada um é fundamental.
Vamos começar com um exemplo prático que demonstra os verbos mais comuns:
package main
import (
"fmt"
)
func main() {
// Inteiros
fmt.Printf("Decimal: %d\n", 42) // Saída: Decimal: 42
fmt.Printf("Octal: %o\n", 42) // Saída: Octal: 52
fmt.Printf("Hexadecimal: %x\n", 42) // Saída: Hexadecimal: 2a
fmt.Printf("Hexadecimal maiúsculo: %X\n", 42) // Saída: Hexadecimal maiúsculo: 2A
fmt.Printf("Binário: %b\n", 42) // Saída: Binário: 101010
// Strings e caracteres
fmt.Printf("String: %s\n", "Hello") // Saída: String: Hello
fmt.Printf("Caractere: %c\n", 65) // Saída: Caractere: A
fmt.Printf("Aspas: %q\n", "Hello") // Saída: Aspas: "Hello"
// Números de ponto flutuante
fmt.Printf("Float padrão: %f\n", 3.14159) // Saída: Float padrão: 3.141590
fmt.Printf("Float científico: %e\n", 3.14159) // Saída: Float científico: 3.141590e+00
fmt.Printf("Float compacto: %g\n", 3.14159) // Saída: Float compacto: 3.14159
// Valor genérico
valor := 42
fmt.Printf("Valor genérico: %v\n", valor) // Saída: Valor genérico: 42
fmt.Printf("Tipo: %T\n", valor) // Saída: Tipo: int
}
O verbo %v é especial porque tenta representar o valor de forma "natural" para seu tipo. Para inteiros, funciona como %d; para strings, como %s. Já %T mostra o tipo dinâmico, extremamente útil em debug. O verbo %q é particularmente interessante pois formata strings com escapes apropriados, útil para gerar código Go válido.
Verbos Avançados e Comportamentos Especiais
Além dos básicos, existem verbos menos conhecidos mas poderosos para casos específicos. O %p formata ponteiros em hexadecimal, %U formata runes no formato Unicode, e %v com flags consegue fazer muito mais. Cada verbo pode ser modificado com flags que alteram seu comportamento.
package main
import (
"fmt"
)
func main() {
// Ponteiros
x := 42
fmt.Printf("Ponteiro: %p\n", &x) // Saída: Ponteiro: 0xc0000160d8 (endereço varia)
// Unicode
fmt.Printf("Rune: %U\n", 'Ω') // Saída: Rune: U+03A9
fmt.Printf("Caractere unicode: %c\n", 0x03A9) // Saída: Caractere unicode: Ω
// Booleanos
fmt.Printf("Booleano: %v\n", true) // Saída: Booleano: true
fmt.Printf("Booleano com verbo: %t\n", true) // Saída: Booleano com verbo: true
// Valores nil
var ptr *int
fmt.Printf("Nil: %v\n", ptr) // Saída: Nil: <nil>
fmt.Printf("Nil com tipo: %#v\n", ptr) // Saída: Nil com tipo: (*int)(nil)
}
O modificador # (denominado "flag de alternativa") muda o comportamento de certos verbos. Com %#v, obtém-se uma representação mais detalhada que inclui o tipo. Com %#x, adiciona o prefixo 0x. Com %#o, adiciona o prefixo 0. Isso é fundamental para gerar código Go válido ou representações mais claras.
Flags, Largura e Precisão: Controle Fino
Compreendendo Flags e Largura
Cada verbo pode ser precedido por flags que modificam seu comportamento. A flag 0 preenche com zeros, - alinha à esquerda em vez de à direita, + sempre mostra o sinal mesmo para positivos, e o espaço coloca um espaço em vez de sinal para positivos. A largura especifica o número mínimo de caracteres na saída.
package main
import (
"fmt"
)
func main() {
// Largura básica
fmt.Printf("Padrão: |%5d|\n", 42) // Saída: Padrão: | 42|
fmt.Printf("Alinhado: |%-5d|\n", 42) // Saída: Alinhado: |42 |
fmt.Printf("Zeros: |%05d|\n", 42) // Saída: Zeros: |00042|
// Sinais
fmt.Printf("Sem sinal: %d\n", 42) // Saída: Sem sinal: 42
fmt.Printf("Com sinal +: %+d\n", 42) // Saída: Com sinal +: +42
fmt.Printf("Espaço: % d\n", 42) // Saída: Espaço: 42
fmt.Printf("Negativo com +: %+d\n", -42) // Saída: Negativo com +: -42
// Combinações
fmt.Printf("Completo: |%+08d|\n", 42) // Saída: Completo: |+000042|
fmt.Printf("Completo neg: |%+08d|\n", -42) // Saída: Completo neg: |-000042|
}
A ordem das flags importa semanticamente. Go processa flags na ordem padrão, mas sua aplicação segue uma lógica específica: largura é sempre aplicada por último. Note que quando você usa 0 com -, a flag de zero é ignorada porque alinhar à esquerda é incompatível com preenchimento de zeros.
Precisão para Strings e Floats
A precisão, especificada após um ponto decimal (como em %.3f), tem significados diferentes dependendo do verbo. Para floats, especifica o número de casas decimais. Para strings, especifica o número máximo de caracteres a exibir. Para inteiros com %d, o comportamento é controlado por 0.
package main
import (
"fmt"
)
func main() {
// Precisão em floats
fmt.Printf("Padrão: %f\n", 3.14159) // Saída: Padrão: 3.141590
fmt.Printf("2 casas: %.2f\n", 3.14159) // Saída: 2 casas: 3.14
fmt.Printf("0 casas: %.0f\n", 3.14159) // Saída: 0 casas: 3
fmt.Printf("5 casas: %.5f\n", 3.14159) // Saída: 5 casas: 3.14159
// Precisão em strings
fmt.Printf("Padrão: %s\n", "Hello World") // Saída: Padrão: Hello World
fmt.Printf("Truncado: %.5s\n", "Hello World") // Saída: Truncado: Hello
fmt.Printf("Com largura: %10.5s\n", "Hello World") // Saída: Com largura: Hello
// Combinando largura e precisão
fmt.Printf("Float: |%8.2f|\n", 3.14159) // Saída: Float: | 3.14|
fmt.Printf("String: |%10.5s|\n", "Hello World") // Saída: String: | Hello|
}
A combinação de largura e precisão é poderosa para gerar saídas formatadas profissionais. Note que a largura é aplicada ao resultado final após a precisão ser aplicada. Isso permite criar tabelas e relatórios estruturados com precisão.
Funções de Saída e Manipulação de Strings Avançada
Println, Print, Printf e Sprintf
Go oferece três principais famílias de funções no pacote fmt. A família Print (Print, Println) são mais simples e não usam verbos. A família Printf (Printf, Sprintf) usa verbos de formato. A distinção entre estas funções é crucial para diferentes contextos. Println adiciona espaços entre argumentos e uma quebra de linha; Print apenas concatena; Printf oferece controle total.
package main
import (
"fmt"
)
func main() {
// Print vs Println vs Printf
fmt.Print("Hello", "World") // Saída: HelloWorld
fmt.Println("Hello", "World") // Saída: Hello World\n
fmt.Printf("Hello %s\n", "World") // Saída: Hello World\n
// Sprintf retorna a string sem exibir
resultado := fmt.Sprintf("Número: %d", 42)
fmt.Println("Resultado:", resultado) // Saída: Resultado: Número: 42
// Sprintln para gerar strings com quebras de linha
resultado2 := fmt.Sprintln("Um", "Dois", "Três")
fmt.Print("String gerada: |" + resultado2 + "|") // Note as quebras
// Aplicação prática: construir strings sem alocação excessiva
nome := "Alice"
idade := 30
mensagem := fmt.Sprintf("Olá, %s! Você tem %d anos.", nome, idade)
fmt.Println(mensagem) // Saída: Olá, Alice! Você tem 30 anos.
}
Sprintf é particularmente útil porque retorna a string formatada sem exibir, permitindo armazená-la para processamento posterior. Isso é mais eficiente do que concatenação manual de strings em Go, especialmente quando você precisa formatar múltiplos valores.
Tratamento de Erros com Errorf
A função Errorf combina formatação com criação de erros. Internamente, cria uma nova instância de error com a mensagem formatada, sendo extremamente útil para propagar erros com contexto apropriado.
package main
import (
"fmt"
"errors"
)
func divide(a, b float64) (float64, error) {
if b == 0 {
// Errorf cria um erro com mensagem formatada
return 0, fmt.Errorf("divisão por zero: %f / %f", a, b)
}
return a / b, nil
}
func conectarServidor(host string, porta int) error {
// Simulando falha de conexão
if porta < 1 || porta > 65535 {
return fmt.Errorf("porta inválida %d para host %s", porta, host)
}
return nil
}
func main() {
// Testando divide
resultado, err := divide(10, 0)
if err != nil {
fmt.Println("Erro:", err) // Saída: Erro: divisão por zero: 10.000000 / 0.000000
}
// Testando conexão
err = conectarServidor("localhost", 99999)
if err != nil {
fmt.Println("Erro de conexão:", err) // Saída: Erro de conexão: porta inválida 99999 para host localhost
}
// Wrapping de erros (Go 1.13+)
originalErr := errors.New("falha na leitura")
wrappedErr := fmt.Errorf("falha ao processar arquivo: %w", originalErr)
fmt.Println("Erro wrappado:", wrappedErr) // Saída: Erro wrappado: falha ao processar arquivo: falha na leitura
}
Note o uso de %w na última função. Este verbo especial (introduzido em Go 1.13) permite envolver erros mantendo a cadeia de erros intacta para errors.Is() e errors.As(). Sem %w, a cadeia seria perdida e causaria problemas em tratamento de erros mais sofisticado.
Stringer Interface e Formatação Customizada
Quando você implementa a interface Stringer (com método String() string), a função fmt.Println e o verbo %v usam automaticamente este método para representar seu tipo. Isso permite controle total sobre como seus tipos customizados são exibidos.
package main
import (
"fmt"
)
type Pessoa struct {
Nome string
Idade int
Email string
}
// Implementando a interface Stringer
func (p Pessoa) String() string {
return fmt.Sprintf("Pessoa{Nome: %s, Idade: %d, Email: %s}", p.Nome, p.Idade, p.Email)
}
type Ponto struct {
X, Y float64
}
func (p Ponto) String() string {
return fmt.Sprintf("(%.2f, %.2f)", p.X, p.Y)
}
func main() {
// Sem Stringer, seria exibido com os nomes dos campos
pessoa := Pessoa{"João", 25, "joao@example.com"}
fmt.Println(pessoa) // Saída: Pessoa{Nome: João, Idade: 25, Email: joao@example.com}
// Também funciona com Printf e %v
fmt.Printf("Pessoa: %v\n", pessoa) // Saída: Pessoa: Pessoa{Nome: João, Idade: 25, Email: joao@example.com}
// Para tipos simples
ponto := Ponto{3.14159, 2.71828}
fmt.Println(ponto) // Saída: (3.14, 2.72)
// %v usa String(), %#v não quando há Stringer
fmt.Printf("Com %%v: %v\n", ponto) // Saída: Com %v: (3.14, 2.72)
fmt.Printf("Com %%#v: %#v\n", ponto) // Saída: Com %#v: fmt.Ponto{X:3.14159, Y:2.71828}
}
A implementação de Stringer é uma prática excelente para tipos estruturados. Torna debug mais simples e logs mais legíveis. Note que %#v bypassa o método String() e mostra a representação Go do valor, útil quando você precisa da representação literal em vez da customizada.
Casos de Uso Avançados e Otimizações
Formatação de Slices e Mapas
Go permite formatar coleções diretamente com %v ou %#v, mas cada um produz saída diferente. Entender essas diferenças é importante para debug eficaz.
package main
import (
"fmt"
)
func main() {
// Slices
numeros := []int{1, 2, 3, 4, 5}
fmt.Printf("Slice com %%v: %v\n", numeros) // Saída: Slice com %v: [1 2 3 4 5]
fmt.Printf("Slice com %%#v: %#v\n", numeros) // Saída: Slice com %#v: []int{1, 2, 3, 4, 5}
// Mapas
config := map[string]int{"porta": 8080, "timeout": 30}
fmt.Printf("Map com %%v: %v\n", config) // Saída depende da ordem de iteração
fmt.Printf("Map com %%#v: %#v\n", config) // Saída inclui tipos
// Slices de structs
pessoas := []Pessoa{
{"Alice", 30, "alice@example.com"},
{"Bob", 25, "bob@example.com"},
}
fmt.Printf("Slice de structs: %v\n", pessoas)
fmt.Printf("Slice de structs #v: %#v\n", pessoas)
// Estrutura aninhada
type Config struct {
Banco map[string]string
Portas []int
}
cfg := Config{
Banco: map[string]string{"usuario": "admin", "senha": "secret"},
Portas: []int{80, 443, 8080},
}
fmt.Printf("Estrutura complexa: %#v\n", cfg)
}
type Pessoa struct {
Nome string
Idade int
Email string
}
func (p Pessoa) String() string {
return fmt.Sprintf("%s (%d)", p.Nome, p.Idade)
}
A representação de estruturas aninhadas com %#v é especialmente útil para reproduzir estruturas literalmente em código. Cópie a saída e terá código Go válido que pode ser compilado.
Formatter Interface para Controle Total
Além de Stringer, existe a interface Formatter que oferece controle ainda maior sobre como seu tipo é formatado. Qualquer verbo pode ser interceptado e processado customizadamente.
package main
import (
"fmt"
)
type Cor struct {
R, G, B uint8
}
// Implementando fmt.Formatter para controle total
func (c Cor) Format(s fmt.State, verb rune) {
switch verb {
case 'v':
if s.Flag('#') {
fmt.Fprintf(s, "Cor{R:%d, G:%d, B:%d}", c.R, c.G, c.B)
} else {
fmt.Fprintf(s, "rgb(%d, %d, %d)", c.R, c.G, c.B)
}
case 's':
// Exibe como string hexadecimal
fmt.Fprintf(s, "#%02x%02x%02x", c.R, c.G, c.B)
case 'q':
// Exibe com aspas
fmt.Fprintf(s, "\"#%02x%02x%02x\"", c.R, c.G, c.B)
default:
fmt.Fprintf(s, "%%!%c(Cor=%#v)", verb, c)
}
}
func main() {
vermelho := Cor{255, 0, 0}
azul := Cor{0, 0, 255}
fmt.Printf("Padrão: %v\n", vermelho) // Saída: Padrão: rgb(255, 0, 0)
fmt.Printf("Com #: %#v\n", vermelho) // Saída: Com #: Cor{R:255, G:0, B:0}
fmt.Printf("Como string: %s\n", vermelho) // Saída: Como string: #ff0000
fmt.Printf("Com aspas: %q\n", azul) // Saída: Com aspas: "#0000ff"
// Verbos não suportados caem no default
fmt.Printf("Verbo não suportado: %d\n", vermelho)
}
A interface Formatter recebe um fmt.State que oferece informações sobre flags, largura e precisão. Você pode chamar s.Flag() para verificar se uma flag foi setada e s.Width() e s.Precision() para obter valores. Esta é a forma mais poderosa de customização de formatação em Go.
Performance e Alocações
Ao trabalhar com formatação intensiva, é importante compreender as implicações de performance. Sprintf aloca memória para cada chamada, então em loops críticos pode ser problemático. Usar strings.Builder com fmt.Fprintf é mais eficiente.
package main
import (
"fmt"
"strings"
)
func exemploIneficiente(nomes []string) string {
resultado := ""
for _, nome := range nomes {
resultado += fmt.Sprintf("- %s\n", nome) // Aloca a cada iteração
}
return resultado
}
func exemploEficiente(nomes []string) string {
var builder strings.Builder
for _, nome := range nomes {
fmt.Fprintf(&builder, "- %s\n", nome) // Evita alocações
}
return builder.String()
}
func main() {
nomes := []string{"Alice", "Bob", "Carol", "David"}
// Ambos produzem o mesmo resultado, mas o segundo é mais eficiente
resultado1 := exemploIneficiente(nomes)
resultado2 := exemploEficiente(nomes)
fmt.Println("Resultado 1:")
fmt.Print(resultado1)
fmt.Println("\nResultado 2:")
fmt.Print(resultado2)
// Para verificação
if resultado1 == resultado2 {
fmt.Println("\nAmbas as saídas são idênticas!")
}
}
strings.Builder não realoca memória a cada Fprintf, acumulando eficientemente. Em loops que formatam milhares de linhas, esta técnica pode resultar em diferenças de performance significativas. Builder é otimizado especificamente para este padrão de construção de strings.
Conclusão
Dominar o pacote fmt em Go significa compreender três pilares fundamentais. Primeiro, os verbos de formatação e como cada um interpreta diferentes tipos de dados, desde inteiros em múltiplas bases até pontos flutuantes e unicode. Segundo, como flags, largura e precisão permitem controle fino da saída, essencial para gerar relatórios estruturados e logs legíveis. Terceiro, implementar Stringer e Formatter oferece controle total sobre como seus tipos customizados são representados, facilitando debug e tornando código mais manutenível.
A prática constante com estes conceitos tornará sua codificação em Go muito mais fluida. Você saberá instantaneamente qual verbo usar em cada situação e conseguirá debugar estruturas complexas com precisão. Lembre-se que fmt não é apenas para "imprimir coisas na tela" — é uma ferramenta fundamental para construir abstrações legíveis e manuteníveis.