Chase Mao's blog

Exploring Go: Stringer Usage

2024-07-06

TLDR

Printing Go structs with pointer fields using the fmt library often displays the pointer’s address instead of the struct’s content. This blog explores why Go adopts this approach, how to customize struct printing effectively, and introduces stringergen, a tool for automating String method generation to simplify struct customization and improve code maintainability.

Raise of Question

When you print a Go struct that contains a field which is a pointer to another struct using the fmt library, you might notice that the field is printed as an address rather than the actual content of the pointed-to struct.

We will dive into why Go desinn fmt library this way and how to proper print such kind structs.

Design of fmt

Lets have a quick example of the question.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
	"fmt"
)

type InnerStruct struct {
	Value int
}

type OuterStruct struct {
	Inner *InnerStruct
}

func main() {
	inner := &InnerStruct{Value: 42}
	outer := &OuterStruct{Inner: inner}

	fmt.Printf("Outer struct: %+v\n", outer)
}

In this example, OuterStruct contains a pointer Inner to InnerStruct. When printing outer using %+v, Go prints the address of Inner, not the contents of InnerStruct. The output would look like:

1
Outer struct: &{Inner:0x123456789}

Through the source code of fmt, we can find the reason why it only print address for fields within a struct. If the arg is in top level, Go will get its reference, otherwise Go will not get its reference to avoid loop. Aussming a situtaion, where a pointer field in struct contain the pointer to the struct itself, if Go dereference the pointer field, there will be a infinite loop.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// in src/fmt/print.go:681 in go1.22.4
func (p *pp) printValue(value reflect.Value, verb rune, depth int) {
	// Skip...

	switch f := value; value.Kind() {
	// Skip other cases...
	case reflect.Pointer:
		// pointer to array or slice or struct? ok at top level
		// but not embedded (avoid loops)
		if depth == 0 && f.UnsafePointer() != nil {
			switch a := f.Elem(); a.Kind() {
			case reflect.Array, reflect.Slice, reflect.Struct, reflect.Map:
				p.buf.writeByte('&')
				p.printValue(a, verb, depth+1)
				return
			}
		}
		fallthrough
	}
}

Ways to Print a Struct

Since fmt refuse to print pointer field, while it is a very common situation in Go. What if we want to print it, and it is actually very normal case such as in debuging and logging.

I have come up with some ways to do it.

  • spew is a common lib to print go structs recursive. We can call spew lib, and print its output.
  • Same way it that we can use json to marshal structs and print its result, if the fields we need are export fields, because json donnt marshal unexport fields.
  • Also we can implement String method for struct, so when print them as a pointer, Go will call its String method directly. For example:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type complicated struct {
	F1 int
	F2 string
	F3 float64
	F4 *subComplicated
	F5 []*subComplicated
	F6 map[string]*subComplicated
	F7 map[string][]*subComplicated
}

type subComplicated struct {
	F1 int
	F2 string
	F3 float64
	S1 *subSubComplicated
}

func (m *subComplicated) String() string {
	return fmt.Sprintf(`{"F1": %d, "F2": "%s", "F3": %f, "S1": %v}`, m.F1, m.F2, m.F3, m.S1)
}

func (m *subSubComplicated) String() string {
	return fmt.Sprintf(`{"F1": %d, "F2": "%s", "F3": %f}`, m.F1, m.F2, m.F3)
}

In the example, we implement String method for subComplicated and subSubComplicated structs, so when we print a complicated struct, Go will call the String method when trying to print pointer field in complicated struct.

As for how to implement String method, there are few options:

  • call spew, json, jsoniter or some other lib.
  • print each fields by custom.
  • reuse fmt lib itself.

How to resue fmt lib, here is a example:

1
2
3
func (c *subComplicated) String() string {
	return fmt.Sprintf("%+v", *c)
}

It may seems wield, but it is quite useful and simple. When fmt print the pointer to its struct, it will call this String method, and it will reuse fmt lib and return the result of struct but not pointer to struct.

Besides it, I come up with a another similar way:

1
2
3
4
5
type subComplicatedTarget subComplicated

func (c *subComplicated) String() string {
	return fmt.Sprintf("%+v", (*subComplicatedTarget)(c))
}

In String method, we cast the input type into another type, and resue fmt to print another type. It can do too.

Performance of Ways

We have talk about many ways to print a struct recusively, but which one is the best when it comes to performance. I did a benchmark and the result is like below. The source code and fully result can be checked Here.

Way CPU Time Consumed
json 100
jsoniter 103
cat string with stringbuilder 189
cat string with sprintf 196
reuse fmt with dereference 265
Spew 298
reuse fmt with type case 310

Simplify Generating String Method

From the benchmark, we find that json have best performance. But still it is inconvenient to use to print struct. Because we have to either marshal struct every time and then print its result, or define String method for all struct we gonna use and call json in that method.

In order to simplify the process, I come up with a idea to use a tool to auto generate String method for all structs defined in project. With the tool, we just need to run a command, and then String method for all structs will be generated. We dont have to worry how to print a pointer field any more.

I have already implement the tool stringergen. We can generate String method for all structs like this, feel free to test it and explore more usage:

1
stringergen -recursive=/path/to/directory/ -save -skipdir=/path/to/directory/skip1/,/path/to/directory/skip2/

Note

Be cautious that we haven’t solved the problem of cyclic reference, and if it exist, we may have error or infinite loop cauing stackoverflow.

Summary

In this blog, we explored the intricacies of printing Go structs with pointer fields using the fmt library. We delved into Go’s design philosophy behind struct printing, examining how Go prioritizes memory safety and performance by defaulting to printing addresses for pointer fields. We examined practical solutions for customizing struct printing, such as implementing the String method for structs or using serialization libraries like json or jsoniter for recursive printing. Additionally, we benchmarked these approaches to understand their performance implications. Finally, we introduced stringergen, a tool designed to automate the generation of String methods for Go structs, simplifying the process of struct customization and enhancing code maintainability.

By understanding these concepts and tools, Go developers can effectively manage struct printing, balancing readability, performance, and ease of use in their projects.