A interface Stringer e fmt
Nesta aula, exploramos a interface Stringer do pacote fmt, que permite controlar a representação textual de tipos personalizados. Aprendemos sobre os verbos de formatação, como implementar o método String() e como usar a interface para debug.
A interface Stringer do pacote fmt é uma das interfaces mais importantes em Go. Ela permite que qualquer tipo defina sua própria representação textual, substituindo a saída padrão de fmt.Println e funções similares. Implementar String() dá controle total sobre como seus dados são exibidos, facilitando logging, depuração e formatação de saída.
Além disso, entender os verbos de formatação do pacote fmt é essencial para produzir saídas bem formatadas. Vamos ver como usar %v, %+v, %#v e outros para diferentes propósitos.
fmt.Stringer
A interface Stringer é definida como:
type Stringer interface {
String() string
}Qualquer tipo que implemente o método String() string satisfaz essa interface. Quando você passa um valor para fmt.Println, fmt.Printf com o verbo %s ou %v, a função chama automaticamente String() para obter a representação textual.
Exemplo básico:
package main
import "fmt"
type Person struct {
Name string
Age int
}
func (p Person) String() string {
return fmt.Sprintf("%s (%d years old)", p.Name, p.Age)
}
func main() {
p := Person{"Alice", 30}
fmt.Println(p) // Output: Alice (30 years old)
}Sem o método String(), a saída seria algo como {Alice 30}. Com ele, você controla a formatação.
Verbos de formatação
O pacote fmt oferece vários verbos para formatar valores. Os mais comuns são:
%v: valor padrão. Para tipos que implementamStringer, chamaString(). Para structs, exibe campos.%+v: como%v, mas inclui nomes dos campos em structs.%#v: representação Go-syntax do valor (útil para debug).%s: string. ParaStringer, chamaString().%d: inteiro decimal.%f: ponto flutuante.%T: tipo do valor.
Exemplo:
package main
import "fmt"
type Point struct {
X, Y int
}
func main() {
p := Point{3, 4}
fmt.Printf("v: %%v\n", p) // {3 4}
fmt.Printf("+v: %%+v\n", p) // {X:3 Y:4}
fmt.Printf("#v: %%#v\n", p) // main.Point{X:3, Y:4}
fmt.Printf("T: %%T\n", p) // main.Point
}Implementando String()
Ao implementar String(), lembre-se de usar um receptor de valor ou ponteiro. Se você usar receptor de ponteiro, apenas ponteiros para o tipo satisfazem a interface; valores não. Normalmente, usa-se receptor de valor para tipos pequenos e imutáveis, e ponteiro para tipos grandes ou mutáveis.
Exemplo com ponteiro:
func (p *Person) String() string {
return fmt.Sprintf("%s (%d years old)", p.Name, p.Age)
}
func main() {
p := &Person{"Bob", 25}
fmt.Println(p) // funciona
// fmt.Println(Person{"Bob",25}) // erro: Person does not implement Stringer
}Para evitar inconsistências, implemente sempre com receptor de valor a menos que haja motivo para ponteiro.
Debug
Para debug, é comum usar %#v para exibir a representação completa do valor, incluindo nomes de campos e tipos. Isso é útil para inspecionar structs complexas. Outra técnica é implementar um método Debug() separado, mas String() com %#v já atende bem.
Exemplo:
package main
import "fmt"
type Config struct {
Host string
Port int
Debug bool
}
func main() {
c := Config{"localhost", 8080, true}
fmt.Printf("%%#v: %#v\n", c)
// Output: main.Config{Host:"localhost", Port:8080, Debug:true}
}Além disso, você pode implementar String() para produzir uma saída mais legível para logs, enquanto usa %#v para depuração detalhada.
Boas práticas
- Não chame
fmt.Sprintfdentro deString()com o mesmo tipo recursivamente, ou causará loop infinito. - Use
String()para saída amigável ao usuário; para debug, use%#vou implemente um método separado. - Sempre documente o método
String()se a formatação não for óbvia.
Referências
- Documentação oficial do pacote fmt - Stringer
- Go Blog: Strings, bytes, runes and characters
- Documentação dos verbos de formatação
- Go by Example: Stringer
- YourBasic: fmt.Printf formatting cheat sheet
Exercícios
- Implemente o método
String()para o tipoBookcom camposTitle(string) ePages(int). A saída deve ser algo como "'Title' has Pages pages". Teste comfmt.Println. - Crie um tipo
Temperature(float64) que implementeString()retornando a temperatura com "°C" no final. Exemplo: 23.5 → "23.5°C". - Dada a struct
Rectanglecom camposWidtheHeight, implementeString()para retornar "Rectangle(width=W, height=H)". Use%ve%+vpara ver a diferença. - Implemente
String()para um tipoListque é um slice de inteiros. A saída deve ser "[elem1, elem2, ...]" (com vírgulas e espaços). - Use
%#vpara exibir o debug de uma structServerConfigcom camposAddress(string),Port(int) eTLS(bool). Compare com a saída de%v.
type Book struct {
Title string
Pages int
}
func (b Book) String() string {
return fmt.Sprintf("'%s' has %d pages", b.Title, b.Pages)
}
func main() {
b := Book{"Go Programming", 400}
fmt.Println(b)
}type Temperature float64
func (t Temperature) String() string {
return fmt.Sprintf("%.1f°C", t)
}
func main() {
temp := Temperature(23.5)
fmt.Println(temp) // 23.5°C
}type Rectangle struct {
Width, Height float64
}
func (r Rectangle) String() string {
return fmt.Sprintf("Rectangle(width=%.2f, height=%.2f)", r.Width, r.Height)
}
func main() {
rect := Rectangle{10.5, 5.2}
fmt.Printf("%%v: %v\n", rect)
fmt.Printf("%%+v: %+v\n", rect)
}type List []int
func (l List) String() string {
if len(l) == 0 {
return "[]"
}
s := "["
for i, v := range l {
if i > 0 {
s += ", "
}
s += fmt.Sprintf("%d", v)
}
s += "]"
return s
}
func main() {
l := List{1, 2, 3}
fmt.Println(l) // [1, 2, 3]
}type ServerConfig struct {
Address string
Port int
TLS bool
}
func main() {
cfg := ServerConfig{"0.0.0.0", 443, true}
fmt.Printf("%%v: %v\n", cfg) // {0.0.0.0 443 true}
fmt.Printf("%%#v: %#v\n", cfg) // main.ServerConfig{Address:"0.0.0.0", Port:443, TLS:true}
}