Go by Example: Generics
Go 1.23
Write flexible, reusable code that works with any data type using Generics (introduced in Go 1.18). This example shows how to define generic functions and types with type constraints.
Code
package main
import "fmt"
// MapKeys takes a map of any type and returns a slice of its keys
// K is constrained to comparable types (valid map keys)
// V can be any type
func MapKeys[K comparable, V any](m map[K]V) []K {
r := make([]K, 0, len(m))
for k := range m {
r = append(r, k)
}
return r
}
// List is a singly-linked list with values of any type
type List[T any] struct {
head, tail *element[T]
}
type element[T any] struct {
next *element[T]
val T
}
func (l *List[T]) Push(v T) {
if l.tail == nil {
l.head = &element[T]{val: v}
l.tail = l.head
} else {
l.tail.next = &element[T]{val: v}
l.tail = l.tail.next
}
}
func (l *List[T]) GetAll() []T {
var elems []T
for e := l.head; e != nil; e = e.next {
elems = append(elems, e.val)
}
return elems
}
func main() {
var m = map[int]string{1: "2", 2: "4", 4: "8"}
fmt.Println("Keys:", MapKeys(m))
lst := List[int]{}
lst.Push(10)
lst.Push(13)
lst.Push(23)
fmt.Println("List:", lst.GetAll())
}Explanation
Generics, introduced in Go 1.18, enable you to write functions and data structures that are agnostic to the specific types they operate on, without sacrificing type safety. This is achieved through Type Parameters, which act as placeholders for actual types that are specified (or inferred) when the code is used. Generics eliminate the need for code duplication (writing the same function for int, float, etc.) and reduce reliance on interface{}/reflection, which can be slow and unsafe.
Key concepts in Go Generics:
- Type Parameters: Defined in square brackets
[T any]immediately after the function or type name. - Constraints: Interfaces that define the set of permissible types. The
anyconstraint allows any type, whilecomparablerestricts types to those that support==and!=(essential for map keys). - Type Inference: In many cases, the compiler can automatically deduce the type arguments from the function arguments, making the calling syntax cleaner (e.g.,
MapKeys(m)instead ofMapKeys[int, string](m)).
Code Breakdown
8
Defining a generic function with type parameters [K comparable, V any]. "K" represents the map key type and is constrained to "comparable" because map keys must be hashable/comparable. "V" can be any type.
17
Defining a generic struct "List[T any]". "T" is a type parameter that will be replaced by a concrete type (like int or string) when the list is instantiated.
26
Method definitions for generic types must also declare the type parameters in the receiver: (l *List[T]). This binds the method to the specific instantiation of the List.
45
Calling the generic function MapKeys(m). The compiler infers that K=int and V=string based on the type of the argument "m". You don't need to write MapKeys[int, string](m).
48
Instantiating the generic struct with a concrete type: List[int]. This creates a List where "T" is replaced by "int" everywhere, ensuring type safety (you can't Push a string into this list).

