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
- Build
Pair[T]first — e.g.Pair[Point2D]. _(Here Pair's own constraintfmt.Stringeris checked —Point2Dmust haveString().) - Pass those Pairs to
FindCloser—FindCloser(pair2Da, pair2Db). - FindCloser takes the type from
Pair[T]— it infersT = Point2Dby matching the argumentPair[Point2D]against the parameterPair[T]. - The function checks the constraint with its own
T Differ[T]— doesPoint2DsatisfyDiffer[Point2D]? (needsString()andDiff(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