Index

Go CBOR encoder: Episode 4, reflect and pointers


In the previous episode we encoded positive integers and learned how to write a CBOR item with a variable size. Our CBOR encoder can now encode nil, true, false, and unsigned integers. cbor.Encoder has grown strong, but type switches have their limits, we need more powerful weapons for the battles ahead: we’re about to take on pointers, and reflect will be our sword.

In the first episode of the series we encoded of the nil value since it was the easiest value to start with, but we aren’t finished with the nil we still got work to do to cover all cases. That’s because our encoder only handles the “naked” nil value, but not typed pointers that are nil. Whaaat? There are two kinds of nil pointers? Yep, that’s because nil by itself is special. Consider the following code:

var p *int = nil
var v interface{} = p
switch v.(type) {
case nil:
    fmt.Println("nil")
case *int:
    fmt.Println("int pointer")
}

The example above prints “int pointer”, because v isn’t a regular value but an interface that points to a int pointer value. Go interfaces are pairs of 32 or 64bits addresses: one for the type and one for the value. So in the type switch above we match the *int case because p’s type is *int. If we replaced the v definition with var v interface{} = nil, the program would print “nil”. That’s because the type of a nil value is itself nil, but typed-pointers’ type aren’t. Russ Cox’s article Go Data Structures: Interfaces is a superb introduction to how Go interfaces work if you’d like to learn more.

Let’s exhibit the problem in our code and add a test for typed nil pointers:

func TestNilTyped(t *testing.T) {
    var i *int = nil
    testEncoder(t, i, nil, []byte{0xf6})

    var v interface{} = nil
    testEncoder(t, v, nil, []byte{0xf6})
}

And run our tests with go test to see what happens:

--- FAIL: TestNilTyped (0.00s)
    cbor_test.go:18: err: &errors.errorString{s:"Not Implemented"} != <nil> with (*int)(nil)

The *int(nil) value isn’t recognized. So why did plain nil worked? Because it’s special: both its type and its value are nil. The Encode function matches the naked nil with the case nil statement in the type switch, this means only interfaces with a nil type will be matched. Therefor the code only works with the naked nil value, but not with typed pointers.

It turns out there’s a package to address that: reflect introspects the type system and let us match pointer types individually. The Law of reflection is a great introduction to reflection and the use of this package.

So we want to know if a value is a pointer. How does reflect help us? Consider this snippet:

fmt.Println(reflect.ValueOf(nil).Kind())
var i *int = nil
fmt.Println(reflect.ValueOf(i).Kind())

It prints:

invalid
ptr

What happens here? First we convert each Go value to a reflect.Value, then we query its type with the method Kind that returns a reflect.Kind enumeration. reflect.Kind represents the specific kind of type that a Type represents. Kinds are families of types. For example there is a kind for structs —reflect.Struct—, for functions —reflect.Func—, and for pointers —reflect.Pointer.

We see above that the naked nil value and a nil pointer to integer have different kinds: invalid, and ptr. We’ll have to handle the two cases separately.

Refactoring time! We replace the type switch with a switch statement on the Kind of our value. In the example below x.Kind() allows us to distinguish types the same way the type switch x.(type) did:

func (e *Encoder) Encode(v interface{}) error {
    x := reflect.ValueOf(v)
    switch x.Kind() {
    case reflect.Invalid:
        // naked nil value == invalid type
        return e.writeHeader(majorSimpleValue, simpleValueNil)
    case reflect.Bool:
        var minor byte
        if x.Bool() {
            minor = simpleValueTrue
        } else {
            minor = simpleValueFalse
        }
        return e.writeHeader(majorSimpleValue, minor)
    case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
        return e.writeInteger(x.Uint())
    }
    return ErrNotImplemented
}

To identify pointer types reflect has a Kind called reflect.Ptr. We add a another case clause for reflect.Ptr, then if the pointer is nil we write the encoded nil value to the output:

case reflect.Ptr:
	if x.IsNil() {
		return e.writeHeader(majorSimpleValue, simpleValueNil)
	}

After we add that, a quick run of go test confirms that TestNilTyped works.

Splendid! We solved nil pointers. How about non-nil pointers? They are relatively easy to handle: if we detect a pointer we can fetch the value it refers to via reflect.Indirect. So when we get a pointer we get the value it references instead of the memory address. Here’s an example of how reflect.Indirect works:

var i = 1
var p = &i
var reflectValue = reflect.ValueOf(p)
fmt.Println(reflectValue.Kind())
fmt.Println(reflect.Indirect(reflectValue).Kind())

It prints:

int
ptr

When we find a non-nil pointer type, we call the Indirect function to retrieve the pointed value and we recursively call the Encode method on that value. We add a new test: TestPointer that verifies pointer referencing works as intended:

func TestPointer(t *testing.T) {
    i := uint(10)
    pi := &i  // pi is a *uint

    // should output the number 10
    testEncoder(t, pi, nil, []byte{0x0a})
}

With our test written let’s add the code necessary to handle valid pointers in our case clause:

case reflect.Ptr:
	if x.IsNil() {
		return e.writeHeader(majorSimpleValue, simpleValueNil)
	} else {
		return e.Encode(reflect.Indirect(x).Interface())
	}

reflect.Indirect(x).Interface()) retrieves an interface to x’s underlying value, we pass it recursively to Encode and return the result. So if we passed a pointer to pointer to pointer to integer (***int) we’d have 3 recursive calls to Encode. TestPointer now passes, we are done with pointers!

There’s a repository with the code for this episode.

The reflect package will help us to handle more complex types in subsequent episodes. Next time we will encode string types: byte string, and Unicode strings.