A interface Stringer e fmt
Esta aula aborda a interface Stringer do Go, que permite controlar a representação textual de tipos personalizados. Exploramos os verbos de formatação do pacote fmt, como implementar o método String() e técnicas de debug com a interface GoStringer.
A interface Stringer é uma das interfaces mais importantes do Go, definida no pacote fmt. Ela permite que qualquer tipo controle como será representado como string, especialmente quando usado com funções como fmt.Println, fmt.Printf e fmt.Sprintf. Implementar String() é essencial para criar saídas legíveis e personalizadas para seus tipos.
Nesta aula, vamos explorar a fundo a interface Stringer, os verbos de formatação que controlam a saída, como implementar corretamente o método String() e como usar a interface GoStringer para debug. Ao final, você será capaz de criar representações textuais elegantes e úteis para qualquer tipo.
fmt.Stringer
A interface Stringer está definida no pacote fmt da seguinte forma:
type Stringer interface {
String() string
}Qualquer tipo que implemente o método String() string satisfaz essa interface. Quando você passa um valor para funções como fmt.Println, fmt.Printf com o verbo %v ou %s, o pacote fmt verifica se o tipo implementa Stringer. Se sim, ele chama o método String() para obter a representação textual. Caso contrário, ele usa uma representação padrão baseada na estrutura do valor.
Por exemplo, considere um tipo Pessoa que armazena nome e idade. Sem implementar String(), imprimir uma variável desse tipo exibirá algo como {João 30}. Com String(), podemos personalizar a saída para algo como Pessoa: João (30 anos).
type Pessoa struct {
Nome string
Idade int
}
func (p Pessoa) String() string {
return fmt.Sprintf("Pessoa: %s (%d anos)", p.Nome, p.Idade)
}
func main() {
p := Pessoa{"João", 30}
fmt.Println(p) // Saída: Pessoa: João (30 anos)
}Verbos de formatação
O pacote fmt oferece vários verbos de formatação que determinam como um valor é exibido. Os mais comuns são:
%v: representação padrão (usaStringerse disponível)%+v: para structs, exibe nomes dos campos%#v: representação Go-syntax (como se fosse código)%s: string simples (para strings, chamaString()se implementado)%d: inteiro decimal%f: ponto flutuante%T: tipo do valor
Quando você usa %v ou %s em um tipo que implementa Stringer, o método String() é chamado. Para outros verbos como %d, o pacote fmt espera um tipo numérico; se você passar um tipo que não é número, pode haver comportamento inesperado ou erro de compilação.
Exemplo com diferentes verbos:
type Ponto struct {
X, Y int
}
func (p Ponto) String() string {
return fmt.Sprintf("(%d, %d)", p.X, p.Y)
}
func main() {
p := Ponto{3, 4}
fmt.Printf("%%v: %v\n", p) // %v: (3, 4)
fmt.Printf("%%+v: %+v\n", p) // %+v: {X:3 Y:4} (sem usar String())
fmt.Printf("%%#v: %#v\n", p) // %#v: main.Ponto{X:3, Y:4}
fmt.Printf("%%s: %s\n", p) // %s: (3, 4) (usa String())
}Note que %+v e %#v ignoram String() e mostram a representação interna da struct. Isso é útil para debug.
Implementando String()
Ao implementar String(), algumas boas práticas devem ser seguidas:
- Não use ponteiro receptor a menos que necessário: Se o método não modificar o receptor, use um valor receptor para que o método possa ser chamado tanto em valores quanto em ponteiros. Se usar ponteiro receptor, apenas ponteiros satisfarão a interface.
- Evite recursão infinita: Dentro de
String(), não usefmt.Sprintfcom%vou%sno próprio tipo, pois isso chamariaString()novamente, causando recursão. Use%#vou formatação manual. - Seja conciso e informativo: A saída deve ser legível para humanos.
Exemplo de implementação segura:
type Conta struct {
Titular string
Saldo float64
}
func (c Conta) String() string {
// Usa %f para evitar recursão
return fmt.Sprintf("Conta de %s: R$ %.2f", c.Titular, c.Saldo)
}Se você precisar acessar o valor do próprio tipo dentro de String() sem causar recursão, converta para um tipo primitivo ou use fmt.Sprintf com verbos que não chamam String(), como %d, %f, %#v (este último mostra a representação Go, não chama String()).
Debug
Para debug, o Go oferece a interface GoStringer, também no pacote fmt:
type GoStringer interface {
GoString() string
}Quando você usa o verbo %#v, o pacote fmt verifica se o tipo implementa GoStringer. Se sim, chama GoString(); caso contrário, usa uma representação padrão que se assemelha à sintaxe Go. Isso é útil para obter uma saída que pode ser usada para recriar o valor, ideal para logs e depuração.
Exemplo:
type Ponto struct {
X, Y int
}
func (p Ponto) GoString() string {
return fmt.Sprintf("Ponto{X: %d, Y: %d}", p.X, p.Y)
}
func main() {
p := Ponto{3, 4}
fmt.Printf("%%#v: %#v\n", p) // %#v: Ponto{X: 3, Y: 4}
}Implementar GoString() é uma boa prática para tipos complexos, pois permite que logs e ferramentas de debug exibam informações completas. Lembre-se de que GoString() não deve chamar %#v no próprio tipo para evitar recursão.
Boas práticas
- Sempre implemente
String()para tipos que serão frequentemente impressos. - Para tipos que precisam de representação detalhada para debug, implemente
GoString(). - Use
String()com receptor de valor se o método não modificar o estado. - Evite alocações desnecessárias dentro de
String()(mas em Go, isso geralmente não é problema). - Documente o formato da string retornada, especialmente se for usada para parsing.
Referências
- Documentação oficial do pacote fmt - Stringer
- Documentação oficial do pacote fmt - GoStringer
- Blog oficial Go: Error handling and Go
- Go by Example: Stringer
- YourBasic: fmt.Printf formatting cheat sheet
- Stack Overflow: How to implement Stringer in Go
Exercícios
Crie um tipo
Livrocom camposTitulo(string) eAutor(string). Implemente o métodoString()que retorna". Teste compor " fmt.Println.✓ Resposta:type Livro struct { Titulo string Autor string } func (l Livro) String() string { return fmt.Sprintf("%s por %s", l.Titulo, l.Autor) } func main() { l := Livro{"1984", "George Orwell"} fmt.Println(l) // 1984 por George Orwell }Implemente o método
GoString()para o tipoLivrodo exercício anterior, retornando uma string no formatoLivro{Titulo: "...", Autor: "..."}. Teste comfmt.Printf("%#v", livro).✓ Resposta:func (l Livro) GoString() string { return fmt.Sprintf("Livro{Titulo: %q, Autor: %q}", l.Titulo, l.Autor) } func main() { l := Livro{"1984", "George Orwell"} fmt.Printf("%#v\n", l) // Livro{Titulo: "1984", Autor: "George Orwell"} }Explique por que o código abaixo causa recursão infinita e corrija-o:
type Numero int func (n Numero) String() string { return fmt.Sprintf("O número é %v", n) }✓ Resposta:O
%vdentro deString()chama o próprioString()novamente, causando recursão. Para corrigir, use%dque não chamaString():func (n Numero) String() string { return fmt.Sprintf("O número é %d", int(n)) }Crie um tipo
Listaque contém um slice de inteiros. ImplementeString()que retorna os elementos separados por vírgula entre colchetes, ex:[1, 2, 3]. Não usestrings.Join.✓ Resposta:type Lista struct { Elementos []int } func (l Lista) String() string { if len(l.Elementos) == 0 { return "[]" } s := "[" for i, v := range l.Elementos { if i > 0 { s += ", " } s += fmt.Sprintf("%d", v) } s += "]" return s } func main() { l := Lista{[]int{1, 2, 3}} fmt.Println(l) // [1, 2, 3] }Dado o tipo
Pessoacom camposNomeeIdade, implementeString()usando receptor de ponteiro. O que acontece se você tentar imprimir um valor dePessoa(não ponteiro) comfmt.Println? Teste e explique.✓ Resposta:Se
String()tem receptor de ponteiro (*Pessoa), apenas ponteiros paraPessoaimplementamStringer. Um valor dePessoanão implementa, entãofmt.Println(p)(onde p éPessoa) usará a representação padrão, ex:{João 30}. Para usar o método, você precisa passar um ponteiro:fmt.Println(&p).type Pessoa struct { Nome string Idade int } func (p *Pessoa) String() string { return fmt.Sprintf("%s (%d anos)", p.Nome, p.Idade) } func main() { p := Pessoa{"João", 30} fmt.Println(p) // {João 30} fmt.Println(&p) // João (30 anos) }