Goで[]byteをjsonパッケージで扱うときの挙動につまった

Goで[]byteをjsonパッケージで扱うときの挙動につまったので調べました。

不思議な挙動

以下をみてください。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
 "encoding/json"
 "fmt"
 "log"
)

type A struct {
 Hoge []byte `json:"hoge"`
}

var data = []byte(`{"hoge": "fuga"}`)

func main() {
 a := A{}

 if err := json.Unmarshal(data, &a); err != nil {
  log.Fatal(err)
 }

 fmt.Println(string(a.Hoge))
}

感覚では、stringはバイト列なので、

1
fuga

と出力しそうに思えるんですが、実際には謎の文字列が表示されます (playground)

1
~�

理由

jsonパッケージのコードを追っていってわかりました。

1
func (d *decodeState) literalStore(item []byte, v reflect.Value, fromQuoted bool) error

の中に、以下のような処理があります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
case reflect.Slice:
  if v.Type().Elem().Kind() != reflect.Uint8 {
   d.saveError(&UnmarshalTypeError{Value: "string", Type:v.Type(), Offset: int64(d.readIndex())})
   break
  }
  b := make([]byte, base64.StdEncoding.DecodedLen(len(s)))
  n, err := base64.StdEncoding.Decode(b, s)
  if err != nil {
   d.saveError(err)
   break
  }
  v.SetBytes(b[:n])

view on GitHub

文字列をセットするときに、セットする対象がreflect.Uint8の型のスライスならばbase64.StdEncodingでエンコードしてからセットされるらしいです。byteuint8へのエイリアスなので、最初に挙げた例ではhogeがエンコードされた~�が出力されたんだと思います。

ちなみに、ちゃんとドキュメントにも書いてありました。ドキュメントを読みましょう。ごめんなさい。

Unmarshal

Unmarshal uses the inverse of the encodings that Marshal uses, …

Marshal

Array and slice values encode as JSON arrays, except that []byte encodes as a base64-encoded string, and a nil slice encodes as the null JSON value.

対処法

stringを使う

型を[]byteではなくてstringにすると動きます。[]byte(s)でコピーなしで変換できるので良さそうと思ったんですが実はコピーが走るらしいです(!?)

以下の記事ではunsafeパッケージを使用したノーコピーのやり方が書いてありました。

[]byte と string のキャスト - tocsatoの備忘録

RawMessageを使う

jsonパッケージにRawMessageという[]byteへのエイリアスの型があって、UnmarshalJSONが実装されているので、たとえば最初の例では"付きで"fuga"がセットされます。[]byteに変換して適切にカットしてあげれば$O(1)$でお目当ての[]byteが得られるはずです(多分)。

自分でUnmarshalJSONを書く

たとえば以下のようなコードを書けます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
 "encoding/json"
 "fmt"
 "log"
)

type A struct {
 Hoge []byte `json:"hoge"`
}

func (a *A) UnmarshalJSON(data []byte) error {
 type A2 struct {
  Hoge string `json:"hoge"`
 }
 a2 := new(A2)
 if err := json.Unmarshal(data, a2); err != nil {
  return err
 }
 a.Hoge = []byte(a2.Hoge)
 return nil
}

感想

なぜbase64でエンコード/デコードするのか、理由があるのでしょうか。不思議。

ライセンス表記

以下のライセンスに基づきGoのソースコードを引用しています。

LICENSE - The Go Programming Language