Generics in Go
| Generic functions | |
| Constraints with comparable | |
| Generic structs | |
| When generics pay off |
Generic functions
A generic function declares type parameters in [square brackets] before the regular parameter list. The caller passes ordinary values, the compiler fills in the types, and one body serves every matching type.
package main
import "fmt"
func First[T any](s []T) (T, bool) {
if len(s) == 0 {
var zero T
return zero, false
}
return s[0], true
}
func main() {
n, ok := First([]int{1, 2, 3})
fmt.Println(n, ok)
w, ok2 := First([]string{})
fmt.Println(w, ok2)
}
First works for slices of ints, strings, and anything else, returning the zero value plus false for empty input. You may spell the type out as First[int], but inference usually fills it in for you.
Constraints with comparable
A constraint limits which types a parameter accepts, so the body can use operations those types support. The predeclared comparable allows exactly the types that work with double equals, which covers searching, keys, and deduplication.
package main
import "fmt"
func IndexOf[T comparable](s []T, v T) int {
for i := range s {
if s[i] == v {
return i
}
}
return -1
}
func main() {
fmt.Println(IndexOf([]string{"a", "b"}, "b"))
fmt.Println(IndexOf([]int{4, 5}, 9))
}
Because T must be comparable, the double equals inside the body always compiles. Slices themselves are not comparable, so the compiler rejects them at the call site instead of deep inside your logic.
Generic structs
Structs take type parameters too, which makes containers reusable without giving up static types. Methods repeat the parameters on the receiver, and each instantiation, such as Stack[int], is its own concrete type.
package main
import "fmt"
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(v T) {
s.items = append(s.items, v)
}
func (s *Stack[T]) Pop() (T, bool) {
if len(s.items) == 0 {
var zero T
return zero, false
}
last := len(s.items) - 1
v := s.items[last]
s.items = s.items[:last]
return v, true
}
func main() {
st := Stack[int]{}
st.Push(10)
st.Push(20)
fmt.Println(st.Pop())
fmt.Println(st.Pop())
}
One Stack definition serves ints, strings, and your own structs, with Push and Pop type-checked for each. Prefer pointer receivers for containers so every method shares the same backing slice.
When generics pay off
Generics pay off when one algorithm truly serves many types: searching, ordering, sets, and containers. Custom constraints name the operations you need, such as comparison or ordering, and keep the body honest about what it uses.
package main
import "fmt"
type Ordered interface {
~int | ~float64 | ~string
}
func Max[T Ordered](a, b T) T {
if a > b {
return a
}
return b
}
func main() {
fmt.Println(Max(3, 7))
fmt.Println(Max(2.5, 1.5))
fmt.Println(Max("a", "b"))
}
Max compares ints, floats, and strings with one body, while other types fail fast at the call. If only one or two types ever qualify, plain functions or small interfaces stay simpler to read.
Next: Packages and imports
Article author: Arthur Isaev