Diary

Diary

日々学んだことをアウトプットする場として初めてみました

go の標準パッケージ json がどのようにタグを解釈するかを理解し、独自のタグを設定してみる

json パッケージが最終的に Tag を解釈しているところまでに、以下のような関数呼び出しが発生します。

Marshal > marshal > reflectValue > valueEncoder >
typeEncoder > newTypeEncoder > newStructEncoder > cachedTypeFields > typeFields

StructTag

json パッケージにおいて Tag の管理は reflect#StructTag が行なっています。

json パッケージで tag を解釈するまでの流れを、超完結にして実装してみました!

type Cat struct {
    Name string `cat:"name,required"`
    Age  int    `cat:"age"`
}

func main() {
    c := Cat{
        Name: "Tom",
        Age:  3,
    }

    parse(c)
}

func parse(v any) {
    rv := reflect.ValueOf(v)
    fmt.Printf("rv.Type(): %v\n", rv.Type())

    t := rv.Type()

    switch t.Kind() {
    case reflect.Struct:
        fmt.Println("v is a struct")
        parseType(t)
    }
}

func parseType(t reflect.Type) {
    for i := 0; i < t.NumField(); i++ {
        fmt.Printf("field %d: %v\n", i, t.Field(i))

        sf := t.Field(i)
        // 埋め込みフィールドかどうか。
        if sf.Anonymous {
            t := sf.Type
            if t.Kind() == reflect.Pointer {
                t = t.Elem()
            }
            if !sf.IsExported() && t.Kind() != reflect.Struct {
                continue
            }
        } else if !sf.IsExported() {
            // 非公開フィールドはスキップ。
            continue
        }
        tag := sf.Tag.Get("cat")
        if tag == "-" {
            continue
        }
        tagName, opts := parseTag(tag)
        fmt.Printf("tagName: %v\n", tagName)
        fmt.Printf("opts: %v\n", opts)
    }
}

type tagOptions string

// 1つ目のタグを必須、それ以降をオプションとして扱う。
// 例) a,b,c => 'a' + 'b,c'
func parseTag(tag string) (string, tagOptions) {
    tag, opt, _ := strings.Cut(tag, ",")
    return tag, tagOptions(opt)
}

// options に関しては、個別で扱うことはせずに『含む』or『含まない』のみを扱う。
func (o tagOptions) Contains(optionName string) bool {
    if len(o) == 0 {
        return false
    }
    s := string(o)
    for s != "" {
        var name string
        name, s, _ = strings.Cut(s, ",")
        if name == optionName {
            return true
        }
    }
    return false
}

実際には同一の型で何回もリフレクション等をしなくてもいいようにキャッシュしたりしています。

tag の使いどき

tag を有意義に使ってる例としては、標準の json パッケージや ORM である gorm があると思います。

共通して当てはまることといえば次の性質でしょうか。

  • クライアントによって独自の型を受け付ける必要がある
  • Go の概念(構造体)と外の概念(json, db schema など)で別の名前で定義したい
  • バリデーションなどをフィールドによって使い分けたい

自分でライブラリやフレームワークを開発する際に、ユーザーに対して柔軟な設定やカスタマイズを提供したい場合に特に有効そうです。