github gitlab twitter linkedin email rss
String representations for Go values
Mar 13, 2017
4 minutes read

While working on go-librariesio, a new open source project, I had come to realize that it can be quite cumbersome to print Go structs if they use pointer fields.

In this blog post, I will explain why I think it’s a good idea to use pointer variables for struct, that represent web API resources. I will also introduce you to a small library named go-repr as a possible solution to create a strings for them.

Pointer field values

The reason I use pointers for struct in go-librariesio is because it connects to the Libraries.io API and then deserializes JSON from the responses into structs that represent resources.

Please note that what I’m about to describe is not specific to go-librariesio, but merely applies to API client libraries in general.


Depending on the API endpoint that we’re requesting, resources might come back in a slightly different form. You don’t want to hit a particular backend service or query a database for every single endpoint unless you really need to. This means you wont always get all the related information for a resource.

In Go all variables have a zero value even when not explicitly initialized:

Each element of such a variable or value is set to the zero value for its type: false for booleans, 0 for integers, 0.0 for floats, "" for strings, and nil for pointers, functions, interfaces, slices, channels, and maps. This initialization is done recursively, so for instance each element of an array of structs will have its fields zeroed if no value is specified.

See Go Language Spec.

For API client libraries, which do not use pointer values, this means that there is no way of knowing whether a field value is not returned at all or just happens to have a value that is equal to the zero value for the field’s type.

Let’s have a look at an example.

No pointer fields

type Project struct {
	Description   string    `json:"description,omitempty"`
	Keywords      []string  `json:"keywords,omitempty"`
	Language      string    `json:"language,omitempty"`
	LatestRelease Release   `json:"latest_stable_release,omitempty"`
	Name          string    `json:"name,omitempty"`
	Versions      []Release `json:"versions,omitempty"`
}

type Release struct {
	Number      string    `json:"number,omitempty"`
	PublishedAt time.Time `json:"published_at,omitempty"`
}

Now if we get back a response from the API that contains a Project, it would have a LatestRelease with a PublishedAt datetime attached to it, even if it hasn’t been released yet.

LatestRelease:{Number: PublishedAt:0001-01-01 00:00:00 +0000 UTC}}

The value for Number is an empty string and PublishedAt is set to the zero value for a time.Time variable.

With pointer fields

type Project struct {
	Description   *string    `json:"description,omitempty"`
	Keywords      []*string  `json:"keywords,omitempty"`
	Language      *string    `json:"language,omitempty"`
	LatestRelease *Release   `json:"latest_stable_release,omitempty"`
	Name          *string    `json:"name,omitempty"`
	Versions      []*Release `json:"versions,omitempty"`
}

type Release struct {
	Number      *string    `json:"number,omitempty"`
	PublishedAt *time.Time `json:"published_at,omitempty"`
}
LatestRelease:<nil>

On the other hand, if we define structs with pointer values, LatestRelease is nil, the zero value for a pointer. As a user of the API client library we can now check against nil, which is great! 😁

go-repr

The problem that I had experienced, while working on the new project, was that my tests’ failures did not provide me with a lot of information when printing the struct as such. I had to manually pluck out the information that I was particularly interested in. For this, structs with non-pointer fields are definitely more convenient.

I knew that the go-github project uses pointer fields too, so I had a glance and discovered a helper function named Stringify(). It uses Go’s reflect package to recursively generate a string representation of an arbitrary value, while checking types, derefencing values and omitting nil pointers.

This functionality surely is useful for all sorts of Go projects, so I thought it’s worth creating a library from it. After getting in touch with the maintainers of the project, I have created a new project based on and inspired by Stringify()

github.com/hackebrot/go-repr

Installation

go get github.com/hackebrot/go-repr

Usage

Import go-repr and then use repr.Repr() to create a string representation for an value. It resolves pointers to their values and omits unexported struct fields as well as struct fields with nil values.

repr.Repr("hello world!") // "hello world!"

repr.Repr(1234) // 1234

type Gopher struct {
    Hair *string
    Eyes *string
}

e := "Goofy Eyes"
g := &Gopher{Eyes: &e}

repr.Repr(g) // main.Gopher{Eyes:"Goofy Eyes"}

Contributing

Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. Please check out the contributing guide and be sure to read the project’s code of conduct. Thank you!

Feedback

Do you find go-repr useful? Tweet at me with feedback! 😄


Back to posts