How to implement generic interfaces?

Types don’t actually implement generic interfaces, they implement instantiations of generic interfaces. You can’t use a generic type (including interfaces) without instantiation. From there, it is just like pre-generics Go, including the difference between methods with pointer receiver.

Therefore it is helpful to think what the methods that use type parameters would look like if you rewrote them with concrete types.

Let’s consider a generic interface and some type:

type Getter[T any] interface {
    Get() T
}

type MyStruct struct {
    Val string
}

There’s a few possible cases

Interface with concrete type argument

Instantiate as Getter[string], implemented by types with method Get() string

// implements Getter[string]
func (m MyStruct) Get() string {
   return m.Val
}

// ok
func foo() Getter[string] {
    return MyStruct{}
}

Interface with type parameter as type argument

Functions that have type parameters may use those to instantiate generic types, e.g. Getter[T]. Implementors must have exactly the Get() T method. For that to be valid, they are also generic and instantiated with the same type parameter:

So this doesn’t compile even if T is string

// Getter[T] literally needs implementors with `Get() T` method
func bar[T any]() Getter[T] {
    return MyStruct{} // doesn't compile, even if T is string
}

Making MyStruct also parametrized works:

type MyStruct[T any] struct {
    Val T
}

func (m MyStruct[T]) Get() T {
    return m.Val
}

func bar[T any]() Getter[T] {
    return MyStruct[T]{} // ok
}

Concrete interface with generic implementor

Let’s reverse the previous cases. We keep the parametrized MyStruct[T any] but now the interface is not parametrized:

type Getter interface {
    Get() string
}

In this case, MyStruct implements Getter only when it is instantiated with the necessary concrete type:

// Getter requires method `Get() string`
func baz() Getter {
    return MyStruct[string]{} // instantiate with string, ok
    // return MyStruct[int]{} // instantiate with something else, doesn't compile
}

Pointer receivers

This follows the same rules as above, but requires instantiating pointer types, as usual:

// pointer receiver, implements Getter[string]
func (m *MyStruct) Get() string {
   return m.Val
}

func foo() Getter[string] {
    return &MyStruct{} // ok
    // return MyStruct{} // doesn't implement
}

and it is the same if MyStruct is generic.

// parametrized pointer receiver
func (m *MyStruct[T]) Get() T {
   return m.Val
}

func foo() Getter[string] {
    return &MyStruct[string]{} // ok
}

So in your case, the mental exercise of replacing the type params with concrete types gives that Dao[ReturnType] has method FindOne(id string) *ReturnType. The type that implements this method is *MyDao (pointer receiver), therefore:

func NewMyDao() Dao[ReturnType] {
    return &MyDao{}
}

Leave a Comment