go go/book/learninggo

History of Generics in go and More about generics👇

Before generics

Using interface{} in the Past
Types for a and b are checked only at runtime, increasing the possibility of errors.

func Add(a, b interface{}) interface{} {
    return a.(int) + b.(int)  // Requires type assertion and is unsafe
}

With Generics

Generics, with compile-time type checking, solve this problem.

func Add[T Addable](a, b T) T {
    return a + b  // Type-safe
}


Generics allow you to write code that works with any type without sacrificing type safety. For example, instead of creating separate functions for int slices and string slices, you can write a single, generic function.

Type Parameters

placeholder for a specific type.

// Define the function f with a type parameter T
func f[T any](t T) {
  // ...
}
 
// ...
 
// Call the function with a concrete type
f[int](1)
 
// Define a struct with a type parameter T
type s[T any] struct {
  t T
}
 
// Instantiate the struct with a concrete type
myInstance := s[int]{t: 1}

Type Constraints

Type constraints specify what kinds of types a type parameter can accept. These are defined using interfaces.

any Predeclared Identifier

The introduction of generics also adds a small, but not insignificant, new type alias (a.k.a. predeclared identifier) as a replacement for the empty interface (interface{}). The new keyword (any) is fully backwards compatible with the empty interface and can be used as a replacement for it.
[!video]- Go - Generics

// Define a custom constraint that includes int, float64, and string
type NumericOrString interface {
	int | float64 | string
}
 
// Generic function using the custom constraint
func AddValues[T NumericOrString](a, b T) T {
	return a + b
}
 
func main() {
	// Add integers
	fmt.Println("Integer addition:", AddValues(10, 20)) // Output: 30
 
	// Add floating-point numbers
	fmt.Println("Float addition:", AddValues(1.5, 2.3)) // Output: 3.8
 
	// Concatenate strings
	fmt.Println("String concatenation:", AddValues("Go", "Lang")) // Output: GoLang
}

Built-in Constraints

Go has built-in type constraints like any, meaning any type can be used as a parameter.

func PrintSlice[T any](s []T) {
    for _, v := range s {
        fmt.Println(v)
    }
}

Custom Constraints

Beyond built-in constraints, Go allows developers to define their constraints. This is usually done through interfaces.

// Addable allows only int or float64 types
type Addable interface {
    int | float64
}
 
func Add[T Addable](a, b T) T {
    return a + b
}

constraints package

go get golang.org/x/exp/constraints

  • Signed: all signed integers

    • ~int | ~int8 | ~int16 | ~int32 | ~int64

    • ex: -10, 10

  • Unsigned: all signed integers

    • ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr

    • ex: 42

  • Integer, the union of Signed and Unsigned

    • Signed | Unsigned
  • Float all floating point numbers

    • ~float32 | ~float64
  • Complex all complex numbers

    • ~complex64 | ~complex128
  • Ordered all types that we can order

    • Integer | Float | ~string

Underlying Types

  • every type has an underlying type. For pre-defined types like int, float64, etc., the underlying type is themselves. For defined types (like type MyInt int), the underlying type is the type defined before.
  • The ~ symbol is used to represent all types that have the same underlying type as the specified type. When you use the ~ symbol in the constraints of a type parameter, you specify a set of types that include all types with the same underlying type as the specified type in the constraint.
// The underlying types of the following ones are both int.
type (
	MyInt int
	Age   MyInt
)
 
// The following new types have different underlying types.
type (
	IntSlice   []int   // underlying type is []int
	MyIntSlice []MyInt // underlying type is []MyInt
	AgeSlice   []Age   // underlying type is []Age
)
 
// The underlying types of []Age, Ages, and AgeSlice
// are all the unnamed type []Age.
type Ages AgeSlice

Generic Functions

package main
 
import "fmt"
 
// Generic function to find the largest element in a slice
func FindMax[T comparable](items []T) T {
	max := items[0]
	for _, item := range items {
		if item > max {
			max = item
		}
	}
	return max
}
 
func main() {
	fmt.Println(FindMax([]int{1, 2, 3, 4, 5}))    // 5
	fmt.Println(FindMax([]string{"a", "b", "c"})) // c
}

Info

The new comparable keyword, in Go 1.18, was added for specifying types that can be compared with the == and != operators.

Comparable types include: structs, pointers, interfaces, channels, and builtin types. comparable can also be embedded in other constraints since it is a constraint.

Generic Structures

package main
 
import "fmt"
 
// Generic type: Pair
type Pair[K, V any] struct {
	Key   K
	Value V
}
 
func main() {
	p := Pair[string, int]{Key: "Age", Value: 30}
	fmt.Printf("Key: %s, Value: %d\n", p.Key, p.Value)
}
func (b Box[T]) Empty() bool {
    return b.Content == nil
}

Generic Methods

package main
 
import "fmt"
 
// A generic type with a method
type Container[T any] struct {
	Items []T
}
 
func (c *Container[T]) Add(item T) {
	c.Items = append(c.Items, item)
}
 
func main() {
	c := Container[int]{}
	c.Add(42)
	fmt.Println(c.Items) // [42]
}

  • The square brackets ([]) specify a type parameter list for the function.
  • The type constraint (int | float64) specifies that N can only be one of the types listed: either int or float64.
package main
 
import "fmt"
 
func genericAddNums[N int | float64](n1, n2 N) N {
    return n1+n2
}
 
func main() {
    fmt.Println(genericAddNums(1, 2))
    fmt.Println(genericAddNums(1.1, 2.2))
}

Type Lists

Type Unions

Go generics allow the use of type unions, specifying multiple allowed types in a constraint.

type Numeric interface {
    int | float64
}
 
func Sum[T Numeric](s []T) T {
    var total T
    for _, v := range s {
        total += v
    }
    return total
}

The Numeric constraint allows int and float64 types, making the Sum function operable on slices of these two types.

Multiple Constraints

type Serializable interface {
    json.Marshaler | xml.Marshaler
}

Go - Data Serialization

Interaction between Generics and Interfaces

Generics as Methods of Interfaces

You can define methods that include generics in interfaces.

type Container[T any] interface {
    Add(element T)
    Get(index int) T
}

The Container interface can be used for any type of container, such as a slice, list, or custom container type, as long as these containers implement the Add and Get methods.