Slice Notation
Go’s slice notation I feel is often overlooked, as using colons within brackets ([:]), makes for a powerful way to work with arrays and slices. We just specify a range of indices and an optional capacity to create a new slice from an existing array or slice without duplicating any data.
Let’s take a closer look at Go’s slice notation, emphasizing how slices relate to their underlying arrays.
click the buttons to visualize the slice notation
Basic Slice Notation: slice[start:end]
- start: Where our slice begins, it’s starting index.
- end: Determines the slice’s length as
end - start
, it’s ending index. - underlying array: Changes to the slice or array reflect on each other.
nums := [5]int{1, 2, 3, 4, 5}
fullSlice := nums[:]
Full Slice Expression: slice[start:end:cap]
- cap: Sets the slice’s max growth potential as
cap - start
. - underlying array: This limit ensures slice safety, guarding against unintended data overwrites or expansions.
fullSliceWithCap := nums[:5:5]
Variations and Their Effects
slice[:]
- Includes every element, matching the array’s full length and capacity.
- underlying array: Fully accessible through the slice.
fullSlice := nums[:]
slice[start:]
- Ends at the array’s last element, revealing elements from
start
to end. - underlying array: Partially exposed, with changes affecting
start
onwards.
secondHalf := nums[5:]
slice[:end]
- Begins at element zero, ending just before
end
. - underlying array: Shows the initial segment, with alterations up to
end-1
.
firstHalf := nums[:5]
slice[start:end]
- A precise segment from
start
toend
for exact control. - underlying array: Affected within the specified range.
middleSection := nums[2:7]
slice[start:end:cap]
- Adds a capacity ceiling to prevent extending beyond
cap
. - underlying array: Maintains controlled exposure with
cap
acting as a boundary.
customCapSlice := nums[2:5:7]
The Underlying Array’s Role
Creating a slice from an array, or another slice, is like opening a window into that array, defined by the slice length and capacity:
- length: Current element count within the slice.
- capacity: The slice’s expansion limit before it must allocate a new array, beginning from the original array’s starting index.
Remember, slices are mere references to arrays. Modifying a slice’s elements alters the underlying array. This shared reference model underscores the importance of understanding slice dynamics for data integrity and efficient resource use.
Memory Allocation, the slice or the array?
You may be asking yourself, if slices are windows into an array, which one of them actually consumes the memory? Well, it’s pretty straight forward.
Slices do take up some memory, separate from the memory consumed by the array they reference. This is because a slice in Go is just a data structure with three components, look at the below taken directly from the Go source code.
type slice struct {
array unsafe.Pointer
len int
cap int
}
So, both the slice and the array consume memory, but they do so differently:
- The array consumes memory based on its size, storing the actual data.
- The slice consumes a smaller, fixed amount of memory to maintain its window into the array, the pointer, length, and capacity.
Practical Takeaways
- Performance: Slices minimize data duplication, enabling swift manipulations.
- Memory Management: Grasping slice capacity and its relationship with the underlying array aids in avoiding memory inefficiencies.
- Data Integrity: Managing slices cautiously helps prevent unintended changes to the underlying array, safeguarding shared data.
Happy Coding Friends!