Generic Data Structures: Sets, Queues, Maps
Generics let you ship typed sets, queues, and map helpers once per module instead of maintaining StringSet, IntSet, and go generate output.
Summary
A generic set is usually map[T]struct{} with comparable keys.
Queues wrap slices with head/tail indices or use ring buffers for fixed capacity.
Map helpers centralize safe get-or-default, copy, and transform logic with two type parameters K and V.
Keep exported container APIs small and document zero-value behavior.
Recipe
Quick-reference recipe card - copy-paste ready.
type Set[T comparable] map[T]struct{}
func NewSet[T comparable](vals ...T) Set[T] {
s := make(Set[T], len(vals))
for _, v := range vals {
s.Add(v)
}
return s
}
func (s Set[T]) Add(v T) { s[v] = struct{}{} }
func (s Set[T]) Has(v T) bool { _, ok := s[v]; return ok }When to reach for this:
- Duplicate
map[string]struct{}patterns across packages. - FIFO work queues inside workers without
interface{}casts. - Internal libraries shared by HTTP handlers and gRPC services.
Working Example
package main
import (
"fmt"
)
type Set[T comparable] map[T]struct{}
func (s Set[T]) Add(v T) { s[v] = struct{}{} }
func (s Set[T]) Has(v T) bool {
_, ok := s[v]
return ok
}
type Queue[T any] struct {
buf []T
}
func (q *Queue[T]) Enqueue(v T) { q.buf = append(q.buf, v) }
func (q *Queue[T]) Dequeue() (T, bool) {
if len(q.buf) == 0 {
var zero T
return zero, false
}
v := q.buf[0]
q.buf = q.buf[1:]
return v, true
}
func GetOr[K comparable, V any](m map[K]V, k K, def V) V {
if v, ok := m[k]; ok {
return v
}
return def
}
func main() {
s := make(Set[string])
s.Add("go")
fmt.Println(s.Has("go"), s.Has("rust"))
var q Queue[int]
q.Enqueue(1)
q.Enqueue(2)
v, _ := q.Dequeue()
fmt.Println(v)
m := map[string]int{"a": 1}
fmt.Println(GetOr(m, "b", 0))
}What this demonstrates:
Set[T]requirescomparablefor map keys.Queue[T]usesanybecause elements are only stored and moved.GetOrseparates key comparability from value type flexibility.
Deep Dive
How It Works
- Generic named types (
type Set[T comparable] map[T]struct{}) create a distinct named type per instantiation. - Methods attach to the generic type and share its parameters.
- Under the hood, the compiler monomorphizes each instantiation you use in the package graph.
- Zero values are usable:
var q Queue[int]is an empty queue;make(Set[string])allocates the map.
Container Patterns
| Structure | Constraint | Backing | Notes |
|---|---|---|---|
| Set | comparable | map[T]struct{} | Empty struct values use no extra value memory |
| Queue (simple) | any | Slice with slice re-slicing | Amortized O(1) enqueue; dequeue shifts or copies head |
| Queue (ring) | any | Fixed []T + indices | Better for known max depth |
| Stack | any | Slice | Same as LIFO append/pop |
| Map helper | K comparable, V any | Caller map | No ownership - pure functions |
Go Notes
// Ring queue avoids O(n) dequeue from slice reslice at scale
type RingQueue[T any] struct {
buf []T
head int
tail int
n int
}
func NewRingQueue[T any](cap int) *RingQueue[T] {
return &RingQueue[T]{buf: make([]T, cap)}
}- For concurrent sets, wrap with
sync.Mutexor usesync.Mapwhen keys areanyand contention patterns fit. - JSON serialization of generic sets usually requires custom
MarshalJSONper instantiation or helper functions - maps marshal fine when keys are strings. - Consider
container/listonly when you need intrusive links; generics plus slices cover most service queues.
Gotchas
- Dequeuing with slice
buf = buf[1:]- Keeps backing array alive, leaking memory on long-lived queues. Fix: use ring buffer or periodic copy whenlen<<cap. - Set as map without initialization - Zero value
Set[T]is nil map; writes panic. Fix:make(Set[T])or constructor. - Non-comparable element in set - Slice-valued keys fail compilation. Fix: hash key type or use
map[uint64]struct{}. - Exporting generic containers too early - API lock-in across modules. Fix: keep unexported until shape stabilizes.
- Deep copy omitted -
map[K]*Vhelpers copy pointers, not pointed-to data. Fix: document shallow semantics or clone values. - TinyGo/WASM size - Many instantiations bloat WASM. Fix: limit instantiations or use non-generic paths on small targets.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
go generate per type | Maximum control, no bracket syntax in API | Few types, team prefers generics |
map[string]struct{} only | Single string set | Multiple element types |
container/heap | Priority queue semantics | Simple FIFO |
| Channel buffer queue | Goroutine handoff | Single-threaded batching |
FAQs
Why map[T]struct{} for sets?
Empty struct values occupy zero bytes.
Membership is O(1) average with good hash properties on T.
Should queues be generic in public APIs?
Often keep queues internal.
Export behavior (batch processor), not Queue[T] types, unless the library is a data-structure kit.
How do I iterate a Set?
for k := range s on the underlying map pattern.
Add a Values() method returning []T if callers need slices.
Are generic sets thread-safe?
No - standard maps are not safe for concurrent writes.
Add a mutex wrapper or use concurrent maps.
Can I serialize Set[T] to JSON?
Convert to []T for marshaling.
map[T]struct{} does not JSON-marshal as an array by default.
Queue vs channel?
Channels synchronize goroutines.
Generic queues suit in-memory batching inside one goroutine or with external locking.
How many type parameters for maps?
K comparable and V any cover most helpers.
Add a third parameter only for transformation pipelines.
Does Set need pointers?
No for small comparable values.
Use pointers as elements when values are large and identity matters.
What about ordered sets?
Use a map plus sorted slice cache, or tree structures from third-party packages.
Generics do not imply ordering.
When is codegen still better?
Dozens of instantiations on tiny embedded targets where binary size dominates.
Measure before choosing.
Can I embed Set in other generics?
Yes - compose Set[T] inside Graph[N comparable] style types.
Watch compile time and binary size.
How do I test generic containers?
Table tests with multiple instantiations (Set[int], Set[string]) in the same package test file.
Related
- Type Sets & the comparable Constraint - set key requirements
- Generics Basics - stack example
- Generic Algorithms: slices, maps & cmp Packages - map copy helpers
- Performance Implications of Generics - instantiation cost
- When to Avoid Generics in Go APIs - export guidance
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).