Tags in Golang

Declaration of struct fields can be enriched by string literal placed afterwards — tag. Tags add meta information used either by current package or external ones. Let’s first recall how struct declarations look like, then we’ll deep dive into tags themselves and we’ll wrap up with couple of use cases.

Struct type

Struct is a sequence of fields. Each field consists of optional name and required type (source code):

package main
import "fmt"
type T1 struct {
f1 string
}
type T2 struct {
T1
f2 int64
f3, f4 float64
}
func main() {
t := T2{T1{"foo"}, 1, 2, 3}
fmt.Println(t.f1) // foo
fmt.Println(t.T1.f1) // foo
fmt.Println(t.f2) // 1
}
Field T1 is called an embedded field since it’s declared with type but no name.

Field declaration can specify more than one identifier like f3 and f4 from 3rd field declaration in T2.

Language specification states that each field declaration is followed by semicolon but as we’ve seen above it can be omitted. Semicolon might be useful if there is a need to put multiple fields declarations into the same line (source code):

package main
import "fmt"
type T struct {
f1 int64; f2 float64
}
func main() {
t := T{1, 2}
fmt.Println(t.f1, t.f2) // 1 2
}

Tag

A field declaration may be followed by an optional string literal (tag) which becomes an attribute of all the fields in the corresponding field declaration (single field declaration can specify multiple identifiers). Let’s see it in action (source code):

type T struct {
f1 string "f one"
f2 string
f3 string `f three`
f4, f5 int64 `f four and five`
}
Either raw string literals or interpreted string literals can be used but conventional format described below requires raw string literals. Differences between raw and interpreted string literals are described in spec.

If field declarations contains more than one identifier then tag is attached to all fields from field declaration (like fields f4 and f5 above).

Reflection

Tags are accessible through reflect package which allows run-time reflection (source code):

package main
import (
"fmt"
"reflect"
)
type T struct {
f1 string "f one"
f2 string
f3 string `f three`
f4, f5 int64 `f four and five`
}
func main() {
t := reflect.TypeOf(T{})
f1, _ := t.FieldByName("f1")
fmt.Println(f1.Tag) // f one
f4, _ := t.FieldByName("f4")
fmt.Println(f4.Tag) // f four and five
f5, _ := t.FieldByName("f5")
fmt.Println(f5.Tag) // f four and five
}

Setting up empty tag has the same effect as not using tag at all (source code):

type T struct {
f1 string ``
f2 string
}
func main() {
t := reflect.TypeOf(T{})
f1, _ := t.FieldByName("f1")
fmt.Printf("%q\n", f1.Tag) // ""
f2, _ := t.FieldByName("f2")
fmt.Printf("%q\n", f2.Tag) // ""
}

Conventional format

Introduced in commit “reflect: support for struct tag use by multiple packages” allows to set meta information per package. This provides simple namespacing. Tags are formatted as a concatenation of key:"value" pairs. Key might be name of the package like json. Pairs can be optionally separated by spaces — key1:"value1" key2:"value2" key3:"value3". If conventional format is used then we can use two methods of struct tag (StructTag) — Get or Lookup. They allow to return value associated with desired key inside tag.

Lookup function returns two values — value associated with key (or blank if not set) and bool indicating if key has been found at all (source code):

type T struct {
f string `one:"1" two:"2"blank:""`
}
func main() {
t := reflect.TypeOf(T{})
f, _ := t.FieldByName("f")
fmt.Println(f.Tag) // one:"1" two:"2"blank:""
v, ok := f.Tag.Lookup("one")
fmt.Printf("%s, %t\n", v, ok) // 1, true
v, ok = f.Tag.Lookup("blank")
fmt.Printf("%s, %t\n", v, ok) // , true
v, ok = f.Tag.Lookup("five")
fmt.Printf("%s, %t\n", v, ok) // , false
}

Get method is simply wrapper of Lookup which discards boolean flag (source code):

func (tag StructTag) Get(key string) string {
v, _ := tag.Lookup(key)
return v
}
Return value of Get or Lookup is unspecified if tag doesn’t have conventional format.

Even if tag is any string literal (interpreted or raw) then Lookup and Get methods will find value for key only if value is enclosed between double quotes (source code):

type T struct {
f string "one:`1`"
}
func main() {
t := reflect.TypeOf(T{})
f, _ := t.FieldByName("f")
fmt.Println(f.Tag) // one:`1`
v, ok := f.Tag.Lookup("one")
fmt.Printf("%s, %t\n", v, ok) // , false
}

It’s possible to use escaped double quotes within interpreted strings literals (source code):

type T struct {
f string "one:\"1\""
}
func main() {
t := reflect.TypeOf(T{})
f, _ := t.FieldByName("f")
fmt.Println(f.Tag) // one:"1"
v, ok := f.Tag.Lookup("one")
fmt.Printf("%s, %t\n", v, ok) // 1, true
}

but it’s much less readable.

Conversion

Converting struct type value into other type requires that underlaying types are identical but tags are ignored (source code):

type T1 struct {
f int `json:"foo"`
}
type T2 struct {
f int `json:"bar"`
}
t1 := T1{10}
var t2 T2
t2 = T2(t1)
fmt.Println(t2) // {10}
This behaviour has been introduced in Go 1.8 (proposal). In Go 1.7 and older above code could would throw a compile-time error.

Use cases

(Un)marshaling

Probably the most common use of tags in Go is marshalling. Let’s see how it’s used by function Marshal from json package (source code):

import (
"encoding/json"
"fmt"
)
func main() {
type T struct {
F1 int `json:"f_1"`
F2 int `json:"f_2,omitempty"`
F3 int `json:"f_3,omitempty"`
F4 int `json:"-"`
}
t := T{1, 0, 2, 3}
b, err := json.Marshal(t)
if err != nil {
panic(err)
}
fmt.Printf("%s\n", b) // {"f_1":1,"f_3":2}
}

Package xml also takes advantage of tags — https://golang.org/pkg/encoding/xml/#MarshalIndent.

ORM

Object-relation mapping tools like GORM use tags extensively — example.

Digesting forms data

https://godoc.org/github.com/gorilla/schema

Other

There’re more potential uses cases of tags like configuration management, default values for structs, validation, command-line arguments description etc. (list of well-known struct tags).

go vet

Go compiler doesn’t enforce conventional format of struct tags but go vet does that so it’s worth to use it e.g. as a part of CI pipeline.

package main
type T struct {
f string "one two three"
}
func main() {}
> go vet tags.go
tags.go:4: struct field tag `one two three` not compatible with reflect.StructTag.Get: bad syntax for struct tag pair

Thanks to struct tags programmers can benefit from single-sourcing. Go is a pragmatic language so even if it’s possible to solve JSON / XML encoding using other ways like dedicated data structures to control the whole process, Golang gives something which makes software engineer’s life a bit easier. It’s worth to mention that length of tag isn’t limited by spec.

👏👏👏 below to help others discover this story. Please follow me here or on Twitter if you want to get updates about new posts or boost work on future stories.