Cody Oss
31 May 2019
•
4 min read
So, you have started to poke around in Go a bit and you are now starting to wonder what sort of metaprogramming options you have at your disposal. Well one option you have, that is in most programming languages, is using reflection. One usage of reflection in Go is working with struct field tags.
If you have ever written a RESTful JSON api in Go, you probably already know, or at least you have seen them. This is first time most Gophers run into these field tags.
package main
import (
"encoding/json"
"fmt"
)
type FooNoTag struct {
Bar string
}
type FooWithTag struct {
Bar string `json:"bar"`
}
func main() {
f1 := FooNoTag{"something"}
f2 := FooWithTag{"something"}
b1, _ := json.Marshal(f1)
b2, _ := json.Marshal(f2)
fmt.Printf("%s\n", b1)
fmt.Printf("%s\n", b2)
// Output:
// {"Bar":"something"}
// {"bar":"something"}
}
In the example above you will notice that I declared two structs that both have a field Bar of type string. The main difference I am trying to point out is that the second struct, FooWithTag, also declares a field tag. This tag is used by the json package when marshaling and unmarshaling data. If you pay close attention to the output you will notice Bar is upper-cased in the first output and lower-cased in the second. In this instance, the field tag is telling the json package to write the key as bar not Bar. If I would have declared my tag to be:
type FooWithTag struct {
Bar string `json:"tomato"`
}
It would have printed:
{"tomato":"something"}
Now that we have seen how the json package makes use of field tags let’s explore how we can create our own.
Let’s start by playing a little fast and loose. When one behaves is such a way they often forget their manners. So let’s make a struct field tag to make our code a little extra polite.
type Foo struct {
Bar string `manners`
}
Every time we see a field with the tag manners we should post-fix the string with pretty please. The snippet below outlines what we are trying to accomplish.
package main
import "fmt"
type Foo struct {
Bar string `manners`
}
func main() {
f := Foo{"I want a tomato"}
Say(f)
// Output:
// {I want a tomato}
// Wanted output:
// I want a tomato pretty please
}
// Say should use struct field tags to postfix marked fields with `pretty please`.
func Say(v interface{}) {
fmt.Printf("%v\n", v)
}
In order to enable this behavior we need to lean on our good friend the reflect package.
Reflection is the way we can, at runtime, figure out all of the tags a struct holds. Let’s first just print out all of the tags we see in our struct.
func Say(v interface{}) {
t := reflect.TypeOf(v)
for i := 0; i < t.NumField(); i++ {
fmt.Printf("%v\n", t.Field(i).Tag)
}
// Output: manners
}
Now we need to hone in on the tag we actually want to work with. To get at a particular tag the reflect package provides a Lookup method.
func Say(v interface{}) {
t := reflect.TypeOf(v)
for i := 0; i < t.NumField(); i++ {
if value, ok := t.Field(i).Tag.Lookup("manners"); ok {
fmt.Println(value)
}
}
// Output:
}
Notice there is no output. 🤔 Why not?
Because we were playing too fast and loose 😱. Let’s take a look at some documentation from the reflect package.
By convention, tag strings are a concatenation of optionally space-separated key:”value” pairs. Each key is a non-empty string consisting of non-control characters other than space (U+0020 ‘ ‘), quote (U+0022 ‘“‘), and colon (U+003A ‘:’). Each value is quoted using U+0022 ‘“‘ characters and Go string literal syntax.
Now that we have read the docs we know we need to change how our struct field tag was defined. Once we know the field is properly marked with the tag we are looking for, we need to figure out a way to access the value of that field. We can accomplish this with the reflect package as well. After making all of these edits the code should look something like this…
package main
import (
"fmt"
"reflect"
)
type Foo struct {
Bar string `manners:"-"`
}
func main() {
f := Foo{"I want a tomato"}
Say(f)
// Output:
// I want a tomato pretty please
}
func Say(v interface{}) {
rv := reflect.ValueOf(v)
t := rv.Type()
for i := 0; i < t.NumField(); i++ {
if value, ok := t.Field(i).Tag.Lookup("manners"); ok {
if value == "" || value == "-" {
fmt.Printf("%s pretty please\n", rv.Field(i).String())
}
}
}
}
```## Taking it one step further##
Great. We defined our custom tag, got it to do what we want, and now it is all good right? Not exactly. You see, anytime you are using the *reflect* package you are opening yourself up to panics. To be safe, we should be checking the Kind of fields we are operating on. For this tag, let’s keep it simple and say it should only work with fields of type *string*. Also, since we now know we can associate a value with our struct field tag, how about we say the tag can optionally provide a number that defines how many times the word pretty is printed.
package main
import ( "fmt" "reflect" "strconv" "strings" )
type Foo struct {
Bar string manners:"3"
}
func main() { f := Foo{"I want a tomato"} Say(f) // Output: // I want a tomato pretty pretty pretty please }
func Say(v interface{}) { rv := reflect.ValueOf(v) t := rv.Type() for i := 0; i < t.NumField(); i++ { if value, ok := t.Field(i).Tag.Lookup("manners"); ok { if rv.Field(i).Kind() == reflect.String { handleManners(rv.Field(i).String(), value) } } } }
func handleManners(fieldValue, tagValue string) { var prettyTimes int if tagValue == "" || tagValue == "-" { prettyTimes = 1 } if i, err := strconv.Atoi(tagValue); err == nil { prettyTimes = i }
var sb strings.Builder
sb.WriteString(fieldValue)
for i := 0; i < prettyTimes; i++ {
sb.WriteString(" pretty")
}
if prettyTimes > 0 {
sb.WriteString(" please")
}
fmt.Println(sb.String())
}
🎉*Congratulations*🎉, *we have now created a tag that makes our code a little more polite!*
## What I made##
You now know everything you need to know to venture out into the wild and start creating your own custom tags. If you would like to reference an example that is a little more complex I encourage you to [take a look verify](https://github.com/codyoss/verify). It is a package I threw together that mimics some of the stuff you might find in *javax.validation.constraints* if you are familiar. There are other more complete packages in the Go community that do the same thing as the one I created, but I wanted to build something small and real to help myself understand tags in Go. Hope you found this all helpful!
And if you made it this far, Thank You. 🙌
Cody Oss
See other articles by Cody
Ground Floor, Verse Building, 18 Brunswick Place, London, N1 6DZ
108 E 16th Street, New York, NY 10003
Join over 111,000 others and get access to exclusive content, job opportunities and more!