Go Generics Laboratory
Work through examples, try the reasoning yourself, then put it into practice.
View SlidesBefore you start
- Read Go slice/map types and write a non-generic stable deduplication loop.
What you will practice
- Choose comparable keys and preserve a caller-defined slice type.
- Trace first-seen retention and distinguish a shallow value copy from a deep clone.
In this lesson
Lesson
5 min readGo Generics Laboratory
This module moves from "what generics look like" to "how to write API-friendly generics".
1) Type parameters are constraints-first design
Start with constraints, not types.
- Are you deduplicating by keys? you need
comparable. - Are you calculating max/min? you need ordered operations.
- Are you transforming values? prefer narrow
~constraints if you need defined types with a particular underlying type.
If constraints are too broad, compile-time checks become weaker and code paths become less precise.
2) The ~ operator in type sets
~intmeans "any type whose underlying type isint."- This helps support
type MyInt intwithout forcing a cast.
Use this when you want APIs that feel custom-type friendly.
3) Two-parameter patterns with slice return types
For data-shaping helpers, one parameter often controls element type, one controls caller-facing defined type.
Example:
func F[S ~[]T, T any](in S) S
This preserves a defined slice type like:
type IDs []int
and returns IDs, not just []int.
4) API shape: keep semantics explicit
For generic helpers:
- keep argument shape obvious
- avoid hidden behavior
- document zero values and nil cases
The caller should infer why a result changed, not reverse engineer your constraints.
5) Performance notes for generics
Generics are not always slower and not always faster:
- optimization depends on the compiler, code shape, and concrete types,
anyas a type constraint does not itself box every value into an interface,- changing constraints alone does not guarantee an allocation or speed improvement.
Benchmark against hand-written versions if this is in a hot path.
6) Practical exercise
The module practice problem asks you to build DeduplicateByKey with:
- stable order retention,
- first-seen semantics,
- generic key extraction,
- custom slice type preservation via a second type parameter.
This demonstrates:
- higher-order constraints,
- map-backed dedupe,
- practical generic API ergonomics in real learner workflows.
7) Self-check before moving on
- Can your function accept defined types like
type RowSlice []Row? - Do you preserve first occurrence order?
- Does zero input return zero output predictably?
Trace identity separately from the retained value
Use type Row struct { ID int; Name string } and type Rows []Row. A key
function reads ID; T is Row, K is int and S is Rows. T need not itself be
comparable: a Row containing a slice could still be deduplicated by its integer ID.
| Input row | Key already seen? | Output after this row |
|---|---|---|
(4,"Ada") |
no | [(4,"Ada")] |
(2,"Lin") |
no | [(4,"Ada"),(2,"Lin")] |
(4,"Bea") |
yes | unchanged |
(3,"Kai") |
no | [(4,"Ada"),(2,"Lin"),(3,"Kai")] |
The map answers membership; the output slice records order. Storing rows in a map and later iterating it loses first-seen ordering. Updating the stored row when key 4 reappears also changes the retained value from Ada to Bea.
The loop invariant is: after processing the first i rows, output contains the first representative of each key in that prefix, in first-appearance order; seen contains exactly those keys. Derive both branches from that invariant: unseen keys add one representative; seen keys change neither structure.
Return shape is part of the API
S ~[]T allows the defined type Rows and preserves it as the return type.
A caller can keep using methods defined on Rows without a type assertion.
Returning []T would preserve elements but lose that named return type. The
constraint K comparable is required because keys are used in a map; any
permits arbitrary row types without promising map-key operations on the rows.
For empty input or a nil key function, the contract returns the zero slice S, which is nil. A caller may distinguish a nil slice from an allocated empty one. The reference allocates a separate output slice, but copying a Row is shallow: if it contains pointers, its pointed-to data is not automatically cloned. Generics change which types the API accepts; they do not supply deep-copy semantics.
Reconstruct before implementing
Change the key function in the table to the first letter of Name. Predict the
output after adding (9,"Ari") at the end. Then explain how to retain the
last value for each key while preserving first-key appearance order.
Check the changed identity
The original four names start with A, L, B and K, so all four original rows survive. Ari shares key A with Ada and is discarded. For the last-value variant, map each key to its output position; overwrite that position on later input instead of appending or sorting. That variant changes the exercise's first-value contract and should be implemented separately.
Implement DeduplicateByKey, then instantiate it with a defined slice type
and rows containing a non-comparable field. Explain why deduplicating by integer
ID still compiles and which type would fail if used directly as a map key.
Practice sequence
Use the order below for a guided path, or choose an item to revisit. A pass records a successful attempt; try a fresh example later to check what you can reconstruct.
- Step 1
Deduplicate with Generics
- Step 2
Go Generics Checkpoint
Type sets, underlying types, and practical generic API design.