Sometimes the same algorithm works for several types without changing its steps. A stack can store integers or strings; a total can add several numeric types. Generics let you express that shared structure while keeping the compiler's type checks. They are useful when the algorithm is the same and only the types vary.
A type parameter is a placeholder for a type supplied by the caller. Its constraint describes which types are allowed and therefore which operations the implementation can use. The compiler checks the generic body against the constraint, rather than waiting for a surprising value at runtime.
Run a generic function and container
Create a fresh module and save this complete program as main.go:
package main
import "fmt"
type Number interface {
~int | ~int64 | ~float64
}
func Total[T Number](values []T) T {
var total T
for _, value := range values {
total += value
}
return total
}
type Points int
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(value T) {
s.items = append(s.items, value)
}
func (s *Stack[T]) Pop() (T, bool) {
var zero T
if len(s.items) == 0 {
return zero, false
}
last := len(s.items) - 1
value := s.items[last]
s.items[last] = zero
s.items = s.items[:last]
return value, true
}
func main() {
fmt.Println("integers:", Total([]int{1, 2, 3}))
fmt.Println("points:", Total([]Points{5, 10}))
var stack Stack[string]
stack.Push("types")
stack.Push("generics")
for i := 0; i < 3; i++ {
value, ok := stack.Pop()
fmt.Printf("%q %t\n", value, ok)
}
}
Run go run .. Expected output:
integers: 6
points: 15
"generics" true
"types" true
"" false
Total[T Number] declares a type parameter named T. The parameter values is a slice of that type, and the result has the same type. Total([]int{...}) lets Go infer T from the argument. You can also spell it explicitly as Total[int](...). The official generics tutorial introduces constraints and type inference.
Read a constraint as a set of types
In Number, the vertical bars combine alternatives. The tilde means types with the specified underlying type are included. Points is a distinct defined type whose underlying type is int, so it is allowed by ~int. Without the tilde, an int term alone would not include Points.
This deliberately small constraint includes three numeric families. It does not claim to cover every number type. Adding a new type to the constraint is a design choice: every operation in the function must remain valid for every allowed type. With T any, for example, total += value would fail to compile because addition is not available for all types.
An interface containing type terms such as these is used as a constraint, not as an ordinary runtime variable type. Behavioral interfaces from the previous lesson and constraints share syntax, but serve different purposes here. See the type-set specification for the exact rules.
Understand the generic stack
Stack[T any] needs no arithmetic or equality operations on its elements, so any is enough. Stack[string] and Stack[int] are distinct instantiated types. An empty stack is useful immediately: its nil slice can accept appends.
Pop returns a value and a boolean because the zero value might itself be a valid stored item. "", false means the stack was empty; "", true could mean an empty string was stored. var zero T obtains the zero value without knowing whether T is a string, pointer, or struct.
Clearing the removed element prevents unused backing-array slots from unnecessarily retaining references for reference-containing element types. Shortening a slice alone does not overwrite its previous elements. This is a memory-lifetime detail, not a promise of a particular memory usage or benchmark result.
Choose generics only when they clarify
Use a behavioral interface when code needs an operation such as Write or Read across different implementations. Use generics when preserving a relationship between types matters, such as “accept and return the same element type.” Keep a concrete function when there is only one real type to support. The Go team's guidance on when to use generics discusses this choice.
Common mistakes include constraining more than needed, using any and then repeatedly asserting concrete types, or rewriting existing standard library functionality without a reason. Generics do not promise faster code. Also, ordinary methods cannot introduce their own additional type parameter list; a generic receiver uses its type's parameters, as Stack[T] does.
Exercise: remove duplicates while keeping order
Write Unique[T comparable](values []T) []T. Return the first occurrence of each value in input order. For []string{"go", "test", "go"}, expect []string{"go", "test"}. Also try integers and empty input.
Solution guidance
Create seen := make(map[T]bool) and an output slice. For each value, skip it if already present; otherwise mark it and append it. The comparable constraint allows the map key operation. Do not build the result by ranging over the map, because that loses input order. A slice of slices is unsuitable for this constraint. For this exercise, use concrete comparable element types such as strings and integers rather than interface values that might hold non-comparable data.
Previous: Error Handling. Next: Concurrency and Context.

