Reflecting on Go's tags and reflect package
Intro
If you wrote code in the past in Go and had to work with jsons (or other filetypes for storing data) then you’ve probably seen code that looks something this:
package main
import (
"encoding/json"
"io"
"net/http"
)
type Resp struct {
ID int `json:"id"`
Name string `json:"name"`
}
func GetResp() (*Resp, error) {
resp, err := http.Get("some.api/v1/endpoint")
if err != nil {
return &Resp{}, err
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return &Resp{}, err
}
var r *Resp
err = json.Unmarshal(data, &r)
return r, err
}
Here we just make a request, read body and decode it into Resp
structure, and it just works. After seeing this code you might have following questions: “What are these json:"id"
and json:"name"
lines? Why are they even there?”. And so I decided to write a bit about these things, what do they do and how you can work with them.
Really short explanation
These json:"id"
and json:"name"
are called tags. They, essentially, allow you to pass additional information along with that structure’s fields (so you can’t use them with, for example, interfaces). So in the example above we tell our code that Resp
struct has these fields with id
and name
json keys associated with them.
“Okay”, you might think, “but I think I haven’t seen a way to read these tags from the structure directly” and yeah, you can’t access them through structure instance itself. However, this is where reflect
package comes in. Description from the pkg.go.dev describes it as follows: “Package reflect implements run-time reflection, allowing a program to manipulate objects with arbitrary types. The typical use is to take a value with static type interface{} and extract its dynamic type information by calling TypeOf, which returns a Type.”
So, what can we do with it? And will it be of any use in the future?
Playing with tags and reflect
Imagine that we have the following structure in our code:
type Test struct {
Field int `some_text`
}
If you scan this with gopls (go lsp) it will throw a warning “structtag: struct field tag some_text not compatible with reflect.StructTag.Get: bad syntax for struct tag pair (default)” - ignore it for now :^). So, how does one read this tag? For this we will be using aforementioned reflect
package:
import (
"reflect"
)
...
func main() {
test := Test{1}
fmt.Println(reflect.TypeOf(t).Field(0).Tag)
}
Here’s what is happening here: we get a type reflect.Type
of our variable test
which in our case will be of type main.Test
, which makes sense. If, for example, we attempted to call this on variable of type int
, then we would’ve gotten int
, on string
- string
and so on. Then, since we know that test is of type Test
, we can attempt to access first field by calling Field(n)
method, where n
is the index of the field in the structure, starting with 0. This returns reflect.StructField
which we will work with later, but keep it in mind. Note that code will panic if you attempt to call Field()
on a variable that is not a struct or if index is out of range.
Now that we have our struct field, and we know that it is valid we can obtain its tag by getting Tag
attribute which will give us some_text
, which is an expected behavior. But we are getting a warning and that something is missing, and what would you do with this tag anyway? And what if you want to add more tags to the same field and structure them? Which is why let us rewrite our struct a bit:
type Test struct {
ID int `ref:"field1"`
Name string `ref:"field2"`
}
// assume respective changes to the test variable in the main function
// test := Test{1, "Ame"}
Now we have introduced keys and values in our tags. If we wanted our fields to have more than one key-value pair then it would’ve looked like this: key1:"val1" key2:"val2"
, so we separate tags using spaces, but for now we will work with one key only. Now that we have a key, we can obtain an associated value by doing the following
reflect.TypeOf(t).Field(0).Tag.Get("ref")
//field1
reflect.TypeOf(t).Field(1).Tag.Get("ref")
//field2
fmt.Println(reflect.TypeOf(t).Field(0).Tag.Get("notref"))
// <we get empty string here!>
So it is fairly easy to get keys our struct tags, and checking if key is there by comparing value with empty string is also possible. Now that we know how we can get tags, we can go deeper by working more with reflect package and coming up with a use case for that.
«Real» use case
Imagine the following situation: we are writing a lazy wrapper for some cli tool (for example yt-dlp) and we want to have parameters to be passed to it, but they are, naturally, are passed using flags (tbh in yt-dlp’s case there might be a way to do that using json config of sorts, but imagine that we can only use flags). Assume that this is our struct of parameters that we want to pass to the yt-dlp:
type YTdlp struct {
NoProgress bool
NoOverwrites bool
ExtractAudio bool
DefaultSearch string
Format string
FileType string
Quality string
}
You could hardcode all fields and manually set output flags:
func toArgs(y YTdlp) []string{
out := make([]string, 0, 7)
if y.NoProgress {
out = append(out, "--no-progress")
}
if y.NoOverwrites {
out = append(out, "--no-overwrites")
}
if y.ExtractAudio {
out = append(out, "-x")
}
if y.DefaultSearch != "" {
out = append(out, "--default-search")
out = append(out, y.DefaultSearch)
}
// assume that we repeat this with Format, FileType, Quality
return out
}
This approach works, but:
- What if we get more parameters as we write more code? You’d have to hardcode them too
- Why would you have these flags located somewhere in depths of your code and not near to the structure? If someone were to read/debug your code they’d have to read source in 2 places at the same time
- Typing all that is tiring, no one wants to do that (and even I skipped 3 flags while writing this)
- Function already looks bad and it would grow in the future if we were to add something to it, or, if we decided that some flags can be grouped by a rule and thus require custom processing
So, can we do better? And yeah, we can try using reflect
package along with tags here instead! Lets modify our structure to include flags in our tags and let them be accessible by cmd
key:
type YTdlp struct {
NoProgress bool `cmd:"--no-progress"`
NoOverwrites bool `cmd:"--no-overwrites"`
ExtractAudio bool `cmd:"-x"`
DefaultSearch string `cmd:"--default-search"`
Format string `cmd:"--format"`
FileType string `cmd:"--audio-format"`
Quality string `cmd:"--audio-quality"`
}
As you can see, it is already better than previous approach - we can clearly see which flags are associated with our fields right in the struct definition instead of looking for them who-knows-where. In this case we want that bool flags are going to be included only if respective field is set to true
, and other fields are just going to be included anyway but with value of that field. So, how can we achieve this?
Since we have these flags as arguments, then it would make sense if our function signature had []string
as output, so let us have a method toArgs() []string
on YTdlp
struct. The output slice’s structure is going to be an interleaved sequence of flags and arguments (or just a flag if its boolean). We want all fields, so we have to iterate over them, and natural thought chain would be: “I know that I can get a field by its index -> It panics when I get out of range -> I have to get last index somehow” and that is simple enough:
func (y *YTdlp) toArgs() []string{
v := reflect.ValueOf(*y)
out := make([]string, 0, v.NumField())
for i := 0; i < v.NumField(); i++ {
out[i] = fmt.Sprintf("%d", i)
}
return out
}
Here we get values of our struct YTdlp
and then get number of these values using NumField()
to limit our iterations. We also create an output, which for now just contains indices as strings. Now that we can iterate we need a way to differentiate between field types, i.e. how do we know that field is a string or a bool?
First, we can obtain a related flag like this:
cmdstr := reflect.TypeOf(*y).Field(i).Tag.Get("cmd")
reflect.StructField
(the one that we can obtain by Field()
method) has fields and methods other than Tag
but in our case we only care about one that would provide us with its type:
reflect.TypeOf(*y).Field(i).Type.Kind()
// returns reflect.Kind
One of the ways to approach this would be converting to string and comparing with string
, bool
and so on, but we can do better. Since it does not return a string, then there must be a reason why they return a custom type, right? reflect
provides us with convenient reflect.Bool
, reflect.String
, reflect.Int
and so on for comparing types just like that. So, we wrap this in a switch statement:
switch reflect.TypeOf(*y).Field(i).Type.Kind() {
case reflect.Bool:
if fmt.Sprintf("%v", v.Field(i)) == "true" {
out = append(out, cmdstr)
}
case reflect.String:
if cmdstr == ""{
continue
}
out = append(out, cmdstr)
out = append(out, fmt.Sprintf("\"%v\"", v.Field(i)))
default:
continue
}
And now we have finished writing our function that converts structure with cmd flags to the respective list of arguments:
func (y *YTdlp) toArgs() []string {
v := reflect.ValueOf(*y)
out := make([]string, 0, v.NumField())
for i := 0; i < v.NumField(); i++ {
cmdstr := reflect.TypeOf(*y).Field(i).Tag.Get("cmd")
switch reflect.TypeOf(*y).Field(i).Type.Kind() {
case reflect.Bool:
if fmt.Sprintf("%v", v.Field(i)) == "true" {
out = append(out, cmdstr)
}
case reflect.String:
if cmdstr == ""{
continue
}
out = append(out, cmdstr)
out = append(out, fmt.Sprintf("\"%v\"", v.Field(i)))
default:
continue
}
}
return out
}
After this you can plug its output into os/exec
command as an argument and it should work just fine. So, as you can see, we converted our structure into slice of flags & arguments without hardcoding. Now, lets imagine a separate case, where struct has fields which are integers and more importantly, maps. We want to iterate over these maps, get keys and values and concatenate them - what would our case for reflect.Map
look like then?
Further reflecting
Once again, imagine that we have to convert a structure into list of command line arguments for some tool:
type ArgStruct struct {
Recursive bool `cmd:"--rec"`
Mode string `cmd:"--mode"`
Offset int `cmd:"--offset"`
Params map[string]string `cmd:"--params"`
}
We already know how to work with reflect.Bool
, reflect.String
and reflect.Int
, but what do we do with reflect.Map
? We are aiming for dynamic conversion, that is, we don’t want to hardcode fields’ access and iterate over that map as you would usually do - imagine that we get more maps as we write more code. Lets change function that we wrote above a bit:
func (a *ArgStruct) toArgs() []string {
v := reflect.ValueOf(*a)
out := make([]string, 0, v.NumField())
for i := 0; i < v.NumField(); i++ {
cmdstr := reflect.TypeOf(*a).Field(i).Tag.Get("cmd")
switch reflect.TypeOf(*a).Field(i).Type.Kind() {
case reflect.Bool:
if fmt.Sprintf("%v", v.Field(i)) == "true" {
out = append(out, cmdstr)
}
case reflect.String, reflect.Int:
if cmdstr == "" {
continue
}
out = append(out, cmdstr)
out = append(out, fmt.Sprintf("\"%v\"", v.Field(i)))
case reflect.Map:
continue
default:
continue
}
}
return out
}
Now, lets look into what we can do with our map. For our purposes it is sufficient to use MapRange()
method that returns a map iterator:
iter := v.Field(i).MapRange()
for iter.Next(){
// key: iter.Key()
// value: iter.Value()
}
So, assume that we want our map entries to be separated by spaces and k-v pairs to be concatenated with colons:
case reflect.Map:
// note: not sure if its ok to concat strings with + and not joining with strings pkg instead
// so we ignore trailing space on the right
tmp := ""
iter := v.Field(i).MapRange()
for iter.Next(){
tmp += fmt.Sprintf("%s:%s ", iter.Key(), iter.Value())
}
out = append(out, cmdstr)
out = append(out, tmp)
And so, if you plug this case statement into the function defined above and run on following structure it will output the following:
func main(){
var args ArgStruct = ArgStruct{
Recursive: true,
Mode: "dev",
Offset: 5,
Params: map[string]string{
"key1": "val1",
"key2": "val2",
"key3": "val3",
},
}
args.toArgs()
}
// [--rec --mode "dev" --offset "5" --params key3:val3 key1:val1 key2:val2 ]
And sure enough, it works as expected: boolean flags are included as just flags, string and integer flags have both flag and value, map flags have flag and concatenated key-value pairs. You could also introduce branching to this - for example do something differently if value equals something, but that, too, can be controlled with tags (for example map:"maptype"
and then one more switch, which is might look bad will get the job done)
Outro
I think that tags are a nice feature which can find many applications in applicable situation and reflect
package allows you to do many indirect manipulations with your variables if you only know their type. Maybe I should’ve talked more about reflect
itself because I am certain it has more applications, but I decided to focus on tags and how you can work with them, so maybe ill write more about reflect
in the future. You might also be disappointed that I did not go into details how this works under the hood i.e. how Go processes tags along with structs, but I did not think that would be required here. All code was written by me (that yt-dlp part was taken directly from one of my repos, hence “real” in the header). Text, too, was written by me because I like writing things myself.