1. go
  2. /data structures
  3. /slices

Understanding Slices in Go Programming

Slices are a fundamental data structure in Go that provides a flexible, dynamic view into an array. This guide covers everything you need to know about working with slices effectively.

Creating Slices

Basic Slice Creation

// Create empty slice
var s []int

// Create slice with initial values
numbers := []int{1, 2, 3, 4, 5}

// Create slice with make
s = make([]int, 5)    // len=5, cap=5
s = make([]int, 5, 10) // len=5, cap=10

// Create slice from array
arr := [5]int{1, 2, 3, 4, 5}
s = arr[1:4]  // [2, 3, 4]

Slice Expressions

// Basic slicing
s := []int{1, 2, 3, 4, 5}
s1 := s[1:4]    // [2, 3, 4]
s2 := s[:3]     // [1, 2, 3]
s3 := s[2:]     // [3, 4, 5]
s4 := s[:]      // [1, 2, 3, 4, 5]

// Full slice expression
s5 := s[1:4:5]  // length=3, capacity=4

Slice Operations

Appending Elements

// Append single element
s = append(s, 6)

// Append multiple elements
s = append(s, 7, 8, 9)

// Append one slice to another
s1 := []int{1, 2, 3}
s2 := []int{4, 5, 6}
s1 = append(s1, s2...)

Copying Slices

// Copy slices
src := []int{1, 2, 3}
dst := make([]int, len(src))
copied := copy(dst, src)

// Partial copy
dst = make([]int, 2)
copied = copy(dst, src)  // copies only first 2 elements

Deleting Elements

// Delete from middle
s := []int{1, 2, 3, 4, 5}
i := 2 // index to delete
s = append(s[:i], s[i+1:]...)  // [1, 2, 4, 5]

// Delete from start
s = s[1:]  // [2, 4, 5]

// Delete from end
s = s[:len(s)-1]  // [2, 4]

Memory Management

Capacity Management

// Pre-allocate capacity
s := make([]int, 0, 1000)
for i := 0; i < 1000; i++ {
    s = append(s, i)
}

// Check capacity
fmt.Printf("len=%d cap=%d\n", len(s), cap(s))

// Grow capacity
s = append(s, 1001)  // may trigger reallocation

Memory Leaks Prevention

// Avoid memory leaks
original := []int{1, 2, 3, 4, 5}
// Wrong: might retain large array
leaked := original[1:2]

// Right: copy to new slice
safe := make([]int, 1)
copy(safe, original[1:2])

Best Practices

1. Slice Initialization

// Good: Initialize with expected size
s := make([]int, 0, expectedSize)

// Good: Small literal initialization
s := []int{1, 2, 3}

// Avoid: Don't use pointer to slice
func bad(sp *[]int) {
    // Working with pointer to slice
}

// Good: Pass slice directly
func good(s []int) {
    // Working with slice
}

2. Efficient Appending

// Good: Pre-allocate when size is known
s := make([]int, 0, size)
for i := 0; i < size; i++ {
    s = append(s, i)
}

// Avoid: Growing one by one without pre-allocation
var s []int
for i := 0; i < size; i++ {
    s = append(s, i)  // May cause multiple reallocations
}

3. Safe Slicing

// Good: Check bounds before slicing
func safeSlice(s []int, start, end int) ([]int, error) {
    if start < 0 || end > len(s) || start > end {
        return nil, fmt.Errorf("invalid slice bounds")
    }
    return s[start:end], nil
}

Common Patterns

1. Stack Operations

// Stack implementation using slice
type Stack struct {
    items []interface{}
}

func (s *Stack) Push(item interface{}) {
    s.items = append(s.items, item)
}

func (s *Stack) Pop() interface{} {
    if len(s.items) == 0 {
        return nil
    }
    item := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return item
}

2. Filtering

// Filter slice elements
func filter(s []int, fn func(int) bool) []int {
    var result []int
    for _, v := range s {
        if fn(v) {
            result = append(result, v)
        }
    }
    return result
}

// Usage
evens := filter([]int{1, 2, 3, 4}, func(x int) bool {
    return x%2 == 0
})

3. Map-Reduce Operations

// Map operation
func mapSlice(s []int, fn func(int) int) []int {
    result := make([]int, len(s))
    for i, v := range s {
        result[i] = fn(v)
    }
    return result
}

// Reduce operation
func reduce(s []int, fn func(int, int) int, initial int) int {
    result := initial
    for _, v := range s {
        result = fn(result, v)
    }
    return result
}

Performance Considerations

1. Memory Allocation

// Efficient: Single allocation
s := make([]int, 0, 1000)
for i := 0; i < 1000; i++ {
    s = append(s, i)
}

// Inefficient: Multiple allocations
var s []int
for i := 0; i < 1000; i++ {
    s = append(s, i)
}

2. Slice Operations

// Efficient: In-place operations
func reverse(s []int) {
    for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
        s[i], s[j] = s[j], s[i]
    }
}

// Inefficient: Creating new slice
func inefficientReverse(s []int) []int {
    result := make([]int, len(s))
    for i, v := range s {
        result[len(s)-1-i] = v
    }
    return result
}

Common Mistakes

1. Slice Capacity Issues

// Wrong: Not considering capacity
s1 := []int{1, 2, 3, 4, 5}
s2 := s1[1:3]
s2 = append(s2, 6)  // Modifies s1

// Right: Create new slice
s2 := make([]int, 2)
copy(s2, s1[1:3])
s2 = append(s2, 6)  // Doesn't affect s1

2. Nil Slice vs Empty Slice

// Nil slice
var s1 []int
fmt.Println(s1 == nil)  // true

// Empty slice
s2 := make([]int, 0)
fmt.Println(s2 == nil)  // false

// Both work with append
s1 = append(s1, 1)
s2 = append(s2, 1)

3. Range Variable Capture

// Wrong: Capturing range variable
var funcs []func()
for i := range []int{1, 2, 3} {
    funcs = append(funcs, func() {
        fmt.Println(i)  // Will print last value
    })
}

// Right: Create new variable in loop
for i := range []int{1, 2, 3} {
    i := i  // Create new variable
    funcs = append(funcs, func() {
        fmt.Println(i)
    })
}

Next Steps

  1. Learn about arrays
  2. Explore maps
  3. Study memory model
  4. Practice with algorithms

Additional Resources