This is a dev log for using the reflect package to access fields of unexported struct.

Motivations

Using reflections has this stigma of “dirtiness” because of its performance hit and lack of type check safety. However, under certain restrictions, it we should feel justified to use it.

Programmatic access to fields of certain type

Imagine you have a type Foo that you want to write a validator for and validate structs with such field type. Something like the following signature.

func validate(X: aStructWithPossibleFieldOfTypeFoo): bool

anon := struct{a: Foo}{a: Foo{}}
// Since anon is created on the fly, we couldn't 
// have statically defined it.
validate(anon) 

Because of the infinite possibilities of X, we cannot cast it to a known type and then accessing the fields that we know are of type Foo. Conceptually, we would ask what are the fields of X, and what are the types of the field, if it is Foo, then validate it.

Reflect package can help with this because it allows traversing the fields and checking their types.

val := reflect.ValueOf(anon)
// If you need to programmatically go through the field
for i := 0; i < val.NumField(); i++ {
    f := val.Field(i)
    if f.Type().Name() != "Foo" {
        continue
	}
	// To get the field's name
	fName := val.Type().Field(i).Name
	fmt.Printf("%s is of type Foo!\n", fName)
    ...
}

Reading unexported struct and fields

Imagine you have to debug a third party library in production. Modifying module’s code or vendored libraries to add debug line might be difficult, and feels dirty. Making an upstream change just to support debugging is too much effort. In this scenario, using reflection to do probe the third party unexported structs will allow for access to the internals.

Suppose this is the unexported struct that we are given from a third party code.

type s struct {
	x map[string]int

	Y int
	X map[string]int
}

func Constructor() *s {
	var i = 2
	return &s{
		x: map[string]int{"a": 1},
		X: map[string]int{"a": 1},
		Y: i,
	}
}

Our goal is to be able to retrieve x and probe for its keys.

func main() {
	// Imagine this is initialized outside of our control
	t := Constructor()

	// Expecting a pointer.
	val := reflect.ValueOf(t)
	// Follow the pointer.
	val = reflect.Indirect(val)

	// If you know the name, you can refer to it by name. 
	theMap := val.FieldByName("x")

	// Let's explore 4 cases: 
	// {Read,Write} X {exported, Unexported}

	// Exported reading
	// If it's an exported field, you can extract it
	// and cast to the type you know it is supposed to be 
	// using Interface()
	a := val.FieldByName("Y").Interface().(int)
	fmt.Println(a)

	// E.g. Exported writing to pointers.
	theMap = val.FieldByName("X")
	m := theMap.Interface().(map[string]int)
	m["a"] = 2

	// E.g. Another way via the setters.
	theMap.SetMapIndex(reflect.ValueOf("a"), reflect.valueOf(3))

	// E.g. Exported writing to primitives.
	val.FieldByName("Y").SetInt(42)

	// Unexported reading
	// You'll have to use methods read the values.
	fmt.Println(theMap.MapIndex(reflect.ValueOf("a")).Int()) // 1

	// Trying to do interface will cause a panic.
	// fmt.Println(val.FieldByName("y").Interface())

	// Modifying unexported fields is not possible.
	// Trying to do so will cause a panic.

	fmt.Printf("%+v\n", t)
}

Closing thoughts

The reflect package has some nice APIs to get the job done. For example, reflect.Indirect is a convenient method to turn pointer into the underlying value without writing the case statements ourselves. Despite that, using the reflect package can lead to runtime panics easily if we’re not careful. E.g. calling Elem() on a Value that is not an interface is a common one. The key to prevent runtime panics is to code defensively and consult the documentation extensively. Check the val.Kind() before calling such methods, or use the guards like CanInterface() or CanSet.

Even with the possible run time panics, it is in some sense safe because it causes the program to quit before damage can be done. For example, it doesn’t expose any APIs to mess around with address pointers which can cause memory corruption. While there are API’s to read addresses, mutating values with it requires importing the “unsafe” package.

If the goal is to read values, then the reflect package gives a way to bypass the unexported struct restriction at the cost of more verbose code and performance hit.