Go 3 min read

[Go Tutorials P5] Generic interfaces

Starting from Go 1.18, the language introduced generics, allowing developers to write more reusable and type-safe code.

While Go does not allow interfaces themselves to have type parameters directly, you can use interfaces within generic types or functions, and define generic constraints on interfaces. This opens up powerful patterns for abstraction.

1. Traditional Interface (Non-Generic)

Before generics, an interface in Go simply defined a set of methods a type must implement. For example:

type Reader interface {
    Read(p []byte) (n int, err error)
}

Here, any type that implements a Read method matches the Reader interface.

2. Creating Generic Interfaces with Type Parameters

While you can't add type parameters directly to an interface, you can define an interface inside a generic type.

Here's a simple example of a generic repository interface:

type Repository[T any] interface {
    Save(entity T) error
    FindByID(id int) (T, error)
}

In this example:

  • Repository[T] is a generic interface for any type T.

  • T any means T can be any type without restrictions.

Usage Example:

type User struct {
    ID   int
    Name string
}

type UserRepository struct{}

func (r UserRepository) Save(user User) error {
    fmt.Println("Saving user:", user.Name)
    return nil
}

func (r UserRepository) FindByID(id int) (User, error) {
    return User{ID: id, Name: "John Doe"}, nil
}

The UserRepository automatically implements the Repository[User] interface by matching its method signatures.

3. Applying Constraints to Generic Interfaces

You can also restrict the types that a generic interface can accept using constraints.

Example with a constraint:

type Identifiable interface {
    GetID() int
}

type Repository[T Identifiable] interface {
    Save(entity T) error
    FindByID(id int) (T, error)
}

Now, only types that implement the GetID() method can be used with Repository[T].

4. Full Example: A Generic Service

Here’s a complete example demonstrating generic interfaces, constraints, and implementations:

package main

import "fmt"

// constraint interface
type Entity interface {
    GetID() int
}

// generic interface
type Service[T Entity] interface {
    Get(id int) T
}

// a struct that satisfies the Entity constraint
type User struct {
    ID   int
    Name string
}

func (u User) GetID() int {
    return u.ID
}

// a concrete service
type UserService struct{}

func (s UserService) Get(id int) User {
    return User{ID: id, Name: "Alice"}
}

func main() {
    var svc Service[User] = UserService{}
    user := svc.Get(1)
    fmt.Println(user.Name) // Output: Alice
}

Key Takeaways

  • Interfaces themselves cannot have type parameters, but generic types and functions can work with interfaces.

  • Generic interfaces are made by combining interfaces with type parameters.

  • Constraints help ensure type safety by restricting which types are valid for a generic interface.

  • Using generics and interfaces together makes your Go code more reusable, flexible, and safe.