コンテキスト
- Goの構造体のフィールドには"tag_name:tag_value"というようにタグをつけることができる。
- jsonのエンコードやデコードで使われるjsonタグというものがある。
- json.Marshalで構造体->jsonのエンコードする場合に、構造体のフィールドのjsonタグに紐づいた文字列がjsonにおける文字列となる。
- jsonタグに"-"という値を紐付けると、jsonには出力されないフィールドとなる。
type Struct struct {
Name string `json:"name"`
Age int
Credential string `json:"-"`
}
上の構造体は以下のようなjsonに変換されてエンコードされる。
{"name": "Tomori Yu", "Age": 21}
疑問
- エンコードに関する挙動は以上に見た通りだが、デコードの挙動はどうなっているのだろう。
試してみる
package main
import (
"bytes"
"encoding/json"
"fmt"
)
type person struct {
Name string `json:"name"`
Age int
Credential string `json:"-"`
}
func main() {
p1 := person{"yu", 21, "AKIi9854u4t8394j8gf"}
bs1, _ := json.Marshal(p1)
fmt.Printf("marshaled person: %v\n", string(bs1))
// -> marshaled person: {"name":"yu","Age":21}
var p2 person
_ = json.Unmarshal(bs, &p2)
fmt.Printf("unmarshaled person: %v\n", p2)
// -> unmarshaled person: {yu 21 }
bs2 := []byte(`{"name": "yu", "Age": 21, "Credential": "AKIi9854u4t8394j8gf"}`)
decoder := json.NewDecoder(bytes.NewBuffer(bs2))
var p3 person
_ = decoder.Decode(&p3)
fmt.Printf("decoded person: %v\n", p3)
// -> decoded person: {yu 21 }
}
- json.MarshalではAgeがアウトプットに残るが、json.Unmarshal&json.DecodeではAgeが残らない。
- 構造体->jsonの場合と同じように、json->構造体のデコードにおいても構造体のフィールドにjson:"-"というタグが付いていれば無視される。
json.Decoderに関して構造体のフィールドにjson:"-"というタグが付いていたら無視する処理を探してみた
encoding/json.Decoder
decoder := json.NewDecoder(buffer)
json.NewDecoderはDecoder構造体を返す。
type Decoder struct {
r io.Reader
buf []byte
d decodeState
scanp int // start of unread data in buf
scanned int64 // amount of data already scanned
scan scanner
err error
tokenState int
tokenStack []int
}
Decoder構造体のポインタ型に対するDecodeメソッドは引数として渡した変数にデコードした結果を返す。
Decoder.Decode
func (dec *Decoder) Decode(v interface{}) error {
if dec.err != nil {
return dec.err
}
if err := dec.tokenPrepareForDecode(); err != nil {
return err
}
if !dec.tokenValueAllowed() {
return &SyntaxError{msg: "not at beginning of value", Offset: dec.InputOffset()}
}
// Read whole value into buffer.
n, err := dec.readValue()
if err != nil {
return err
}
dec.d.init(dec.buf[dec.scanp : dec.scanp+n])
dec.scanp += n
// Don't save err from unmarshal into dec.err:
// the connection is still usable since we read a complete JSON
// object from it before the error happened.
err = dec.d.unmarshal(v)
// fixup token streaming state
dec.tokenValueEnd()
return err
}
dec.dは、Decoder構造体のフィールドであり、decodeState構造体のインスタンスである。
dec.d.initでdecodeState.dataにまだ読まれてない[]byteを初期化している。
そして、err = dec.d.unmarshal(v)に、デコードの処理がありそうだ。
decodeState
type decodeState struct {
data []byte
off int // next read offset in data
opcode int // last read result
scan scanner
errorContext struct { // provides context for type errors
Struct reflect.Type
FieldStack []string
}
savedError error
useNumber bool
disallowUnknownFields bool
}
func (d *decodeState) unmarshal(v interface{}) error {
rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Ptr || rv.IsNil() {
return &InvalidUnmarshalError{reflect.TypeOf(v)}
}
d.scan.reset()
d.scanWhile(scanSkipSpace)
// We decode rv not rv.Elem because the Unmarshaler interface
// test must be applied at the top level of the value.
err := d.value(rv)
if err != nil {
return d.addErrorContext(err)
}
return d.savedError
}
d.scanは、decodeState構造体のscanフィールドであり、scanner構造体のフィールドである。
scannerはjsonをパースする処理におけるステートマシンとして利用される。
d.scan.reset()でdecodeStateのフィールドであるscannerインスタンスが初期化される。
d.value(rv)でデコード処理が行われていそうだ。
scanner
// A scanner is a JSON scanning state machine.
// Callers call scan.reset and then pass bytes in one at a time
// by calling scan.step(&scan, c) for each byte.
// The return value, referred to as an opcode, tells the
// caller about significant parsing events like beginning
// and ending literals, objects, and arrays, so that the
// caller can follow along if it wishes.
// The return value scanEnd indicates that a single top-level
// JSON value has been completed, *before* the byte that
// just got passed in. (The indication must be delayed in order
// to recognize the end of numbers: is 123 a whole value or
// the beginning of 12345e+6?).
type scanner struct {
// The step is a func to be called to execute the next transition.
// Also tried using an integer constant and a single func
// with a switch, but using the func directly was 10% faster
// on a 64-bit Mac Mini, and it's nicer to read.
step func(*scanner, byte) int
// Reached end of top-level value.
endTop bool
// Stack of what we're in the middle of - array values, object keys, object values.
parseState []int
// Error that happened, if any.
err error
// total bytes consumed, updated by decoder.Decode (and deliberately
// not set to zero by scan.reset)
bytes int64
}
// value consumes a JSON value from d.data[d.off-1:], decoding into v, and
// reads the following byte ahead. If v is invalid, the value is discarded.
// The first byte of the value has been read already.
func (d *decodeState) value(v reflect.Value) error {
switch d.opcode {
default:
panic(phasePanicMsg)
case scanBeginArray:
if v.IsValid() {
if err := d.array(v); err != nil {
return err
}
} else {
d.skip()
}
d.scanNext()
case scanBeginObject:
if v.IsValid() {
if err := d.object(v); err != nil {
return err
}
} else {
d.skip()
}
d.scanNext()
case scanBeginLiteral:
// All bytes inside literal return scanContinue op code.
start := d.readIndex()
d.rescanLiteral()
if v.IsValid() {
if err := d.literalStore(d.data[start:d.readIndex()], v, false); err != nil {
return err
}
}
}
return nil
}
v.IsValid()はvがゼロ値でなければtrueを返します。
構造体を引数に渡した場合は、d.object(v)が呼ばれるっぽいです。
decodeState.object()を読むと、switch文で引数のunderlyingな型がStructな時の処理が書かれてあります。
case reflect.Struct:
fields = cachedTypeFields(t)
// ok
fieldsはstructFields構造体のインスタンスです。
structFields
type structFields struct {
list []field
nameIndex map[string]int
}
cachedTypeFields関数は、typeFields関数にキャッシュ機能を足した薄いラッパーとなっています。
typeFieldsは以下のようになっています。
https://github.com/golang/go/blob/master/src/encoding/json/encode.go#L1209-L1391
// typeFields returns a list of fields that JSON should recognize for the given type.
// The algorithm is breadth-first search over the set of structs to include - the top struct
// and then any reachable anonymous structs.
func typeFields(t reflect.Type) structFields {
// Anonymous fields to explore at the current level and the next.
current := []field{}
next := []field{{typ: t}}
[省略]
tag := sf.Tag.Get("json")
if tag == "-" {
continue
}
[省略]
ありました。sfは構造体のフィールドです。
forの中でcontinueされなかったフィールドはforブロックの後の処理でfields = append(fields, field)という感じでフィールドとして登録されます。
構造体のフィールドのうちjsonタグに"-"が付いているものはcontinueされているので、無視することになっているみたいです。
先ほど見たdecodeState.object()ではfields = cachedTypeFields(t)という感じで構造体の有効なフィールドだけが処理されることになります。
その後にfields変数に関して、以下の処理が書かれています。
https://github.com/golang/go/blob/master/src/encoding/json/decode.go#L696-L709
if i, ok := fields.nameIndex[string(key)]; ok {
// Found an exact name match.
f = &fields.list[i]
} else {
// Fall back to the expensive case-insensitive
// linear search.
for i := range fields.list {
ff := &fields.list[i]
if ff.equalFold(ff.nameBytes, key) {
f = ff
break
}
}
}
jsonのkeyにマッチする有効なStructのフィールドが見つかればfという変数に入れられて後の処理に続くようです。
まとめ
- json.Decodeもjson:"-"タグのついた構造体のフィールドは無視する。