DEV Community

Cover image for Comprehensive guide to slice of slices
Bravian
Bravian

Posted on

Comprehensive guide to slice of slices

#go

When I first started working with slices in Go, I was pretty confused. Once I got the hang of it, slices quickly became one of my favorite features of Go. Discovering slices of slices and how they work in go was pretty exciting. For instance, I wanted to separate a single slice into different slices based on a condition, like segregating even and odd numbers. Let's dive into how slices of slices work in Go and why they're so powerful!

Initializing a Slice of Slices

Slices are a dynamic and flexible way to work with sequences of data in Go. Unlike arrays, which have a fixed size, slices can be resized and are thus more versatile. A slice is essentially a window into an underlying array, providing a convenient way to handle sequences of elements.

There are a few ways to initialize a slice of slices in Go. One approach is using a slice literal with nested slice literals:

sliceOfSlices := [][]int{{1, 2}, {3, 4, 5}}
Enter fullscreen mode Exit fullscreen mode

This creates an outer slice containing two inner slices. The first inner slice has elements 1 and 2, and the second has 3, 4, and 5.

You can initialize a slice of slices with a specific size:

sliceOfSlices := make([][]int, 3)
sliceOfSlices[0] = []int{1, 2}
sliceOfSlices[1] = []int{3, 4, 5}
sliceOfSlices[2] = []int{6, 7}
Enter fullscreen mode Exit fullscreen mode

Example: Splitting a Slice into Multiple Slices

Let's say we have a slice of integers and we want to split it into two separate slices: one containing even numbers and the other containing odd numbers. We'll use slices of slices to achieve this.

Step-by-Step Example

  1. Initialize the input slice:

    inputSlice := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    
  2. Initialize the slice of slices to hold even and odd numbers:

    result := make([][]int, 2)
    result[0] = []int{}  // For even numbers
    result[1] = []int{}  // For odd numbers
    
  3. Split the input slice into even and odd numbers:

    for _, num := range inputSlice {
        if num%2 == 0 {
            result[0] = append(result[0], num)
        } else {
            result[1] = append(result[1], num)
        }
    }
    
  4. Output the result:

    fmt.Println("Even numbers:", result[0])
    fmt.Println("Odd numbers:", result[1])
    

Complete Code

Here’s the complete code for splitting a slice into even and odd numbers:

package main

import (
    "fmt"
)

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

    // Initialize slice of slices
    result := make([][]int, 2)
    result[0] = []int{}  // For even numbers
    result[1] = []int{}  // For odd numbers

    // Split input slice into even and odd numbers
    for _, num := range inputSlice {
        if num%2 == 0 {
            result[0] = append(result[0], num)
        } else {
            result[1] = append(result[1], num)
        }
    }

    // Output the result
    fmt.Println("Even numbers:", result[0])
    fmt.Println("Odd numbers:", result[1])
}

Enter fullscreen mode Exit fullscreen mode

test the code

Working with append, copy, and delete

The real power of slices of slices comes when working with functions like append, copy, and delete that operate on slice types.

Append

With append, you can add a slice onto the end of a slice of slices given that you included space for it at the variable defination:

sliceOfSlices = append(sliceOfSlices, []int{6, 7})
Enter fullscreen mode Exit fullscreen mode

This will add the slice {6, 7} as a new entry at the end of sliceOfSlices.

You can also use append to add an element to one of the inner slices:

sliceOfSlices[0] = append(sliceOfSlices[0], 8)
Enter fullscreen mode Exit fullscreen mode

This appends the element 8 onto the first inner slice of sliceOfSlices.

Copy

The copy function allows you to copy data from one slice of slices to another.

copy(result[1], result[0])
Enter fullscreen mode Exit fullscreen mode

This would copy from slice at index 0 to the slice at index 1.

Delete

You can delete entries from a slice of slices using the delete function:

result = append(result[:1], result[2:]...)
Enter fullscreen mode Exit fullscreen mode

This deletes the slice at index 1 from result.

Where Slices of Slices are Useful

So why bother with this nested data structure? Slices of slices are useful anytime you need to represent a group of sequences. Some examples:

  • Separating a sequence into different buckets based on criteria (like the even/odd example)
  • Representing test cases as a slice of slices, with each inner slice being the inputs/outputs for a test
  • Storing 2D data like a game board or matrix calculation
  • Flattening or combining data from different slices into a slice of slices

Compared to using a map of slices, slices of slices have some performance benefits. Slices are just contiguous regions of array data, so they are very efficient. Maps, on the other hand, use more memory and have computational overhead for hashing and lookups.

Slices of slices also allow you to preserve order, which you can't do with a map. The order of the inner slices, and the order of elements within each inner slice, is maintained.

How Slices of Slices Differ from Maps

Speaking of maps, it's worth comparing and contrasting slices of slices to maps of slices, since these two data structures can sometimes be used for similar purposes.

With a map, the keys act as a way to group and access the inner slice values. For example:

evenOdds := make(map[string][]int)
evenOdds["even"] = []int{2, 4, 6}
evenOdds["odd"] = []int{1, 3, 5}
Enter fullscreen mode Exit fullscreen mode

Here the map keys "even" and "odd" provide strings to access and differentiate the inner slices.

With slices of slices, there are no explicit keys. The outer slice just maintains the inner slices in order:

sliceOfSlices := [][]int{{2, 4, 6}, {1, 3, 5}}
Enter fullscreen mode Exit fullscreen mode

Pros and Cons

Slices of slices are more compact, efficient, and better if you want to preserve ordering. Maps can be easier if you want an explicit key to access each inner sequence.

Common Pitfalls

  • Index Out of Range: Ensure you check the length of the slice before accessing an index.
  • Memory Leaks: When deleting elements, be cautious of memory leaks due to residual references.
  • Capacity Issues: Be mindful of slice capacity to avoid unnecessary allocations.

Conclusion

Slices of slices might take some getting used to, especially if you're coming from a different programming background. However, they are an incredibly useful and efficient way to represent nested sequences in Go. From separating data into buckets to representing complex structures like game boards, slices of slices offer both flexibility and performance. I hope this guide helps you get comfortable with slices of slices and see its potential in your Go projects. Happy coding!

Top comments (1)

Collapse
 
andrewwphillips profile image
Andrew W. Phillips

There is no need to initialise the slices result[0] and result[1] as a nil slice is treated the same as an empty slice but if you are going to why not set the capacity to avoid extra heap allocations.

result[0] = make([]int, 0, len(input)/2)