go-data-structures
npx machina-cli add skill cxuu/golang-skills/go-data-structures --openclawGo Data Structures
Source: Effective Go
This skill covers Go's built-in data structures and allocation primitives.
Allocation: new vs make
Go has two allocation primitives: new and make. They do different things.
new
new(T) allocates zeroed storage for a new item of type T and returns *T:
p := new(SyncedBuffer) // type *SyncedBuffer, zeroed
var v SyncedBuffer // type SyncedBuffer, zeroed
Zero-value design: Design data structures so the zero value is useful without
further initialization. Examples: bytes.Buffer, sync.Mutex.
type SyncedBuffer struct {
lock sync.Mutex
buffer bytes.Buffer
}
// Ready to use immediately upon allocation
make
make(T, args) creates slices, maps, and channels only. It returns an
initialized (not zeroed) value of type T (not *T):
make([]int, 10, 100) // slice: length 10, capacity 100
make(map[string]int) // map: ready to use
make(chan int) // channel: ready to use
The Difference
var p *[]int = new([]int) // *p == nil; rarely useful
var v []int = make([]int, 100) // v is a usable slice of 100 ints
// Idiomatic:
v := make([]int, 100)
Rule: make applies only to maps, slices, and channels and does not return
a pointer.
Composite Literals
Create and initialize structs, arrays, slices, and maps in one expression:
// Struct with positional fields
f := File{fd, name, nil, 0}
// Struct with named fields (order doesn't matter, missing = zero)
f := &File{fd: fd, name: name}
// Zero value
f := &File{} // equivalent to new(File)
// Arrays, slices, maps
a := [...]string{Enone: "no error", Eio: "Eio", Einval: "invalid"}
s := []string{Enone: "no error", Eio: "Eio", Einval: "invalid"}
m := map[int]string{Enone: "no error", Eio: "Eio", Einval: "invalid"}
Note: It's safe to return the address of a local variable in Go—the storage survives after the function returns.
Arrays
Arrays are values in Go (unlike C):
- Assigning one array to another copies all elements
- Passing an array to a function passes a copy, not a pointer
- The size is part of the type:
[10]intand[20]intare distinct
func Sum(a *[3]float64) (sum float64) {
for _, v := range *a {
sum += v
}
return
}
array := [...]float64{7.0, 8.5, 9.1}
x := Sum(&array) // Pass pointer for efficiency
Recommendation: Use slices instead of arrays in most cases.
Slices
Slices wrap arrays to provide a flexible, powerful interface to sequences.
Slice Basics
Slices hold references to an underlying array. Assigning one slice to another makes both refer to the same array:
func (f *File) Read(buf []byte) (n int, err error)
// Read into first 32 bytes of larger buffer
n, err := f.Read(buf[0:32])
Length and Capacity
len(s): current lengthcap(s): maximum length (from start of slice to end of underlying array)
The append Function
func append(slice []T, elements ...T) []T
Always assign the result—the underlying array may change:
x := []int{1, 2, 3}
x = append(x, 4, 5, 6)
// Append a slice to a slice
y := []int{4, 5, 6}
x = append(x, y...) // Note the ...
Two-Dimensional Slices
Method 1: Independent inner slices (can grow/shrink independently):
picture := make([][]uint8, YSize)
for i := range picture {
picture[i] = make([]uint8, XSize)
}
Method 2: Single allocation (more efficient for fixed sizes):
picture := make([][]uint8, YSize)
pixels := make([]uint8, XSize*YSize)
for i := range picture {
picture[i], pixels = pixels[:XSize], pixels[XSize:]
}
For detailed slice internals, see references/SLICES.md.
Declaring Empty Slices
Normative: This is required per Go Wiki CodeReviewComments.
When declaring an empty slice, prefer:
var t []string
over:
t := []string{}
The former declares a nil slice, while the latter is non-nil but zero-length.
They are functionally equivalent—their len and cap are both zero—but the nil
slice is the preferred style.
Exception for JSON encoding: A nil slice encodes to null, while an empty
slice []string{} encodes to []. Use non-nil when you need a JSON array:
// nil slice → JSON null
var tags []string
json.Marshal(tags) // "null"
// empty slice → JSON array
tags := []string{}
json.Marshal(tags) // "[]"
Interface design: When designing interfaces, avoid making a distinction between a nil slice and a non-nil zero-length slice, as this can lead to subtle programming errors.
Maps
Maps associate keys with values. Keys must support equality (==).
Creating and Using Maps
var timeZone = map[string]int{
"UTC": 0*60*60,
"EST": -5*60*60,
"CST": -6*60*60,
}
offset := timeZone["EST"] // -18000
Testing for Presence
An absent key returns the zero value. Use the "comma ok" idiom to distinguish:
seconds, ok := timeZone[tz]
if !ok {
log.Println("unknown time zone:", tz)
}
// Or combined:
if seconds, ok := timeZone[tz]; ok {
return seconds
}
Deleting Entries
delete(timeZone, "PDT") // Safe even if key doesn't exist
Implementing a Set
Use map[T]bool:
attended := map[string]bool{"Ann": true, "Joe": true}
if attended[person] { // false if not in map
fmt.Println(person, "was at the meeting")
}
Printing
The fmt package provides rich formatted printing.
Basic Functions
| Function | Output |
|---|---|
Printf | Formatted to stdout |
Sprintf | Returns formatted string |
Fprintf | Formatted to io.Writer |
Print/Println | Default format |
fmt.Printf("Hello %d\n", 23)
fmt.Println("Hello", 23)
s := fmt.Sprintf("Hello %d", 23)
The %v Format
%v prints any value with a reasonable default:
fmt.Printf("%v\n", timeZone)
// map[CST:-21600 EST:-18000 MST:-25200 PST:-28800 UTC:0]
For structs:
%v: values only%+v: with field names%#v: full Go syntax
type T struct {
a int
b float64
c string
}
t := &T{7, -2.35, "abc\tdef"}
fmt.Printf("%v\n", t) // &{7 -2.35 abc def}
fmt.Printf("%+v\n", t) // &{a:7 b:-2.35 c:abc def}
fmt.Printf("%#v\n", t) // &main.T{a:7, b:-2.35, c:"abc\tdef"}
Other Useful Formats
| Format | Purpose |
|---|---|
%T | Type of value |
%q | Quoted string |
%x | Hex (strings, bytes, ints) |
The Stringer Interface
Define String() string to control default formatting:
func (t *T) String() string {
return fmt.Sprintf("%d/%g/%q", t.a, t.b, t.c)
}
Warning: Don't call Sprintf with %s on the receiver—infinite recursion:
// Bad: infinite recursion
func (m MyString) String() string {
return fmt.Sprintf("MyString=%s", m)
}
// Good: convert to basic type
func (m MyString) String() string {
return fmt.Sprintf("MyString=%s", string(m))
}
Constants and iota
Constants are created at compile time and can only be numbers, characters, strings, or booleans.
iota Enumerator
iota creates enumerated constants:
type ByteSize float64
const (
_ = iota // ignore first value (0)
KB ByteSize = 1 << (10 * iota)
MB
GB
TB
PB
EB
)
Combine with String() for automatic formatting:
func (b ByteSize) String() string {
switch {
case b >= EB:
return fmt.Sprintf("%.2fEB", b/EB)
case b >= PB:
return fmt.Sprintf("%.2fPB", b/PB)
// ... etc
}
return fmt.Sprintf("%.2fB", b)
}
Copying
Advisory: This is a best practice recommendation from Go Wiki CodeReviewComments.
To avoid unexpected aliasing, be careful when copying a struct from another
package. For example, bytes.Buffer contains a []byte slice. If you copy a
Buffer, the slice in the copy may alias the array in the original, causing
subsequent method calls to have surprising effects.
// Dangerous: copying a bytes.Buffer
var buf1 bytes.Buffer
buf1.WriteString("hello")
buf2 := buf1 // buf2's internal slice may alias buf1's array!
buf2.WriteString(" world") // May affect buf1 unexpectedly
General rule: Do not copy a value of type T if its methods are associated
with the pointer type *T.
This applies to many types in the standard library and third-party packages:
bytes.Buffersync.Mutex,sync.WaitGroup,sync.Cond- Types containing the above
// Bad: copying a mutex
var mu sync.Mutex
mu2 := mu // Copying a mutex is almost always a bug
// Good: use pointers or embed carefully
type SafeCounter struct {
mu sync.Mutex
count int
}
// Pass by pointer, not by value
func increment(sc *SafeCounter) {
sc.mu.Lock()
sc.count++
sc.mu.Unlock()
}
Quick Reference
| Topic | Key Point |
|---|---|
new(T) | Returns *T, zeroed |
make(T) | Slices, maps, channels only; returns T, initialized |
| Arrays | Values, not references; size is part of type |
| Slices | Reference underlying array; use append |
| Maps | Key must support ==; use comma-ok for presence |
| Copying | Don't copy T if methods are on *T; beware aliasing |
%v | Default format for any value |
%+v | Struct with field names |
%#v | Full Go syntax |
iota | Enumerated constants |
See Also
- go-style-core - Core Go style principles
- go-control-flow - Control structures including range
- go-interfaces - Interface patterns and embedding
- go-concurrency - Channels and goroutines
Source
git clone https://github.com/cxuu/golang-skills/blob/main/skills/go-data-structures/SKILL.mdView on GitHub Overview
This skill covers Go's built-in data structures and allocation primitives. It explains when to use new vs make, how arrays and slices differ, and how to construct composite literals for structs, arrays, maps, and channels.
How This Skill Works
Go provides two allocation primitives: new and make. new(T) returns a pointer to zeroed storage of type T, while make(T, ...) returns an initialized value for maps, slices, and channels (not a pointer). Arrays are fixed-size values, while slices wrap an underlying array and grow via append.
When to Use It
- You need a pointer to a zero-valued type, such as a new struct with internal synchronization.
- You want a ready-to-use container like a slice, map, or channel created by make.
- You need to initialize various data structures in one expression using composite literals.
- You're choosing between arrays and slices for APIs and parameters; prefer slices in most cases.
- You're building two-dimensional data structures like 2D slices.
Quick Start
- Step 1: Identify the data structure you need and decide between new (pointer) vs make (initialized value).
- Step 2: For slices, initialize with make or a composite literal and reassign the result when using append.
- Step 3: Prefer slices to arrays and use composite literals to simplify initialization.
Best Practices
- Prefer slices over arrays in most cases.
- Use make to initialize maps, slices, and channels; remember it returns a non-pointer value.
- When mutating a slice with append, assign the result back to the slice variable.
- Use composite literals to initialize structs, arrays, slices, and maps in one expression.
- Design zero-value types so the zero value is immediately usable with no extra initialization.
Example Use Cases
- Allocate a zeroed SyncedBuffer with new to get a ready-to-use pointer.
- Create a slice with make and append elements to grow it.
- Create and populate a map with make, then assign key values.
- Build a 2D slice with independent inner slices using a double loop.
- Build a 2D slice with a single backing array to improve memory locality.