Skip to content
Lesson 4 of 7

Step 1 of 4 · Reading · ~3 min

Learn

Generics and Reflection

Reflection

Reflection lets a function inspect and modify values whose type it does not know at compile time: list a struct's fields, read their tags, and set them. By the end you can write the kind of tag-driven loader encoding/json is built on.

In Interfaces a type switch opened an interface value's (type, value) pair for types you named in advance; reflect opens it for any type.

Type, Kind and Value

reflect.TypeOf(x) returns the dynamic type and reflect.ValueOf(x) the dynamic value. The Kind is the underlying category, and it differs from the type for every named type:

go

Branch on Kind in generic code: Celsius and float64 are different types but the same kind. Unnamed types such as []string have an empty Name.

Walking a struct and reading tags

A struct tag is the raw string after a field's type, by convention key:"value" pairs. Type.Field(k) returns a StructField with Name, Type, Tag, PkgPath (non-empty only for unexported fields) and Anonymous (true for an embedded field).

go
SKU   string exported=true  get="sku" lookup="sku",true
Cents int    exported=true  get="price_cents" lookup="price_cents",true
Stock int    exported=true  get="" lookup="",true
bin   string exported=false get="" lookup="",false

Tag.Get returns "" both for db:"" (Stock) and for no db key (bin); only Lookup's second result tells them apart. The compiler does not check tags: a typo such as db:"sku is silently ignored.

Names and tags live on the Type; the current value lives on the Value: reflect.ValueOf(p).Field(k), and .Interface() turns it back into an interface{} (it panics on an unexported field).

Writing: the addressability rule

reflect.ValueOf(p) holds a copy of p. This is the addressability rule from pointer receivers, enforced with a panic:

go

CanSet reports whether a write would succeed: the value must be addressable and the field exported. Other misuse panics too: SetString on an int field gives call of reflect.Value.SetString on int Value, and calling Field on the pointer without Elem gives call of reflect.Value.Field on ptr Value. On this grader the pointer Kind is spelled reflect.Ptr.

Slices, maps, methods and DeepEqual

Len() and Index(i) walk a slice. MapKeys() returns keys in random order, so sort them before printing. MethodByName("Has") finds an exported method and Call([]reflect.Value{...}) invokes it; a missing name gives a Value whose IsValid() is false. reflect.DeepEqual compares recursively but strictly: DeepEqual([]int(nil), []int{}) and DeepEqual(int32(1), int64(1)) are both false.

The three laws: an interface value becomes a reflection object; a reflection object becomes an interface value (Interface()); to modify one, it must be settable. The costs: type errors become run-time panics, and every call is much slower than direct code. If the type is known at compile time, use generics.

Common mistakes

  • Reflecting a copy. reflect.ValueOf(cfg) then any Set panics with unaddressable value; pass &cfg and call Elem().
  • Tag.Get for an optional tag. A missing min and min:"" both read as ""; parsing that as 0 rejects -5 on a field that has no lower bound. Use Lookup.
  • Inferring "was it set" from the zero value. A field deliberately set to 0 or "" looks untouched. Record what was set separately.

Your exercise

Complete load and report so a Config is filled from key=value lines using the field tags and nothing else. The most likely mistake is storing a value before its range check, or deciding "required" from the zero value.

Up nextError Chains: Unwrap, Is and As Under the HoodErrors, Testing, and Performance

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…