How to Implement the Stringer Interface for Custom Types

Implement the String() method on your type to satisfy the fmt.Stringer interface for custom formatting.

When raw structs ruin your logs

You are debugging a production service. The logs are scrolling with &{42 0xc000010230 true} instead of Order{ID: 42, Status: "pending"}. You stare at a hex address trying to figure out which order failed. The data is there, but the output is useless. Go gives you a way to fix this without changing every print statement.

Implementing the fmt.Stringer interface lets your type define how it looks in logs, errors, and debug output. The interface is tiny: one method, no arguments, returns a string. Once you add the method, every fmt function automatically uses your format.

The Stringer interface

Go's fmt package checks for a String() string method before it decides how to print a value. If the method exists, fmt calls it. If not, fmt falls back to a default printer that dumps the internal structure.

Think of Stringer like a label maker. The default behavior is to read the serial number and circuit board layout of a device. Implementing Stringer lets you stick a human-readable label on the object. The interface is defined in the standard library as:

type Stringer interface {
    String() string
}

You never write this interface in your code. Go checks for interface satisfaction implicitly. If your type has a method with the exact signature String() string, it satisfies the interface. The compiler enforces the signature, but the interface itself is invisible to your code.

Minimal example

Here's the simplest implementation: a struct with a String method that formats its fields.

package main

import "fmt"

type Point struct {
    X int
    Y int
}

// Point.String returns a readable representation of the coordinates.
func (p Point) String() string {
    // Use Sprintf to format the fields into a single string.
    return fmt.Sprintf("(%d, %d)", p.X, p.Y)
}

func main() {
    p := Point{3, 4}
    // fmt.Println detects the String method and calls it automatically.
    fmt.Println(p)
}

The output is (3, 4). No pointers, no field names, just the data you care about. The fmt package finds the method and uses it.

How fmt finds your method

When you call fmt.Println(p), the fmt package inspects the type of p. It uses reflection to check if the type implements fmt.Stringer. Reflection looks at the method set of the type. If String() string is present, fmt calls it and prints the result.

This check happens at runtime. The compiler doesn't insert calls to String for you. The fmt package does the work dynamically. This means fmt can handle any type, even ones defined in packages you don't control. If a third-party type implements Stringer, fmt will use it.

Reflection has a cost, but fmt is already doing reflection to inspect the value. Implementing Stringer actually saves work because fmt can skip the deep inspection of fields and just call your method. The overhead of the interface check is negligible compared to the allocation and formatting fmt does anyway.

Go doesn't have an implements keyword. The compiler checks interface satisfaction by looking at the method set. If your type has the method, it satisfies the interface. This happens at compile time for type checking, but fmt uses reflection to find the method at runtime. The two mechanisms work together: the compiler ensures the method exists and has the right signature, while fmt discovers it dynamically.

Formatting verbs and behavior

The fmt package supports format verbs that control output. %v prints the default representation. %s prints the string representation. If your type implements Stringer, both %v and %s call your String method.

package main

import "fmt"

type Color struct {
    Name string
    Hex  string
}

// Color.String formats the color for display.
func (c Color) String() string {
    // Return a compact representation with the name and hex code.
    return fmt.Sprintf("%s (#%s)", c.Name, c.Hex)
}

func main() {
    c := Color{"crimson", "DC143C"}
    // Both %v and %s use the String method.
    fmt.Printf("Default: %v\n", c)
    fmt.Printf("String:  %s\n", c)
}

The output shows the same result for both verbs. %#v prints the Go syntax representation. That uses a different interface, fmt.GoStringer. If you want to customize %#v, implement GoString() string. Most code only needs Stringer.

Realistic example

Real code often has enums, nested structs, or complex state. Here's a task tracker where both the status and the task implement Stringer.

package main

import (
    "fmt"
    "time"
)

type Status int

const (
    Pending Status = iota
    Running
    Done
)

// Status.String maps the integer constant to a readable name.
func (s Status) String() string {
    // Switch on the value to return the correct label.
    switch s {
    case Pending:
        return "pending"
    case Running:
        return "running"
    case Done:
        return "done"
    default:
        return fmt.Sprintf("unknown(%d)", s)
    }
}

type Task struct {
    ID        string
    Status    Status
    CreatedAt time.Time
}

// Task.String formats the task for logging.
func (t Task) String() string {
    // Include the ID, status, and age of the task.
    age := time.Since(t.CreatedAt).Round(time.Second)
    return fmt.Sprintf("Task{ID:%s, Status:%s, Age:%s}", t.ID, t.Status, age)
}

func main() {
    t := Task{
        ID:        "abc-123",
        Status:    Running,
        CreatedAt: time.Now().Add(-5 * time.Minute),
    }
    // The output uses the custom String methods for both Task and Status.
    fmt.Println(t)
}

The output is Task{ID:abc-123, Status:running, Age:5m0s}. The Status field prints as running instead of 1 because Status also implements Stringer. fmt calls String recursively on nested values.

Convention aside: receiver names should be short. (t Task) is standard. (this Task) or (self Task) are not Go style. The receiver name usually matches the first letter of the type. Keep it consistent.

Pitfalls and runtime panics

The most common bug is infinite recursion. If your String method calls fmt.Sprint on the receiver, you trigger the printer again, which calls String again. The program panics with a stack overflow.

// BAD: This causes infinite recursion.
func (p Point) String() string {
    // fmt.Sprint calls String again, creating a loop.
    return fmt.Sprint(p)
}

Always format fields directly. Never format the receiver itself. If you need to print the receiver, use %+v on a copy or access fields explicitly.

The method signature must match exactly. If you add an argument, the interface is not satisfied. The compiler won't complain about the interface, but fmt won't call the method. You'll see raw output. If you return the wrong type, the compiler rejects it with cannot use ... as string value in return statement.

Pointer receivers change the interface satisfaction. If you define String() on *MyType, only pointers satisfy the interface. Passing a value to fmt won't trigger the method. Go can auto-dereference for method calls in some cases, but fmt checks the interface on the value. If the value doesn't have the method, it falls back. Define String() on the value receiver unless you need to mutate state or the struct is huge. Value receivers keep the interface simple and safe.

A String method is a contract. The caller expects a string. If your method panics, the caller crashes. Never panic in String. Handle missing data gracefully. Return "unknown" or "nil" rather than crashing the logger. A panic in String propagates up and can take down the whole program.

Convention aside: gofmt handles the formatting of your code. Stringer handles the formatting of your data. Two different tools. Trust gofmt for indentation and layout. Implement Stringer for readable output.

When to use Stringer

Use fmt.Stringer when you want fmt functions to print a readable summary instead of raw fields. Use encoding.TextMarshaler when you need to control how the type serializes to text for storage or transmission. Use fmt.GoStringer when you want to customize the %#v format to show Go source code syntax. Use the default printer when the type is internal and never appears in logs.

Implement Stringer for logs, not for logic. The method is for presentation. Don't use String to generate keys for maps or to compare values. Use explicit methods for those operations. Keep String focused on human readability.

Never call fmt on self inside String. Value receivers for String keep the interface simple.

Where to go next