cxuu

go-data-structures

Go data structures including allocation with new vs make, arrays, slices, maps, printing with fmt, and constants with iota. Use when working with Go's built-in data structures, memory allocation, or formatted output.

cxuu 148 18 Updated 7mo ago

Resources

1
GitHub

Install

npx skillscat add cxuu/golang-skills/go-data-structures

Install via the SkillsCat registry.

About this skill

We need to produce a 2-3 sentence plain-text summary, objective, factual, no marketing language, no superlatives, no calls to action, natural prose, no bullet points, no headings, no markdown formatting, no quotes, at most 60 words. Summarize the skill: Go data structures including allocation with new vs make, arrays, slices, maps, printing with fmt, constants with iota. Use when working with Go's built-in data structures, memory allocation, or formatted output. We need to mention what the skill does, what problem it solves, when to use it. 2-3 sentences, plain text, no formatting.

SKILL.md

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

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.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

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