A Question about Go Error Formatting

Wednesday, April 21, 2021

On a code review a coworker left me the following comment:

return fmt.Errorf("Error %s: %v", thing, err)

Might want to use %s for err here in case there’s any additional context given as a part of err’s Error() function.

This comment surprised me as it seemed to disagree with a lot of my knowledge about how go’s formatters work. This got me digging a bit.

First I tossed together a sample app to test how things worked:

package main

import (
	"fmt"
)

type MyError struct {
	message string
}

func (m MyError) Error() string {
	return fmt.Sprintf("This does a thing: %s", m.message)
}

func (m MyError) String() string {
	return fmt.Sprintf("This does a different thing: %s", m.message)
}

func main() {
	var err error
	err = MyError{"hello world"}
	fmt.Printf("Error with %%v: %v\n", err)
	fmt.Printf("Error with %%s: %s\n", err)

}

Playground

Giving it a run and everything works as I expect:

Error with %v: This does a thing: hello world Error with %s: This does a thing: hello world

Both %s and %v work as I thought they did, by calling the Error() function.

So it is working as I expect, but to understand it deeper lets take a look at what the documentation says should be happening. Just to make sure we aren’t missing anything.

Except when printed using the verbs %T and %p, special formatting considerations apply for operands that implement certain interfaces. In order of application:

  1. If the operand is a reflect.Value, the operand is replaced by the concrete value that it holds, and printing continues with the next rule.

  2. If an operand implements the Formatter interface, it will be invoked. In this case the interpretation of verbs and flags is controlled by that implementation.

  3. If the %v verb is used with the # flag (%#v) and the operand implements the GoStringer interface, that will be invoked.

If the format (which is implicitly %v for Println etc.) is valid for a string (%s %q %v %x %X), the following two rules apply:

  1. If an operand implements the error interface, the Error method will be invoked to convert the object to a string, which will then be formatted as required by the verb (if any).

  2. If an operand implements method String() string, that method will be invoked to convert the object to a string, which will then be formatted as required by the verb (if any).

Item #1 doesn’t apply so we can skip that. Next we have #2, the Formatter.

Formatter is implemented by any value that has a Format method. The implementation controls how State and rune are interpreted, and may call Sprint(f) or Fprint(f) etc. to generate its output.

type Formatter interface {
    Format(f State, verb rune)
}

This doesn’t seem too relevant.

So number #3. This is a bit more interesting.

GoStringer is implemented by any value that has a GoString method, which defines the Go syntax for that value. The GoString method is used to print values passed as an operand to a %#v format.

type GoStringer interface {
    GoString() string
}

So we now know if we had called %#v this function would take precedence.

A quick GoPlayground confirms this works as expected.

#4 Says that if the object implements the error interface, then %v will call the Error() function. Then only if the object isn’t an error, will the String() function get called.

So there we go, the documentation seems to confirm both my expectations, and my initial test. While everything came back working as I had originally expected I learned quite a bit. Additional interfaces (GoStringer in particular), the actual order of operations, and I’ve been able to solidify how I was thinking about go formatting.

gogolangerrors

Better living with Markdown Tables