Exercises — Module 15: Reflection and Metaprogramming¶
Work through exercises in order — they're designed to build on each other. Attempt each problem genuinely before looking at the solution. Seeing the solution first might feel like progress, but it isn't.
Instructions¶
- Attempt first. Spend at least the estimated time on each problem before checking hints or solutions.
- Write your work. Don't just run code mentally — actually write or type your attempt.
- Check your answer against the acceptance criteria, not just the solution code.
- Score yourself honestly in the Scoring Log at the bottom.
- If you're stuck after a genuine effort, use the hints one at a time — not all at once.
Difficulty Legend¶
| Symbol | Difficulty | Expected Time | Points |
|---|---|---|---|
| 🟢 Easy | Recall and basic application | 5–10 min | 1 pt |
| 🟡 Medium | Requires combining 2+ concepts | 15–25 min | 2 pts |
| 🔴 Hard | Multi-step, requires real problem-solving | 30–60 min | 3 pts |
| ⭐ Challenge | Open-ended; more than one good answer | 60+ min | 5 pts |
Exercise 1: Read Type and Kind [🟢 Easy] [1 pt]¶
Context¶
reflect.TypeOf and reflect.ValueOf are the entry points to all reflection. This exercise confirms you can call them correctly and understand the difference between Type (the specific named type) and Kind (the underlying structural category).
Task¶
Write a function typeInfo(v interface{}) that prints, on one line:
- The reflect.Type of the value (via reflect.TypeOf)
- The reflect.Kind of the value (via .Kind())
Call it with at least five different values: an int, a string, a struct of your choice, a []int slice, and a map[string]int.
Requirements¶
- Uses
reflect.TypeOf(v)and.Kind() - Called with at least 5 different values covering: int, string, struct, slice, map
- Output shows both the type name and the kind for each value
- The program compiles and runs without errors
Hints¶
Hint 1 (try without this first)
`reflect.TypeOf(v).Name()` gives the type name (e.g., `"int"`, `"MyStruct"`; empty for unnamed composite types like `[]int`). `reflect.TypeOf(v).Kind()` gives the underlying kind (e.g., `reflect.Int`, `reflect.Slice`). Use `reflect.TypeOf(v).String()` to get a printable representation that works for both named and unnamed types.Expected Output / Acceptance Criteria¶
Output should look similar to (exact struct name will vary):
Solution¶
Show Solution (attempt first!)
package main
import (
"fmt"
"reflect"
)
type Point struct{ X, Y float64 }
func typeInfo(v interface{}) {
t := reflect.TypeOf(v)
fmt.Printf("%-20s kind=%-10s\n", t.String(), t.Kind())
}
func main() {
typeInfo(42)
typeInfo("hello")
typeInfo(Point{1, 2})
typeInfo([]int{1, 2, 3})
typeInfo(map[string]int{"a": 1})
}
Exercise 2: Read Struct Tags [🟢 Easy] [1 pt]¶
Context¶
Struct tags are the metadata system used by encoding/json, database/sql, and countless other packages. This exercise confirms you can read them programmatically using reflect.StructField.Tag.
Task¶
Define the following struct:
type Employee struct {
ID int `json:"id" db:"employee_id"`
FirstName string `json:"first_name" db:"first_name"`
Department string `json:"department,omitempty"`
secret string // unexported — no tags accessible
}
Write a function printTags(v interface{}) that iterates over all exported fields of any struct and prints, for each field: the field name, the json tag value, and the db tag value (empty string if absent).
Requirements¶
- Uses
reflect.TypeOfto get the struct type,NumField()andField(i)to iterate - Uses
field.Tag.Get("json")andfield.Tag.Get("db")to read tags - Skips unexported fields (check
field.IsExported()) - Prints all three exported fields of
Employeewith their tag values
Hints¶
Hint 1
`reflect.TypeOf(v)` when `v` is a struct gives you the struct's `reflect.Type`. If `v` might be a pointer, first call `reflect.TypeOf(v).Elem()` after checking `Kind() == reflect.Pointer`. Then `t.NumField()` gives the count, and `t.Field(i)` returns a `reflect.StructField` with `.Name`, `.Tag`, and `.IsExported()`.Expected Output / Acceptance Criteria¶
Field: ID json:"id" db:"employee_id"
Field: FirstName json:"first_name" db:"first_name"
Field: Department json:"department,omitempty" db:""
Solution¶
Show Solution
package main
import (
"fmt"
"reflect"
)
type Employee struct {
ID int `json:"id" db:"employee_id"`
FirstName string `json:"first_name" db:"first_name"`
Department string `json:"department,omitempty"`
secret string
}
func printTags(v interface{}) {
t := reflect.TypeOf(v)
if t.Kind() == reflect.Pointer {
t = t.Elem()
}
if t.Kind() != reflect.Struct {
fmt.Println("not a struct")
return
}
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
if !f.IsExported() {
continue
}
jsonTag := f.Tag.Get("json")
dbTag := f.Tag.Get("db")
fmt.Printf("Field: %-14s json:%-30q db:%q\n", f.Name, jsonTag, dbTag)
}
}
func main() {
printTags(Employee{})
}
Exercise 3: Struct-to-Map Serializer [🟡 Medium] [2 pts]¶
Context¶
Understanding how encoding/json works internally means being able to build a minimal version yourself. This exercise requires combining struct field iteration, tag reading, omitempty logic, and val.Interface() — the same pattern that powers JSON marshaling, YAML serialization, and ORM field mapping.
Task¶
Write a function ToMap(v interface{}) map[string]interface{} that:
1. Accepts any struct (or pointer to struct)
2. Returns a map[string]interface{} where each key is determined by the json struct tag (or the field name if no tag)
3. Skips fields tagged json:"-"
4. Skips fields with omitempty when their value is the zero value (use reflect.Value.IsZero())
5. Skips unexported fields
Test it with:
type Product struct {
SKU string `json:"sku"`
Name string `json:"name"`
Price float64 `json:"price,omitempty"`
Internal string `json:"-"`
Notes string // no tag — use field name
}
Requirements¶
- Handles pointer-to-struct input by dereferencing with
Elem() - Uses
field.Tag.Get("json")and splits on","to separate name from options - Correctly applies
omitemptyusingval.IsZero() - Fields with
json:"-"are completely omitted - Unexported fields are skipped
- Returns
nil(not panic) for non-struct input
Hints¶
Hint 1
Parse the tag with `strings.SplitN(tag, ",", 2)`. `parts[0]` is the JSON key name (use the field name if it's empty). Check `len(parts) > 1 && parts[1] == "omitempty"` for the omitempty flag. Use `val.IsZero()` to detect zero values for any kind.Hint 2
To get the `reflect.Value` of a field for reading, use `rv.Field(i).Interface()`. This returns the field's value as `interface{}`. Make sure `rv` is the struct value (not pointer) before calling `.Field(i)`.Expected Output / Acceptance Criteria¶
With Product{SKU: "ABC-1", Name: "Widget", Internal: "hidden"} (Price is zero):
// map should contain:
// "sku" -> "ABC-1"
// "name" -> "Widget"
// "Notes" -> "" (no tag, field name used; not omitempty, so included even if zero)
// price and Internal are absent
Solution¶
Show Solution
package main
import (
"fmt"
"reflect"
"strings"
)
func ToMap(v interface{}) map[string]interface{} {
rv := reflect.ValueOf(v)
rt := reflect.TypeOf(v)
if rv.Kind() == reflect.Pointer {
rv, rt = rv.Elem(), rt.Elem()
}
if rv.Kind() != reflect.Struct {
return nil
}
result := make(map[string]interface{})
for i := 0; i < rt.NumField(); i++ {
field := rt.Field(i)
val := rv.Field(i)
if !field.IsExported() {
continue
}
tag := field.Tag.Get("json")
if tag == "-" {
continue
}
name := field.Name
omitempty := false
if tag != "" {
parts := strings.SplitN(tag, ",", 2)
if parts[0] != "" {
name = parts[0]
}
if len(parts) > 1 && parts[1] == "omitempty" {
omitempty = true
}
}
if omitempty && val.IsZero() {
continue
}
result[name] = val.Interface()
}
return result
}
type Product struct {
SKU string `json:"sku"`
Name string `json:"name"`
Price float64 `json:"price,omitempty"`
Internal string `json:"-"`
Notes string // no tag
}
func main() {
p := Product{SKU: "ABC-1", Name: "Widget", Internal: "hidden"}
m := ToMap(p)
for k, v := range m {
fmt.Printf(" %q: %v\n", k, v)
}
// Expected keys: "sku", "name", "Notes"
// Absent: "price" (omitempty + zero), "Internal" (tagged "-")
}
Exercise 4: Set Fields via Reflection [🟡 Medium] [2 pts]¶
Context¶
Setting values via reflection is trickier than reading — it requires an addressable value obtained from a pointer. This exercise is the canonical use case: a function that applies default values to any struct whose fields are still at their zero value.
Task¶
Write a function ApplyDefaults(ptr interface{}, defaults map[string]interface{}) that:
1. Takes a pointer to any struct
2. For each key in defaults, finds the exported struct field with that name
3. If the field's current value is zero (IsZero()), sets it to the default value
4. Skips non-existent field names silently
Use it to populate a Config struct:
Requirements¶
- Panics (or returns an error) if
ptris not a pointer to a struct - Uses
reflect.ValueOf(ptr).Elem()to get the addressable struct value - Calls
CanSet()before callingSetor kind-specific setters - Uses
reflect.ValueOf(defaultVal)and aSetcall to apply the default - Does not overwrite fields that are already non-zero
Hints¶
Hint 1
Use `s.FieldByName(name)` to find a field by name. It returns a zero `reflect.Value` if the field does not exist — check `f.IsValid()` before proceeding. Then check `f.CanSet()` before calling `f.Set(reflect.ValueOf(defaultVal))`.Hint 2
`reflect.ValueOf(defaultVal)` gives you a `reflect.Value` to pass to `f.Set()`. But `Set` requires that the types match exactly. Use `f.Set(reflect.ValueOf(defaultVal).Convert(f.Type()))` to handle cases where the default is an `int` literal but the field is `int64`, etc.Expected Output / Acceptance Criteria¶
cfg := Config{Host: "prod.example.com"} // Port, Timeout, Debug are zero
ApplyDefaults(&cfg, map[string]interface{}{
"Host": "localhost", // already set — should NOT be overwritten
"Port": 8080,
"Timeout": 30,
})
// cfg.Host == "prod.example.com" (unchanged)
// cfg.Port == 8080
// cfg.Timeout == 30
// cfg.Debug == false (not in defaults map)
Solution¶
Show Solution
package main
import (
"fmt"
"reflect"
)
func ApplyDefaults(ptr interface{}, defaults map[string]interface{}) {
rv := reflect.ValueOf(ptr)
if rv.Kind() != reflect.Pointer || rv.Elem().Kind() != reflect.Struct {
panic("ApplyDefaults: requires a pointer to a struct")
}
s := rv.Elem() // addressable struct
for name, defaultVal := range defaults {
f := s.FieldByName(name)
if !f.IsValid() || !f.CanSet() {
continue // field doesn't exist or is unexported
}
if !f.IsZero() {
continue // already has a value — don't overwrite
}
dv := reflect.ValueOf(defaultVal)
// Convert if underlying kinds are compatible (e.g., int → int64)
if dv.Type().ConvertibleTo(f.Type()) {
f.Set(dv.Convert(f.Type()))
}
}
}
type Config struct {
Host string
Port int
Timeout int
Debug bool
}
func main() {
cfg := Config{Host: "prod.example.com"}
ApplyDefaults(&cfg, map[string]interface{}{
"Host": "localhost",
"Port": 8080,
"Timeout": 30,
})
fmt.Printf("Host=%s Port=%d Timeout=%d Debug=%v\n",
cfg.Host, cfg.Port, cfg.Timeout, cfg.Debug)
// Host=prod.example.com Port=8080 Timeout=30 Debug=false
}
Exercise 5: go:generate with stringer [🔴 Hard] [3 pts]¶
Context¶
go generate is the compile-time alternative to runtime reflection for repetitive code generation. The stringer tool is the canonical example: it generates String() methods for integer-typed constants, saving both boilerplate writing and runtime reflection overhead.
Task¶
- Create a file
priority.goin a new directory (go mod init) with: - A
//go:generatedirective targetingstringer - A
Priority inttype with at least four constants:Low,Normal,High,Critical(usingiota) - Install
stringerif needed:go install golang.org/x/tools/cmd/stringer@latest - Run
go generate ./...to generatepriority_string.go - Write a
mainfunction that prints all fourPriorityconstants and demonstrates thatfmt.Printlnuses the generatedString()method
Requirements¶
- The
//go:generatedirective is a standalone comment at the top ofpriority.go -
Priorityusesiotafor its four constants - Running
go generate ./...produces apriority_string.gofile without errors - The generated file has the
// Code generated by stringerheader comment -
fmt.Println(Low)prints"Low", not"0" - The generated
String()method does not usereflectinternally
Hints¶
Hint 1 (structural hint — try designing without this first)
The `//go:generate` directive must start at column zero — no leading space. The format is: This tells `go generate` to run `stringer -type=Priority` in the package directory. The `-output` flag is optional; by default, stringer writes `priority_string.go`.Hint 2 (conceptual hint)
After running `go generate`, open `priority_string.go` and find the compile-time array bounds check: or similar. This is the generated code's compile-time safety net: if you add a constant without regenerating, this line fails to compile. That is intentional — it forces regeneration when the type changes.Hint 3 (near-solution hint — only if truly stuck)
The full `priority.go` should look like: Run `go generate ./...`, then `go run .`.Expected Output / Acceptance Criteria¶
After generating and running:
Key acceptance criterion: The output uses the constant names, not integers. Inspect priority_string.go to confirm it uses a string table and a switch/index — not reflect or fmt.Sprintf("%d").
Solution¶
Show Solution
// priority.go
//go:generate stringer -type=Priority
package main
import "fmt"
// Priority represents a task priority level.
type Priority int
const (
Low Priority = iota // 0
Normal // 1
High // 2
Critical // 3
)
func main() {
priorities := []Priority{Low, Normal, High, Critical}
for _, p := range priorities {
fmt.Printf("Value %d = %s\n", int(p), p)
}
}
// Code generated by "stringer -type=Priority"; DO NOT EDIT.
package main
import "strconv"
func _() {
// Compile-time check that constants haven't changed
var x [1]struct{}
_ = x[Low-0]
_ = x[Normal-1]
_ = x[High-2]
_ = x[Critical-3]
}
const _Priority_name = "LowNormalHighCritical"
var _Priority_index = [...]uint8{0, 3, 9, 13, 21}
func (i Priority) String() string {
if i < 0 || i >= Priority(len(_Priority_index)-1) {
return "Priority(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _Priority_name[_Priority_index[i]:_Priority_index[i+1]]
}
Exercise 6: Generic Printer vs Reflect Printer [🔴 Hard] [3 pts]¶
Context¶
One of the most important skills in modern Go is knowing when to reach for reflection versus generics. Since Go 1.18, many tasks that previously required reflection can be expressed as generic functions — with full compile-time type safety and better performance. This exercise asks you to implement the same task two ways and compare them.
Task¶
Implement PrintSlice in two versions:
Version A (reflection): func PrintSliceReflect(v interface{}) — accepts any slice via interface{}, uses reflect.ValueOf and reflect.Value.Index to iterate, and prints each element.
Version B (generics): func PrintSlice[T any](s []T) — accepts any slice as a type-parameterized function.
Call both versions with a []int, a []string, and a []float64. Then write a brief comment in your code (3–5 lines) explaining: in what situations would you still choose the reflect version over the generic version?
Requirements¶
- Version A uses
reflect.ValueOf(v), checksKind() == reflect.Slice, and uses.Len()and.Index(i).Interface()to iterate - Version B uses a type parameter
[T any]and a standardrangeloop - Both versions produce identical output for the same input
- The code compiles on Go 1.22+
- The comment explains at least one scenario where reflection is genuinely preferable
Hints¶
Hint 1 (structural hint)
For the reflect version: `rv := reflect.ValueOf(v)`, then check `rv.Kind() == reflect.Slice`, then `for i := 0; i < rv.Len(); i++ { fmt.Println(rv.Index(i).Interface()) }`.Hint 2 (conceptual hint)
The generic version is strictly simpler and faster. But the generic version requires the caller to know the slice type at compile time. The reflect version accepts `interface{}` — useful when you receive data from a JSON decoder or database scanner that returns `[]interface{}` or some other dynamically typed collection.Expected Output / Acceptance Criteria¶
Both versions should print identical output for each slice type. The comment should identify at least one real scenario (e.g., "when the slice type is only known at runtime, such as after JSON unmarshaling into interface{}").
Solution¶
Show Solution
package main
import (
"fmt"
"reflect"
)
// Version A: Reflection-based — accepts any slice as interface{}
func PrintSliceReflect(v interface{}) {
rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Slice {
fmt.Println("(not a slice)")
return
}
for i := 0; i < rv.Len(); i++ {
fmt.Println(rv.Index(i).Interface())
}
}
// Version B: Generic — compile-time type safety, no reflection
func PrintSlice[T any](s []T) {
for _, elem := range s {
fmt.Println(elem)
}
}
// When to prefer reflection over generics:
// 1. When the concrete type is only known at runtime (e.g., value came from
// json.Unmarshal into interface{} or from a plugin loaded via os/exec).
// 2. When writing a framework that must handle user-defined types it cannot
// import at compile time (e.g., an ORM mapping []MyRow where MyRow is
// defined by the user, not the library).
// 3. When the value is already in interface{} form and a type assertion to the
// specific slice type would require a large type switch.
func main() {
ints := []int{1, 2, 3}
strs := []string{"a", "b", "c"}
floats := []float64{1.1, 2.2, 3.3}
fmt.Println("--- Reflect version ---")
PrintSliceReflect(ints)
PrintSliceReflect(strs)
PrintSliceReflect(floats)
fmt.Println("--- Generic version ---")
PrintSlice(ints)
PrintSlice(strs)
PrintSlice(floats)
}
Exercise 7: Method Caller via Reflection [⭐ Challenge] [5 pts]¶
Context¶
Reflection can discover and call methods dynamically — the mechanism behind RPC frameworks, dependency injection containers, and test harnesses that call methods by name. This exercise combines method discovery, argument building, and error handling into a realistic mini-framework.
Task¶
Write a function CallMethod(obj interface{}, methodName string, args ...interface{}) ([]interface{}, error) that:
1. Finds the method named methodName on obj (or a pointer to obj)
2. Validates that the number of provided args matches the method's expected parameter count
3. Converts each arg to a reflect.Value using reflect.ValueOf
4. Calls the method and returns its results as []interface{}
5. Returns an error (not panic) if the method does not exist or the argument count is wrong
Test it with a Calculator struct that has methods Add(a, b int) int and Greet(name string) string.
Requirements¶
- Uses
reflect.Value.MethodByNameto look up the method - Validates argument count against
method.Type().NumIn() - Converts results with
result.Interface()for each element in the returned[]reflect.Value - Returns a descriptive
error(not a panic) for method-not-found and wrong-arg-count - Tested with at least two methods of different signatures
Hints¶
Hint 1 (structural hint)
`method.Type().NumIn()` gives the number of parameters. Build `[]reflect.Value{reflect.ValueOf(args[0]), ...}` and call `method.Call(argVals)`.Hint 2 (conceptual hint)
The value returned by `rv.MethodByName` already has the receiver bound — you only need to pass the non-receiver arguments. So a method `func (c *Calculator) Add(a, b int) int` when called via `method.Call(args)` only needs `args` to contain `[a, b]`, not `[c, a, b]`.Hint 3 (near-solution hint — only if truly stuck)
Expected Output / Acceptance Criteria¶
Add(3, 4) = 7
Greet("World") = Hello, World!
Error: method "Divide" not found on Calculator
Error: method "Add" expects 2 args, got 1
Solution¶
Show Solution
package main
import (
"fmt"
"reflect"
)
// CallMethod invokes a named method on obj with the given args.
// Returns the results as []interface{} or an error.
func CallMethod(obj interface{}, methodName string, args ...interface{}) ([]interface{}, error) {
rv := reflect.ValueOf(obj)
method := rv.MethodByName(methodName)
if !method.IsValid() {
return nil, fmt.Errorf("method %q not found on %T", methodName, obj)
}
mt := method.Type()
if mt.NumIn() != len(args) {
return nil, fmt.Errorf("method %q expects %d args, got %d",
methodName, mt.NumIn(), len(args))
}
argVals := make([]reflect.Value, len(args))
for i, a := range args {
argVals[i] = reflect.ValueOf(a)
}
results := method.Call(argVals)
out := make([]interface{}, len(results))
for i, r := range results {
out[i] = r.Interface()
}
return out, nil
}
type Calculator struct{ Name string }
func (c Calculator) Add(a, b int) int { return a + b }
func (c Calculator) Greet(name string) string { return "Hello, " + name + "!" }
func main() {
calc := Calculator{Name: "MyCalc"}
if res, err := CallMethod(calc, "Add", 3, 4); err != nil {
fmt.Println("Error:", err)
} else {
fmt.Printf("Add(3, 4) = %v\n", res[0])
}
if res, err := CallMethod(calc, "Greet", "World"); err != nil {
fmt.Println("Error:", err)
} else {
fmt.Printf("Greet(\"World\") = %v\n", res[0])
}
if _, err := CallMethod(calc, "Divide", 10, 2); err != nil {
fmt.Println("Error:", err)
}
if _, err := CallMethod(calc, "Add", 5); err != nil {
fmt.Println("Error:", err)
}
}
Scoring Log¶
Record your performance honestly. Include the date and whether you used hints.
| Exercise | Date | Score | Used Hints? | Notes |
|---|---|---|---|---|
| Exercise 1 — Read Type and Kind | — | —/1 | — | — |
| Exercise 2 — Read Struct Tags | — | —/1 | — | — |
| Exercise 3 — Struct-to-Map Serializer | — | —/2 | — | — |
| Exercise 4 — Set Fields via Reflection | — | —/2 | — | — |
| Exercise 5 — go:generate with stringer | — | —/3 | — | — |
| Exercise 6 — Generic vs Reflect Printer | — | —/3 | — | — |
| Exercise 7 — Method Caller via Reflection | — | —/5 | — | — |
| Total | —/17 |
Passing threshold: 11/17 (65%). Aim for 15/17 (88%) before taking the test.