LoginSignup
0
0

More than 3 years have passed since last update.

reflectを使ったあれこれ

Last updated at Posted at 2020-11-09

目的

reflectへの理解力が欠如しているため、備忘録として残す。

reflectを使って構造体のフィールドに値を入れる

ゴール

フィールドの要素が不明な構造体に対して、特定の値を入れたい。

go playground

こちら

主な処理

該当コード

再帰関数を利用した。

func recursive(in reflect.Value, v int) {
    rt := in.Kind()
    if rt == reflect.Ptr {
        vPtr := in
        if in.IsNil() {
            vPtr = reflect.New(in.Type().Elem())
        }
        recursive(vPtr.Elem(), v)
        if in.CanSet() {
            in.Set(vPtr)
        }
        return
    }
    if rt == reflect.Struct {
        tValue := in.Type()
        for i := 0; i < in.NumField(); i++ {
            sf := tValue.Field(i)
            if sf.PkgPath != "" && !sf.Anonymous {
                continue
            }
            recursive(in.Field(i), v)
        }
        return
    }
    if rt == reflect.String {
        strV := strconv.Itoa(v)
        in.Set(reflect.ValueOf(strV))
        return
    }
    if rt == reflect.Int {
        in.Set(reflect.ValueOf(v))
        return
    }
}

処理概要

reflect.Valueの種別がポインタの場合

こちらの処理で、フィールドがポインタかどうか判別し、nilの場合、reflct.Valueを生成。
nilでなければ、再帰関数の第一引数をそのまま利用。
ポインタの参照先を再帰関数の第一引数に渡して、再度呼び出す。
呼び出し後、CanSettrueであれば、第一引数に、ValueSet

    if rt == reflect.Ptr {
        vPtr := in
        if in.IsNil() {
            vPtr = reflect.New(in.Type().Elem())
        }
        recursive(vPtr.Elem(), v)
        if in.CanSet() {
            in.Set(vPtr)
        }
        return
    }
reflect.Valueの種別が構造体の場合

構造体のフィールドを列挙し、それぞれのフィールドに対し、再帰関数の第一引数に渡して、再度呼び出す。
ただし、PkgPath(パッケージ)のケースとAnonymous(埋め込み型)のケースは除外(除外しないと永久loopになるケースがある)。

    if rt == reflect.Struct {
        tValue := in.Type()
        for i := 0; i < in.NumField(); i++ {
            sf := tValue.Field(i)
            if sf.PkgPath != "" && !sf.Anonymous {
                continue
            }
            recursive(in.Field(i), v)
        }
        return
    }
reflect.Valueの種別がstringintの場合

stringの場合、変換し、intの場合、そのまま、reflect.ValueOfSet
stringint以外の場合もあるので、もう少し単純化できるか調査中・・・)

    if rt == reflect.String {
        strV := strconv.Itoa(v)
        in.Set(reflect.ValueOf(strV))
        return
    }
    if rt == reflect.Int {
        in.Set(reflect.ValueOf(v))
        return
    }

reflectを使って、mapから構造体のフィールドに値を入れ、不足データがあれば、その内容を出力

ゴール

map[string]stringから任意の構造体に変換し、構造体のフィールドの要素が不足であれば、その詳細を知りたい。
構造体のフィールドにはタグを指定し、フィールドがポインタ側であれば、オプショナル扱い。
また、bindParametersの第一引数をinterface型にすることで、任意の構造体に対応。

go playground

こちら

主な処理

該当コード

こちらの再帰関数をベースに改修。

func recursive(in reflect.Value, tag string, required bool, v map[string]string, res *map[string]interface{}) bool {
    rk := in.Kind()
    if rk == reflect.Ptr {
        vPtr := in
        if in.IsNil() {
            vPtr = reflect.New(in.Type().Elem())
        }
        isSet := recursive(vPtr.Elem(), tag, false, v, res)
        if in.CanSet() && isSet {
            in.Set(vPtr)
        }
        return false
    }
    if rk == reflect.Struct {
        tValue := in.Type()
        isSet := false
        for i := 0; i < in.NumField(); i++ {
            sf := tValue.Field(i)
            if sf.PkgPath != "" && !sf.Anonymous {
                continue
            }
            tagName := ""
            required = false
            if uriTag := sf.Tag.Get(key); uriTag != "" {
                tagName = uriTag
                required = true
            }
            r := recursive(in.Field(i), tagName, required, v, res)
            if r {
                isSet = true
            }
        }
        return isSet
    }

    ptrValue, errString := getValue(rk, required, tag, v)
    if errString != "" {
        e := *res
        e[tag] = errString
        res = &e
    }
    if ptrValue != nil {
        value := *ptrValue
        in.Set(value)
        return true
    }
    return false
}

func getValue(kind reflect.Kind, required bool, tag string, m map[string]string) (*reflect.Value, string) {
    var value string
    var ok bool
    if value, ok = m[tag]; !ok && required {
        return nil, "parameter is not specified"
    }

    if value == "" && !required {
        return nil, ""
    }
    if kind == reflect.String {
        r := reflect.ValueOf(value)
        return &r, ""
    }
    if kind == reflect.Int {
        i, err := strconv.Atoi(value)
        if err != nil {
            return nil, "not numeric value"
        }
        r := reflect.ValueOf(i)
        return &r, ""
    }

    return nil, ""
}

処理概要

構造体のタグ解析

reflect.StructField.sf.Tag.Getで構造体のタグ名が取得可能。

    if rk == reflect.Struct {
        tValue := in.Type()
        isSet := false
        for i := 0; i < in.NumField(); i++ {
            sf := tValue.Field(i)
            if sf.PkgPath != "" && !sf.Anonymous {
                continue
            }
            tagName := ""
            required = false
            if uriTag := sf.Tag.Get(key); uriTag != "" {
                tagName = uriTag
                required = true
            }
            r := recursive(in.Field(i), tagName, required, v, res)
            if r {
                isSet = true
            }
        }
        return isSet
    }
タグに相当する値をmapから抽出

抽出できない場合、parameter is not specifiedstringに変換できない場合、not numeric valuereturn
(他の型の対応方法も含め、もう少し、賢い書き方を調査中・・・)

    if value, ok = m[tag]; !ok && required {
        return nil, "parameter is not specified"
    }

    if value == "" && !required {
        return nil, ""
    }
    if kind == reflect.String {
        r := reflect.ValueOf(value)
        return &r, ""
    }
    if kind == reflect.Int {
        i, err := strconv.Atoi(value)
        if err != nil {
            return nil, "not numeric value"
        }
        r := reflect.ValueOf(i)
        return &r, ""
    }

その他

タグに-omitemptyを指定できるように実現するケースも考慮したい。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0