Deep dive into Go Struct Tags

After writing Go for about 8 months now, one of the things that kept bamboozling me was the presence of struct tags. These struct tags magically serialize/deserialize data in the tags specified to and from a Go struct. But as they say, there’s no magic! This actually is a really powerful language feature of Go.

Growing as a Go developer: From dark age to advance language features

type Todo struct {
ID int `json:"id"`
Content string `json:"content"`
Timestamp int64 `json:"ts"`
}

and it did get me thinking that these tags are built inside of the language’s core and can’t be customized, until I found a few third party libraries making use of it. Especially the DB connectors and ORM libraries. Here’s how the DB struct looks for the above Todo struct (for a third party library sqlx):

type Todo struct {
ID int `db:"id"`
Content string `db:"content"`
Timestamp int64 `db:"ts"`
}

which gets converted to a row in DB.

Question is, how?

Unleashing reflection in Go

The reflect package shipped with Go enables accessing meta data and values of arbitrary types. Covering everything it does falls out of the scope of this story and will require many! Lets stick to Struct tags for now.

Lets make use of reflect to access a type’s metadata at runtime:

Outputs:

ID id 1
Content content Chop carrots
Timestamp ts 1257894000

Lets dive into the code to understand how does it works line by line. Notice that I will be skipping imports and struct type declaration. Lets start with main() then:

todo := NewTodo(1, "Chop carrots", time.Now().Unix())

Declared a new Todo with specified values.

v := reflect.ValueOf(todo)

The ValueOf(interface{}) takes in a variable and returns a reflect.Value value, which contains the actual value contained within a variable along with additional info like ‘what type is this variable?’, ‘is it equal to Zero value of its type?’, etc.

v.Kind() == reflect.Ptr 

The reflect.Value type has Kind() defined on it that returns its kind. The return type of Kind() is reflect.Kind, which is a derived type of uint representing type of values in Go. In simpler words, reflect.Kind is a number assigned to every type in Go for the reflect library. These numbers are unique for each type and are defined on package level inside reflect. For example, reflect.Bool is a reflect.Kind that represents a value of type Boolean in reflect, reflect.Ptr represents pointer type, etc. Hence, in this line of code, we are checking if todo is a pointer to some value.

v.Kind() == reflect.Struct

Since Struct Tags can be only applied to struct kinds, panic if v’s kind is not reflect.Struct.

v = v.Elem()

In case todo is a pointer or an interface to some value, we need to get its value. This is done using Elem() present on reflect.Value. Elem() returns the value that the interface v contains or that the pointer v points to. Notice that this is applicable only if v is of reflect.Interface or reflect.Ptr. If v is nil, then Zero value is returned.

t := v.Type()

Finally get the type actual type representation of todo. The Type() on reflect.Value returns the underlying type representation. The return type here is the reflect.Type interface, which is the type representation of type it is associated to.

for i := 0; i < v.NumField(); i++

We need to iterate over every field present in the struct. The NumField() on reflect.Value returns the number of fields present on the underlying struct type.

fmt.Println(t.Field(i).Name,
t.Field(i).Tag.Get("db"),
v.Field(i).Interface())

Next, use Field(int) inside reflect.Type to access the field information in form of reflect.StructField type. The StructField type contains metadata of the field. We’re here printing the Name of the field using t.Field(i).Name . The struct tag of this field is present in a field called Tag, i.e. t.Field(i).Tag contains tag information. The type of this field reflect.StructTag and it’s a derived type of string. reflect.StructTag has Get(key string) method defined on it, which accepts the struct tag key we are searching for and returns the corresponding value. In our case, we need the tag associated with key "db" , hence we use t.Field(i).Tag.Get("db") to get it. Finally, we also would like to print the actual value of the field. To do this, we use v.Field(i) which is the reflect.Value form of a field and we get its value as interface{} type using v.Field(i).Interface() .

Summarizing learning

Where to go from here?

  • Internals of encoding/json package. Especially the unexported typeFields function.

References

SMTS • Web Services • Rust/Go/Py/JS • Cloud & Distributed Systems