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, andnil
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()
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! π