Go
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions and Methods
-
Section 4: Concurrency
-
Section 5: Packages and Tooling
-
Section 6: More Standard Library
-
Section 7: Building Services
-
Section 8: Advanced Go
-
Section 9: Go in the Cloud-Native Ecosystem
-
Section 10: Data Structures and Algorithms in Go
-
Section 11: Testing and Deployment
-
Section 12: Practical Projects
-
Section 13: More Standard Library Practice
-
Section 14: More Practice Projects
-
Section 15: Design Patterns in Go
-
Section 16: Interview Practice
-
Section 17: Package fmt In Depth
-
Section 18: Package strings and strconv
-
Section 19: Package os and io
-
Section 20: Package time
-
Section 21: Package sort and container
-
Section 22: Package encoding
-
Section 23: Package net/http In Depth
-
Section 24: Package context
-
Section 25: Package regexp and bytes
-
Section 26: Package errors In Depth
-
Section 27: Package crypto and hash
-
Section 28: Package flag and log
-
Section 29: Package sync In Depth
-
Section 30: More Practice Exercises
-
Section 31: Go Modules and Workspaces In Depth
-
Section 32: Generics Deep Dive (Go 1.18+)
-
Section 33: Testing Package In Depth
-
Section 34: More Interview and Whiteboard Practice
-
Section 35: Package math and unicode
-
Section 36: Package path and filepath
-
Section 37: Package database/sql
-
Section 38: Package text/template and html/template
-
Section 39: Package archive and compress
-
Section 40: Lower-Level net Package
-
Section 41: Package runtime and reflect
-
Section 42: Package embed and io/fs
-
Section 43: Go Assembly and CGO Basics
-
Section 44: Building CLIs and TUIs
-
Section 45: Go Performance Tuning
-
Section 46: More Real-World Projects
-
Section 47: Go in Production
-
Section 48: Go Design Patterns
-
Section 49: Go Interfaces Deep Dive
-
Section 50: Final Practice and Review
207: Struct Tags and Reflection
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
Userwith fieldsUsername,Email, andAge. - Add the tag
validate:"required"toUsernameandEmail, but leaveAgewithout it. - Write a function
ValidateRequired(s interface{}) errorthat uses reflection to iterate through the fields. - If a field has the
requiredtag 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.
There are no comments for now.