MQTTサーバーを実装しながらGoを学ぶ - その2 go vet, gofmt, go doc

前回、固定ヘッダーを表すstructの実装に着手しました。

// fixed_header.go
package packet

type FixedHeader {
    PacketType byte
}

func ToFixedHeader(bs []byte) FixedHeader {
    b := bs[0]
    packetType := b >> 4
    return FixedHeader{packetType}
}

ここからの続きです。Goでの開発で便利そうなgo vet, gofmtといったコマンドも試してみます。

目次。

MQTT固定ヘッダー

MQTTの固定ヘッダーは以下のようなフォーマットになってた。

Bit 7 6 5 4 3 2 1 0
byte1 MQTT Control Packet type Flags specific to each MQTT Control Packet type
byte2... Remaining Length

MQTT Control Packet type を実装したので、次は Flags specific to each MQTT Control Packet type に着手する。その後、 Remaining Length

Flags

ここはサクッと。

fixed_header_test.go

                want packet.FixedHeader
        }{
                {
-                       "Reserved",
-                       args{[]byte{0x00, 0x00}},
-                       packet.FixedHeader{0},
+                       "Reserved Dup:0 QoS:00 Retain:0",
+                       args{[]byte{0x00, 0x00}}, // 0000 0 00 0
+                       packet.FixedHeader{PacketType: 0, Dup: 0, QoS1: 0, QoS2: 0, Retain: 0},
                },
                {
-                       "CONNECT",
-                       args{[]byte{0x10, 0x00}},
-                       packet.FixedHeader{1},
+                       "CONNECT Dup:1 QoS:01 Retain:1",
+                       args{[]byte{0x1B, 0x00}}, // 0001 1 01 1
+                       packet.FixedHeader{PacketType: 1, Dup: 1, QoS1: 0, QoS2: 1, Retain: 1},
                },
                {
-                       "CONNACK",
-                       args{[]byte{0x20, 0x00}},
-                       packet.FixedHeader{2},
+                       "CONNACK Dup:0 QoS:10 Retain:1",
+                       args{[]byte{0x24, 0x00}}, // 0002 0 10 0
+                       packet.FixedHeader{PacketType: 2, Dup: 0, QoS1: 1, QoS2: 0, Retain: 0},
                },
        }
        for _, tt := range tests {

fixed_header.go

 type FixedHeader struct {
        PacketType byte
+       Dup        byte
+       QoS1       byte
+       QoS2       byte
+       Retain     byte
 }
 
 func ToFixedHeader(bs []byte) FixedHeader {
        b := bs[0]
        packetType := b >> 4
-       return FixedHeader{packetType}
+       dup := refbit(bs[0], 3)
+       qos1 := refbit(bs[0], 2)
+       qos2 := refbit(bs[0], 1)
+       retain := refbit(bs[0], 0)
+       return FixedHeader{
+               PacketType: packetType,
+               Dup:        dup,
+               QoS1:       qos1,
+               QoS2:       qos2,
+               Retain:     retain,
+       }
+}
+
+func refbit(b byte, n uint) byte {
+       return (b >> n) & 1
 }

Remining Length

次は、固定ヘッダーの2バイト目が表しているRemining Length。Remining Lengthは、固定ヘッダーに続く「可変ヘッダー」「ペイロード」のサイズが合計で何バイトなのかを示す。

ドキュメントにencodeとdecodeのアルゴリズムが書いてある。

ドキュメントを参考にテストコードを修正。

fixed_header_test.go

                want packet.FixedHeader
        }{
                {
-                       "Reserved Dup:0 QoS:00 Retain:0",
-                       args{[]byte{0x00, 0x00}}, // 0000 0 00 0
-                       packet.FixedHeader{PacketType: 0, Dup: 0, QoS1: 0, QoS2: 0, Retain: 0},
+                       "Reserved Dup:0 QoS:00 Retain:0 RemainingLength:0",
+                       args{[]byte{
+                               0x00, // 0000 0 00 0
+                               0x00, // 0
+                       }},
+                       packet.FixedHeader{PacketType: 0, Dup: 0, QoS1: 0, QoS2: 0, Retain: 0, RemainingLength: 0},
                },
                {
-                       "CONNECT Dup:1 QoS:01 Retain:1",
-                       args{[]byte{0x1B, 0x00}}, // 0001 1 01 1
-                       packet.FixedHeader{PacketType: 1, Dup: 1, QoS1: 0, QoS2: 1, Retain: 1},
+                       "CONNECT Dup:1 QoS:01 Retain:1 RemainingLength:127",
+                       args{[]byte{
+                               0x1B, // 0001 1 01 1
+                               0x7F, // 127
+                       }},
+                       packet.FixedHeader{PacketType: 1, Dup: 1, QoS1: 0, QoS2: 1, Retain: 1, RemainingLength: 127},
                },
                {
-                       "CONNACK Dup:0 QoS:10 Retain:1",
-                       args{[]byte{0x24, 0x00}}, // 0002 0 10 0
-                       packet.FixedHeader{PacketType: 2, Dup: 0, QoS1: 1, QoS2: 0, Retain: 0},
+                       "CONNACK Dup:0 QoS:10 Retain:1 RemainingLength:128",
+                       args{[]byte{
+                               0x24,       // 0002 0 10 0
+                               0x80, 0x01, //128
+                       }},
+                       packet.FixedHeader{PacketType: 2, Dup: 0, QoS1: 1, QoS2: 0, Retain: 0, RemainingLength: 128},
                },
        }
        for _, tt := range tests {

コンパイルが通るように修正。

fixed_header.go

 package packet
 
 type FixedHeader struct {
-       PacketType byte
-       Dup        byte
-       QoS1       byte
-       QoS2       byte
-       Retain     byte
+       PacketType      byte
+       Dup             byte
+       QoS1            byte
+       QoS2            byte
+       Retain          byte
+       RemainingLength uint
 }
 
 func ToFixedHeader(bs []byte) FixedHeader {

テスト実行。

$ go test ./packet/fixed_header_test.go 
--- FAIL: TestToFixedHeader (0.00s)
    --- FAIL: TestToFixedHeader/CONNECT_Dup:1_QoS:01_Retain:1_RemainingLength:127 (0.00s)
        fixed_header_test.go:47: ToFixedHeader() = {1 1 0 1 1 0}, want {1 1 0 1 1 127}
    --- FAIL: TestToFixedHeader/CONNACK_Dup:0_QoS:10_Retain:1_RemainingLength:128 (0.00s)
        fixed_header_test.go:47: ToFixedHeader() = {2 0 1 0 0 0}, want {2 0 1 0 0 128}
FAIL
FAIL    command-line-arguments  0.019s

まだ実装してないので失敗。ドキュメントを参考にdecodeする処理を追加。

fixed_header.go

        qos1 := refbit(bs[0], 2)
        qos2 := refbit(bs[0], 1)
        retain := refbit(bs[0], 0)
+       remainingLength := decodeRemainingLength(bs[1:])
        return FixedHeader{
-               PacketType: packetType,
-               Dup:        dup,
-               QoS1:       qos1,
-               QoS2:       qos2,
-               Retain:     retain,
+               PacketType:      packetType,
+               Dup:             dup,
+               QoS1:            qos1,
+               QoS2:            qos2,
+               Retain:          retain,
+               RemainingLength: remainingLength,
        }
 }
 
 func refbit(b byte, n uint) byte {
        return (b >> n) & 1
 }
+
+func decodeRemainingLength(bs []byte) uint {
+       multiplier := uint(1)
+       var value uint
+       i := uint(0)
+       for ; i < 8; i++ {
+               b := bs[i]
+               digit := b
+               value = value + uint(digit&127)*multiplier
+               multiplier = multiplier * 128
+               if (digit & 128) == 0 {
+                       break
+               }
+       }
+       return value
+}

テスト実行。

$ go test ./packet/fixed_header_test.go 
ok      command-line-arguments  2.739s

OKOK。

テストケースの name を書くのが面倒なのでリファクタリング

fmt.Sprintf を使って、 want で指定してるstructをテストケースの name の代わりに使う。

diff --git a/study/packet/fixed_header_test.go b/study/packet/fixed_header_test.go
index 2670823..ed086a2 100644
--- a/study/packet/fixed_header_test.go
+++ b/study/packet/fixed_header_test.go
@@ -1,6 +1,7 @@
 package packet_test
 
 import (
+       "fmt"
        "reflect"
        "testing"
 
@@ -12,12 +13,10 @@ func TestToFixedHeader(t *testing.T) {
                bs []byte
        }
        tests := []struct {
-               name string
                args args
                want packet.FixedHeader
        }{
                {
-                       "Reserved Dup:0 QoS:00 Retain:0 RemainingLength:0",
                        args{[]byte{
                                0x00, // 0000 0 00 0
                                0x00, // 0
@@ -25,7 +24,6 @@ func TestToFixedHeader(t *testing.T) {
                        packet.FixedHeader{PacketType: 0, Dup: 0, QoS1: 0, QoS2: 0, Retain: 0, RemainingLength: 0},
                },
                {
-                       "CONNECT Dup:1 QoS:01 Retain:1 RemainingLength:127",
                        args{[]byte{
                                0x1B, // 0001 1 01 1
                                0x7F, // 127
@@ -33,7 +31,6 @@ func TestToFixedHeader(t *testing.T) {
                        packet.FixedHeader{PacketType: 1, Dup: 1, QoS1: 0, QoS2: 1, Retain: 1, RemainingLength: 127},
                },
                {
-                       "CONNACK Dup:0 QoS:10 Retain:1 RemainingLength:128",
                        args{[]byte{
                                0x24,       // 0002 0 10 0
                                0x80, 0x01, //128
@@ -42,7 +39,7 @@ func TestToFixedHeader(t *testing.T) {
                },
        }
        for _, tt := range tests {
-               t.Run(tt.name, func(t *testing.T) {
+               t.Run(fmt.Sprintf("%#v", tt.args.bs), func(t *testing.T) {
                        if got := packet.ToFixedHeader(tt.args.bs); !reflect.DeepEqual(got, tt.want) {
                                t.Errorf("ToFixedHeader() = %v, want %v", got, tt.want)
                        }

以下のようにテスト名に []byte{0x23,_0x80,_0x1} というような感じで出力される。

$ go test -v ./packet/fixed_header_test.go
=== RUN   TestToFixedHeader
=== RUN   TestToFixedHeader/[]byte{0x0,_0x0}
=== RUN   TestToFixedHeader/[]byte{0x1b,_0x7f}
=== RUN   TestToFixedHeader/[]byte{0x24,_0x80,_0x1}
--- PASS: TestToFixedHeader (0.00s)
    --- PASS: TestToFixedHeader/[]byte{0x0,_0x0} (0.00s)
    --- PASS: TestToFixedHeader/[]byte{0x1b,_0x7f} (0.00s)
    --- PASS: TestToFixedHeader/[]byte{0x24,_0x80,_0x1} (0.00s)
PASS
ok      command-line-arguments  1.299s

go vet

fmt.Sprintf はすごく便利なのだけど、例えば fmt.Sprinf("x is %v") というように第2引数を指定し忘れていたとしてもコンパイルエラーにならず、ミスに気がつきにくい。

go vet というコマンドを使うと、よくあるミスを指摘してくれる。

さっきのテストコードでわざと間違えてみる。

                },
        }
        for _, tt := range tests {
-               t.Run(fmt.Sprintf("%#v", tt.args.bs), func(t *testing.T) {
+               t.Run(fmt.Sprintf("%#v"), func(t *testing.T) {
                        if got := packet.ToFixedHeader(tt.args.bs); !reflect.DeepEqual(got, tt.want) {
                                t.Errorf("ToFixedHeader() = %v, want %v", got, tt.want)
                        }
$  go vet ./packet/
# github.com/bati11/oreno-mqtt/packet_test
packet/fixed_header_test.go:42: Sprintf format %#v reads arg #1, but call has 0 args

これは助かる!他にどんなチェックをしてくれるのかは以下のページを参照。

実はGo1.10からは go test 実行時に、vetの項目のうちのいくつかをチェックしてくれるようになったらしい。

試しに fmt.Sprintf の第2引数を渡してない状態で go test してみる。

$ go test ./packet/
# github.com/bati11/oreno-mqtt/packet_test
packet/fixed_header_test.go:42: Sprintf format %#v reads arg #1, but call has 0 args
FAIL    github.com/bati11/oreno-mqtt/study/packet [build failed]

良い。

gofmt

お次はフォーマッター。

gofmt コマンドとは別に go fmt というのもある。何か歴史的な経緯があるのだろうか。ちなみに gofmt -l -wgo fmt の結果が同じになった。

$ gofmt --help
...
  -l    list files whose formatting differs from gofmt's
...
  -w    write result to (source) file instead of stdout

gofmt-d オプションで実際にファイルをフォーマットせずに差分の出力だけすることができるのでCIでも使い勝手良さそう。

go doc

お次はドキュメンテーション。2通りある。

  • godoc コマンド
  • go doc というようにgoコマンドにdocを指定する方法の

まずは godoc から。以下のように実行する。

$ godoc -http=:6060

ブラウザで http://localhost:6060 にアクセス。するといつものGoの画面が。

いつものと違って「Packages」自分が作ったパッケージが紛れてます。自分が作ったパッケージの他に、ローカルPCのGOPATHにあるパッケージも載ってます。

ドキュメントの書き方は以下の記事が参考になりそう。

起動時に $ godoc -http=:6060 -analysis=pointer -analysis=type というように analysis オプションをつけてるとコードの解析もしてくれる。ただし、起動に時間がかかる...。

もう一方の go doc (goコマンド+docオプション)ですが、こちらはコマンドラインでGo Docが確認できる。

結構色々あるみたい。こちらの記事が参考になる。

おしまい

FixedHeaderReminingLength フィールドを追加しました。しかし、 ToFixedHeader に渡す []byte のチェックをしてないので1バイトの配列やnilを渡すとpanicしてしまいます。次回はここのエラーハンドリングから考えることにします。

今回の学び