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 implementam Stringer, chama String(). 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. Para Stringer, chama String().
  • %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.Sprintf dentro de String() com o mesmo tipo recursivamente, ou causará loop infinito.
  • Use String() para saída amigável ao usuário; para debug, use %#v ou implemente um método separado.
  • Sempre documente o método String() se a formatação não for óbvia.

Referências

Exercícios

  1. Implemente o método String() para o tipo Book com campos Title (string) e Pages (int). A saída deve ser algo como "'Title' has Pages pages". Teste com fmt.Println.
  2. ✓ Resposta:
    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)
    }
  3. Crie um tipo Temperature (float64) que implemente String() retornando a temperatura com "°C" no final. Exemplo: 23.5 → "23.5°C".
  4. ✓ Resposta:
    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
    }
  5. Dada a struct Rectangle com campos Width e Height, implemente String() para retornar "Rectangle(width=W, height=H)". Use %v e %+v para ver a diferença.
  6. ✓ Resposta:
    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)
    }
  7. Implemente String() para um tipo List que é um slice de inteiros. A saída deve ser "[elem1, elem2, ...]" (com vírgulas e espaços).
  8. ✓ Resposta:
    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]
    }
  9. Use %#v para exibir o debug de uma struct ServerConfig com campos Address (string), Port (int) e TLS (bool). Compare com a saída de %v.
  10. ✓ Resposta:
    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}
    }