[Go]3.Array, Slice, Map and String

Array

The array datastructure is similar to array in other static programming languages. An array defines a sequence of elements of the same type. It has the following characteristics: - An array is statically allocated, its capacity can't be increased once it's declared. - An array is passed by value, not of reference type. - Arrays are comparable, as long as the element type is comparable. - If an array is not initialized, the values will be zero value of the element type of each element.

Declaration

The basic declaration:

var buffer [256]byte

The shorthand declaration:

arr := [4]string{"Yichun", "blog", "go", "learning"} 

We can omit the size of the array, and let the compiler to decide its size:

arr := [...]string{"Yichun", "blog", "go", "learning"} 

We can also declare a multi-dimensional array:

arr := [3][3]string{{"C#", "C", "Python"},  
                    {"Java", "Scala", "Perl"}, 
                    {"C++", "Go", "HTML"}} 

Type

An array's type is related to its size:

package main

import (
    "fmt"
)

func foo(arr [3]int) {
    for num, index := range(arr) {
        fmt.Println(num, index)
    }
}

func main() {
    a := [...]int{1, 2, 3}
    b := [...]int{1, 2, 3, 4}
    fmt.Printf("Type of a: %T, type of b: %T.\n", a, b) // Type of a: [3]int, type of b: [4]int.
    foo(a) // OK
    foo(b) // Error: cannot use b (type [4]int) as type [3]int in argument to foo
}

From the above example, we can know that type [3]int is not equivalent to [4]int.

Pass-by-value

An array is passed by value, which means when assigning one array to another array, we will create a new copy of the original array:

a := [...]int{1, 2, 3}
b := a
b[1] = 100
fmt.Println(a)   // [1 2 3]
fmt.Println(b)   // [1 100 3]

Similar effects will be observed when passing arrays as parameters to functions.

Comparability

An array is comparable to an array of the same type as long as the underlying element type is comparable, but only == operator can be used:

a := [...]int{1, 2, 3}
b := [3]int{1, 2, 3}
c := [3]int{4, 5, 6}
fmt.Println(a == b)  // true
fmt.Println(a == c)  // false
fmt.Println(a < c)   // error: invalid operation: a < c (operator < not defined on array)

Slice

From the above discussion, we know that the ability of array data type is not very strong. In fact, the mainly usage of the array type is to serve as the underlying of slice type. Slice has the following characteristics: - Each slice has an underlying array. - Slices can be dynamically resized after creation. - Slices are passed by value.

Declaration

Similar to declaring an array:

letters1 := []string{"a", "b", "c", "d"}
var letters2 []string = []string{"a", "b", "c", "d"}

Created from an existing array:

arr := [...]int{1, 2, 3, 4, 5}
slice := arr[1:3]

When creating a slice from an exisiting array, number is the starting index of the slice, inclusive. The latter one is the ending index, exclusive. We can also create a slice from an existing slice:

b := []byte{'g', 'o', 'l', 'a', 'n', 'g'}
a := b[1:4]

We can also use make keyword to create a slice. The function head is func make([]T, len, cap) []T. We can use it like:

s = make([]byte, 5, 5)

The values will be initialized with zero value of its element type.

Slice Internals

A slice basicly has three attributes: - ptr: *Elem - The pointer point to the first element of the slice in the underlying array. - len: int - The length of the slice. - cap: int - The length from the ptr to the end of the current underlying array.

The struct can be described as:

type slice struct {
  array unsafe.Pointer
  len int
  cap int 
}

Here is how it looks when we create a slice using s := make([]byte, 5):

And if we call s = s[2:4], the structure will become: Slicing does not copy the slice's data. It creates a new slice value that points to the original array. This makes slice operations as efficient as manipulating array indices.

Copy in Slice

We have a built-in function called copy which can be used to copy one slice from a source to a destination. The function head is: func copy(dst, src []T) int. But attention here, the copy method won't automatically grow the size of the destination slice, which means if the destination slice's size is smaller than the source one, we only get a partial copy:

b := []byte{'g', 'o', 'l', 'a', 'n', 'g'}   
a := make([]byte, 1)
copy(a, b)
fmt.Println(a) // [103]
fmt.Println(b) // [103 111 108 97 110 103]

Append in Slice

The built-in append function for slice will automatically grow the size of a slice if after appending the length will exceed the slice's capacity. This is done by copying the original underlying array to a bigger array, and then append the new element:

a := make([]byte, 0)
fmt.Println(len(a), cap(a))
a = append(a, 1)
fmt.Println(len(a), cap(a))

Each compiler will have different rules to grow the underlying array. If a slice is created from an existing array, after growing the operation on the slice will have no effect on the original array, since it's using a different underlying array now:

arr := [...]int{1, 2, 3, 4, 5}
slice := arr[0:5]
slice = append(slice, 6)
slice[0] = -1
fmt.Println(slice)     // [-1 2 3 4 5 6]
fmt.Println(arr)       // [1 2 3 4 5]

The above is also true when we create a slice from an existing slice.

Pass-by-value

Everything in Go is passed by value. As for slice, we pass the three attributes as a slice header to a function:

package main

import (
    "fmt"
)

func foo(slice []int) {
    slice = append(slice, 1)
    fmt.Println(slice, cap(slice))  // [0 1] 2
}

func main() {
    slice := make([]int, 1)
    foo(slice)
    fmt.Println(slice, cap(slice)) // [0] 1
}

Slice Tricks

We can find the recipe of operations on slice here.

Map

Map is an unordered group of key-value mappings, where the key must be comparable, which means that == and != operator must be defined. The key must not be a function, map or slice. Maps in go are reference to hashmaps, and we can't have duplicate keys, nil as key in go maps. The struct can be described as:

type bmap struct {
    topbits  [8]uint8
    keys     [8]keytype
    values   [8]valuetype
    pad      uintptr
    overflow uintptr
}

Declaration

Using make, we can specify an initial capacity in the parameter to create a map:

make(map[string]int)
make(map[string]int, 100)

Using var keyworkd:

var m map[string]int

Declare with initialization:

commits := map[string]int{
    "rsc": 3711,
    "r":   2138,
    "gri": 1908,
    "adg": 912,
}

Operation

Assign a value to a key:

m["route"] = 66

Extract a key's value:

i := m["route"]

If a key doesn't exist, it will return the zero value of the value type:

j := m["root"] // j == 0

We can delete a key from a map:

delete(m, "route")

We can also use two-value assignment to test whether a key exist or not. This is useful when we have zero values for some key in our map:

i, ok := m["route"] // ok will be false if "route" doesn't exist

We can also iterate a map:

for key, value := range m {
    fmt.Println("Key:", key, "Value:", value)
}

But the order is not guaranteed, we can use the following method to iterate it in acsending order of the key:

import "sort"

var m map[int]string
var keys []int
for k := range m {
    keys = append(keys, k)
}
sort.Ints(keys)
for _, k := range keys {
    fmt.Println("Key:", k, "Value:", m[k])
}

Concurrency

Map are not safe for concurrent use, the operations are not atomic so we can't write it simultaneously using multiple threads.

String

Reference

  1. The Go Programming Language Specification
  2. Arrays, slices (and strings): The mechanics of 'append'
  3. Go Slices: usage and internals
  4. Go maps in action
  5. 3.3 哈希表