Skip to main content

范型 Generics

Before Go 1.20

As we've mentioned, Go does not support classes. For a long time, that meant that Go code couldn't esily be reused in many circumstances. For example, imagine some code that splice into 2 equal parts. The code that splits the slice doesn't really care about the values stored in the slice.
Unfortunately in Go we would need to write it multiple times for each type, which is a very un-DRY thing to do.

func splitIntSlice(s []int) ([]int, []int) {
mid := len(s)/2
return s[:mid], s[mid:]
}
func splitStringSlice(s []string) ([]string, []string) {
mid := len(s)/2
return s[:mid], s[mid:]
}

In Go 1.20 however, suport for generics was released, effectively solving this problem!

Type Parameters (Generics in Go)

Put simply, generics allow us to use variables to refer to specific types. This is an amazing feature because it allows us to write abstract functions that drastically reduce code duplication.

func splitAnySlice[T any](s []T) ([]T, []T) {
mid := len(s)/2
return s[:mid], s[mid:]
}

In the example above, T is the name of the type parameter for the splitAnySlice function, and we've said that is must match the any constraint, which meas it can be anything. This makes sense because the body of the function doesn't care about the types of things stored in the slice.

 := splitAnySlice([]int{0, 1, 2, 3})
fmt.Println(firstInts, secondInts)

Tip: Zero value of a type

Creating a variable that's the zero value of a type is easy:

var myZeroInt int

It's the same with generics, we just have a variable that represents the type:

var myZero T

Example

func getLast[T any](s []T) T {
if len(s) == 0 {
var myZero T
return myZero
}
return s[len(s)-1]
}

Constraints

Somethimes you need to logic in your generic function to know something about the types it operates on. The example we used in the first exercise didn;t need to know anything about the types in the slice, so we used the bult-in any constraint:

func splitAnySlice[T any](s []T) ([]T, []T) {
mid := len(s)/2
return s[:mid], s[mid:]
}

Constraints are just interfaces that allow us to write generics that only operate within the constraint of a given interface type. Inthe example above, the any constraint is the same as the empty interface because it means the type in quesiton can be anything.

Create a custom constraint

Lest' take a look at the example of a concat function. It takes a slice of values and concatenates the values into a string. This should work with any type that can represent itself as string, event if it's not a string under the hood. For example, a user struct can have a .String() that returns a string with the user's name and age.

type stringer interface {
String() string
}

func concat[T stringer](vals []T) string {
result := ""
for _, val := range vals {
// this is where the .String() method
// is used. That's why we need a more specific
// constraint instead of the any constraint
result += val.String()
}
return result
}

Example

type lineItem interface {
GtCost() float64
GetName() string
}
func chargeForLineItem[T lineItem] (
newItem T,
oldItems []T,
balance float64,
) ([]T, float64, error) {
newBalance := balance - newItem.GetCost()
if newBalance < 0.0 {
return nil, 0.0, error.New("insufficient funds")
}
oldItems = append(oldItems, newItem)
return oldItems, newBalance, nil
}

Questions

Why generics?

  • Generics reduce repetitive code
  • Generics are used more often in libraries and packages

Why did it take so long to get generics?

Go palces an emphasis on simplicity. In other words, Go has purposefully left out many features to provide its best feature: being simple and easy to work with.

Interface type lists

When generics were released, a new way of writing interfaces was also released at the same time!

We can now simply list a bunch of types to get a new interface/constraint.

// ordered is a type constraint that matches any ordered type.
// An ordered type is ont that supports the <, <=, > and >= operators.
type Ordered interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~unit | ~unit8 | ~unit16 | ~unit32 | ~unit64 | ~unitptr |
~float32 | ~float64 |
~string
}

Parametric constraints

You interface definitions, which can later be used as constraints, can accept type parameters as well.

Naming generic types

Let's look at this simple example again:

func splitAnySlice[T any](s []T) ([]T, []T) {
mid := len(s)/2
return s[:mid], s[mid:]
}

Remember, T is just a variable name, we could have named the type parameter anything. T happens to be a fairly common convention for a type variable, similar to how i is a convention for index variables in loops.

This is just as valid:

func splitAnySlice[MyAnyType any](s []MyAnyType) ([]MyAnyType, []MyAnyType) {
mid := len(s)/2
return s[:mid], s[mid:]
}