I wrote a small validator that accepted interface{} and walked fields marked validate:"required". The happy path worked, so I naturally declared victory before trying a pointer, a nil pointer, or anything that was not a struct.

The first version began with this:

v := reflect.ValueOf(x)
for i := 0; i < v.NumField(); i++ {
	// inspect v.Field(i)
}

Passing &User{} produced reflect: call of reflect.Value.NumField on ptr Value. Recovering the panic would hide the real question: what inputs does this validator promise to accept? I chose structs and non-nil pointers to structs, with ordinary errors for everything else.

func validate(x interface{}) error {
	v := reflect.ValueOf(x)
	if !v.IsValid() {
		return errors.New("cannot validate nil")
	}
	if v.Kind() == reflect.Ptr {
		if v.IsNil() {
			return errors.New("cannot validate nil pointer")
		}
		v = v.Elem()
	}
	if v.Kind() != reflect.Struct {
		return fmt.Errorf("cannot validate %s", v.Kind())
	}

	t := v.Type()
	for i := 0; i < v.NumField(); i++ {
		if t.Field(i).Tag.Get("validate") != "required" {
			continue
		}
		if isZero(v.Field(i)) {
			return fmt.Errorf("%s is required", t.Field(i).Name)
		}
	}
	return nil
}

The harder policy is what “required” means. For this validator it rejects empty strings, numeric zero, false, and nil pointers, maps, slices, and interfaces. That makes false invalid, so a required boolean is usually a design smell; a pointer can represent the distinct states absent, false, and true.

I ignore unexported fields. A general validator should not turn another package’s private representation into public input policy.

Nested structs are not automatically validated. A field gets nested validation only when its tag asks for it; otherwise adding a private detail to a child type could unexpectedly change the parent’s contract. Nil optional pointers remain acceptable, while a required nil pointer fails at its own field path.

Errors report the Go field path as well as the failed rule. A bare “required” is compact but useless once validation descends into two nested structs with similarly named fields.

Reflection removes field-by-field plumbing, but the useful part is still the contract: accepted top-level shapes, the meaning of “required,” whether nesting is opt-in, and useful error paths. Once those choices are explicit, the reflective code can stay small and unsurprising.