カミナシ エンジニアブログ

株式会社カミナシのエンジニアが色々書くブログです

Goのreflectパッケージを使って構造体の指定フィールドをゼロ値にしてみた

目次

はじめに

こんにちは、そしてはじめまして!カミナシエンジニアのAomanです。

カミナシでは現在、サーバーサイドの開発をGo言語で行っています。 社内用管理画面のAPIを実装をしていたところ、とある処理の中で「構造体の指定したフィールドをゼロ値にして処理をしたいな🤔」という場面に遭遇しました。利用している OR マッパーが SQL クエリを構築する際、構造体のフィールドがゼロ値ではないと自動的にそれらがクエリに組み込まれるという仕様であり、その仕様を回避したかったためです。もちろん、オプションでこの仕様を回避する方法は提供されていますが、既存のモデル構造体を変更することは出来ない状況でした。

reflectパッケージ自体、利用頻度は高くないと思いますが、今回は一つの例としてどのように実装をしてみたのかを紹介したいと思います!

この記事の対象読者

  • Goをある程度触ったことのある人
  • reflectパッケージのごく初歩的な使い方を知りたい人

reflectパッケージとは?

まずはrefleftパッケージとはなんぞや?というところですね。reflect パッケージは、実行時に任意の型を持つオブジェクトを操作出来るようにするものです。リフレクションの意味を調べてみると、 Wikipedia/リフレクション(情報工学)) では次のように説明されています。

情報工学においてリフレクション(reflection) とは、プログラムの実行過程でプログラム自身の構造を読み取ったり書き換えたりする技術のことを指す。

ふむふむ、自身の構造を調べて処理をする、一種のメタプログラミングのようなものですね。実際にGoのリフレクションでは、interface{} を精査するプログラムとなります。

標準パッケージでreflectが使用されているものだと、 encoding/json.Marshal関数 があります。Marshal関数は、構造体のオブジェクトをJSONに変換する関数です。下記のようにメタタグと呼ばれる記法 ( json:"userId" ) で構造体にフィールド名を記述をすることで、JSONへ変換した時のフィールド名を指定することが出来ます。

type User struct {
    ID   uint   `json:"userId"`
    Name string `json:"userName"`
}

user := User{
    ID:   123,
    Name: "Aoman",
}

bytes, err := json.Marshal(user)
if err != nil {
    // error handling
}

fmt.Println(string(bytes))
// {"userId":123,"userName":"Aoman"}

Marshal関数内では、reflectを使用し型のメタタグ情報を読み取ることで、フィールド名を決定しています(指定がない場合は構造体のフィールド名を使用)

もちろん、独自にメタタグを定義し読み取ることが出来るため、様々な関数も作れそうで夢が広がりんぐですね!

基本的な使い方

本項では、GoブログのThe Laws of Reflection を参考に、今回の実装のために必要そうなものをざっくり解説します。詳しく知りたい方はThe Laws of Reflectionやパッケージのドキュメントをご覧ください!

まず、reflectを扱う上で2種類の型を理解しておく必要があります。reflect.Typeとreflect.Valueです。

  • reflect.Type: 型の情報を取り扱うメソッドを持つ型
  • reflect.Value: 値を取り扱うメソッドを持つ型

この2つの型は、次の関数を使うことで取得することができます。

// anyはinterface{}のエイリアス
func TypeOf(i any) Type
func ValueOf(i any) Value

それぞれ、 を取り扱うためのメソッドを持ちますが、共通で使用できる Kind() メソッドがあります。この Kind() メソッドは、型の種類を取得できるメソッドです。

func (v Value) Kind() Kind
var x float64 = 3.4
fmt.Println("type.kind:", reflect.TypeOf(x).Kind())
fmt.Println("value.kind:", reflect.ValueOf(x).Kind())
// type.kind: float64
// value.kind: float64

Kind()メソッドの返り値はreflect.Kind型になっており、型の種類が定数で定義されているため、下記のように型によって処理を分岐させるようなコードを書くことが出来ます。

v := reflect.ValueOf(target)
switch v.Kind() {
case reflect.String:
    // Stringの場合の処理
case reflect.Int:
    // Intの場合の処理
case reflect.Struct:
    // Structの場合の処理
case reflect.Pointer:
    // Pointerの場合の処理
...
}

reflect.Type

reflect.Typeの使い方の1つに、特定の型のゼロ値を取得する方法があります。Int型であれば0、Pointer型であればnilのようなゼロ値です。

次のようなコードになります。

var x float64 = 3.4
typ := reflect.TypeOf(x)
fmt.Println("x zero value:", reflect.Zero(typ))
// x zero value: 0

var str string = "test"
typ2 := reflect.TypeOf(&str)
fmt.Println("&str zero value:", reflect.Zero(typ2))
// &str zero value: <nil>

ただし、reflect.Zero()の返り値はreflect.Value型になるので、そこからよしなに処理をする必要はあります。

reflect.Value

reflect.Valueも見ていきましょう。

(実はreflect.Valueからは、reflect.Typeを簡単に取得することができます…)

reflect.Value型から元の値に復元するInterface()メソッドがあります。

func (reflect.Value).Interface() (i any)
var x float64 = 3.4
v := reflect.ValueOf(x)
origin := v.Interface().(float64)
fmt.Printf("origin type: %T, value: %v\\n", origin, origin)
// origin type: float64, value: 3.4

次に値を書き換えるメソッドSetXxxです。値を書き換える際にはいくつか気をつけなければならないことがあります。書き換えられる条件が若干ややこしや…本記事での肝はまさにここだと思います。

例として、xの値を3.4から10.4に書き換えてみます。

var x float64 = 3.4
v := reflect.ValueOf(&x)
v = v.Elem()
v.SetFloat(10.4)
fmt.Println("v:", v.Interface())
fmt.Println("x:", x)
// v: 10.4
// x: 10.4

ポイントは2つあります。1つ目は、reflect.ValueOf(&x) のようにポインタを渡していること。これは、reflect.ValueOf(x)のように渡してしまうと、値のコピーがされ元のデータを書き換えられないためです。2つ目は、v.Elem()の部分。これは、reflect.Valueがポインタを指しているため、実体を操作する際に必要な処理です。以上、値を書き換えるための流れをまとめると次のようになります。

  1. reflect.ValueOf()にポインタを渡してreflect.Valueを取得
  2. ポインタを示すreflect.ValueからElem()で実体を取得
  3. SetXxxで書き換え

ちなみに、SetXxxで書き換えられるかどうかを確認する、CanSet()メソッドが用意されています。

var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println(v.CanSet())
// -> false
v = reflect.ValueOf(&x)
fmt.Println(v.CanSet())
// -> false
v = v.Elem()
fmt.Println(v.CanSet())
// -> true

続いて、構造体の場合の操作についても少し触れておきます。

フィールドの数だけループさせて、それぞれのフィールドに対して処理をする例です。

type User struct {
    ID   uint
    Name string
}

user := User{
    ID:   777,
    Name: "Sakana-kun-san",
}

v := reflect.ValueOf(&user).Elem()
typ := v.Type()

for i := 0; i < v.NumField(); i++ {
    fieldTyp := typ.Field(i)
    fieldV := v.Field(i)
    fmt.Printf("%d: %s %s = %v\\n",
        i, fieldTyp.Name, fieldV.Type(), fieldV.Interface())
}
// 0: ID uint = 777
// 1: Name string = Sakana-kun-san

また、次のようにフィールド名が一致する値のreflect.Valueを取得することも出来ます。

type User struct {
    ID   uint
    Name string
}

user := User{
    ID:   777,
    Name: "Sakana-kun-san",
}

v := reflect.ValueOf(&user).Elem()
idV := v.FieldByName("ID")
nameV := v.FieldByName("Name")
notFoundV := v.FieldByName("Hoge")

fmt.Println("idV:", idV)
fmt.Println("nameV:", nameV)
fmt.Println("notFoundV:", notFoundV)
// idV: 777
// nameV: Sakana-kun-san
// notFoundV: <invalid reflect.Value>

ざっくりとではありますが、reflectの一部の機能を紹介させていただきました。私自身、まだまだ把握出来ていない機能が沢山ありますが、面白そうなので少しずつ触っていきたいと思っております😆

実装

それでは、タイトル回収の実装をしていきます。

いきなりではありますが、全体像のコードを載せます。ほとんどが前項で紹介した機能を使用しているので、理解に難しくないのでは…と思います。

// SetZeroValues 構造体の指定したフィールドをゼロ値に変換する
// 第一引数 obj: 対象の構造体のポインタ
// 第二引数 names: ゼロ値に変換したいフィールド名
func SetZeroValues(obj any, names ...string) {
    v := reflect.ValueOf(obj)
    if v.Kind() != reflect.Pointer {
        return
    }

    v = v.Elem()
    if v.Kind() != reflect.Struct {
        return
    }

    // 関数の第二指定されたフィールド名のフィールドが存在するか一つ一つ確認していく
    for _, n := range names {
        fv := v.FieldByName(n)
        // "フィールドが存在しない"or"nil"であればIsValid()=trueにになる
        if !fv.IsValid() {
            continue
        }
        if !fv.CanSet() {
            continue
        }

        typ := fv.Type()
        fv.Set(reflect.Zero(typ))
    }
}

この関数を使用することで、第一引数に構造体インスタンスのポインタを、第二引数以降にフィールド名を指定すると、指定したフィールドがゼロ値になります。

type User struct {
    ID   int
    Name string
}

user := User{
    ID:   777,
    Name: "Sakana-kun-san",
}

SetZeroValues(&user, "Name")
fmt.Printf("%#v\\n", user)
// lib.User{ID:777, Name:""}

ただ、このままだと指定したフィールドがゼロ値のままになってしまいます。今回の要件では、特定のフィールドをゼロ値にして、ORマッパーでSQLを構築した後、元の値に戻しておきたいです。よって、復元するrestoreFn関数リターンするように実装してみました。

func SetZeroValues(obj any, names ...string) (restoreFn func()) {
    restoreFn = func() {}

    v := reflect.ValueOf(obj)
    if v.Kind() != reflect.Pointer {
        return
    }

    v = v.Elem()
    if v.Kind() != reflect.Struct {
        return
    }

    restoreFns := make([]func(), 0, len(names))

    for _, fieldName := range names {
        fv := v.FieldByName(fieldName)
        if !fv.IsValid() {
            continue
        }
        if !fv.CanSet() {
            continue
        }

        typ := fv.Type()
        tmpValue := fv.Interface()
        restoreFns = append(restoreFns, func() {
            fv.Set(reflect.ValueOf(tmpValue))
        })
        fv.Set(reflect.Zero(typ))
    }

    restoreFn = func() {
        for _, f := range restoreFns {
            f()
        }
    }

    return
}

下記は、作成した関数を実際に使用してみた結果です。

type Address struct {
    City string
}

type User struct {
    ID      int
    Name    string
    Age     int
    Address *Address
}

user := User{
    ID:   777,
    Name: "Sakana-kun-san",
    Age:  47,
    Address: &Address{
        City: "Tateyama",
    },
}

targetNames := []string{"Age", "Address"}

fmt.Printf("before: %#v\\n", user)
restore := SetZeroValues(&user, targetNames...)
fmt.Printf("after: %#v\\n", user)
// ...何かしらの処理
restore()
fmt.Printf("restored: %#v\\n", user)
// before: lib.User{ID:777, Name:"Sakana-kun-san", Age:47, Address:(*lib.Address)(0x14000060600)}
// after: lib.User{ID:777, Name:"Sakana-kun-san", Age:0, Address:(*lib.Address)(nil)}
// restored: lib.User{ID:777, Name:"Sakana-kun-san", Age:47, Address:(*lib.Address)(0x14000060600)}

うまく動きました🎉

reflect注意点

改訂2版 みんなのGo言語 こちらの本にパフォーマンスについての言及があります。使い方によってはパフォーマンスが著しく低下する恐れがあるため、使用には十分注意したほうが良さそうです。

以下、第6章のまとめの内容を引用させていただきます。詳しくは本をご覧ください!

ここまでreflectのさまざまな機能に関して説明してきましたが、reflectは最後の手段です。Perl/Ruby/Pythonのような言語とは違い、Goはもともと動的にコードを解析・生成することを前提とした言語ではありません。ですので本当に必要なとき以外ではreflectを使ってはいけません。逆に言えば、Goの通常の記法ではどうしても実装不可能な場合ならば自然とreflectを使う以外の方法がないということに気付くはずですので、そのような場合はreflectの力を解放してやると良いでしょう。コードの難読具合が上がることやパフォーマンスに対するインパクトを考慮しつつ上手にreflectと付き合ってください。

参考させていただいた文献

終わりに

ここまで読んでくださりありがとうございます!

最後に宣伝になりますが、カミナシでは、ソフトウェアエンジニア(フロントエンド、バックエンド、セキュリティ、デザインシステム領域のフロントエンド)、エンジニアリングマネージャーと幅広く募集しております!

一緒に働いていただけるかた、もしよろしければご応募お待ちしております!

careers.kaminashi.jp