We use cookies and other tracking technologies to improve your browsing experience on our site, analyze site traffic, and understand where our audience is coming from. To find out more, please read our privacy policy.

By choosing 'I Accept', you consent to our use of cookies and other tracking technologies.

We use cookies and other tracking technologies to improve your browsing experience on our site, analyze site traffic, and understand where our audience is coming from. To find out more, please read our privacy policy.

By choosing 'I Accept', you consent to our use of cookies and other tracking technologies. Less

We use cookies and other tracking technologies... More

Login or register
to apply for this job!

Login or register to start contributing with an article!

Login or register
to see more jobs from this company!

Login or register
to boost this post!

Show some love to the author of this blog by giving their post some rocket fuel ๐Ÿš€.

Login or register to search for your ideal job!

Login or register to start working on this issue!

Engineers who find a new job through Golang Works average a 15% increase in salary ๐Ÿš€

Blog hero image

Tagging Structs fields in Go

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.

What is a struct field tag?

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.

Defining our own custom tag

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

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. 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. ๐Ÿ™Œ

Originally published on itnext.io