Go: Stringer Command, Efficiency Through Code Generation
By: Vincent Blanchon
Tags: Go, Golang

Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French.

ℹ️ This article is based on Go 1.13.

The stringer command aims to automate the creation of methods that satisfy the fmt.Stringer . It generates String() for specified types that describe them as a string.

Case

The documentation of the command provides an example I will use to study the command. Here is the case:

Here is the output:

1

Generating logs with the value of the constants can be a bit confusing.

Let’s generate the String() method with the command stringer -type=Pill :

A new function String() has been generated. Here is the new output when running the previous code:

Aspirin

The type now describes itself as string rather than its inner value.

stringer also works perfectly with the command go generate , making it even more powerful. It is as easy as adding the following instruction in the code:

Then, running go generate command will automatically generate the new functions for all your types.

Efficiency

stringer generates one string that contains the list of the values as a string, and one array containing the index of each string. In our example, reading Aspirin will consist of reading the string from the index seven to thirteen:

But how fast and efficient is it? Let’s compare with two other solutions:

the String() function is generated with hard coded value:

Here is a benchmark with a list of twenty values:

name time/op

Stringer-4 4.16ns ± 2%

StringerWithSwitch-4 3.81ns ± 1%

Here is the same benchmark with a hundred values:

name time/op

Stringer-4 4.96ns ± 0%

StringerWithSwitch-4 4.99ns ± 1%

The more constants you have, the more efficient it is. It actually makes sense. Loading a value from memory is more expansive than going through some jump instructions (assembly instructions that represents a if condition).

However, the bigger the switch, the higher the number of jump instructions are. From a certain point, loading from the memory will become more effective.

the String() function generates a map:

Here is a benchmark with a list of twenty constants:

name time/op

Stringer-4 4.16ns ± 2%

StringerWithMap-4 28.60ns ± 2%

Using a map is much slower since it has to make a function call, and the lookup in the bucket is not as simple as accessing an index of a slice.

For more information about the map and its internal structure, I suggest you read my article “Go: Map Design by Code.”

Self-checker

Among the generated instructions, some are created purely for the purpose of validation. Here are those instructions:

stringer writes the name of the constant on each line along with this value. In this example, Aspirin has the value 2 . Updating the name of the constant or its value would generate an error:

updating the name without re-generating the String() function:

./pill_string.go:12:8: undefined: Aspirin

updating the value without re-generating the String() function:

./pill_string.go:12:7: invalid array index Aspirin - 1 (out of bounds for 1-element array) ./pill_string.go:13:7: invalid array index Ibuprofen - 2 (index must be non-negative

However, in case we add a new constant — here the next key is the number 3 — and do not update the generated file, stringer has a default value:

Pill(3)

Adding this self-check will not have any impact since it is removed at the compilation time. It can be confirmed by looking at the generated asm code for the program:

➜ go tool compile -S main.go pill_string.go | grep "\"\".Pill\.[^\s]* STEXT" "".Pill.String STEXT size=275 args=0x18 locals=0x50