👻

Goで書いたMessagePackパッケージの紹介

2022/07/10に公開

MessagePackパッケージのgo-msgpackをgithubに公開しました。
https://github.com/nnabeyang/go-msgpack

ここでは、go-msgpackの主な機能について紹介します。

arrayにするかmapにするか

goの構造体をMessagePackにエンコードすると、arrayにするかmapにするか任意性が存在します。そのため、Unmarshalの2番目の引数でarrayとしてするかどうかのフラグがついています。簡単な例を挙げます。

main.go
package main

import (
	"encoding/hex"
	"flag"
	"fmt"

	gomsgpack "github.com/nnabeyang/go-msgpack"
)

type Pair struct {
	X int
	Y int
}

func main() {
	isArray := flag.Bool("array", false, "decode as array format")
	flag.Parse()
	v := Pair{X: 1, Y: 2}
	data, _ := gomsgpack.Marshal(v, *isArray)
	fmt.Println(hex.EncodeToString(data))
}

上記コードを実行すると、次のような結果を得ます。

$ go run .
82a15801a15902
$ go run . -array
920102

一般にarrayにした方がキーを含まないので、容量を抑えられ通信には有利になります。

構造体のフィールド内の構造体をarrayにするかどうか

このパッケージを書く主なモチベーションとして、フィールド内の構造体をどちらとするか設定できるようにするというものがあります。これはタグで指定できるようにしました。例としては、次のようなものです。

main.go
package main

import (
	"encoding/hex"
	"fmt"

	gomsgpack "github.com/nnabeyang/go-msgpack"
)

type Pair struct {
	X int
	Y int
}

type Record struct {
	ID    uint
	Value Pair
}

type RecordArray struct {
	ID    uint
	Value Pair `msgpack:"value,array"`
}

func main() {
	v1 := Record{ID: 1, Value: Pair{X: 1, Y: 2}}
	v2 := RecordArray{ID: 1, Value: Pair{X: 1, Y: 2}}
	data1, _ := gomsgpack.Marshal(v1, false)
	fmt.Println("Record     : ", hex.EncodeToString(data1))
	data2, _ := gomsgpack.Marshal(v2, false)
	fmt.Println("RecordArray: ", hex.EncodeToString(data2))
}

実行結果は次の通りです。

$ go run .
Record     :  82a2494401a556616c756582a15801a15902
RecordArray:  82a2494401a576616c7565920102

RecordPairの部分がmapにRecordArrayはarrayとしてエンコードしていることが分かります。この機能のおかげで構造体の中の構造体の中の構造体だけmapにエンコードしないといけないとかいう場合でも対応できます(デコードについても同様)。

mapのキーの名前を変更する

これはencoding/jsonと同様の機能です。次のコードで確かめることができます。

main.go
package main

import (
	"encoding/hex"
	"fmt"

	gomsgpack "github.com/nnabeyang/go-msgpack"
)

type Pair1 struct {
	X int
	Y int
}

type Pair2 struct {
	X int `msgpack:"x"`
	Y int
}

func main() {
	v1 := Pair1{X: 1, Y: 2}
	data1, _ := gomsgpack.Marshal(v1, false)
	fmt.Println("pair1:", hex.EncodeToString(data1))
	v2 := Pair2{X: 1, Y: 2}
	data2, _ := gomsgpack.Marshal(v2, false)
	fmt.Println("pair2:", hex.EncodeToString(data2))
}

結果は次の通りです。

$ go run .
pair1: 82a15801a15902
pair2: 82a17801a15902

上の5878に変わっていると思いますが、意味的にはXxに変わっています。

Timestamp

MessagePackには拡張型としてTimestampが定義されています。拡張型は拡張タイプという番号で識別されます。MessagePackとして定義されるものは-1から-128が割り当て可能で、アプリケーション独自に使える拡張タイプは0から127です。Timestampの拡張タイプは-1です。
簡単な例は次の通りです。

main.go
package main

import (
	"encoding/hex"
	"fmt"
	"time"

	gomsgpack "github.com/nnabeyang/go-msgpack"
)

func main() {
	t := gomsgpack.NewTimestamp(time.Date(2022, 07, 10, 15, 04, 05, 129, time.UTC))
	data, _ := gomsgpack.Marshal(t, false)
	fmt.Println(hex.EncodeToString(data))
	// =>  d7ff0000020462caea65
	var tt gomsgpack.Timestamp
	gomsgpack.Unmarshal(data, &tt)
	fmt.Println(time.Time(tt).Format("2006-01-02 15:04:05.000000000"))
	// => 2022-07-10 15:04:05.000000129
}

拡張型の定義

Marshalerのインターフェイスを実装するとエンコードできるようになり、Unmarshalerを実装するとデコードできるようになります。この辺りはencoding/jsonと同様ですが、上で説明したように拡張タイプ番号を決める必要があります。

type Marshaler interface {
	MarshalMsgPack() ([]byte, error)
	Type() int8
}
type Unmarshaler interface {
	UnmarshalMsgPack([]byte) error
	Type() int8
}

簡単な例を挙げておきます。

main.go
package main

import (
	"encoding/hex"
	"fmt"

	gomsgpack "github.com/nnabeyang/go-msgpack"
)

type position struct {
	X int8
	Y int8
}

func (o *position) Type() int8 {
	return 1
}

func (p *position) MarshalMsgPack() ([]byte, error) {
	return []byte{byte(p.X), byte(p.Y)}, nil
}
func (o *position) UnmarshalMsgPack(data []byte) error {
	*o = position{
		X: int8(data[0]),
		Y: int8(data[1]),
	}
	return nil
}

func main() {
	in := &position{X: 1, Y: 2}
	data, _ := gomsgpack.Marshal(in, false)
	fmt.Println(hex.EncodeToString(data))
	// =>  d5010102
	var out position
	gomsgpack.Unmarshal(data, &out)
	fmt.Printf("X:%d, Y:%d\n", out.X, out.Y)
	// => X:1, Y:2
}

パッケージの方に拡張型を登録する仕組みがあり、拡張タイプの一意性を保証するような作りが多いように思いますが、このパッケージはそこは何も管理しないつくりです。

Discussion