The Guide to Work With Tags in Golang
The Guide to Work With Tags in Golang

The Guide to Work With Tags in Golang

2021, Jan 12    

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

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

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

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

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

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

Tags in Golang - 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

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

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

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

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

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

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 pairs
文章转载自Tags in Golang