[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.