🦊
【Go】reflectパッケージを使って再帰的に構造体のゼロ値のチェックをする
Goのゼロ値について
Goは構造体を初期化する際に全てのフィールドの値を指定しなくても、必要な値だけを指定して構造体を初期化をすることができるという特徴があります。その一方で構造体の初期化の際に値の指定を強要できないというのは、バグを生み出す可能性があります。そこで、ゼロ値を許容したくない場合はvalidatorパッケージを使って、構造体にrequiredのタグを付けることで構造体のフィールドのゼロ値のチェックを行うことができます。
今回はvalidatorパッケージを使わず、構造体にタグを付けないでゼロ値のチェックをする関数を実装してみます。
reflectパッケージのIsZeroについて
reflectパッケージにはIsZeroという、ある値がゼロ値がどうかを判定する関数があります。
// IsZero reports whether v is the zero value for its type.
// It panics if the argument is invalid.
func (v Value) IsZero() bool {
switch v.kind() {
case Bool:
return !v.Bool()
case Int, Int8, Int16, Int32, Int64:
return v.Int() == 0
case Uint, Uint8, Uint16, Uint32, Uint64, Uintptr:
return v.Uint() == 0
case Float32, Float64:
return math.Float64bits(v.Float()) == 0
case Complex64, Complex128:
c := v.Complex()
return math.Float64bits(real(c)) == 0 && math.Float64bits(imag(c)) == 0
case Array:
for i := 0; i < v.Len(); i++ {
if !v.Index(i).IsZero() {
return false
}
}
return true
case Chan, Func, Interface, Map, Pointer, Slice, UnsafePointer:
return v.IsNil()
case String:
return v.Len() == 0
case Struct:
for i := 0; i < v.NumField(); i++ {
if !v.Field(i).IsZero() {
return false
}
}
return true
default:
// This should never happens, but will act as a safeguard for
// later, as a default value doesn't makes sense here.
panic(&ValueError{"reflect.Value.IsZero", v.Kind()})
}
}
このコードを読むと、構造体がゼロ値であることは全てのフィールドがゼロ値であることで、ある構造体が値がゼロ値であるフィールドを持っているかどうかのチェックには使えないことが分かります。また、渡した値が構造体を示すポインタ型だったとしても、その値がnilかどうかをチェックしているだけな点に注意する必要があります。
IsZeroを使って再帰的に構造体のゼロ値のチェックをする
構造体のフィールドに対してそれぞれIsZeroを呼び出して、対象のフィールドが構造体の場合には再帰的に関数を呼び出します。ポインタ型に対してはElem()を使用することで、そのポインタが示す構造体に対してゼロ値を持っているかどうかのチェックをします。
// 第2引数にゼロ値のチェックをスキップしたいフィールド名を入れる
func RecursiveIsZero(val interface{}, ignoredZeroValue []string) bool {
var v reflect.Value
// ポインタにも対応させる
if reflect.TypeOf(val).Kind() == reflect.Ptr {
v = reflect.ValueOf(val).Elem()
if reflect.TypeOf(v).Kind() != reflect.Struct {
return v.IsZero()
}
} else if reflect.TypeOf(val).Kind() == reflect.Struct {
v = reflect.ValueOf(val)
}
var typeName = v.Type().Name()
for i := 0; i < v.NumField(); i++ {
fieldName := v.Type().Field(i).Name
isIgnoredZeroValue := false
for _, val := range ignoredZeroValue {
if fieldName == val {
isIgnoredZeroValue = true
break
}
}
if isIgnoredZeroValue {
continue
}
if v.Field(i).IsZero() {
fmt.Printf("[WARN]%s.%s%s\n", typeName, fieldName, " is zero value.")
return true
}
if v.Field(i).Kind() == reflect.Ptr || v.Field(i).Kind() == reflect.Struct {
if RecursiveIsZero(v.Field(i).Interface(),ignoredZeroValue) {
return true
}
}
}
return false
}
まとめ
需要があるかどうかは分からないのですが、まだまだGoを始めたばかりの自分にとってはreflectパッケージとゼロ値の良い勉強になったのでよしとします。
Discussion