Reviews Go slice and map usage for nil semantics, capacity hints, and iteration patterns. Use when reviewing collection operations, seeing slice/map initialization, or encountering nil panics.
Scanned 2/12/2026
Install via CLI
openskills install majiayu000/claude-skill-registry---
description: Reviews Go slice and map usage for nil semantics, capacity hints, and iteration patterns. Use when reviewing collection operations, seeing slice/map initialization, or encountering nil panics.
---
# Collections
## Purpose
Establish patterns for working with slices and maps in RMS Go code. Understanding collection semantics prevents common bugs and improves performance.
## Core Principles
1. **Understand nil semantics** - Nil slices and maps behave differently
2. **Use capacity hints** - Pre-allocate when size is known
3. **Prefer range** - For iteration over index-based access
4. **Consider thread safety** - Maps are not safe for concurrent access
---
## Slices
### Nil vs Empty Slice
```go
// Nil slice - default zero value
var tasks []*Task // nil slice
len(tasks) // 0
cap(tasks) // 0
tasks == nil // true
// Empty slice - explicitly allocated
tasks := []*Task{} // empty slice
tasks := make([]*Task, 0) // empty slice
len(tasks) // 0
tasks == nil // false
// Behavior difference
json.Marshal(nil) // null
json.Marshal([]*Task{}) // []
```
### When to Use Each
```go
// DO: Use nil slice as return value when appropriate
func (s *Store) List(ctx context.Context) ([]*Task, error) {
// Returns nil when no results - this is fine
return nil, nil
}
// DO: Use empty slice for JSON serialization
func (h *Handler) ListTasks(w http.ResponseWriter, r *http.Request) {
tasks, err := store.List(r.Context())
if tasks == nil {
tasks = []*Task{} // Serialize as [] not null
}
json.NewEncoder(w).Encode(tasks)
}
// DO: Check length, not nil
if len(tasks) == 0 {
// Works for both nil and empty
}
// DON'T: Check nil when you mean empty
if tasks == nil { // Misses empty slices
// ...
}
```
### Capacity Hints
```go
// DO: Pre-allocate when size is known
func convertTasks(items []*Item) []*Task {
tasks := make([]*Task, 0, len(items))
for _, item := range items {
tasks = append(tasks, convertTask(item))
}
return tasks
}
// DO: Pre-allocate with make for known size
func getIDs(tasks []*Task) []rms.ID {
ids := make([]rms.ID, len(tasks))
for i, task := range tasks {
ids[i] = task.ID
}
return ids
}
// DON'T: Grow slice repeatedly
func convertTasks(items []*Item) []*Task {
var tasks []*Task // Starts at 0 capacity
for _, item := range items {
tasks = append(tasks, convertTask(item)) // Multiple reallocations
}
return tasks
}
```
### Slice Operations
```go
// Append
tasks = append(tasks, newTask)
tasks = append(tasks, moreTasks...)
// Copy
dst := make([]*Task, len(src))
copy(dst, src)
// Delete (preserving order)
tasks = append(tasks[:i], tasks[i+1:]...)
// Delete (no order preservation, more efficient)
tasks[i] = tasks[len(tasks)-1]
tasks = tasks[:len(tasks)-1]
// Filter in place
n := 0
for _, task := range tasks {
if task.IsValid() {
tasks[n] = task
n++
}
}
tasks = tasks[:n]
```
---
## Maps
### Nil Map Behavior
```go
// Nil map - read OK, write panics
var m map[string]int // nil
v := m["key"] // Returns 0 (zero value), no panic
m["key"] = 1 // PANIC: assignment to nil map
// Always initialize before writing
m = make(map[string]int)
m["key"] = 1 // OK
// Or use map literal
m := map[string]int{
"key": 1,
}
```
### Capacity Hints
```go
// DO: Hint capacity for large maps
func buildIndex(tasks []*Task) map[rms.ID]*Task {
index := make(map[rms.ID]*Task, len(tasks))
for _, task := range tasks {
index[task.ID] = task
}
return index
}
// DON'T: No hint when size is known
func buildIndex(tasks []*Task) map[rms.ID]*Task {
index := make(map[rms.ID]*Task) // Grows repeatedly
for _, task := range tasks {
index[task.ID] = task
}
return index
}
```
### Comma-Ok Idiom
```go
// DO: Check existence with comma-ok
if task, ok := taskMap[id]; ok {
// task exists
process(task)
} else {
// task doesn't exist
}
// DO: Distinguish zero value from missing
count, exists := counts[key]
if !exists {
counts[key] = 1
} else {
counts[key] = count + 1
}
// DON'T: Assume zero means missing
count := counts[key]
if count == 0 { // Bug: 0 could be a valid value
// ...
}
```
### Map Iteration
```go
// DO: Iterate with range
for key, value := range m {
fmt.Printf("%s: %v\n", key, value)
}
// DO: Keys only
for key := range m {
keys = append(keys, key)
}
// CAUTION: Iteration order is random
// If you need ordered iteration, sort keys first
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Printf("%s: %v\n", k, m[k])
}
```
### Safe Deletion During Iteration
```go
// DO: Delete during iteration is safe
for k, v := range m {
if shouldDelete(v) {
delete(m, k) // Safe
}
}
// DON'T: Add during iteration (undefined behavior)
for k := range m {
m[k+"_copy"] = m[k] // May or may not see new keys
}
```
---
## Thread Safety
### Maps Are Not Thread-Safe
```go
// DON'T: Concurrent map access
var cache = make(map[string]*Task)
func get(key string) *Task {
return cache[key] // Data race!
}
func set(key string, task *Task) {
cache[key] = task // Data race!
}
```
### Use sync.Map for Concurrent Access
```go
// DO: sync.Map for concurrent access
var cache sync.Map
func get(key string) (*Task, bool) {
if v, ok := cache.Load(key); ok {
return v.(*Task), true
}
return nil, false
}
func set(key string, task *Task) {
cache.Store(key, task)
}
func getOrCreate(key string, create func() *Task) *Task {
v, _ := cache.LoadOrStore(key, create())
return v.(*Task)
}
```
### Mutex-Protected Map
```go
// DO: Mutex for complex operations
type TaskCache struct {
mu sync.RWMutex
tasks map[rms.ID]*Task
}
func (c *TaskCache) Get(id rms.ID) (*Task, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
task, ok := c.tasks[id]
return task, ok
}
func (c *TaskCache) Set(id rms.ID, task *Task) {
c.mu.Lock()
defer c.mu.Unlock()
c.tasks[id] = task
}
func (c *TaskCache) GetOrSet(id rms.ID, create func() *Task) *Task {
c.mu.Lock()
defer c.mu.Unlock()
if task, ok := c.tasks[id]; ok {
return task
}
task := create()
c.tasks[id] = task
return task
}
```
---
## Common Patterns
### Grouping
```go
// Group items by key
func groupByStatus(tasks []*Task) map[Status][]*Task {
groups := make(map[Status][]*Task)
for _, task := range tasks {
groups[task.Status] = append(groups[task.Status], task)
}
return groups
}
// Group by workflow with capacity hint
func groupByWorkflow(tasks []*Task) map[rms.ID][]*Task {
groups := make(map[rms.ID][]*Task, len(tasks)/10) // Estimate
for _, task := range tasks {
groups[task.WorkflowID] = append(groups[task.WorkflowID], task)
}
return groups
}
```
### Deduplication
```go
// Deduplicate slice
func uniqueIDs(ids []rms.ID) []rms.ID {
seen := make(map[rms.ID]struct{}, len(ids))
result := make([]rms.ID, 0, len(ids))
for _, id := range ids {
if _, ok := seen[id]; !ok {
seen[id] = struct{}{}
result = append(result, id)
}
}
return result
}
```
### Set Operations
```go
// Set using map
type StringSet map[string]struct{}
func NewStringSet(values ...string) StringSet {
s := make(StringSet, len(values))
for _, v := range values {
s[v] = struct{}{}
}
return s
}
func (s StringSet) Add(value string) { s[value] = struct{}{} }
func (s StringSet) Remove(value string) { delete(s, value) }
func (s StringSet) Contains(value string) bool {
_, ok := s[value]
return ok
}
```
### Index Building
```go
// Build lookup index
func buildTaskIndex(tasks []*Task) map[rms.ID]*Task {
index := make(map[rms.ID]*Task, len(tasks))
for _, task := range tasks {
index[task.ID] = task
}
return index
}
// Multi-index
type TaskIndices struct {
ByID map[rms.ID]*Task
ByWorkflow map[rms.ID][]*Task
ByStatus map[Status][]*Task
}
func buildIndices(tasks []*Task) *TaskIndices {
indices := &TaskIndices{
ByID: make(map[rms.ID]*Task, len(tasks)),
ByWorkflow: make(map[rms.ID][]*Task),
ByStatus: make(map[Status][]*Task),
}
for _, task := range tasks {
indices.ByID[task.ID] = task
indices.ByWorkflow[task.WorkflowID] = append(indices.ByWorkflow[task.WorkflowID], task)
indices.ByStatus[task.Status] = append(indices.ByStatus[task.Status], task)
}
return indices
}
```
---
## Quick Reference
| Operation | Slice | Map |
|-----------|-------|-----|
| Zero value | `nil` (safe to read) | `nil` (panics on write) |
| Initialize | `make([]T, len, cap)` | `make(map[K]V, cap)` |
| Length | `len(s)` | `len(m)` |
| Add | `s = append(s, v)` | `m[k] = v` |
| Delete | `s = append(s[:i], s[i+1:]...)` | `delete(m, k)` |
| Check exists | `i < len(s)` | `_, ok := m[k]` |
| Thread-safe | No | No (use sync.Map) |
### Checklist
- [ ] Pre-allocated with known capacity?
- [ ] Nil vs empty semantics correct for use case?
- [ ] Comma-ok used for map lookups?
- [ ] Thread safety considered?
- [ ] Range used for iteration?
---
## See Also
- [concurrency](../concurrency/Skill.md) - Thread-safe patterns
- [control-flow](../control-flow/Skill.md) - Iteration patterns
No comments yet. Be the first to comment!