Goの勉強、復習を兼ねてシンプルなJSONパーサーを作ってみました。
元となったJSONパーサーは以下の記事です。このPythonで書かれたJSONパーサーをGoで書き直してみました。
つくったもの
こんな感じで、JSONを文字列からinterface{}型にパースします。
Goの標準パッケージのJSONパーサーは[]byteをインプットにしますが、自作のものはStringをインプットにします。 あくまで、勉強用なので、速度やConcurrencyなどは全く考慮していません。
package main import ( "fmt" "os" "github.com/pco2699/simple-json-parser/parser" ) func main() { // 引数で与えられた文字列をパースする val, err := parser.FromString("[\"hoge\", 10]") if err != nil { fmt.Printf("Error: %v", err) os.Exit(1) } switch v := val.(type) { // マップ形式ならマップの中身を表示する case map[string]interface{}: printMap(v) // interface{}型のArrayならそれをすべて表示する case []interface{}: printArray(v) } } // Arrayの表示 func printArray(arr []interface{}) { for _, v := range arr { fmt.Printf("%v\n", v) } } // Mapの表示 func printMap(m map[string]interface{}) { for k, v := range m { switch vv := v.(type) { case string: fmt.Println(k, "is string", vv) case int32: fmt.Println(k, "is int32", vv) case []interface{}: fmt.Println(k, "is an array:") for i, u := range vv { fmt.Println(i, u) } default: fmt.Println(k, "is of a type I don't know how to handle") } } }
大まかな作り
こんな感じです。
文字列をトークン列に変換するlexer
インプットのStringをrune[]にして、以下のどれかに変換を試みます。
- JSONのConstants({, }, [, ]など)
- 文字列
- 数字(float, int)
- Boolean
- null
愚直に、それぞれの型に対応する関数(lexStringなど)を作って、変換できたらOK、変換できなかったら、数字での変換を試みる、という処理方式です。
// stringへの変換を試みる jsonStr, str, err = lexString(str) if err != nil { return nil, err } if len(jsonStr) > 0 { tokens = append(tokens, jsonStr) continue } // 数字への変換を試みる var jsonNum interface{} jsonNum, str, err = lexNumber(str) if err != nil { return nil, err } if jsonNum != nil { tokens = append(tokens, jsonNum) continue }
トークン列をGoのオブジェクトに変換する Parser
作ったトークン列をインプットにGoのオブジェクトを生成します。
オブジェクトの構造としてArrayかObjectをJSONのConstantsを元に判定します。
// Arrayの場合 ["hoge", 10, true] // Objectの場合 {"hoge": 10, "hoge2": "fuga"}
Array, Objectの場合も、中身を再帰的にParseして、最終的にmap[string]interface{}
か[]interface{}
を出力します。
つくってて困ったところ/はまったところ
interface{}地獄になる
当たり前っちゃ当たり前なのですが、汎用的なJSONパーサーだと、int
やstring
などどの型が事前に来るのかわからないためinterface{}を多用することになります。テストコードとかもinterface{}ばっかりなので地獄でした。
{ name: "array case", args: args{ str: "[\"hoge\" , 200 ]", }, want: []interface{}{'[', "hoge", ',', int32(200), ']'}, // 期待値が[]inteface{}なので地獄 wantErr: false, },
テストコードで整数リテラル(int)とint32を比較してコケる
これは単純に自分のGoの知識不足だったのですが、整数リテラル(int)とint32を比較しようとしてテストがこけて1週間ぐらいつぶしました。 intとint32で比較しようとするとテストではNGになるので気を付けましょう。
// これはこける want -> 整数リテラル = int だけど実際は int32 つまり違う型 { name: "integer case", args: args{ str: []rune("1234"), }, want: 1234, want1: []rune(""), wantErr: false, }, // これはok { name: "integer case", args: args{ str: []rune("1234"), }, want: int32(1234), want1: []rune(""), wantErr: false, },
なお、普通に==での比較だとOKです。 play.golang.org
つくった感想
パーサーを全く作ったことがなかったので、作った後だとコードの見方がグッと変わりました。
例えば
- domパーサーとsaxパーサーの違いがわかるようになる(中間表現の違い)
- 他のJSONパーサーを見て何がすごいのかわかるようになる(最近 GitHubにトレンド入りしてた ppelleti/json65 とか
あと、Goの標準パッケージのJSONパーサーの設計が妥当であることがわかります。
- なぜ入力が
string
ではなく[]byte
なのか ->string
だと文字ごとに処理できないため結局、[]rune
や[]byte
に変換することになる - なぜ事前に変換後の値を格納する構造体を定義してポインタで渡さないといけないのか -> そうしないと
interface{}
で返すことになり毎回変換後に型アサーションが必要になってしまう
今後やってみたいこと
- Concurrencyや速度を意識したJSONパーサーを作ってみる
- 何かの実装を参考にせず仕様書のみから実装してみる