Go CBOR encoder: Episode 1, getting started
Let’s write a CBOR encoder in Go. We’ll learn more about type switching and type manipulation with the reflect package. This is going to be a series of posts, each building on the previous one. It requires a good understanding of the Golang syntax.
CBOR is a data format described in RFC 7049, it’s like JSON but binary instead of text. Its design goals are:
- extremely small code size
- fairly small message size
- extensibility without the need for version negotiation
We use an interface similar to the encoding/json package. If you are unfamiliar with this encoding sub-packages, I recommend you read the JSON and Go article.
To start our empty package we’ll create a file named cbor.go like this:
// Implements CBOR encoding:
//
// https://tools.ietf.org/html/rfc7049
//
package cbor
import (
"io"
)
type Encoder struct {
w io.Writer
}
func NewEncoder(w io.Writer) *Encoder {
return &Encoder{w: w}
}
func (enc *Encoder)Encode(v interface{}) error {
return nil
}
Here’s how we use the encoder:
var output = bytes.Buffer{}
var encoder = NewEncoder(&output)
var myvalue = 1234
// write the integer 1234 CBOR encoded into output
if err := encoder.Encode(&myvalue); err != nil {
...
}
We have our basic structure we can now start working on the encoder’s
implementation. In the previous example we encoded the integer 1234, but we
won’t start with integers, instead we will encode the value nil
to start
because it’s the easiest value to encode.
According the CBOR specification the nil value is represented by a single byte:
0xf6
Let’s write a test with the testing package, we’ll
verify the encoder outputs the single byte 0xf6
into the result buffer when we
pass nil. We create a new file cbor_test.go beside cbor.go for our tests:
package cbor
import (
"bytes"
"testing"
)
func TestNil(t *testing.T) {
var buffer = bytes.Buffer{}
var err = NewEncoder(&buffer).Encode(nil)
if !(err == nil && bytes.Equal(result.Bytes(), []byte{0xf6})) {
t.Fatalf(
"%#v != %#v or %#v != %#v",
err, nil, result, []byte{0xf6},
)
}
}
If we run the test with what we currently have we’ll get an error. No surprise: we haven’t implemented anything yet, so the encoder won’t write to the output buffer:
$ go test .
--- FAIL: TestNil (0.00 seconds)
cbor_test.go:15: <nil> != nil or []byte{} != []bytes{0xf6}
FAIL
FAIL _/home/henry/essays/cbor 0.011s
To implement the nil value encoding: we write the byte in the result when calling Encode() and we’ll return an error if the value isn’t nil since we haven’t implemented anything else yet:
var ErrNotImplemented = errors.New("Not Implemented")
// Can only encode nil
func (enc *Encoder) Encode(v interface{}) error {
switch v.(type) {
case nil:
var _, err = enc.w.Write([]byte{0xf6})
return err
}
return ErrNotImplemented
}
Here we’re using a type switch to determine the type of the value we got. There’s only one case for now: nil, where we write the 0xf6 value in the output and return the error to the caller.
And now the test succeeds:
=== RUN TestNil
--- PASS: TestNil (0.00s)
PASS
ok _/home/henry/essays/cbor 0.027s
We created the initial encoder that can encode a single value successfully. In the next episode we’ll implement CBOR encoding for more basic Go types.