Home

Gob encoding an interface

Tuesday 05 February 2013

Package gob is Go's preferred way of serialising variables. Serialisation is common in other languages, and many can use a cross-language format, such as JSON. Go has an excellent JSON package, but package gob is "unashamedly go-centric".
Serialisation allows you to store data in a bytestream. For instance, you could gob encode your program's state, and write it to a file. The next time you start your program it could read the file, gob decode the data and continue where it left off.
The package documentation is, obviously, a great resource. The rationale behind the design of the package and the source code are also great reading, particularly if you want to understand how it works, not just how to use it. This article will give a few examples of how to use gobs and explain a small subtlety of Go's type system that sometimes causes confusion.

Structs

Gobs can only access exported fields.
type Person struct {
    Name string
    Age int
}
Encoding a struct is straightforward:
p := Person{"Joe",30}
var b bytes.Buffer
e := gob.NewEncoder(&b)
if err := e.Encode(p); err != nil {
    panic(err)
}
  1. We create a bytes.Buffer that will act as a Writer, and store the encoded data. (It's basically a wrapped, infinite []byte.)
  2. We create a gob encoder, and tell it where to store the encoded data (&b). We need to use a pointer to b since the Write method is only defined on the pointer, as the encoder needs to be able to change the length of the buffer.
  3. We pass the struct to the encoder and it fills b. We could avoid the copy if we used a pointer, like e.Encode(&p). The encoder flattens all pointers, so the result would be the same.
  4. There shouldn't be an error. If there is, panic!
Decoding is similarly simple. Since we have to write into q we give the decoder a pointer (&q).
var q Person
d := gob.NewDecoder(&b)
if err := d.Decode(&q); err != nil {
    panic(err)
}

Interfaces

Structs are easy. Here's a type that implements an interface:
type Walker interface{
    Walk()
}
func (p Person) Walk(){
    fmt.Printf("%s walks\n", p.Name)
}
Before we can gob-encode this as an interface, we must register it with the gob package. Then it's almost as simple as before.
gob.Register(Person{})
var p = Person{"Amy", 25}
var w Walker = p
var b bytes.Buffer
e := gob.NewEncoder(&b)
//We *must* Encode a *Walker, not a Walker
if err := e.Encode(&w); err != nil {
    panic(err)
}
var v Walker
d := gob.NewDecoder(&b)
if err := d.Decode(&v); err != nil {
    panic(err)
}
So we've captured w and restored it into v. But what happens if we ignore the comment and try to encode w directly?
panic: gob: local interface type *main.Walker can only be decoded from remote interface type; received concrete type Person = struct { Name string; Age int; }

Oh dear. The error message is to-the-point, but won't help you understand why it's wrong. The error says that the decoder has found a Person in the bytestream, and you can't decode that into an Walker (or *Walker). Basically, it says you must encode an interface type if you want to decode into an interface type. Sounds fair enough, but didn't we just do that?

Wrapping

To understand the mistake is a test of your understanding of the Go type system. Two excellent guides are The Laws of Reflection and Russ Cox's blog post on interfaces. Make sure to read them sometime.
We can use fmt.Printf("%T",x) to reveal the type of a variable.
var p = Person{"Bob", 40}
var w Walker = p
func WhatType(i interface {}){
    fmt.Printf("%T\n",i)
}
Predictably, WhatType(p) prints:
main.Person
But WhatType(w) prints
main.Person
as well! The fact that p was wrapped up in a Walker interface has been lost in the call to WhatType(), where it was itself wrapped in an interface{} (the empty interface). As The Laws of Reflection makes clear, "Interfaces do not hold interface values". If we pass w rather than &w, the encoder only sees the concrete Person, and happily encodes it. However the decoder is expecting an interface, and gives up.
We've already seen the fix. If we encode a pointer to a Walker, it (and WhatType()) will see an interface with a dynamic type of:
*main.Walker
Which is flattened and easily decoded into a Walker without error.
To sum up, if you wrap a struct of type S in an interface A and then wrap that in another interface B, you get an interface with a dynamic type of S, not a double-layered interface.

Previous (Error handling in Go)
Next (Amazon Banking)