pco2699’s blog

学んだものについて、メモしておく場所

GoでシンプルなJSONパーサーを作る

Goの勉強、復習を兼ねてシンプルなJSONパーサーを作ってみました。

元となったJSONパーサーは以下の記事です。このPythonで書かれたJSONパーサーをGoで書き直してみました。

notes.eatonphil.com

つくったもの

github.com

こんな感じで、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")
        }
    }
}

大まかな作り

こんな感じです。 f:id:pco2699:20210227224642p:plain

文字列をトークン列に変換する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パーサーだと、intstringなどどの型が事前に来るのかわからないため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パーサーを作ってみる
  • 何かの実装を参考にせず仕様書のみから実装してみる