DEV Community

Cover image for Go: Arrays and Slices, a deep dive.
Yussif Mohammed
Yussif Mohammed

Posted on • Updated on

 

Go: Arrays and Slices, a deep dive.

Welcome,

In this article we're going to discuss all there is to know about arrays and slices in Go, we'll go deep talking about their internal structures and why they behave very differently even though they do similar things. We'll be going through Arrays and Slices and their behaviors and difference under 5 topics;

  • Default or "zero" values
  • Declaration and Initializing Arrays and Slices
  • Value parts of Arrays and Slices
  • Manipulating Arrays and Slices
  • Potential quirks of slices
  • Tips for optimizing performance in your code

Things to note

  • len is a built-in function that returns the number of items in an array of slice
  • cap is a built-in function that returns the capacity of an array or slice. The capacity of an array is equal to it's length and the capacity of a slice is the maximum number of elements that the slice can hold before it needs to be resized (slice capacity may be greater than the length)
  • fmt.Println is a function in the fmt package in Go that is used to print a line to the standard output. It takes one or more values as arguments and prints them to the console, separated by spaces and followed by a newline.

Default or "zero" values

In Go, if you declare a variable without explicitly initializing it, the variable is automatically set to the zero value for its type. The zero value is a default value assigned to a variable of a specific type when it is declared but not explicitly initialized. For example, if you declare an int variable like this:

var x int
Enter fullscreen mode Exit fullscreen mode

The value of x will be initialized to 0.
In other languages like Javascript value of an uninitialized variable is undefined

The zero value for each type is as follows:

  • int: 0
  • float: 0.0
  • bool: false
  • string: "" (empty string)
  • pointer: nil
  • struct: all fields are set to the zero value for their respective types

The zero value of an array in Go is an array with all elements set to the zero value for their respective types. For example, if you have an array of integers:

var arr [5]int
Enter fullscreen mode Exit fullscreen mode

The zero value of this array would be:

[0, 0, 0, 0, 0]
Enter fullscreen mode Exit fullscreen mode

Similarly, if you have an array of strings:

var arr [5]string
Enter fullscreen mode Exit fullscreen mode

The zero value of this array would be:

["", "", "", "", ""]
Enter fullscreen mode Exit fullscreen mode

In Go, the zero value of a slice is nil, which is a slice with a length of 0, a capacity of 0, and no underlying array. For example:

var slice []int
fmt.Println(slice == nil) // => true
Enter fullscreen mode Exit fullscreen mode

Declaring and Initializing Arrays

The format for declaring arrays in Go is var name [L]T.
var is a keyword for declaring all kinds of variables in Go
name is the name of the variable, can be anything
L is the length of the array (must be a constant) and T is the type of the array items.
Let's look at some examples

//Array of 5 Intergers
var nums [5]int
fmt.Println(nums) // => [0 0 0 0 0]

//Array of 10 strings
var strs [10]string
fmt.Println(nums) // => [         ]

// Nested arrays
var nested = [3][5]int{
    {1, 2, 3, 4, 5},
    {6, 7, 8, 9, 10},
    {11, 12, 13, 13, 15},
}
fmt.Println(nested) // => [[1 2 3 4 5] [6 7 8 9 10] [11 12 13 13 15]]
Enter fullscreen mode Exit fullscreen mode

Initializing arrays is simply assigning a value to the variable
var name = [L]T{...} where ... represents the array items of type T

//Intializing an array containing 10 intergers
var nums = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
fmt.Println(nums) // => [1 2 3 4 5 6 7 8 9 10]

//Intializing an array containing 10 strings
var strs = [10]string{"one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"}
fmt.Println(strs) // => [one two three four five six seven eight nine ten]

//Nested arrays
var nested = [3][2]int{}
Enter fullscreen mode Exit fullscreen mode

You can also create an array of structs

type Car struct {
    Brand string
    Color string
    Price float32
}

//Array of 5 items of type Car
var arrayOfCars = [5]Car{
    {Brand: "Porsche", Color: "Black", Price: 20_000.00},
    {Brand: "Volvo", Color: "White", Price: 8_000.00},
    {Brand: "Honda", Color: "Blue", Price: 7_000.00},
    {Brand: "Tesla", Color: "Black", Price: 50_000.00},
    {Brand: "Kia", Color: "Red", Price: 5_000.98},
}
fmt.Println(arrayOfCars) // => [{Porsche Black 20000} {Volvo White 8000} {Honda Blue 7000} {Tesla Black 50000} {Kia Red 5000.98}]
Enter fullscreen mode Exit fullscreen mode

To create an array of items with different types in Go, you can use the interface{} type.
An interface in Go is a type that defines a set of methods that a type must implement. Any type that implements all of the methods listed in an interface is said to satisfy the interface and is considered to be of that interface type. The special interface type interface{} has no methods, which means that every type in Go satisfies this interface.

package main

import "fmt"

func main() {
    //Array containing 5 items of different type
    var randomsArray = [5]interface{}{"Hello world!", 35, false, 33.33, 'A'}
    fmt.Println(randomsArray) // => [Hello world! 35 false 33.33 65]

}
Enter fullscreen mode Exit fullscreen mode

Other ways of initializing arrays

import "fmt"

func main() {
    // Using shorthand syntax
    cars := [3]string{"Tesla", "Ferrari", "Benz"}
    fmt.Println(cars) // => [Tesla Ferrari Benz]

    // Using ... inplace of array length
    digits := [...]int{10, 20, 30, 40}
    fmt.Println(digits) // => [10 20 30 40]

    // Using len keyword
    countries := [len(digits)]string{"China", "India", "Kenya"}
    fmt.Println(countries) // => [China India Kenya]
}
Enter fullscreen mode Exit fullscreen mode

Note you can't use the := shorthand syntax in global scope

Declaring and Initializing Slices

To declare a slice we use the format var name []int, the only difference between declaring arrays and slices is that with slices we omit the length.
Examples

import "fmt"

func main() {
    // A slice of intergers
    var intSlice []int
    fmt.Println(intSlice) // => []

    // A slice of intergers
    var stringSlice []string
    fmt.Println(stringSlice) // => []
}
Enter fullscreen mode Exit fullscreen mode

To initialize a slice in Go, you can use the make function. The make function takes three arguments: the type of the slice, the length of the slice, and the capacity of the slice (which is optional). make([]T, len, cap)

For example, to create a slice of integers with a length of 5 and a capacity of 10, you can use the following code:

package main

import "fmt"

func main() {
    // With capacity
    slice1 := make([]int, 5, 10)
    fmt.Println(len(slice1), cap(slice1)) // => 5 10

    // Without capacity
    slice2 := make([]int, 5)
    fmt.Println(len(slice2), cap(slice2)) // => 5 5
}
Enter fullscreen mode Exit fullscreen mode

When you omit the capacity, the capacity is set to the length of the slice.

You also initialize a slice without the make function by immediately assigning value to it

slice := []int{1, 2, 3}
fmt.Println(len(slice), cap(slice)) // => 3 3
Enter fullscreen mode Exit fullscreen mode

Value parts of Arrays and Slices

This is the most important difference between arrays and slices in Go, Arrays have only one part whiles slices have direct and indirect parts (2 parts). What this means is that arrays in Go are fixed-length data structures that consist of a contiguous block of memory for storing elements. Slices are dynamically-sized and reference a contiguous segment of an underlying array.
To understand this properly, let's look at the value parts of the this array and slice example

var arr = [5]int{1,2,3,4,5}
var slice = []int{1,2,3,4,5}
Enter fullscreen mode Exit fullscreen mode

array
in memory representation of array in Go
'
'
slice
in memory representation of array in Go

From the above diagrams we can see that an array is a fixed-size collection of elements of the same type, stored in contiguous memory locations. On the other hand, a slice consists of a pointer to an underlying array, a length, and a capacity.

internal structure of a slice direct part

type _slice struct {
    // referencing underlying elements
    elements unsafe.Pointer
    // number of elements
    len int
    // capacity of the slice
        cap int
}
Enter fullscreen mode Exit fullscreen mode

What happens when you copy Arrays and Slices

In Go, underlying value parts are not copied in value assignments only direct values a copied. What this means is that when we copy an array we're making a copy of it's elements (because it's has only a direct part) but when we copy a slice, we are making a copy of it's direct part i.e len, cap and pointer to elements and the indirect part (the actual elements are not copied)

Array copy example

x := [5]int{3, 6, 9, 12, 15}
y := v
Enter fullscreen mode Exit fullscreen mode

In the above code example we've initialized an array x and then we created another variable y and we assigned (copied) the value of x to y.

Here is a diagram representation of what happens

array copies creates new array
when we make a copy of an array all element are copied into a separate memory block. In the above code example, if we make changes to to x it doesn't affect y and the vice versa, we'll talk more on that later

Slice copy example

x := []int{2,4,6,8,10}
y := x
Enter fullscreen mode Exit fullscreen mode

In the above code example we've initialized a slice x and then we created another slice y and we assigned (copied) the value of x to y.

in memory representation of a slice

From the above diagram you can see that the direct parts of x is copied into a separate memory for y (Len, Cap, and Elem pointer) but x and y still share the same underlying parts (array elements). So when you modify an element of x it also affects y since the share the same underlying elements, but x and y can have different lengths and capacity since those are in their own separate memories
more on this later

Manipulating Arrays and Slices

In this section, we will discuss manipulating arrays and slices.

Arrays

Since arrays in Go have a fixed constant length, the only manipulation that can be done on arrays is changing values at specific indexes starting from zero.
first element is 0
second element is 1
third element is 2 and so on.
Examples

package main

import "fmt"

func main() {
    var fruits [6]string // Declare string array with zero values
    fmt.Println(fruits)  // => [       ] (string zero value "")

    // 🍊 change first element
    fruits[0] = "Orange"
    fmt.Println(fruits) // => [Orange      ]

    //πŸ‹ change last element
    fruits[5] = "Lemon"
    fmt.Println(fruits) // => [Orange     Lemon]

    // πŸŒπŸ‰πŸπŸchange all
    fruits[1] = "Banana"
    fruits[2] = "Watermelon"
    fruits[3] = "Pear"
    fruits[4] = "Apple"
    fmt.Println(fruits) // => [Orange Banana Watermelon Pear Apple Lemon]

    //🍍 Dont' like oranges? change first element again
    fruits[0] = "Pineapple"
    fmt.Println(fruits) // => [Pineapple Banana Watermelon Pear Apple Lemon]

    //Modify array of integers
    evenNumbers := [5]int{2, 4, 6, 8, 10}

    evenNumbers[0] = 12
    fmt.Println(evenNumbers) // => [12 4 6 8 10]

    evenNumbers[3] = 20
    fmt.Println(evenNumbers) // => [12 4 6 20 10]

}
Enter fullscreen mode Exit fullscreen mode

Accessing array values

import "fmt"

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

    // get the first element
    first := nums[0]
    fmt.Println(first) // => 1

    // get third element
    fmt.Println(nums[2]) // => 3

    // last
    fmt.Println(nums[6]) // => 7

    //alternatively
    fmt.Println(nums[len(nums)-1]) // => 7

}
Enter fullscreen mode Exit fullscreen mode

If we try to change element an index greater than or equal to the arrays length the code will won't compile it'll panic with an index out of bound error

package main

import "fmt"

func main() {
    nums := [7]int{1, 2, 3, 4, 5, 6, 7}
    outOfBound := nums[7]
}
Enter fullscreen mode Exit fullscreen mode
invalid argument: array index 7 out of bounds [0:6]
Enter fullscreen mode Exit fullscreen mode

Slices

Slices are a useful data type in Go because they offer a flexible and convenient way to manipulate collections of data. They can be accessed and modified in the same way as arrays, but they also have some specific behaviors that make them more powerful. In the following sections, we will explore some of these behaviors in more detail.

Slice expression

The slice expression signature is s[start:end:cap]
which is used to create a new slice that includes all the elements of the original slice s, starting from the element at index start and up to but not including the element at index end, the cap is the capacity of the newly created sublice and it's optional. If cap is omitted, the capacity of the sub slice is equal to it's length. The length of the sub slice is end - start

Example

package main

import "fmt"

func main() {
    slice := []int{1, 2, 3, 4, 5, 6}
    subSlice := slice[1:4]
    fmt.Println(subSlice)                     // => [2 3 4]
    fmt.Println(len(subSlice), cap(subSlice)) // => 3 3

    subSliceWithCap := slice[1:4:5]
    fmt.Println(subSliceWithCap)                            // => [2 3 4]
    fmt.Println(len(subSliceWithCap), cap(subSliceWithCap)) // => 3 4
}
Enter fullscreen mode Exit fullscreen mode

note the capacity also starts from zero, so if you use 5 as capacity of a slice s calling cap(s) will return 4

If the start index is zero, you can omit it s[:end] similarly if the end index is the end of the array you can omit it like so s[start:]
Examples

package main

import "fmt"

func main() {
    s := []string{"g", "o", " ", "i", "s", " ", "s", "w", "e", "e", "t"}

    // copy from 0 to index 2 (index 2 is exclusive)
    goSubSlice := s[:2]
    fmt.Println(goSubSlice) // => [g o]

    // copy from index 3 to end
    isSweetSubSlice := s[3:]
    fmt.Println(isSweetSubSlice) // => [i s   s w e e t]

        // copy all items
        copySlice := s[:]
        fmt.Println(copySlice) // => [g o  i s   s w e e t]
}
Enter fullscreen mode Exit fullscreen mode

Earlier we discussed slice value parts and how only the direct parts of a slice is copied, so when we create a sub slice what is actually happening ?
Let's use this examples

package main

import "fmt"

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

    fmt.Println(n1, len(n1), cap(n1)) // => [1 2 3 4 5 6]  6 10
    fmt.Println(n2, len(n2), cap(n2)) // => [4 5 6 7 8]  5 7
    fmt.Println(n3, len(n3), cap(n3)) // => [5 6 7 8 9 10] 6 6

    // change n1 at index 4 to 15
    n1[4] = 15

    fmt.Println(n)  // => [1 2 3 4 15 6 7 8 9 10]
    fmt.Println(n1) // => [1 2 3 4 15 6]
    fmt.Println(n2) // => [4 15 6 7 8]
    fmt.Println(n3) // => [15 6 7 8 9 10]

}
Enter fullscreen mode Exit fullscreen mode

Notice how when we changed the n1[4] to 15 it affected all the other sub slices including the main slice? that's because they share the same underlying array elements, so anytime we make changes to a sub slice it's affects all other sub slices.
Here is a diagram of the above slice and sub slices to help you understand what's happening

Slices sharing the same underlying arrays but cover different parts

From the above diagram we can see all sub slices share the same underlying array but they span different parts of the underlying array. When we make a change at a particular index of a sub slice, all sub slices the span that index are are also modified.
When you modify the element at index 4 of n3, you are also modifying the element at index 8 of the array (n), because n3 and n share the same underlying array. As a result, the change to n3[4] is also reflected in n. Similarly, any changes made to the elements of n between indices 4 and 9 (inclusive) will also be reflected in n3, because n3 spans those indices.

n3[4] = 18
fmt.Println(n)  // => [1 2 3 4 15 6 7 8 18 10]
fmt.Println(n1) // => [1 2 3 4 15 6]
fmt.Println(n2) // => [4 15 6 7 8]
fmt.Println(n3) // => [15 6 7 8 18 10]
Enter fullscreen mode Exit fullscreen mode
Appending items to a slice

The Go append function allows you to add elements to the end of a slice. It has the following syntax:

func append(s []T, x ...T) []T
Enter fullscreen mode Exit fullscreen mode

s is the slice you want to append to, x is a list of one or more elements of type T to be appended, and the function returns a new slice with the appended elements.

For example:

s := []int{1, 2, 3}
s = append(s, 4, 5, 6)
fmt.Println(s) // => s is now [1, 2, 3, 4, 5, 6]
Enter fullscreen mode Exit fullscreen mode

Note that if the capacity of the underlying array is insufficient to accommodate the additional elements, append will allocate a new, larger array to hold the result. If a new larger array is created after append, that slice does no longer share the same underlying array with other sub slices
Let's use the previous example

package main

import "fmt"

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

    fmt.Println(n1, len(n1), cap(n1)) // => [1 2 3 4 5 6]  6 10
    fmt.Println(n2, len(n2), cap(n2)) // => [4 5 6 7 8]  5 7
    fmt.Println(n3, len(n3), cap(n3)) // => [5 6 7 8 9 10] 6 6

}
Enter fullscreen mode Exit fullscreen mode

from the above code n2 has a length of 5 and a capacity of 7 which means we can append two more items without a new array being created and it'll continue so share the same underlying array with the other sub slices
See

n2 = append(n2, 100)
fmt.Println(n)  // => [1 2 3 4 15 6 7 8 100 10]
fmt.Println(n1) // => [1 2 3 4 15 6]
fmt.Println(n2) // => [4 15 6 7 8, 100]
fmt.Println(n3) // => [15 6 7 8 100 10]

n2 = append(n2, 101)
fmt.Println(n)  // => [1 2 3 4 15 6 7 8 100 101]
fmt.Println(n1) // => [1 2 3 4 15 6]
fmt.Println(n2) // => [4 15 6 7 8 100 101]
fmt.Println(n3) // => [15 6 7 8 100 101]

// Check the capacity and length of n2
fmt.Println(cap(n2), len(n2)) // =>  7 7
Enter fullscreen mode Exit fullscreen mode

As we appended more items it was affecting n and n3 but now the n2 slice is full (capacity == length).
Now that capacity of n2 is equal to it's length so appending a new item will cause new array to created for n2 and it will no longer share the same underlying array with the other sub slices

n2 = append(n2, 102)
fmt.Println(n)  // => [1 2 3 4 15 6 7 8 100 101]
fmt.Println(n1) // => [1 2 3 4 15 6]
fmt.Println(n2) // => [4 15 6 7 8 100 101 102]
fmt.Println(n3) // => [15 6 7 8 100 101]
Enter fullscreen mode Exit fullscreen mode

From the above code you can see only n2 was modified and the others weren't affected.

appending multiple items to a slice

example

package main

import "fmt"

func main() {
    s := []int{10, 20, 30, 40, 50, 60}
    s2 := []int{70, 80, 90}

        // Appending slice to slice
    s = append(s, s2...)
    fmt.Println(s) // => [10 20 30 40 50 60 70 80 90]

        // Appending multiple values
    s = append(s, 100, 110, 120)
    fmt.Println(s) // => [10 20 30 40 50 60 70 80 90 100 110 120]
}
Enter fullscreen mode Exit fullscreen mode
Deep value copying of a slice

Deep copying simply means copying the underlying array of a slice instead of the direct part so the destination slice doesn't share the same underlying memory with the source slice.
deep copying using append

package main

import (
    "fmt"
)

func main() {
    slice1 := []int{1, 2, 3, 4, 5, 6}
    slice2 := []int{}
    slice2 = append(slice2, slice1...)

    fmt.Println(slice1) // => [1 2 3 4 5 6]
    fmt.Println(slice2) // => [1 2 3 4 5 6]

    //Modifying slice2 doesn't affect slice1
    slice2[0] = 100
    fmt.Println(slice1) // => [1 2 3 4 5 6]
    fmt.Println(slice2) // => [100 2 3 4 5 6]

    // copying a range of items
    slice3 := []int{}
    slice3 = append(slice3, slice1[3:5]...)
    fmt.Println(slice3) // => [4 5]

    //Again slice3 and slice1 doesn't share underlying array

    slice3[0] = -10
    fmt.Println(slice1) // => [1 2 3 4 5 6]
    fmt.Println(slice3) // => [-10 5]

}
Enter fullscreen mode Exit fullscreen mode

In Go, you can use the copy function to perform a deep copy of a slice. A deep copy creates a new slice with its own, independent copy of the elements of the original slice.

The copy function has the following syntax:

func copy(dst, src []T) int
Enter fullscreen mode Exit fullscreen mode

dst is the destination slice, and src is the source slice. Both slices must have the same element type T. The function returns the number of elements copied, which will be the minimum of the lengths of dst and src.

For example:

package main

import "fmt"

func main() {
    s := []int{1, 2, 3}
    t := make([]int, len(s))
    copy(t, s)
    fmt.Println(t) // => [1, 2, 3], and is a deep copy of s

    // copy a range of items

    t = make([]int, len(s)-1)
    copy(t, s[0:2])
    fmt.Println(t) // => [1, 2], and is a deep copy of s

}
Enter fullscreen mode Exit fullscreen mode

Note that if the length of dst is less than the length of src, only the first len(dst) elements of src will be copied. To make a deep copy of the entire slice, you must ensure that dst has sufficient capacity to hold all of the elements of src.

Potential quirks of slices

As mentioned earlier, a sub slice only copies the direct part of a slice and share the same underlying array. So when we have a slice sBig of size 10 mega bytes and we create a sub slice sTiny of size 3 bytes from sBig, sTiny and sBig will reference the same underlying array. As you might know Go is garbage collected which means it automatically frees up memory when it is not referenced (it can not be reached).
So in the case of sBig and sTiny even if we only need sTiny which is 3 bytes, sBig will continue to be in memory because sTiny references the same underlying array as sBig. To get around this we make a deep copy so that sTiny doesn't share the underlying array as sBig and so it can be garbage collected thereby freeing up memory.
Example

var gopherRegexp = regexp.MustCompile("gopher")

func FindGopher(filename string) []byte {
    //Reading a very huge file  1,000,000,000 bytes (1GB)
    b, _ := ioutil.ReadFile(filename)
    //Taking a just 6 byte sub slice
    gopherSlice := gopherRegexp.Find(b)
    return gopherSlice
}
Enter fullscreen mode Exit fullscreen mode

From the above example we read a very huge file (1GB) and returned a sub slice of it (just 6 bytes), since the gopherSlice still reference the same underlying array as the huge file, which means that 1GB of memory can not be garbage collected even though we are not using it anymore.
If you call the FindGopher function multiples times, you program can eat all the computers memory. To fix this, like I said earlier we make a deep copy so gopherSlice doesn't share the same underlying array as the huge slice
Example

var gopherRegexp = regexp.MustCompile("gopher")

func FindGopher(filename string) []byte {
    //Reading a very huge file  1,000,000,000 bytes (1GB)
    b, _ := ioutil.ReadFile(filename)
    //Taking a just 6 byte sub slice
    gopherSlice := make([]byte, len("gopher"))

    // Make a deep copy
    copy(gopherSlice, gopherRegexp.Find(b...)
    return gopherSlice
}
Enter fullscreen mode Exit fullscreen mode

Now the Go language garbage collector can now free up the ~1GB of memory

Tips for optimizing performance in your code

Like I said earlier the most important difference between arrays and slices in Go is the difference in their value parts this together with Go value copy cost is the reason they differ in performance.

Value copy cost

Value assignments, argument passing, looping using range keyword etc. all involve value copying. The bigger the value size the bigger the value copy cost, copying 10 mega bytes will take longer than copying 10 bytes. And we also learned that only direct parts are copied.
Example

array := [100]int{1,2,3,4,5,6, ..., 100}
slice := []int{1,2,3,4,5,6, ..., 100}
Enter fullscreen mode Exit fullscreen mode

From the above example we've created an array of numbers 1-100 and a slice also of numbers 1-100. When we copy the array all the elements are copied so the value copy cost will be 8 * 100 = 800 bytes (1 int is 8 bytes assuming 64-bit architectures) but when we copy a slice only the direct part is copied (len, cap and elements pointer) so the value copy cost will be 8 + 8 + 8 = 24 bytes. Even though bough slice and array contain 100 elements the value copy cost of the array is way bigger than that of the slice.

From the above scenario the performance concerns only affects arrays and not slices, I'll be focusing on how to use arrays with performance in mind. Also for small array sizes these performances differences are negligible and not worth the effort to make things faster

The only thing here is not using range key word to loop over arrays

package main

import "fmt"

func main() {
    // Don't do this
    arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

    // arr is copied
    for key, value := range arr {
        fmt.Println(key, value)
    }

    // Do this instead
    for i := 0; i < len(arr); i++ {
        fmt.Println(i, arr[i])
    }
}
Enter fullscreen mode Exit fullscreen mode

Conclusions

If you have any questions, ask them in the comments and I'll be happy to answer them, if you something you want me to
write on write in the comments.

Top comments (7)

Collapse
 
amadu9933 profile image
Amadu Hamza

very helpful

Collapse
 
jhelberg profile image
Joost Helberg

Great in depth discussion on a less understood aspect of Go. Thanks!

Collapse
 
dawkaka profile image
Yussif Mohammed

Thank you so much for your kind words

Collapse
 
telemachus profile image
Peter Aronoff • Edited

Re "Potential Quirks of Slices", I think you may be wrong about regexp.Find holding onto memory the way you describe. This used to be a problem, and if you read an earlier Go blog post about slices, you may think it still is a problem. But it hasn't been a problem for a while.

The old version of regexp.Find looked like this (note the last line especially):

// Find returns a slice holding the text of the leftmost match in b of the regular expression.
// A return value of nil indicates no match.
func (re *Regexp) Find(b []byte) []byte {
    a := re.doExecute("", b, 0)
    if a == nil {
        return nil
    }
    return b[a[0]:a[1]]
}
Enter fullscreen mode Exit fullscreen mode

The new version looks like this (again, the last line is key):

// Find returns a slice holding the text of the leftmost match in b of the regular expression.
// A return value of nil indicates no match.
func (re *Regexp) Find(b []byte) []byte {
    var dstCap [2]int
    a := re.doExecute(nil, b, "", 0, 2, dstCap[:0])
    if a == nil {
        return nil
    }
    return b[a[0]:a[1]:a[1]]
}
Enter fullscreen mode Exit fullscreen mode

Because the new version uses a full slice expression, I don't think that it holds onto memory any longer. The third item in a full slice expression specifies the maximum capacity of the slice. The definition states that the capacity of the resulting slice is no greater (in this case) than a[1] - a[0]. That means that the capacity will be no longer than the length of the string found, in your case gopher.

Full slice expressions were introduced in December of 2013, and regexp.Find was fixed sometime after that. This is great to know about because we can also use full slice expressions to control how much of a backing array is exposed by a slice. This can protect us from blocking the garbage collector, as here, and it can also prevent the caller from being able to (accidentally or intentionally) changing parts of the backing array that they shouldn't have access to. (That is, in the old version of regexp.Find, the calling code would have access to a lot more of the 1 GB document in your example than just the word "gopher." In the new version, that's protected against.)

Collapse
 
dawkaka profile image
Yussif Mohammed

I did not know that
Thanks for the correction!

Collapse
 
kevburnsjr profile image
Kevin Burns

Error on line 14 of codeblock 11

    fmt.Println(countries) // => [Chian India Kenya]
Enter fullscreen mode Exit fullscreen mode

Should read

    fmt.Println(countries) // => [China India Kenya ]
Enter fullscreen mode Exit fullscreen mode
Collapse
 
dawkaka profile image
Yussif Mohammed

Thanks for catching that.

Advice For Junior Developers

Advice from a career of 15+ years for new and beginner developers just getting started on their journey.