How can I add a method to an interface already used in a union term?

92 Views Asked by At

I am using Generics with Go 1.21. I want to extend some common code.

If I add foo() below to the Under11AgeGroups interface so only the Under 9s and Under 11s have to implement this function, I get a compile error:

Cannot use interfaces with methods in union.

Am I missing a way to do this ?

The below code runs.

https://go.dev/play/p/w_N7gbsRCjY

package main
    
    import (
        "fmt"
    )
    
    
    type AllAgeGroups interface {
        Under13Player | Under11AgeGroups
        name() string
    }
    
    type Under11AgeGroups interface {
        Under9Player | Under11Player
        // foo() string             <-- this causes cannot use main.Under11AgeGroups in union (main.Under11AgeGroups contains methods)
    }
    
    type Players[T AllAgeGroups] struct {
        Results []T
    }
    
    func (t *Players[T]) PrintTheTeam() {
        fmt.Println("----------------")
        for _, p := range t.Results {
            fmt.Println("Player ", p.name())
        }
    }
    
    type Under9Player struct { Name string }
    func (u Under9Player) name() string {
        return u.Name
    }
    
    type Under11Player struct { Name string }
    func (u Under11Player) name() string {
        return u.Name
    }
    
    type Under13Player struct { Name string; Age int }
    func (u Under13Player) name() string {
        return u.Name + " " + string(u.Age)
    }
    
    func main() {
        bob := Under11Player{Name: "bob"}
        alice := Under11Player{Name: "alice"}
        yves :=Under11Player{Name: "yves"}
        under11team := Players[Under11Player]{Results: []Under11Player{bob, alice, yves}}
        under11team.PrintTheTeam()
    
        foo :=Under13Player{Name: "foo", Age: 12}
        under13team := Players[Under13Player]{Results: []Under13Player{foo}}
        under13team.PrintTheTeam()
    }

1

There are 1 best solutions below

0
blackgreen On

You can't directly add a method to an interface that already appears in a union term. This is forbidden by the language specifications (paragraph about Interface types):

Implementation restriction: A union (with more than one term) cannot contain the predeclared identifier comparable or interfaces that specify methods, or embed comparable or interfaces that specify methods.

However there's a trick! Since you are using these interfaces as constraints, they provide a compile-time check. You can replicate that compile-time check by attempting to instantiate a type parameter constrained to the method with the desired types:

type Under11AgeGroups interface {
    Under9Player | Under11Player
    // these should both have a `foo() string` method
}

// dummy function with type parameter that must have the foo() string method
func mustDefineFoo[T interface{ foo() string }]() {}

// dummy instantiations with the desired types
var _ = mustDefineFoo[Under9Player]
var _ = mustDefineFoo[Under11Player]

Note that mustDefineFoo[Under9Player] are function values. It doesn't actually matter what these are, they exist only to force the compiler to check if Under9Player and Under11Player satisfy the constraint: they do if they have the foo() string method.

Playground: https://go.dev/play/p/2QXzMsw1Qld

Clearly, every time you add a new term to the main interface constraint Under11AgeGroups, you have to add a new dummy instantiation to your source code, but this isn't much different than adding one more term to the existing union.