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 Tfunc f[T any](t T) { // ...}// ...// Call the function with a concrete typef[int](1)// Define a struct with a type parameter Ttype s[T any] struct { t T}// Instantiate the struct with a concrete typemyInstance := 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 stringtype NumericOrString interface { int | float64 | string}// Generic function using the custom constraintfunc 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 typestype Addable interface { int | float64}func Add[T Addable](a, b T) T { return a + b}
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 mainimport "fmt"// Generic function to find the largest element in a slicefunc 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 mainimport "fmt"// Generic type: Pairtype 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)}
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.