In your code there's no relationship between the constraints FooBar and stringer. Furthermore the methods are implemented on the pointer receivers.
A quick and dirty fix for your contrived program is simply to assert that *T is indeed a stringer:
func blah[T FooBar]() {
t := new(T)
do(any(t).(stringer))
}
Playground: https://go.dev/play/p/zmVX56T9LZx
But this forgoes type safety, and could panic at run time. To preserve compile time type safety, another solution that somewhat preserves your programs' semantics would be this:
type FooBar[T foo | bar] interface {
*T
stringer
}
func blah[T foo | bar, U FooBar[T]]() {
var t T
do(U(&t))
}
So what's going on here?
First, the relationship between a type parameter and its constraint is not identity: T is not FooBar. You cannot use T like it was FooBar, therefore *T is definitely not equivalent to *foo or *bar.
So when you call do(t), you're attempting to pass a type *T into something that expects a stringer, but T, pointer or not, just does not inherently have the a() string method in its type set.
Step 1: add the method a() string into the FooBar interface (by embedding stringer):
type FooBar interface {
foo | bar
stringer
}
But that's not enough yet, because now none of your types actually implement it. Both declare the method on the pointer receiver.
Step 2: change the types in the union to be pointers:
type FooBar interface {
*foo | *bar
stringer
}
This constraint now works, but you have another problem. You can't declare composite literals when the constraint doesn't have a core type. So t := T{} is also invalid. We change it to:
func blah[T FooBar]() {
var t T // already pointer type
do(t)
}
Now this compiles, but t is actually the zero value of a pointer type, so it's nil. Your program doesn't crash because the methods just return some string literal.
If you need to also initialize the memory referenced by the pointers, inside blah you need to know about the base types.
Step 3: So you add T foo | bar as one type param, and change the signature to:
func blah[T foo | bar, U FooBar]() {
var t T
do(U(&t))
}
Done? Not yet. The conversion U(&t) is still invalid because the type set of both U and T don't match. You need to now parametrize FooBar in T.
Step 4: basically you extract FooBar's union into a type param, so that at compile time its type set will include only one of the two types:
type FooBar[T foo | bar] interface {
*T
stringer
}
The constraint now can be instantiated with T foo | bar, preserve type safety, pointer semantics and initialize T to non-nil.
func (f *foo) a() string {
fmt.Println("foo nil:", f == nil)
return "foo"
}
func main() {
blah[foo]()
}
Prints:
foo nil: false
foo
Playground: https://go.dev/play/p/src2sDSwe5H
If you can instantiate blah with pointer types, or even better pass arguments to it, you can remove all the intermediate trickery:
type FooBar interface {
*foo | *bar
stringer
}
func blah[T FooBar](t T) {
do(t)
}
func main() {
blah(&foo{})
}