Type constraint with union of interface

66 views
Skip to first unread message

Pierre Durand

unread,
Mar 13, 2026, 10:52:17 PM (2 days ago) Mar 13
to golang-nuts
I'm trying to define a type constraint which is a union of interface.

type ReadStringer interface {
io.Reader | fmt.Stringer
}

Unfortunately the compiler rejects it:
cannot use io.Reader in union (io.Reader contains methods)
cannot use fmt.Stringer in union (fmt.Stringer contains methods)

I understand it's not possible, but I don't understand why.
Is there a logical problem ? Could it be supported in a future Go version ?

My goal is to define a function that accepts a parameter, then check at the execution time if it implements an interface or the other.
I could use "any", but it would be less safe.

Thank you.

Axel Wagner

unread,
Mar 14, 2026, 1:16:30 AM (yesterday) Mar 14
to Pierre Durand, golang-nuts
Hi,

I gave a talk with accompanying blog post about this exact topic, a while back:

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/golang-nuts/375e05d7-151a-4cd9-bb60-bc617aa20bebn%40googlegroups.com.

Kurtis Rader

unread,
Mar 14, 2026, 1:18:40 AM (yesterday) Mar 14
to Pierre Durand, golang-nuts
I googled this and found several explanations, such as https://stackoverflow.com/questions/72267243/unioning-an-interface-with-a-type-in-golang#:~:text=2%20Answers,~%20)%20and%20fmt.Stringer%20.

It's also unclear to me why you think using "any" would be less safe. You still have to use a type-assertion or type-switch to disambiguate the two cases. If the compiler allowed your example it might catch obvious mistakes at compile time but you still have to hand write code to detect at run-time which case is in effect. And such code should always have a "Oops! Got the wrong type" path.

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/golang-nuts/375e05d7-151a-4cd9-bb60-bc617aa20bebn%40googlegroups.com.


--
Kurtis Rader
Caretaker of the exceptional canines Junior and Hank

Axel Wagner

unread,
Mar 14, 2026, 1:41:25 AM (yesterday) Mar 14
to Kurtis Rader, Pierre Durand, golang-nuts
On Sat, 14 Mar 2026 at 06:18, Kurtis Rader <kra...@skepticism.us> wrote:
It's also unclear to me why you think using "any" would be less safe. You still have to use a type-assertion or type-switch to disambiguate the two cases. If the compiler allowed your example it might catch obvious mistakes at compile time but you still have to hand write code to detect at run-time which case is in effect. And such code should always have a "Oops! Got the wrong type" path.

In common parlance, "type-safe" means exactly, that a class of runtime errors is prevented at compile time. That is the purpose of types.

A function constrained on `ReadStringer` would not be safer to *write*, because, you would still need a type-switch that would need to include a runtime panic branch. But it would be safer to *use*, because the compiler would prevent a user from ever triggering that runtime panic.

Kurtis Rader

unread,
Mar 14, 2026, 1:50:23 AM (yesterday) Mar 14
to Axel Wagner, Pierre Durand, golang-nuts
On Fri, Mar 13, 2026 at 10:40 PM Axel Wagner <axel.wa...@googlemail.com> wrote:
In common parlance, "type-safe" means exactly, that a class of runtime errors is prevented at compile time. That is the purpose of types.

A function constrained on `ReadStringer` would not be safer to *write*, because, you would still need a type-switch that would need to include a runtime panic branch. But it would be safer to *use*, because the compiler would prevent a user from ever triggering that runtime panic.

I guess I'm showing my age. But if you have to write an explicit run time type switch/assertion then you should assume the compile time type constraint might allow an unexpected type to "leak" into your function. If the O.P.'s example was supported by the compilation toolchain that might be impossible, in theory, but I would assume it's still possible if I have to write code to disambiguate which type I received.

Kurtis Rader

unread,
Mar 14, 2026, 1:52:48 AM (yesterday) Mar 14
to Axel Wagner, Pierre Durand, golang-nuts
Especially since the type constraint could be changed to allow an unexpected type not already handled by the explicit run time type switch/assertion code in the function I wrote. 

Axel Wagner

unread,
Mar 14, 2026, 2:07:40 AM (24 hours ago) Mar 14
to Kurtis Rader, Pierre Durand, golang-nuts
On Sat, 14 Mar 2026 at 06:49, Kurtis Rader <kra...@skepticism.us> wrote:
I guess I'm showing my age. But if you have to write an explicit run time type switch/assertion then you should assume the compile time type constraint might allow an unexpected type to "leak" into your function.

I don't understand this assertion. It seems plain untrue to me. If we allowed you to write `fmt.Stringer | ~string` (and put a SAT solver into the compiler to do the appropriate checks) but otherwise not changed the language at all, then this would be accepted by the compiler:

func Stringify[T interface{ ~string | fmt.Stringer}](v T) string {
    // still have to write the type switch
    switch v := any(v).(type) {
    case fmt.Stringer:
        return v.String()
    default:
        return reflect.ValueOf(v).String()
    }
}

This is of course somewhat error prone, you might easily forget a branch or otherwise screw up the logic of your function. But it would mean this is rejected by the compiler:

func main() {
    println(Stringify[int](42))
}

If, instead, you use `T any`, this program would be allowed and panic at runtime.

robert engels

unread,
Mar 14, 2026, 2:11:44 AM (24 hours ago) Mar 14
to Kurtis Rader, Axel Wagner, Pierre Durand, golang-nuts
It’s obvious that the solution is to define two different generic functions - one for each type. If you’re writing code using a type switch you’re doing it wrong. Simple as that. But sadly you can’t do this in Go and the reasoning is beyond my comprehension. It’s essentially method over loading and Go prefers … not really sure because it’s never made any sense whatsoever. Yes, there could be ambiguous cases but the compiler would detect this - and the developer would then decide which typed method to call with an explicit cast. 

Very poor design choice. 

On Mar 14, 2026, at 12:52 AM, Kurtis Rader <kra...@skepticism.us> wrote:


--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.

Axel Wagner

unread,
Mar 14, 2026, 2:12:51 AM (24 hours ago) Mar 14
to Kurtis Rader, Pierre Durand, golang-nuts
Okay, well, my bad, I forgot that `String` is a special case, because reflect.Value.String does not panic for non-string types. But it still misbehaves and it *would* panic, if we where talking about `~int` and `.Int()`, for example.

Kurtis Rader

unread,
Mar 14, 2026, 2:14:39 AM (24 hours ago) Mar 14
to Axel Wagner, Pierre Durand, golang-nuts
You wrote:

> This is of course somewhat error prone, you might easily forget a branch or otherwise screw up the logic of your function.

That is precisely my point. If you have to include explicit run-time type checks in your function to support the type polymorphism then you cannot assume the compiler will not allow an unexpected type to leak into your function. Especially since the type signature might be modified without the implementation being updated to correctly handle the new type signature.
Reply all
Reply to author
Forward
0 new messages