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
When I first wrote my first web server in Go, the struct tags were the (only? no) way to go.
A typical entity is represented like this:
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
If you are not familiar with reflection in programming languages, I recommend reading about it here, before proceeding.
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
We have managed to break down struct tags using reflection in Go. All the serialization and deserialization libraries heavily use custom struct tags in order to transform the data whichever way they want.
Where to go from here?
- Since working with types at runtime circles around reflection, it is highly recommended to look into the reflect package.
- Internals of encoding/json package. Especially the unexported typeFields function.