Skip to Content
Course content

207: Struct Tags and Reflection

Click on the "Edit" button in the top corner of the screen to edit your slide content.

You've probably seen struct tags before—those little strings like `json:"user_id"` that you slap next to field definitions. Most of the time, we just let the encoding/json package handle them, but the real magic happens when you realize those tags are just metadata. They don't do anything on their own; they are just labels waiting for some code to read them via reflection.

Reflection in Go is basically the ability for a program to examine its own structure at runtime. It's powerful, but it's also a bit dangerous because you lose the safety of the compiler. If you get a type wrong during reflection, the program won't fail at compile time—it'll just panic while it's running. I've spent more hours than I'd like to admit debugging reflect.Value panics.

Defining our environment mapping

Let's build something practical: a simple configuration loader. Instead of manually calling os.Getenv for every single field in a config struct, we'll create a system where we tag the fields with the environment variable name we want to pull.

type Config struct {
	Port    string `env:"APP_PORT"`
	APIKey  string `env:"API_KEY"`
	Timeout int    `env:"APP_TIMEOUT"`
}

The env:"..." part is our custom tag. Go's reflection package allows us to iterate over the fields of this struct and extract that string.

Plumbing the reflection engine

Now we need a function that takes a pointer to a struct, looks at these tags, and fills in the values. I'll use the reflect package to inspect the type and the os package to grab the actual environment values.

func LoadConfig(cfg interface{}) {
	v := reflect.ValueOf(cfg)
	t := reflect.TypeOf(cfg)

	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)
		tag := field.Tag.Get("env")

		if tag == "" {
			continue
		}

		val := os.Getenv(tag)
		if val != "" {
			// Here is where I'm going to try and set the value
			v.Field(i).SetString(val)
		}
	}
}

The "non-addressable" panic

If I run the code above, it's going to crash immediately with a panic: reflect: reflect.Value.SetString on unaddressable value. This is a classic reflection trip-up that almost every Go developer hits once.

The problem is that reflect.ValueOf(cfg) creates a copy of the value passed in. When I call v.Field(i).SetString(val), I'm trying to change a value inside a temporary copy, which Go doesn't allow for safety reasons. To actually modify the original struct, I need to pass a pointer to the struct and then "dereference" that pointer using .Elem().

Here is the corrected version of the logic:

func LoadConfig(cfg interface{}) {
	// We expect a pointer, so we get the value the pointer points to
	v := reflect.ValueOf(cfg).Elem()
	t := reflect.TypeOf(cfg).Elem()

	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)
		tag := field.Tag.Get("env")

		if tag == "" {
			continue
		}

		val := os.Getenv(tag)
		if val != "" {
			// Now v is addressable because we called .Elem() on the pointer
			v.Field(i).SetString(val)
		}
	}
}

Handling different types

You'll notice my example only works for strings. If I try to use SetString on that Timeout integer field, it'll panic again because of a type mismatch. In a real-world production tool, I'd use a switch v.Field(i).Kind() block to handle reflect.Int, reflect.Bool, and so on, converting the environment string to the appropriate type using strconv.

Reflection is a "break glass in case of emergency" tool. It's great for libraries and generic configuration loaders, but if you can achieve your goal with a simple interface or a map, do that instead. It's faster and much easier for the next person reading your code to understand.




📋 Practical Task

Building a Required-Field Validator

Your task is to create a validation function that checks if "required" fields in a struct are populated. Instead of using env tags, you will use a tag called validate:"required".

Requirements:

  • Create a struct called User with fields Username, Email, and Age.
  • Add the tag validate:"required" to Username and Email, but leave Age without it.
  • Write a function ValidateRequired(s interface{}) error that uses reflection to iterate through the fields.
  • If a field has the required tag but the value is the "zero value" for its type (e.g., an empty string for strings), the function should return an error naming the missing field.
  • If all required fields are present, return nil.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.