Struct Tags in Action
Struct tags provide metadata for fields that can be accessed at runtime via reflection. They’re commonly used by serialization libraries (e.g., json, xml) but are fully customizable.
field.Tagretrieves the raw tag string.field.Tag.Get("key")extracts the value associated with a specific key.field.Tag.Lookup("key")returns both value and existence flag—safer for optional keys.
package main
import (
"fmt"
"reflect"
)
type Animal struct {
Name string `json:"name" schema:"required"`
Kind int `json:"type" id:"42" deprecated:"true"`
}
func main() {
example := Animal{Name: "Luna", Kind: 3}
t := reflect.TypeOf(example)
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
fmt.Printf("Field: %s → Tag: %q\n", f.Name, f.Tag)
}
if f, ok := t.FieldByName("Kind"); ok {
if jsonVal, ok := f.Tag.Lookup("json"); ok {
fmt.Printf("JSON key: %s\n", jsonVal)
}
if idVal, ok := f.Tag.Lookup("id"); ok {
fmt.Printf("ID: %s\n", idVal)
}
}
}
Type vs Kind: A Runtime Dsitinction
In Go’s type system, Type refers to the declared or concrete type (e.g., MyInt, map[string]int), while Kind is the underlying category recognized by the runtime—such as int, struct, ptr, or slice. This distinction is essential when writing generic-like logic over arbitrary values.
Every reflect.Kind corresponds to one of Go’s low-level memory representations. For example:
type MyString stringhasKind == reflect.StringbutType == main.MyString.*inthasKind == reflect.Ptr, regardless of whether it points toint,string, or a custom struct.
package main
import (
"fmt"
"reflect"
)
func describe(v interface{}) {
rv := reflect.ValueOf(v)
rt := reflect.TypeOf(v)
fmt.Printf("Value: %v | Type: %v | Kind: %v\n", rv, rt, rt.Kind())
}
func main() {
describe("hello") // Type: string, Kind: string
describe(int64(42)) // Type: int64, Kind: int64
describe(struct{ X int }{X: 1}) // Type: struct { X int }, Kind: struct
describe([]byte("data")) // Type: []uint8, Kind: slice
}
Reflect.Type and Reflect.Value: Complementary Views
reflect.Type describes *what* a value is—its name, methods, fields, and composition. reflect.Value represents *the value itself*, enabling reading, setting, and method invocation.
Crucially, only exported (capitalized) fields and methods are accessible through reflection. Unexported members remain invisible—evenif the struct is passed by pointer.
package main
import (
"fmt"
"reflect"
"strconv"
)
func formatValue(v reflect.Value) string {
switch v.Kind() {
case reflect.String:
return strconv.Quote(v.String())
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return strconv.FormatInt(v.Int(), 10)
case reflect.Bool:
return strconv.FormatBool(v.Bool())
case reflect.Ptr, reflect.Slice, reflect.Map, reflect.Chan, reflect.Func:
return fmt.Sprintf("%v (0x%x)", v.Type(), v.Pointer())
case reflect.Struct:
return fmt.Sprintf("%v struct", v.Type())
default:
return fmt.Sprintf("%v", v.Kind())
}
}
type Config struct {
Host string `env:"HOST"`
Port int `env:"PORT"`
}
func main() {
cfg := Config{Host: "localhost", Port: 8080}
v := reflect.ValueOf(cfg)
fmt.Println(formatValue(v.Field(0))) // "localhost"
fmt.Println(formatValue(v.Field(1))) // "8080"
}
Converting Back from reflect.Value
To recover the original Go value from a reflect.Value, use v.Interface(). This returns an interface{}, requiring a type assertion for safe usage:
v := reflect.ValueOf(Config{"api.example.com", 3000})
original := v.Interface().(Config) // Safe only if type is known
fmt.Printf("%+v\n", original) // {Host:"api.example.com" Port:3000}
For dynamic scenarios, check compatibility first using v.Type().AssignableTo(expectedType).
Unwrapping Underlying Types
Go allows aliasing types (type MyInt int) and defining new structs. To bypass user-defined abstractions and inspect the foundational representation, use v.Kind() or t.Kind(). For deeper unwrapping (e.g., pointers, interfaces), chain Elem() or Underlying():
type ID int
var x ID = 99
v := reflect.ValueOf(&x).Elem()
fmt.Println(v.Kind()) // int
fmt.Println(v.Type()) // main.ID
Enumerating Fields and Methods
Reflection enables introspection of struct layout and method sets at runtime:
t.NumField()+t.Field(i)iterates over exported struct fields.t.NumMethod()+t.Method(i)lists exported methods (not receivers).
func printLayout(t reflect.Type) {
fmt.Printf("Type: %v (%v)\n", t, t.Kind())
fmt.Println("Fields:")
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
fmt.Printf(" %s %v `%.100s`\n", f.Name, f.Type, f.Tag)
}
fmt.Println("Methods:")
for i := 0; i < t.NumMethod(); i++ {
m := t.Method(i)
fmt.Printf(" %s %v\n", m.Name, m.Type)
}
}
Reading and Modifying Values Safely
Reading is straightforward: call v.Int(), v.String(), etc. Writing requires extra care:
- The value must be addressable (e.g., passed as pointer).
- It must be settable:
v.CanSet() == true. - Use
v.Elem()to dereference pointers before setting.
func updateValue() {
var count int = 5
v := reflect.ValueOf(&count).Elem()
if v.CanSet() && v.Kind() == reflect.Int {
v.SetInt(42)
}
fmt.Println(count) // 42
}
Dynamic Method Invocation
Call methods dynamically using v.MethodByName(name), then pass arguments as a slice of reflect.Value. Arguments must match the method signature exactly—including receiver kind (value vs. pointer).
type Service struct{ Name string }
func (s Service) Greet(prefix string) string {
return prefix + ", " + s.Name
}
func main() {
s := Service{Name: "API"}
v := reflect.ValueOf(s)
if m := v.MethodByName("Greet"); m.IsValid() {
result := m.Call([]reflect.Value{reflect.ValueOf("Hello")})
fmt.Println(result[0].String()) // "Hello, API"
}
}