tl;dr the trade-off is between static typing and flexible containers.
Up to Go 1.17 you cannot have a struct field with different static types. The best you can have is interface{}
, and then assert the dynamic type upon usage. This effectively allows you to have containers of testCases
with either type at run time.
type testCase struct {
input interface{}
isValid bool
}
func main() {
// can initialize container with either type
cases := []testCase{{500, false}, {"foobar", true}}
// must type-assert when using
n := cases[0].(int)
s := cases[1].(string)
}
With Go 1.18, you can slightly improve on type safety, in exchange for less flexibility.
- Parametrize the struct with a union. This statically restricts the allowed types, but the struct now must be instantiated explicitly, so you can't have containers with different instantiations. This may or may not be compatible with your goals.
type testCase[T int | string] struct {
input T
isValid bool
}
func main() {
// must instantiate with a concrete type
cases := []testCase[int]{
{500, false}, // ok, field takes int value
/*{"foobar", true}*/, // not ok, "foobar" not assignable to int
}
// cases is a slice of testCase with int fields
}
No, instantiating as testCase[any]
is a red herring. First of all, any
just doesn't satisfy the constraint int | string
; even if you relax that, it's actually worse than the Go 1.17 solution, because now instead of using just testCase
in function arguments, you must use exactly testCase[any]
.
- Parametrize the struct with a union but still use
interface{}
/any
as field type: (How) can I implement a generic `Either` type in go? . This also doesn't allow to have containers with both types.
In general, if your goal is to have flexible container types (slices, maps, chans) with either type, you have to keep the field as interface{}
/any
and assert on usage. If you just want to reuse code with static typing at compile-time and you are on Go 1.18, use the union constraint.