Generic Functions

Updated

September 13, 2026

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
  • T is a type parameter.
  • Constraint is an interface (often a small one from cmp or a union of types). any means no extra methods. comparable means == is allowed. ~int means int or a defined type whose underlying type is int.
  • At the call site the compiler infers T from the arguments when it can. You write Name[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.go

Output:

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.go

Output:

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.go

Output:

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.go

Output:

""
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.go

Output:

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 (and maps, cmp) before you write a type parameter.
  • Name constraints after what the body does (comparable for ==, cmp.Ordered for <).
  • Use ~T for defined types with an underlying type you actually operate on.
  • Let inference work. Write [T] when the call has no value to infer from.
  • any means “I do not touch T except to store or return it.” If you compare or order, say so in the constraint.

Try this

  1. In shift_ids.go, replace the Contains prints with slices.Index. Print the index of 7 and of 9.
  2. Change double in cents.go to T ~int | ~int64 and call it with int64(150). Confirm Cents still works.
  3. Delete index from roster_index.go and call slices.Index instead. The output should match.