Go のリフレクションの練習のために JSON パーサーを作った
筆者は Go を書き始めて間もないですが、HTTP サーバーなどの実装に Go を選択することがあります。とあるライブラリを使っているときにオブジェクト構造を変換するユーティリティが欲しくなったのですが、任意のデータを Go の構造にパースする方法や Struct Tag の参照方法などを知らなかったためリフレクションに入門することにしました。今回は簡単な練習として JSON デコーダ・エンコーダを作成することにしました。
Go でリフレクションを使うためには reflect
パッケージを使用します。
成果物
成果物は以下の GitHub レポジトリにあります。
最終的に次のようにオブジェクトのパース・文字列化ができるようになりました。
type Address struct {
Street string
City string
State string
Zip string `json:"zip_code"`
}
type User struct {
ID int `json:"id"`
Name string
Email string
Addresses []Address
}
func main() {
input := `{
"id": 1,
"name": "John Doe",
"email": "johndoe@example.com",
"addresses": [
{
"street": "123 Main St",
"city": "Springfield",
"state": "IL",
"zip_code": "62701"
},
{
"street": "456 Elm St",
"city": "Springfield",
"state": "IL",
"zip_code": "62701"
}
]
}`
// デコード
var user User
err := json.Decode(input, &user)
if err != nil {
panic(err)
}
// エンコード
encoded, err := json.Encode(user)
if err != nil {
panic(err)
}
println(encoded)
}
実装方針
デコーダ
デコーダはトークナイザと (トークン列に対する) デコーダの 2 つの部品により実装しました。トークナイザ (Tokenizer
) は []byte
を持ち、Next()
メソッドにより入力を消費してトークン (終端記号) を返します。デコーダは Tokenizer
とパース先変数へのポインタを受け取る関数として実装し、Tokenizer.Next
の返すトークンを消費して対象変数にパースします。
すなわち今回の実装では AST を経由せずトークン列を直接オブジェクトに展開しています。
エンコーダ
エンコーダはオブジェクトを直接文字列にエンコードするように実装しました。トークン列に変換するほうがより一般性のある実装になりますが練習のため簡単に実装できるようにしました。
デコーダの実装
トークナイザの実装
トークナイザの実装は本題から逸れるため折りたたんでおきます。
トークナイザの実装
トークンは Kind
に型を、Value
に実際の値を持つ構造体として定義しました。
type TokenKind string
const (
EOFToken = TokenKind("EOF")
BeginArrayToken = TokenKind("BeginArray")
EndArrayToken = TokenKind("EndArray")
BeginObjectToken = TokenKind("BeginObject")
EndObjectToken = TokenKind("EndObject")
NameSeparatorToken = TokenKind("NameSeparator")
ValueSeparatorToken = TokenKind("ValueSeparator")
NullToken = TokenKind("Null")
BooleanToken = TokenKind("Boolean")
NumberToken = TokenKind("Number")
StringToken = TokenKind("String")
)
type Token struct {
Kind TokenKind
Value interface{}
}
トークナイザは単純に読んでいる文字列の先頭から順にトークンとして解釈しています。
type Tokenizer struct {
input []byte
index int
}
func (t *Tokenizer) Next() (*Token, error) {
t.skipWhitespace()
if t.index >= len(t.input) {
return &Token{Kind: EOFToken}, nil
}
switch t.input[t.index] {
case '[':
t.index += 1
return &Token{Kind: BeginArrayToken}, nil
case ']':
t.index += 1
return &Token{Kind: EndArrayToken}, nil
case '{':
t.index += 1
return &Token{Kind: BeginObjectToken}, nil
case '}':
t.index += 1
return &Token{Kind: EndObjectToken}, nil
case ':':
t.index += 1
return &Token{Kind: NameSeparatorToken}, nil
case ',':
t.index += 1
return &Token{Kind: ValueSeparatorToken}, nil
}
if t.matchesHeadingIdentifier([]byte("null")) {
t.index += 4
return &Token{Kind: NullToken}, nil
}
if t.matchesHeadingIdentifier([]byte("true")) {
t.index += 4
return &Token{Kind: BooleanToken, Value: true}, nil
}
if t.matchesHeadingIdentifier([]byte("false")) {
t.index += 5
return &Token{Kind: BooleanToken, Value: false}, nil
}
stringToken, err := t.takeString()
if err != nil {
return nil, err
}
if stringToken != nil {
return stringToken, nil
}
numberToken := t.takeNumber()
if numberToken != nil {
return numberToken, nil
}
return nil, ErrUnexpectedCharacter
}
デコーダの実装
デコーダは Tokenizer
とパース先のオブジェクトへのポインタを受け取る関数として実装します。オブジェクトの型は reflect.TypeOf()
により reflect.Type
として取得できます。これがポインタや Slice などの場合は reflect.Type.Elem()
により参照先・アイテムの型を取得できます。今回の場合パース先の型は reflect.TypeOf(out).Elem()
のようにして取得します。
func decode(t *tokenizer.Tokenizer, out any) error {
// out が *T であるとする
// out がポインタであることを確認
outTypePtr := reflect.TypeOf(out)
if outTypePtr.Kind() != reflect.Ptr {
panic("out must be a pointer")
}
// outType は T
outType := outTypePtr.Elem()
// ...
}
outType
によって分岐して型ごとにパースの処理を書いていきます。たとえば真偽値をパースする場合は単純にトークンから bool
として値を取り出し out
に格納します。文字列や数値の場合も同様です。
switch outType.Kind() {
case reflect.Bool:
return decodeBoolean(t, out.(*bool))
}
func decodeBoolean(t *tokenizer.Tokenizer, out *bool) error {
token, err := t.Next()
if err != nil {
return err
}
if token.Kind == tokenizer.BooleanToken {
*out = token.BoolValue()
} else {
return ErrUnexpectedTokenType
}
return nil
}
配列・オブジェクトのパースは多少複雑になります。reflect.Net()
を使ってアイテムの型の変数を作成し、その変数に対して再帰的に decode()
を呼び出します。
配列の場合は reflect.Append()
を使ってパースしたアイテムをスライスに追加し、reflect.Set()
を使って out
に格納し直します。
func decodeArray(t *tokenizer.Tokenizer, out any) error {
// out が *[]T とする
// out がポインタであることを確認
outTypePtr := reflect.TypeOf(out)
if outTypePtr.Kind() != reflect.Ptr {
panic("out must be a pointer")
}
// outType, outValue は []T
outType := outTypePtr.Elem()
if outType.Kind() != reflect.Slice {
panic("out must be a slice")
}
outValue := reflect.ValueOf(out).Elem()
// itemType は T
itemType := outType.Elem()
// '[' があることを確認
if _, err := expectToken(t, tokenizer.BeginArrayToken); err != nil {
return err
}
for {
// item は *T
// 再帰的に decode を呼び出して item にパースする
item := reflect.New(itemType).Interface()
if err := decode(t, item); err != nil {
return err
}
// *out = append(*out, *item) のようなことをやっている
outValue.Set(reflect.Append(outValue, reflect.ValueOf(item).Elem()))
// 次のトークンが ']' ならループを抜ける
// ']' でも ',' でもなかったらエラー
token, err := t.Next()
if err != nil {
return err
}
if token.Kind == tokenizer.EndArrayToken {
break
} else if token.Kind != tokenizer.ValueSeparatorToken {
return ErrUnexpectedTokenType
}
}
return nil
}
オブジェクトをパースする場合はさらにフィールドの名前を扱う必要があります。reflect.Type.NumField()
でフィールドの数を知ることができ、実際のフィールドは reflect.Type.Field(i)
で reflect.StructureField
としてアクセスすることができます。Structure Tag は reflect.StructureField.Tag.Get()
により取得できます。
今回の実装では簡単のため JSON に存在するフィールドが Go の構造体に存在しない場合にはエラーとなるようにしています。逆に Go の構造体に存在するフィールドが JSON に存在しない場合は値は設定されません。
func decodeObject(t *tokenizer.Tokenizer, out any) error {
// out がポインタであることを確認
outTypePtr := reflect.TypeOf(out)
if outTypePtr.Kind() != reflect.Ptr {
panic("out must be a pointer")
}
outValue := reflect.ValueOf(out).Elem()
// out が構造体のポインタであることを確認
outType := outTypePtr.Elem()
if outType.Kind() != reflect.Struct {
panic("out must be a struct")
}
// 構造体タグを収集してタグに指定された名前→フィールド名の map を作る
tagFieldNames := map[string]string{}
numFields := outType.NumField()
for i := 0; i < numFields; i++ {
field := outType.Field(i)
tag := field.Tag.Get("json")
if tag != "" {
tagFieldNames[tag] = field.Name
}
}
// '{' があることを確認
if _, err := expectToken(t, tokenizer.BeginObjectToken); err != nil {
return err
}
for {
// フィールド名 (文字列トークン) を期待
nameToken, err := expectToken(t, tokenizer.StringToken)
if err != nil {
return err
}
// フィールド名が Structure Tag に指定されていたらその値を使い、
// 指定されていない場合フィールド名を Upper Camel Case にして使う
name := nameToken.StringValue()
fieldName := tagFieldNames[name]
if fieldName == "" {
fieldName = SnakeCaseToUpperCamelCase(name)
}
// ':' があることを確認
if _, err := expectToken(t, tokenizer.NameSeparatorToken); err != nil {
return err
}
// fieldName の名前のフィールドを探し、その値に対して decode を再帰的に呼び出す
field := outValue.FieldByName(fieldName)
if !field.IsValid() {
return ErrUnexpectedTargetType
}
if err = decode(t, field.Addr().Interface()); err != nil {
return err
}
// 次のトークンが '}' ならループを抜ける
// '}' でも ',' でもなかったらエラー
token, err := t.Next()
if err != nil {
return err
}
if token.Kind == tokenizer.EndObjectToken {
break
} else if token.Kind != tokenizer.ValueSeparatorToken {
return ErrUnexpectedTokenType
}
}
return nil
}
あとは []byte
を入力とするメソッドを作成して完成です。
func Decode(input []byte, out any) error {
t := tokenizer.NewTokenizer(input)
return decode(t, out)
}
エンコーダの作成
エンコーダはデコーダに比べてかなりシンプルです。reflect.TypeOf
で取得した方に応じて分岐し、内容をそのまま strings.Builder
に突っ込んでいます。配列・構造体のエンコードでは各アイテムに対して再帰的に encode()
を呼び出しています。構造体のエンコードでは Decoder
の際と同様にリフレクションを用いてフィールド上をイテレートすることで文字列化しています。
func encode(v any, sb *strings.Builder) error {
if v == nil {
sb.WriteString("null")
return nil
}
tv := reflect.TypeOf(v)
rv := reflect.ValueOf(v)
switch tv.Kind() {
case reflect.Bool:
sb.WriteString(strconv.FormatBool(rv.Bool()))
// 文字列, 数値に対しても同様
case reflect.Array, reflect.Slice:
sb.WriteByte('[')
for i := 0; i < rv.Len(); i++ {
if i > 0 {
sb.WriteByte(',')
}
if err := encode(rv.Index(i).Interface(), sb); err != nil {
return err
}
}
sb.WriteByte(']')
case reflect.Struct:
numFields := rv.NumField()
sb.WriteByte('{')
for i := 0; i < numFields; i++ {
if i > 0 {
sb.WriteByte(',')
}
field := rv.Field(i)
fieldName := rv.Type().Field(i).Tag.Get("json")
if fieldName == "" {
fieldName = CamelCaseToSnakeCase(rv.Type().Field(i).Name)
}
sb.WriteByte('"')
sb.Write(EscapeString([]byte(fieldName)))
sb.WriteByte('"')
sb.WriteByte(':')
if err := encode(field.Interface(), sb); err != nil {
return err
}
}
sb.WriteByte('}')
}
return nil
}
参考記事
(2025/3/25 時点)
Discussion