skip to content

Why does fmt.Println(v) skip a String() method declared with a pointer receiver?

level: middleimportance: should knowfreq 44%

answer

  1. the value and the pointer have different method sets
  2. an interface holds a copy, not the variable
  3. a copy inside an interface is not addressable
  4. no address is taken when satisfying an interface
  5. printing the address makes it work

basics

~20 s

Only the pointer type's method set contains a pointer-receiver method, so the value copy stored in fmt's interface argument does not satisfy fmt.Stringer. fmt cannot take the address of that copy, so it falls back to default formatting.

solid answer

~50 s

`fmt.Println` takes `...any`, so the argument is copied into an interface value whose dynamic type is `Status`, not `*Status`. A method declared as `func (s *Status) String() string` is in the method set of `*Status` only, so `Status` does not implement `fmt.Stringer` and `fmt` never finds the method — it formats the value structurally instead. The usual escape hatch for calling a pointer method on a value, where the compiler silently takes the address, does not apply here: the copy inside an interface is not addressable, and by the time `fmt` inspects it the original variable is out of reach. Fix it by declaring `String()` on the value receiver, which puts it in both method sets, or by printing `&v`. The same rule explains why a `[]Status` prints its elements raw while a `[]*Status` prints them through `String()`.

code

go · 8 lines
go
type Status string

func (s *Status) String() string { return "status:" + string(*s) }

var s Status = "queued"

// fmt.Println(s) prints: queued
// fmt.Println(&s) prints: status:queued

go deeper

for a junior

Recall the two method sets: the pointer type has both value- and pointer-receiver methods, the value type has only the value-receiver ones. Know that printing the address makes a pointer-receiver String work.

for a middle

Explain that an interface holds an unaddressable copy, so no automatic address-taking happens at the assertion, and contrast that with a direct method call on an addressable variable which the compiler does rewrite.

for a senior

Bring the review habit: check the receiver of every String method against how the type is stored and passed, and add a formatting assertion that fails if the receiver changes.

for a principal

Make it a convention rather than a case-by-case catch: value receivers for rendering methods across the codebase, with the exceptions documented, so log output does not silently degrade as types move into collections.

## Method sets, restated exactly For a defined type `T`: - the method set of `T` contains the methods declared with receiver `T`; - the method set of `*T` contains the methods declared with receiver `T` **and** those declared with receiver `*T`. An interface is satisfied by a type whose method set contains all the interface's methods. So if `String()` is declared on `*Status`, then `*Status` implements `fmt.Stringer` and `Status` does not. ## Why the usual convenience does not rescue you In ordinary code you can call a pointer-receiver method on a value: ```go var s Status = "queued" _ = s.String() // compiles: shorthand for (&s).String() ``` That works because `s` is **addressable** — it is a variable, and the compiler rewrites the call to take its address. Interface satisfaction is a different question, and it is decided on the method set alone, with no address-taking. When you write `fmt.Println(s)`, the argument is boxed into an `any`. Boxing copies the value; the copy lives inside the interface and is not addressable, precisely so that no code can mutate it behind the interface's back. `fmt` then does a type assertion against `fmt.Stringer` on that interface value. The dynamic type is `Status`, whose method set lacks `String`, so the assertion fails and `fmt` falls through to formatting the value by its kind. The compiler does not warn you at the call site, because `fmt.Println(s)` is a perfectly legal call — `Status` satisfies `any`. The failure is silent and appears only in the output. ## What the output looks like With `type Status string` and a pointer-receiver `String()`: - `fmt.Println(s)` prints the underlying string, `queued`. - `fmt.Println(&s)` prints the `String()` rendering, because the dynamic type is now `*Status`. - `fmt.Println([]Status{"queued"})` prints `[queued]` — each element is a `Status`. - `fmt.Println([]*Status{&s})` prints the elements through `String()`. For a struct-shaped type the mismatch is louder: the value form prints `{...}` with all the fields, which is how this bug is usually noticed in a log line. ## Which receiver should `String()` have? Default to a value receiver. `String()` is a read-only rendering; it rarely needs to mutate, and a value receiver puts the method in both method sets, so both `Status` and `*Status` satisfy `fmt.Stringer` and printing works either way. That single choice removes an entire class of surprise. Use a pointer receiver only when there is a reason: the type is large enough that copying it per format call matters, the type is only ever handled through pointers, or it embeds something that must not be copied, such as a mutex. If you take that route, be deliberate — every place the type is printed must pass a pointer, including elements of collections. There is also a subtle safety point in the other direction. A `String()` on a pointer receiver runs on whatever the pointer points at, so it must handle a nil pointer without dereferencing it; otherwise printing a nil `*Status` panics inside formatting. `fmt` recovers panics raised inside a `String` method and prints a `%!v(PANIC=...)` marker instead of crashing, which is helpful, but a log line reading `%!v(PANIC=...)` is still a defect to fix rather than tolerate. ## How to catch it in review When you see a `String()` method, look at its receiver, then look at how values of the type are passed around. A pointer-receiver `String()` on a type that is stored in slices, maps or struct fields by value is almost always a mistake, because those elements are handed to `fmt` as copies and will never render through the method. The cheapest test is a one-line assertion that `fmt.Sprint(v)` of a value equals the expected rendering — it fails immediately if the receiver is wrong, and it keeps failing if someone changes it later.

  • Why does s.String() compile directly on a value if the value does not implement fmt.Stringer?
    Because a direct method call on an addressable variable is rewritten by the compiler as `(&s).String()`. Interface satisfaction has no such rewrite: it is decided purely by the method set of the dynamic type, and the copy held inside an interface has no address to take.
  • Which receiver would you choose for String(), and why?
    A value receiver by default. It puts the method in the method sets of both the value and the pointer type, so printing works whether callers pass `v` or `&v`, and elements of a `[]T` render correctly. Reserve a pointer receiver for large types, types never copied, or types embedding something uncopyable.
  • What happens if a pointer-receiver String() is called on a nil pointer?
    It dereferences nil and panics. `fmt` recovers panics raised inside a `String` method and substitutes a `%!v(PANIC=...)` marker rather than crashing the program, so the log line degrades instead of the process. Still a defect: guard the nil case inside the method.

The compiler will fetch your keys for you when you call a method directly, because it knows where you left them. Once the value has been photocopied into an interface, there is no original to fetch from.

saying these in an interview costs you the question

  • Says the compiler takes the address automatically for interfaces
  • Thinks a value and its pointer always have the same method set
  • Claims fmt should have dereferenced or addressed the argument
  • Blames the verb rather than the receiver declaration
  • Assumes printing a slice of values will still use String