Generic Functions
Generic Functions
A generic function is a function whose signature names types the caller supplies. The boring default is not to write one. The boring default is to call slices.Contains, slices.Clone, slices.Index, and friends. Write your own when the standard library does not already do the job.
Mental model
The syntax is:
func Name[T Constraint](args) results
Tis a type parameter.Constraintis an interface (often a small one fromcmpor a union of types).anymeans no extra methods.comparablemeans==is allowed.~intmeansintor a defined type whose underlying type isint.- At the call site the compiler infers
Tfrom the arguments when it can. You writeName[int](...)only when inference fails.
The body is type-checked once against the constraint, then instantiated per concrete type. There is no boxing and no runtime type switch unless you add one.
Worked examples
Case 1: The standard library is the helper
Ticket IDs on a shift. Need “is 7 on this list?” and “a copy I can sort without touching the original.” Do not invent ContainsInt.
Save as shift_ids.go:
// shift_ids.go
package main
import (
"fmt"
"slices"
)
func main() {
ids := []int{3, 7, 12}
fmt.Println("has 7:", slices.Contains(ids, 7))
fmt.Println("has 9:", slices.Contains(ids, 9))
copy := slices.Clone(ids)
copy[0] = 99
fmt.Println("original", ids)
fmt.Println("clone ", copy)
}Run:
go run shift_ids.goOutput:
has 7: true
has 9: false
original [3 7 12]
clone [99 7 12]
slices.Clone copies the slice header and the elements. Nested slices and maps inside elements are still shared; for integers that does not matter. slices.Contains requires comparable elements, which int is.
Case 2: comparable when you must write it
A tiny “index of this name on the roster.” slices.Index already exists; this listing is so you can see a constraint in a function you own.
Save as roster_index.go:
// roster_index.go
package main
import "fmt"
func index[T comparable](list []T, want T) int {
for i, v := range list {
if v == want {
return i
}
}
return -1
}
func main() {
roster := []string{"Amina", "Bo", "Chen"}
fmt.Println("Bo at", index(roster, "Bo"))
fmt.Println("Dee at", index(roster, "Dee"))
fmt.Println("table 12 at", index([]int{3, 7, 12}, 12))
}Run:
go run roster_index.goOutput:
Bo at 1
Dee at -1
table 12 at 2
Inference picks T from list and want together. After you have typed this once, delete it and use slices.Index. Same contract, same -1, maintained by the Go team.
Case 3: ~T for defined types
The desk stores money as Cents, not raw int, so you cannot pass a Cents to a function that demands int. The approximation ~int means “int or anything declared as type X int.”
Save as cents.go:
// cents.go
package main
import "fmt"
type Cents int
func double[T ~int](v T) T {
return v * 2
}
func main() {
tip := Cents(150)
fmt.Println(double(tip))
fmt.Println(double(8))
}Run:
go run cents.goOutput:
300
16
If the constraint were int instead of ~int, double(tip) would not compile. ~ is how defined types keep their name while still using integer algorithms. Do not sprinkle ~ on every parameter. Use it when a defined type is a real part of the desk (money, IDs, table numbers) and the algorithm is numeric.
Case 4: Inference, and when to write the type
Most calls need no brackets. Composite literals, conversions, and channel sends can also infer in Go 1.27. This program shows a call that infers, a call that needs an explicit type (no arguments to infer from), and a slice of function values that infers in the literal.
Save as infer.go:
// infer.go
package main
import "fmt"
func zero[T any]() T {
var v T
return v
}
func label[T any](v T) string {
return fmt.Sprintf("ticket:%v", v)
}
func main() {
fmt.Printf("%q\n", zero[string]())
fmt.Println(zero[int]())
fmt.Println(label(41))
printers := []func(int) string{label}
fmt.Println(printers[0](41))
}Run:
go run infer.goOutput:
""
0
ticket:41
ticket:41
zero() cannot infer: there are no arguments. You must write zero[string](). label(41) infers T as int. A composite literal of type []func(int) string infers T as int (Go 1.27). append(s, label) still needs label[int] — inference there is not a composite literal. If inference is ugly, write the type argument. Clarity beats a puzzle.
The trap
Hand-rolling what slices already provides, then getting the edge cases wrong. This “clone” shares the backing array.
Save as fake_clone.go:
// fake_clone.go
package main
import "fmt"
func fakeClone[T any](s []T) []T {
return s[:]
}
func main() {
ids := []int{3, 7, 12}
copy := fakeClone(ids)
copy[0] = 1
fmt.Println("original", ids)
fmt.Println("copy ", copy)
}Run:
go run fake_clone.goOutput:
original [1 7 12]
copy [1 7 12]
s[:] reuses the backing array. Writing copy[0] also writes ids[0]. slices.Clone allocates a new array. Use it. Write a generic slice helper only when you have a desk-specific rule the standard library cannot express.
The boring rule
- Import
slices(andmaps,cmp) before you write a type parameter. - Name constraints after what the body does (
comparablefor==,cmp.Orderedfor<). - Use
~Tfor defined types with an underlying type you actually operate on. - Let inference work. Write
[T]when the call has no value to infer from. anymeans “I do not touchTexcept to store or return it.” If you compare or order, say so in the constraint.
Try this
- In
shift_ids.go, replace theContainsprints withslices.Index. Print the index of7and of9. - Change
doubleincents.gotoT ~int | ~int64and call it withint64(150). ConfirmCentsstill works. - Delete
indexfromroster_index.goand callslices.Indexinstead. The output should match.