Go Stringer Interface: Custom String Representation
The `fmt.Stringer` interface is one of Go's most frequently implemented interfaces, yet many developers overlook its power. Defined in the `fmt` package, it contains a single method:
Key Insights
- The Stringer interface (
String() string) lets you control how your types appear in logs, debug output, and user interfaces—implement it on any type that needs a human-readable representation - Always use pointer receivers for String() methods and handle nil cases explicitly to avoid panics in production logging code
- Avoid infinite recursion by never calling fmt.Sprintf with %v or %s on the receiver itself—use %d, %t, or access fields directly instead
Understanding the Stringer Interface
The fmt.Stringer interface is one of Go’s most frequently implemented interfaces, yet many developers overlook its power. Defined in the fmt package, it contains a single method:
type Stringer interface {
String() string
}
Any type that implements this method automatically gains custom string representation across Go’s standard library. When you print a value using fmt.Println, fmt.Sprintf, or convert to string in logging frameworks, Go checks if your type implements Stringer. If it does, your custom String() method runs instead of the default reflection-based output.
Here’s a basic implementation:
type Person struct {
FirstName string
LastName string
Age int
}
func (p *Person) String() string {
return fmt.Sprintf("%s %s (age %d)", p.FirstName, p.LastName, p.Age)
}
func main() {
person := &Person{FirstName: "Alice", LastName: "Chen", Age: 32}
fmt.Println(person) // Output: Alice Chen (age 32)
}
Without the String() method, you’d see &{Alice Chen 32}—functional but ugly. With it, you get clean, readable output that makes sense to humans.
Where Stringer Provides Real Value
The Stringer interface shines in three scenarios: debugging, security, and domain modeling.
For debugging, custom string representations make logs infinitely more readable. Consider a User struct that contains sensitive data:
type User struct {
ID int
Email string
Password string
APIKey string
}
func (u *User) String() string {
return fmt.Sprintf("User{ID: %d, Email: %s, Password: [REDACTED], APIKey: [REDACTED]}",
u.ID, u.Email)
}
func main() {
user := &User{
ID: 1001,
Email: "alice@example.com",
Password: "super-secret",
APIKey: "sk_live_abc123",
}
fmt.Printf("Processing user: %v\n", user)
// Output: Processing user: User{ID: 1001, Email: alice@example.com, Password: [REDACTED], APIKey: [REDACTED]}
}
This prevents accidentally logging passwords or API keys. Without String(), the default output would expose everything.
For domain objects, Stringer lets you present data in business-friendly formats:
type Coordinate struct {
Latitude float64
Longitude float64
}
func (c *Coordinate) String() string {
latDir := "N"
if c.Latitude < 0 {
latDir = "S"
}
lonDir := "E"
if c.Longitude < 0 {
lonDir = "W"
}
return fmt.Sprintf("%.4f°%s, %.4f°%s",
abs(c.Latitude), latDir,
abs(c.Longitude), lonDir)
}
func abs(n float64) float64 {
if n < 0 {
return -n
}
return n
}
func main() {
loc := &Coordinate{Latitude: 37.7749, Longitude: -122.4194}
fmt.Println("San Francisco:", loc)
// Output: San Francisco: 37.7749°N, 122.4194°W
}
Implementing String() for Different Type Categories
Stringer works on any named type, not just structs. Enum-like constants benefit enormously from custom string representations:
type Status int
const (
StatusPending Status = iota
StatusProcessing
StatusCompleted
StatusFailed
)
func (s Status) String() string {
switch s {
case StatusPending:
return "pending"
case StatusProcessing:
return "processing"
case StatusCompleted:
return "completed"
case StatusFailed:
return "failed"
default:
return fmt.Sprintf("Status(%d)", s)
}
}
func main() {
var status Status = StatusProcessing
fmt.Printf("Current status: %s\n", status)
// Output: Current status: processing
}
For nested structs, each level can implement Stringer independently:
type Address struct {
Street string
City string
State string
}
func (a *Address) String() string {
return fmt.Sprintf("%s, %s, %s", a.Street, a.City, a.State)
}
type Company struct {
Name string
Address *Address
}
func (c *Company) String() string {
return fmt.Sprintf("%s (%s)", c.Name, c.Address)
}
func main() {
company := &Company{
Name: "Acme Corp",
Address: &Address{
Street: "123 Main St",
City: "Springfield",
State: "IL",
},
}
fmt.Println(company)
// Output: Acme Corp (123 Main St, Springfield, IL)
}
For collection types, implement Stringer on wrapper types:
type Tags []string
func (t Tags) String() string {
if len(t) == 0 {
return "[no tags]"
}
return "[" + strings.Join(t, ", ") + "]"
}
func main() {
tags := Tags{"go", "backend", "api"}
fmt.Println("Article tags:", tags)
// Output: Article tags: [go, backend, api]
}
Stringer vs. GoStringer: Two Representations
The fmt package defines a second interface for Go-syntax representation:
type GoStringer interface {
GoString() string
}
Use GoString() when you want output that looks like Go code—useful for debugging and testing. The %#v verb triggers GoString() instead of String():
type Point struct {
X, Y int
}
func (p *Point) String() string {
return fmt.Sprintf("(%d, %d)", p.X, p.Y)
}
func (p *Point) GoString() string {
return fmt.Sprintf("Point{X: %d, Y: %d}", p.X, p.Y)
}
func main() {
p := &Point{X: 10, Y: 20}
fmt.Printf("%%v: %v\n", p) // Output: %v: (10, 20)
fmt.Printf("%%s: %s\n", p) // Output: %s: (10, 20)
fmt.Printf("%%#v: %#v\n", p) // Output: %#v: Point{X: 10, Y: 20}
}
String() produces human-friendly output for logs and UIs. GoString() produces copy-pasteable Go code for debugging sessions.
How fmt Verbs Interact with Stringer
Understanding which verbs trigger String() helps you use the interface effectively:
%v- calls String() if available, otherwise uses reflection%s- calls String() if available, otherwise errors for non-string types%#v- calls GoString() if available, otherwise uses Go-syntax reflection%+v- ignores String(), uses reflection with field names%q- quotes the String() output
type Product struct {
Name string
Price int
}
func (p *Product) String() string {
return fmt.Sprintf("%s ($%d)", p.Name, p.Price)
}
func main() {
prod := &Product{Name: "Widget", Price: 29}
fmt.Printf("%%v: %v\n", prod) // Output: %v: Widget ($29)
fmt.Printf("%%s: %s\n", prod) // Output: %s: Widget ($29)
fmt.Printf("%%+v: %+v\n", prod) // Output: %+v: &{Name:Widget Price:29}
fmt.Printf("%%q: %q\n", prod) // Output: %q: "Widget ($29)"
}
The %+v verb explicitly bypasses String()—useful when you need to see the raw struct fields for debugging.
Critical Pitfalls to Avoid
Always use pointer receivers. Value receivers create copies, and String() often gets called in contexts where you have pointers:
// Good: pointer receiver
func (p *Person) String() string {
return fmt.Sprintf("%s %s", p.FirstName, p.LastName)
}
// Bad: value receiver - won't work with pointer values in many contexts
func (p Person) String() string {
return fmt.Sprintf("%s %s", p.FirstName, p.LastName)
}
Handle nil explicitly. Pointer receivers can be nil, and String() gets called on nil values:
type Node struct {
Value int
Next *Node
}
func (n *Node) String() string {
if n == nil {
return "<nil>"
}
return fmt.Sprintf("Node{%d}", n.Value)
}
func main() {
var n *Node
fmt.Println(n) // Output: <nil> (instead of panic)
}
Never create infinite recursion. This is the most common mistake:
// WRONG: infinite recursion
func (p *Person) String() string {
return fmt.Sprintf("Person: %v", p) // calls p.String() again!
}
// CORRECT: access fields directly
func (p *Person) String() string {
return fmt.Sprintf("Person: %s %s", p.FirstName, p.LastName)
}
// ALSO CORRECT: use %+v to bypass Stringer
func (p *Person) String() string {
return fmt.Sprintf("Person: %+v", p)
}
The %v and %s verbs call String(), creating infinite loops. Use specific verbs like %d, %t, %f, or access fields directly.
Consider performance for hot paths. String() gets called frequently in logging. If it’s expensive, guard it:
type Dataset struct {
Values []int
}
func (d *Dataset) String() string {
if len(d.Values) > 100 {
return fmt.Sprintf("Dataset{%d values}", len(d.Values))
}
return fmt.Sprintf("Dataset%v", d.Values)
}
The Stringer interface is a small addition to your types that pays massive dividends in code clarity. Implement it on domain objects, enums, and any type that appears in logs. Your future self—and your teammates—will thank you when debugging production issues at 2 AM.