Skip to main content

Pro Tips — Composite Types

Slice Memory Leak Patterns

Because slices share a backing array, a small slice referencing a large array keeps the entire array alive and prevents it from being garbage-collected.

package main

import "fmt"

func loadHugeData() []int {
huge := make([]int, 10000)
for i := range huge {
huge[i] = i
}
return huge
}

// Bad: only 3 elements needed, but 10 000-element array stays in memory
func badPattern() []int {
data := loadHugeData()
return data[:3]
}

// Good: independent 3-element array
func goodPattern() []int {
data := loadHugeData()
result := make([]int, 3)
copy(result, data[:3])
return result
}

// Pointer slices — nil out removed slots so GC can collect them
func removeElement(s []*int, i int) []*int {
s[len(s)-1] = nil // allow GC to collect the pointer
copy(s[i:], s[i+1:])
return s[:len(s)-1]
}

func main() {
bad := badPattern()
good := goodPattern()
fmt.Println(bad, good)
}

Pre-allocation for Better Performance

Repeated append calls that trigger reallocation slow things down. When the final size is known, use make with a capacity hint.

package main

import (
"fmt"
"time"
)

func withoutPrealloc(n int) []int {
var result []int
for i := 0; i < n; i++ {
result = append(result, i*2)
}
return result
}

func withPrealloc(n int) []int {
result := make([]int, 0, n)
for i := 0; i < n; i++ {
result = append(result, i*2)
}
return result
}

func withLength(n int) []int {
result := make([]int, n)
for i := 0; i < n; i++ {
result[i] = i * 2
}
return result
}

func benchmark(name string, fn func(int) []int, n int) {
start := time.Now()
for i := 0; i < 1000; i++ {
fn(n)
}
fmt.Printf("%-20s %v\n", name, time.Since(start))
}

func main() {
const N = 10000
benchmark("without prealloc", withoutPrealloc, N)
benchmark("with prealloc", withPrealloc, N)
benchmark("with length", withLength, N)
// "with length" is fastest
}

Map vs Struct — Choosing the Right Type

SituationRecommended
Keys fixed at compile timeStruct
Keys determined at runtimeMap
JSON serialisation, DB mappingStruct (tags)
Frequent key insertion/deletionMap
Type safety is criticalStruct
Config files, plugin metadataMap
package main

import "fmt"

// Struct — fields are fixed, IDE autocomplete, compile-time checks
type Config struct {
Host string
Port int
Debug bool
Timeout int
}

// Map — keys are dynamic
type DynamicConfig map[string]any

func main() {
cfg := Config{Host: "localhost", Port: 8080, Debug: true, Timeout: 30}
fmt.Println(cfg.Host) // typo = compile error

dyn := DynamicConfig{
"host": "localhost",
"port": 8080,
"version": "2.1.0",
}
dyn["new_feature"] = true // impossible with a struct
fmt.Println(dyn["host"])
}

Extending Behaviour with Embedding

Embedding expresses composition (has-a), not inheritance.

package main

import (
"fmt"
"sync"
"time"
)

// Reusable timestamp mixin
type Timestamps struct {
CreatedAt time.Time
UpdatedAt time.Time
}

func (t *Timestamps) Touch() {
t.UpdatedAt = time.Now()
}

type Article struct {
Timestamps
Title string
Content string
}

type Comment struct {
Timestamps
ArticleID int
Body string
}

// Goroutine-safe counter via embedded Mutex
type SafeCounter struct {
sync.Mutex
count int
}

func (sc *SafeCounter) Increment() {
sc.Lock()
defer sc.Unlock()
sc.count++
}

func main() {
a := Article{
Title: "Go Struct Embedding",
Content: "Embedding is a composition tool.",
}
a.CreatedAt = time.Now()
a.Touch()

fmt.Printf("Article: %s, updated: %s\n", a.Title, a.UpdatedAt.Format("15:04:05"))
}

Generic Slice Utilities (Go 1.18+)

Repetitive slice operations can be generalised with type parameters. Go 1.21 also ships a standard slices package.

package main

import (
"fmt"
"slices" // Go 1.21+
)

// Filter — keep elements satisfying the predicate
func Filter[T any](s []T, fn func(T) bool) []T {
result := make([]T, 0)
for _, v := range s {
if fn(v) {
result = append(result, v)
}
}
return result
}

// Map — transform each element
func MapSlice[T, U any](s []T, fn func(T) U) []U {
result := make([]U, len(s))
for i, v := range s {
result[i] = fn(v)
}
return result
}

// Reduce — fold a slice into a single value
func Reduce[T, U any](s []T, init U, fn func(U, T) U) U {
acc := init
for _, v := range s {
acc = fn(acc, v)
}
return acc
}

func main() {
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

evens := Filter(nums, func(n int) bool { return n%2 == 0 })
fmt.Println(evens) // [2 4 6 8 10]

squares := MapSlice(nums, func(n int) int { return n * n })
fmt.Println(squares) // [1 4 9 16 25 36 49 64 81 100]

sum := Reduce(nums, 0, func(acc, n int) int { return acc + n })
fmt.Println(sum) // 55

// Go 1.21 slices package
words := []string{"banana", "apple", "cherry", "date"}
slices.Sort(words)
fmt.Println(words) // [apple banana cherry date]
fmt.Println(slices.Contains(words, "apple")) // true
idx, _ := slices.BinarySearch(words, "cherry")
fmt.Println(idx) // 2
}

Common Map Initialisation Patterns

package main

import "fmt"

func main() {
// Pattern 1: Increment counter (zero value makes this safe)
counter := make(map[string]int)
words := []string{"go", "is", "great", "go", "is"}
for _, w := range words {
counter[w]++ // safe even for missing keys
}
fmt.Println(counter) // map[go:2 great:1 is:2]

// Pattern 2: Append to slice values safely
groups := make(map[string][]string)
data := [][2]string{{"A", "alice"}, {"B", "bob"}, {"A", "carol"}}
for _, d := range data {
groups[d[0]] = append(groups[d[0]], d[1]) // nil slice is appendable
}
fmt.Println(groups) // map[A:[alice carol] B:[bob]]

// Pattern 3: Set implementation
set := make(map[string]struct{})
for _, item := range []string{"a", "b", "a", "c", "b"} {
set[item] = struct{}{} // empty struct uses no memory
}
fmt.Println(len(set)) // 3 (duplicates removed)
_, inSet := set["a"]
fmt.Println(inSet) // true

// Pattern 4: Shallow map copy
original := map[string]int{"a": 1, "b": 2}
clone := make(map[string]int, len(original))
for k, v := range original {
clone[k] = v
}
clone["c"] = 3
fmt.Println(original) // map[a:1 b:2] — unaffected
}

Constructor Pattern for Structs

Go has no constructor syntax. By convention, use a New function to encapsulate validation and default values.

package main

import (
"errors"
"fmt"
)

type Server struct {
host string // unexported fields
port int
timeout int
}

func NewServer(host string, port int) (*Server, error) {
if host == "" {
return nil, errors.New("host must not be empty")
}
if port < 1 || port > 65535 {
return nil, fmt.Errorf("invalid port: %d", port)
}
return &Server{
host: host,
port: port,
timeout: 30,
}, nil
}

func (s *Server) Address() string {
return fmt.Sprintf("%s:%d", s.host, s.port)
}

func main() {
s, err := NewServer("localhost", 8080)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println(s.Address()) // localhost:8080

_, err = NewServer("", 8080)
fmt.Println(err) // host must not be empty
}