04 Collection Types How to Properly Use Array, Slice, and Map

04 Collection Types - How to Properly Use array, slice, and map #

The exercise in the previous lesson was to practice using continue in a for loop. By now, you should understand that continue is used to skip the current iteration and move to the next one. Let’s use calculating the sum of even numbers within 100 as an example to demonstrate the usage of continue:

sum := 0

for i := 1; i < 100; i++ {

   if i % 2 != 0 {
      continue
   }

   sum += i
}

fmt.Println("the sum is", sum)

The key to this example is that if i is not an even number, the continue statement will skip the current iteration and move to the next one. If i is an even number, the loop will continue with sum += i and then proceed to the next iteration. This way, we only calculate the sum of even numbers within 100.

Now, let’s start this lesson and I will introduce the collection types in Go.

In actual scenarios, we often encounter situations where elements of the same type are grouped together, which are known as collections. Examples of collections include 100 numbers, 10 strings, etc. In Go, arrays, slices, and maps are collection types used to store elements of the same type. Although they are all collections, they have different purposes. I will explain them in detail in this lesson.

Array #

An array is used to store elements of the same type with a fixed length, and these elements are stored continuously. The type of data stored in an array can be any type, such as integers, strings, or even custom types.

Array Declaration #

Declaring an array is straightforward and follows the same syntax as declaring basic types that we learned in the second lesson.

In the code example below, I declared a string array with a length of 5. Therefore, its type is defined as [5]string, and the elements within the curly braces are used to initialize the array. Additionally, by putting [] square brackets with a length in front of the type name, we can infer the type of the array.

Note: [5]string and [4]string are not the same type. In other words, the length is also part of the array type.

ch04/main.go

array := [5]string{"a", "b", "c", "d", "e"}

In memory, an array is stored continuously. The following diagram shows how an array is stored in memory:

Drawing 1.png

As you can see, each element of the array is stored consecutively, and each element has an index. The index starts from 0, so the index of the first element a is 0, and the index of the second element b is 1. And so on. By using array[index], we can quickly access elements.

As shown in the code below, when executed, it will print “c”, which is the third element of the array array:

ch04/main.go

func main() {

    array := [5]string{"a", "b", "c", "d", "e"}

    fmt.Println(array[2])
}

When defining an array, the length can be omitted. In this case, Go will automatically infer the length based on the number of elements inside the curly braces {}. Therefore, the above example can also be declared as follows:

array := [...]string{"a", "b", "c", "d", "e"}

Omitting the length declaration is only suitable for arrays where all elements are initialized. If you want to initialize specific elements based on their indices, omitting the length declaration is not appropriate, as shown in the following example:

array1 := [5]string{1: "b", 3: "d"}

In this example, “1: “b”, 3: “d”” means initializing the value “b” for index 1 and the value “d” for index 3. The length of the entire array is 5. If I omit the length, the array will have a length of only 4, which is not what we intended when defining the array.

Additionally, for uninitialized indices, their default values are the zero value of the array type, which is an empty string for the string type.

In addition to using the [] operator to quickly access array elements by their indices, you can also use a for loop to print all the array elements, as shown in the code below:

ch04/main.go

for i := 0; i < 5; i++ {
    fmt.Printf("Array index:%d, corresponding value:%s\n", i, array[i])
}

Array Iteration #

Looping through an array using a traditional for loop to output the corresponding indices and values is cumbersome and not commonly used. Most of the time, we use the new for range loop in Go, as shown in the following code:

for i, v := range array {
    fmt.Printf("Array index:%d, corresponding value:%s\n", i, v)
}

This way of iterating is equivalent to the traditional for loop. For arrays, the range expression returns two values:

  1. The first one is the index of the array;
  2. The second one is the value of the array.

In the example above, assign the two returned results to the variables i and v respectively, and you can use them.

Compared to traditional for loops, the for range loop is more concise. If the returned values are not used, you can use an underscore (_) to discard them, as shown in the following code:

for _,v:=range array{

    fmt.Printf("Value:%s\n", v)

}

The index of the array is discarded using an underscore (_) and only the value of the array v is used.

Slice #

A slice is similar to an array and can be understood as a dynamic array. It is implemented based on an array, and its underlying structure is an array. By partitioning an array arbitrarily, a slice can be obtained. Now let’s understand it better through an example, based on the previous array example.

Generating a Slice based on an Array #

In the following code, array[2:5] is an operation to obtain a slice. It includes the elements from index 2 to index 5 of the array:

array := [5]string{"a", "b", "c", "d", "e"}

slice := array[2:5]

fmt.Println(slice)

Note: The index 2 is included, but the index 5 is not. The number on the right side of the colon (:) is not included.

ch04/main.go

// Generating a slice based on an array, including the start index but not the end index

slice := array[start:end]

So, array[2:5] obtains the elements c, d, and e. These three elements are assigned as a slice to the variable slice.

Like an array, elements in a slice can be located by index. Taking the newly obtained slice as an example, the value of slice[0] is c, and the value of slice[1] is d.

Did you notice that in the array array, the index of element c is 2, but after slicing, its index in the new slice slice is 0? This is what slicing means. Although the underlying array used by the slice is also the array array, the index range of the slice has changed after slicing.

From the following diagram, you can see that a slice is a data structure with three fields: a pointer data pointing to the array, a length len, and a capacity cap:

image28.png

Here’s a little trick: both the start and end indexes in the slice expression array[start:end] can be omitted. If the start is omitted, its value defaults to 0. If the end is omitted, its value defaults to the length of the array. For example:

  1. array[:4] is equivalent to array[0:4].
  2. array[1:] is equivalent to array[1:5].
  3. array[:] is equivalent to array[0:5].

Modifying a Slice #

The value of a slice can also be modified. This also proves that a slice is based on an array.

Assigning a value to the corresponding index element of a slice is how to modify it. In the following code, modify the value of index 1 in the slice slice to f, and then print the array array:

slice := array[2:5]

slice[1] = "f"

fmt.Println(array)

You can see the following result:

[a b c f e]

The value of the array has been modified to f. This proves that when a slice is based on an array, the underlying array used is still the original array. Once the elements of a slice are modified, the corresponding values in the underlying array will also be modified.

Declaring a Slice #

Apart from obtaining a slice from an array, you can also declare a slice. The simplest way is to use the make function.

The following code declares a slice with the element type string and a length of 4. The make function can also take a capacity parameter:

slice1 := make([]string, 4)

In the example below, a capacity of 8 is specified when creating the slice []string:

slice1 := make([]string, 4, 8)

Note that the capacity of a slice cannot be smaller than its length.

You already know the length of a slice, which is the number of elements in the slice. So, what is the capacity? It is actually the space of the slice.

The above examples indicate that Go language allocates a content space with a capacity of 8 in memory (capacity is 8) but only 4 memory spaces contain elements (length is 4). The other memory spaces are in a free state. When elements are appended to the slice using the append function and the length of the slice exceeds the capacity, it will be expanded.

A slice can be declared and initialized using the literal syntax as shown below:

// 删除指定键值对
delete(nameAgeMap, "飞雪无情")
delete(nameAgeMap, "飞雪无情")

The delete function has two parameters: the first parameter is the map, and the second parameter is the key of the key-value pair to be deleted.

Traversing a Map #

A map is a collection of key-value pairs, and it can also be traversed in Go using the for range loop.

For a map, for range returns two values:

  1. The key of the map
  2. The value of the map

Let’s demonstrate this with the following code:

ch04/main.go

// Testing for range

nameAgeMap["飞雪无情"] = 20

nameAgeMap["飞雪无情1"] = 21

nameAgeMap["飞雪无情2"] = 22

for k, v := range nameAgeMap {
    fmt.Println("Key is", k, ", Value is", v)
}

Note that the traversal of a map is unordered, meaning that each time you traverse, the order of key-value pairs may be different. If you want to traverse in order, you can first get all the keys, sort them, and then retrieve the corresponding values based on the sorted keys. I will not demonstrate this here, but you can consider it as an exercise.

Tip: When using for range on a map, you can also simply use one return value. When using one return value, the default return value is the key of the map.

Size of a Map #

Unlike arrays and slices, maps do not have a capacity. They only have a length, which is the size of the map (the number of key-value pairs). To get the size of a map, you can use the built-in len function, as shown in the following code:

fmt.Println(len(nameAgeMap))

String and []byte #

A string is also an immutable byte sequence, so it can be directly converted to a byte slice []byte, as shown in the following code:

ch04/main.go

s := "Hello飞雪无情"

bs := []byte(s)

Not only can a string be directly converted to []byte, but you can also use the [] operator to obtain the byte value of a specified index. For example:

ch04/main.go

s := "Hello飞雪无情"

bs := []byte(s)

fmt.Println(bs)

fmt.Println(s[0], s[1], s[15])

You may be confused because the letters and Chinese characters in string s add up to 9 characters, right? How can we use s[15] which exceeds the index of 9? In fact, it is precisely because a string is a sequence of bytes, and each index corresponds to a byte. Under the UTF-8 encoding, a Chinese character corresponds to three bytes. Therefore, the length of string s is actually 17.

If you run the following code, you will see that the printed result is 17.

fmt.Println(len(s))

If you want to count a Chinese character as one length, you can use the utf8.RuneCountInString function. If you run the following code, you will see that the printed result is 9, which is the number of Unicode (UTF-8) characters, consistent with what we see.

fmt.Println(utf8.RuneCountInString(s))

When using for range to loop through a string, it iterates over Unicode characters, so for string s, it loops 9 times.

In the following code example, i is the index, r is the Unicode character corresponding to the Unicode code point. This also indicates that when using for range loop to process a string, it automatically decodes the Unicode string implicitly.

ch04/main.go

for i, r := range s {
    fmt.Println(i, r)
}

Summary #

This lesson comes to an end here. In this lesson, I’ve explained the declaration and usage of arrays, slices, and maps. With these collection types, you can put a certain type of data you need into a collection, such as retrieving a list of users, a list of products, etc.

Arrays and slices can also be two-dimensional or multi-dimensional, such as two-dimensional byte slices [][]byte, and three-dimensional [][][]byte. Due to their infrequent usage, they were not detailed in this lesson, but you can practice it yourself by combining the one-dimensional []byte slice I explained. This is also the homework I leave for you: create a two-dimensional array and use it.

In addition, if the key type of a map is an integer and the number of elements in the collection is small, it is recommended to use a slice because it is more efficient. In actual project development, arrays are not commonly used, especially when passing them as parameters between functions. The most commonly used is slices, which are more flexible and consume less memory.