Get the FREE Ultimate OpenClaw Setup Guide →

go-data-structures

npx machina-cli add skill cxuu/golang-skills/go-data-structures --openclaw
Files (1)
SKILL.md
9.7 KB

Go 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]int and [20]int are 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 length
  • cap(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

FunctionOutput
PrintfFormatted to stdout
SprintfReturns formatted string
FprintfFormatted to io.Writer
Print/PrintlnDefault 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

FormatPurpose
%TType of value
%qQuoted string
%xHex (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.Buffer
  • sync.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

TopicKey Point
new(T)Returns *T, zeroed
make(T)Slices, maps, channels only; returns T, initialized
ArraysValues, not references; size is part of type
SlicesReference underlying array; use append
MapsKey must support ==; use comma-ok for presence
CopyingDon't copy T if methods are on *T; beware aliasing
%vDefault format for any value
%+vStruct with field names
%#vFull Go syntax
iotaEnumerated 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

  1. Step 1: Identify the data structure you need and decide between new (pointer) vs make (initialized value).
  2. Step 2: For slices, initialize with make or a composite literal and reassign the result when using append.
  3. 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.

Frequently Asked Questions

Add this skill to your agents
Sponsor this space

Reach thousands of developers