🦊

【Go】reflectパッケージを使って再帰的に構造体のゼロ値のチェックをする

2022/08/18に公開

Goのゼロ値について

Goは構造体を初期化する際に全てのフィールドの値を指定しなくても、必要な値だけを指定して構造体を初期化をすることができるという特徴があります。その一方で構造体の初期化の際に値の指定を強要できないというのは、バグを生み出す可能性があります。そこで、ゼロ値を許容したくない場合はvalidatorパッケージを使って、構造体にrequiredのタグを付けることで構造体のフィールドのゼロ値のチェックを行うことができます。
https://github.com/go-playground/validator

今回は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