← O-Reilly Learning Go

Generics

genericsinterfaces#Golang

Generics and Interface

Any interface can be used as a type constraint, not just any and comparable - this restricts a type parameter to types that have the required methods, enforced at compile time. Interfaces can also be generic (take their own type parameters), which lets a method's argument or return type track whichever type implements the interface. Combined, this lets you write one function that works across many types while keeping full type safety, instead of hardcoding a concrete type or falling back to any.

Example
the flow explanation

  1. Build Pair[T] first — e.g. Pair[Point2D]. _(Here Pair's own constraint fmt.Stringer is checked — Point2D must have String().)
  2. Pass those Pairs to FindCloserFindCloser(pair2Da, pair2Db).
  3. FindCloser takes the type from Pair[T] — it infers T = Point2D by matching the argument Pair[Point2D] against the parameter Pair[T].
  4. The function checks the constraint with its own T Differ[T] — does Point2D satisfy Differ[Point2D]? (needs String() and Diff(Point2D)). If yes → compiles; if no → compile error.
    Note
func Convert[T1, T2 Integer](in T1) T2
//            └──┬──┘ └──┬──┘
//        type params   constraint
type Pair[T fmt.Stringer] struct {
	Val1 T
	Val2 T
}

type Differ[T any] interface {
    fmt.Stringer
    Diff(T) float64
}

func FindCloser[T Differ[T]](pair1, pair2 Pair[T]) Pair[T] {
    d1 := pair1.Val1.Diff(pair1.Val2)
    d2 := pair2.Val1.Diff(pair2.Val2)
    if d1 < d2 {
        return pair1
    }
    return pair2
}

type Point2D struct {
    X, Y int
}

func (p2 Point2D) String() string {
    return fmt.Sprintf("{%d,%d}", p2.X, p2.Y)
}

func (p2 Point2D) Diff(from Point2D) float64 {
    x := p2.X - from.X
    y := p2.Y - from.Y
    return math.Sqrt(float64(x*x) + float64(y*y))
}

func main(){
	pair2Da := Pair[Point2D]{Point2D{1, 1}, Point2D{5, 5}}
	pair2Db := Pair[Point2D]{Point2D{10, 10}, Point2D{15, 5}}
	closer := FindCloser(pair2Da, pair2Db)
	fmt.Println(closer)
}

Use Type Terms to Specify Operators

Be aware that interfaces with type elements are valid only as type constraints. It is a compile-time error to use them as the type for a variable, field, return value, or parameter.

By default, type terms match exactly. If you try to use divAndRemainder with a user-defined type whose underlying type is one of the types listed in Integer, you’ll get an error.

type Integer interface {
	int | int8 | int16 | int32 | int64 |
	uint | uint8 | uint16 | uint32 | uint64 | uintptr
}
type CusType int
const ct1 CusType = 10
const ct2 CusType = 20
fmt.Println(divAndRemainder(myA, myB))
// divAndRemainder's Type Constraint is using type Integer
// produces the following error:
// MyInt does not satisfy Integer (possibly missing ~ for int in Integer)

We can put ~ before the type term to make type term to be valid for any type that has the type term as its underlying type

type Integer interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64 |
	~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
}

Type Inference and Generics

TLDR: For type inference in Generics for functions, all the necessary types included in the function should be inside the argument. Thus, go can infer types from them. If one doesn't exist in the argument we need to be given explicitly.

Normally Go infers type from the argument you pass.
So, we don't have to explicitly write them.

Map(someSlice, fn) // Go infers T from someSlice — you don't write Map[int](...)

When it comes for the two type parameters, but exist only one in the argument and different type is exist in the return, go can't infer the return type as it is not included in the argument.
So, we need to explicitly give it.

func Convert[T1, T2 Integer](in T1) T2 { return T2(in) }
var a int = 10 b := Convert(a) // ❌ Go knows T1=int, but T2=??? — convert to what? int64? uint8? no idea
b := Convert[int, int64](a)   // ✅ you tell it: T1=int, T2=int64