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 (usa Stringer se disponível)
  • %+v: para structs, exibe nomes dos campos
  • %#v: representação Go-syntax (como se fosse código)
  • %s: string simples (para strings, chama String() 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 use fmt.Sprintf com %v ou %s no próprio tipo, pois isso chamaria String() novamente, causando recursão. Use %#v ou 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

Exercícios

  1. Crie um tipo Livro com campos Titulo (string) e Autor (string). Implemente o método String() que retorna " por ". Teste com 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
    }
  2. Implemente o método GoString() para o tipo Livro do exercício anterior, retornando uma string no formato Livro{Titulo: "...", Autor: "..."}. Teste com fmt.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"}
    }
  3. 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 %v dentro de String() chama o próprio String() novamente, causando recursão. Para corrigir, use %d que não chama String():

    func (n Numero) String() string {
        return fmt.Sprintf("O número é %d", int(n))
    }
  4. Crie um tipo Lista que contém um slice de inteiros. Implemente String() que retorna os elementos separados por vírgula entre colchetes, ex: [1, 2, 3]. Não use strings.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]
    }
  5. Dado o tipo Pessoa com campos Nome e Idade, implemente String() usando receptor de ponteiro. O que acontece se você tentar imprimir um valor de Pessoa (não ponteiro) com fmt.Println? Teste e explique.

    ✓ Resposta:

    Se String() tem receptor de ponteiro (*Pessoa), apenas ponteiros para Pessoa implementam Stringer. Um valor de Pessoa não implementa, então fmt.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)
    }