Go / Advanced: Safety, Performance, and Service Design / Lesson 15
Generics and Type Constraints
Use type parameters to express genuinely shared algorithms while preserving the operations and ownership each type permits.
On this page
Lesson goals
- Explain type sets, underlying-type terms, and comparable.
- Design a generic function with a justified operation set.
- Choose generics versus interfaces without erasing semantics.
A constraint limits permitted operations
Go introduced type parameters in Go 1.18. A generic declaration names type parameters with constraints describing allowed types and operations. The compiler checks the body for all types admitted by the constraint, not merely the first type used at a call. any is an alias for interface{} and supplies no arithmetic operations by itself.
A constraint can combine method requirements and type terms. The term ~int includes types whose underlying type is int; int alone names the exact type in that type-set position. Constraint interfaces with type terms are not ordinary runtime value interfaces. Keep the permitted set as narrow as the algorithm’s actual requirements.
Preserve a named numeric type
The first example accepts int-like types and sums a slice. A named Count type satisfies the ~int constraint and the result remains Count. Type inference obtains the type argument from the supplied slice. The fixed values are small; a generic declaration does not remove overflow concerns or create a universal checked-sum policy.
The function accepts nil and empty slices, returning the zero value of T as the additive identity for this chosen type set. If a domain requires rejecting empty input or saturating overflow, that is a different contract. Type reuse should not conceal domain-specific semantics behind a familiar operation name.
package main
import "fmt"
type IntLike interface{ ~int }
type Count int
func Sum[T IntLike](values []T) T {
var total T
for _, value := range values {
total += value
}
return total
}
func main() {
total := Sum([]Count{2, 3, 4})
fmt.Printf("%T %v\n", total, total)
}
main.Count 9
Count has underlying type int, so inference selects Count and produces a Count value of nine.
Equality is a semantic requirement
The comparable constraint permits == and != for admitted types and is appropriate for map keys in a generic set. The second example removes repeated string values while preserving their first-occurrence order. It uses a map for membership but appends in input order, so no map-iteration order appears in the output.
Comparable does not mean every equality is a useful domain equality. Floating NaN is not equal to itself, and interface values can contain dynamic values that are not comparable; comparing such values can panic. Since Go 1.20, comparable’s constraint-satisfaction rules permit some interface type arguments, so callers must still respect dynamic comparability when those types are used.
package main
import "fmt"
func Unique[T comparable](input []T) []T {
seen := make(map[T]struct{})
output := make([]T, 0, len(input))
for _, value := range input {
if _, ok := seen[value]; ok {
continue
}
seen[value] = struct{}{}
output = append(output, value)
}
return output
}
func main() { fmt.Println(Unique([]string{"b", "a", "b", "c", "a"})) }
[b a c]
Membership uses the map, while output follows the input sequence b, a, c.
Choose the abstraction that carries the right promise
Use a behavioral interface when implementations differ but consumers need a small method contract, such as io.Reader. Use type parameters when the same algorithm or data structure should preserve static relationships among several types. A generic type can still accept interfaces as dependencies; the choices are complementary rather than competing fashions.
Avoid parameterizing every helper merely to reduce a few duplicated lines. Reuse is valuable when the shared semantics are stable. Copy behavior, error policy, ordering, and resource ownership remain part of a generic API. A generic slice clone still copies reference-bearing elements shallowly unless a deeper policy is supplied.
Keep generic syntax inside the declared baseline
This course uses generic functions and types available in Go 1.22. Do not assume every feature in the current specification existed then: newer releases can expand generic syntax. For example, the reviewed Go 1.27 release notes describe generic methods; those are outside this course’s baseline, so examples use package-level generic functions instead.
Test representative concrete types, named underlying types, empty collections, and semantic edge cases. Compile failures for disallowed operations are useful design feedback but should remain non-runnable fragments. Performance is implementation-dependent; generics do not promise automatic speed gains or one universal code-generation strategy.
Common errors and fixes
A generic body cannot add T values constrained by any.
- Why it happens
- The constraint provides no numeric operation guarantee.
- How to fix it
- Use a justified numeric type set or inject behavior.
- Prevent it next time
- Derive constraints from required operations.
A named int type fails an exact int constraint.
- Why it happens
- The type set names int instead of underlying-int types.
- How to fix it
- Use ~int when that wider set is intended.
- Prevent it next time
- Test a defined domain type, not only predeclared types.
A generic set panics with an interface element.
- Why it happens
- Its dynamic value is not comparable.
- How to fix it
- Restrict the accepted domain or validate a documented representation.
- Prevent it next time
- Do not equate static interface acceptance with dynamic comparability.
Check your understanding
1. Why does Unique preserve input order even though a map is involved?
Show worked solution
The function never ranges over the map. It checks membership while scanning the input in order and appends only the first occurrence to output. The map’s unspecified iteration order is therefore irrelevant. The proof depends on stable input values and the documented equality semantics; concurrent mutation of reference-bearing data is a separate concern.
2. A generic function clones []Record where Record contains a map. What does element copying guarantee, and what remains shared?
Show worked solution
It creates independent outer slice storage and copies each Record value. The map fields still refer to the same underlying maps, so updates through either record can be observed by the other. Provide an element-cloning callback or an explicit nested-copy operation if independence is required. Generics preserve types, not automatic recursive ownership semantics.
Key takeaways
- Constraints specify the operations valid for every admitted type.
- Type preservation does not erase domain or ownership requirements.
- Check release-specific generic features against the module’s language baseline.