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.
Search across all documentation pages
Generics let you ship typed sets, queues, and map helpers once per module instead of maintaining StringSet, IntSet, and go generate output.
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.
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:
map[string]struct{} patterns across packages.interface{} casts.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] requires comparable for map keys.Queue[T] uses any because elements are only stored and moved.GetOr separates key comparability from value type flexibility.type Set[T comparable] map[T]struct{}) create a distinct named type per instantiation.var q Queue[int] is an empty queue; make(Set[string]) allocates the map.| 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 |
// 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)}
}sync.Mutex or use sync.Map when keys are any and contention patterns fit.MarshalJSON per instantiation or helper functions - maps marshal fine when keys are strings.container/list only when you need intrusive links; generics plus slices cover most service queues.buf = buf[1:] - Keeps backing array alive, leaking memory on long-lived queues. Fix: use ring buffer or periodic copy when len << cap.Set[T] is nil map; writes panic. Fix: make(Set[T]) or constructor.map[uint64]struct{}.map[K]*V helpers copy pointers, not pointed-to data. Fix: document shallow semantics or clone values.
| 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 |
Empty struct values occupy zero bytes.
Membership is O(1) average with good hash properties on T.
Often keep queues internal.
Export behavior (batch processor), not Queue[T] types, unless the library is a data-structure kit.
for k := range s on the underlying map pattern.
Add a Values() method returning []T if callers need slices.
No - standard maps are not safe for concurrent writes.
Add a mutex wrapper or use concurrent maps.
Convert to []T for marshaling.
map[T]struct{} does not JSON-marshal as an array by default.
Channels synchronize goroutines.
Generic queues suit in-memory batching inside one goroutine or with external locking.
K comparable and V any cover most helpers.
Add a third parameter only for transformation pipelines.
No for small comparable values.
Use pointers as elements when values are large and identity matters.
Use a map plus sorted slice cache, or tree structures from third-party packages.
Generics do not imply ordering.
Dozens of instantiations on tiny embedded targets where binary size dominates.
Measure before choosing.
Yes - compose Set[T] inside Graph[N comparable] style types.
Watch compile time and binary size.
Table tests with multiple instantiations (Set[int], Set[string]) in the same package test file.
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).
Reviewed by Chris St. John·Last updated Jul 19, 2026