skip to content

When does fmt call a type's String() method, and why can a pointer receiver hide it?

level: middleimportance: must knowfreq 66%

answer

  1. one method, one interface, one string
  2. not every verb asks for it
  3. method sets decide who satisfies it
  4. a value and its address differ here
  5. value receiver makes both forms print

basics

~20 s

fmt calls String() when the operand satisfies fmt.Stringer and the verb is %v, %s, %q, %x or %X. If String has a pointer receiver, only *T satisfies the interface, so printing a T value falls back to default formatting.

solid answer

~40 s

`fmt.Stringer` is the one-method interface `String() string`. When you pass an operand to `fmt.Printf` and friends, fmt does a type assertion at runtime: if the value satisfies `Stringer` and the verb is one that accepts a string — `%v`, `%s`, `%q`, `%x`, `%X` — it calls `String()` and formats the result. Verbs like `%d` or `%f` do not consult it, so a named integer type with a `String()` still prints its number under `%d`. The receiver trap follows from Go's method sets: a method declared on `*Conn` is in `*Conn`'s method set but not `Conn`'s, so `fmt.Printf("%v", c)` on a `Conn` value skips `String()` entirely and prints `{7}`, while `&c` prints `conn#7`. The same bites for `[]Conn`, whose elements are values. Declaring `String()` on the value receiver makes both work.

code

go · 7 lines
go
type Conn struct{ id int }

func (c *Conn) String() string { return "conn#" + strconv.Itoa(c.id) }

c := Conn{id: 7}
fmt.Printf("%v\n", c)  // {7} — Conn's method set has no String
fmt.Printf("%v\n", &c) // conn#7 — *Conn's does

go deeper

for a junior

Recall that fmt.Stringer is just String() string, and that implementing it changes how your type appears under %v and %s in Printf and Sprintf.

for a middle

Explain the mechanism: fmt type-asserts the operand at runtime, only for the string-accepting verbs, and method sets mean a pointer-receiver String is invisible when a value is passed.

for a senior

Show how you catch it in review and in production: printed output that suddenly shows raw struct braces, a var _ fmt.Stringer = T{} assertion, and choosing the receiver so both forms print correctly.

for a principal

Treat a type's printed form as a contract others depend on: log parsers, dashboards and incident runbooks read it, so decide who may change String and whether it may ever expose sensitive field values.

## The interface ```go type Stringer interface { String() string } ``` That is the whole of `fmt.Stringer`. Any type with a `String() string` method satisfies it — Go interfaces are satisfied structurally, with no `implements` keyword — and that is how a type controls how it appears in printed output. ## When fmt actually calls it fmt's formatting functions take `...any`, so every operand arrives as an interface value carrying a dynamic type. Before falling back to reflection-based default formatting, fmt type-asserts the operand against a small set of interfaces. For `Stringer` the rule has two parts, and candidates usually remember only the first: 1. The operand must satisfy `Stringer`. 2. The verb must be one that accepts a string: `%v`, `%s`, `%q`, `%x` or `%X`. So a `type Level int` with a `String()` method prints `INFO` under `%v` and `%s`, prints `"INFO"` (quoted) under `%q` — fmt formats the *returned string* according to the verb — and prints `2` under `%d`, because `%d` is a numeric verb and never consults the method. That last case surprises people who assume `String()` is a universal override. ## The pointer-receiver trap Go's method sets decide interface satisfaction. For a defined type `T`: - `T`'s method set contains methods declared with receiver `T`. - `*T`'s method set contains methods declared with receiver `T` **and** those declared with receiver `*T`. So if you write `func (c *Conn) String() string`, then `*Conn` satisfies `fmt.Stringer` and `Conn` does not. Now: ```go c := Conn{id: 7} fmt.Printf("%v", c) // {7} — default struct formatting fmt.Printf("%v", &c) // conn#7 — String() is called ``` Why doesn't the compiler take the address of `c` for you, as it does when you write `c.String()` directly? Because that shortcut only applies to *addressable* values in a method call expression. Here `c` is being copied into an `any` parameter; once inside an interface value there is no address to take, and the assertion to `Stringer` simply fails. The compiler cannot help, and — crucially — nothing warns you. The output is merely wrong. The same failure appears in collections. `[]Conn` printed with `%v` formats each element as a value, so no element's `String()` is called even though `[]*Conn` would work. A `map[string]Conn` behaves the same way. ## Which receiver to choose The usual advice is to declare `String()` on the value receiver when the type is small and copying it is harmless, because then both `T` and `*T` print correctly — `*T`'s method set includes value-receiver methods. Use a pointer receiver when the type is large, when the rest of the type's methods are already on the pointer (mixing receivers on one type is a smell), or when `String()` must observe mutations through the pointer. If you do use a pointer receiver, be consistent about printing pointers. ## Nil receivers and panics fmt calls your method defensively. If `String()` panics, fmt recovers and embeds the failure in the output rather than crashing the program: you get something like `%!v(PANIC=String method: runtime error: invalid memory address or nil pointer dereference)`. There is one special case ahead of that: if the operand is a nil pointer, fmt prints `<nil>` instead of calling the method at all, which is why a nil `*Conn` does not blow up your log line. That safety net has a hard limit. It catches panics, and unbounded recursion is not a panic — a `String()` method that formats its own receiver with `%v` exhausts the goroutine stack and takes the process down with a fatal error that no `recover` can intercept. ## Writing a good String method - Keep it cheap and allocation-light; it runs inside log statements and hot loops. - Never let it mutate the receiver or take locks that the caller may already hold — fmt can call it from anywhere, including a debugger's inspection of a value. - Guard a pointer receiver against a nil receiver if the type is routinely used as `*T` inside another struct: `if c == nil { return "<nil conn>" }` works because a method with a pointer receiver can be called on a nil pointer. - Do not format the receiver itself with `%v` inside it. ## Checking it works The cheapest test is a one-line assertion that `fmt.Sprintf("%v", value)` equals the string you expect — written with the same value form (value or pointer) your production code actually prints. A compile-time assertion, `var _ fmt.Stringer = Conn{}`, fails the build if the method set is wrong, and is worth adding on a type whose printed form is part of an operator-facing contract.

  • A named integer type has a String() method. What does %d print for it?
    The number. fmt only consults `Stringer` for verbs that accept a string — `%v`, `%s`, `%q`, `%x`, `%X` — so `%d` formats the underlying integer and ignores the method entirely. This is why enum types print their name under `%v` and their ordinal under `%d`, which is often exactly what you want.
  • What does fmt do if String() panics while it is formatting?
    It recovers and embeds the failure in the output, e.g. `%!v(PANIC=String method: runtime error: invalid memory address or nil pointer dereference)`, so one bad log line does not kill the process. Before that, if the operand is itself a nil pointer, fmt prints `<nil>` without calling the method at all.
  • Why doesn't the compiler take the address of the value for you, the way it does for c.String()?
    That shortcut applies to addressable operands in a method-call expression. Passing `c` to `Printf` copies it into an `any` parameter first; inside an interface value there is nothing to address, so the runtime assertion to `fmt.Stringer` just fails and fmt falls back to default formatting.

Declaring String() on a pointer receiver is like putting your name on the mailbox rather than on yourself: mail reaches the address, but nobody recognises you when you show up in person.

saying these in an interview costs you the question

  • Says String() overrides formatting for every verb
  • Believes fmt takes the address of the value automatically
  • Thinks a compile error catches the wrong receiver
  • Claims elements of a []T slice get their String called
  • Expects a nil pointer operand to panic in Printf