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
- Learn about arrays
- Explore maps
- Study memory model
- Practice with algorithms