This is a creation in Article, where the information may have evolved or changed.
Objective
The Go language Pro knows, slice (Chinese translation as a slice) is often used in programming, it represents a variable length sequence, each element of the sequence has the same type, similar to a dynamic array, the use of append can achieve dynamic growth, the use of slice features can be easily cut slice, How do they achieve these characteristics? Now let's explore what the nature of these features is.
First look at the characteristics of slice
s := []int{1,2,3,4,5} fmt.Println(s) // [1 2 3 4 5]
A slice type general writing []t, where T represents the type of element in slice; Slice syntax and arrays are like, but not fixed-length.
s := []int{1,2,3,4,5} s = append(s, 6) fmt.Println(s) // [1 2 3 4 5 6]
Built-in Append function when the length of an existing array is < 1024, the cap growth is doubled, and then up the growth rate is 1.25, as to why it will be said later.
s := []int{1,2,3,4,5,6} s1 := s[0:2] fmt.Println(s1) // [1 2] s2 := s[4:] fmt.Println(s2) // [5 6] s3 := s[:4] fmt.Println(s3) // [1 2 3 4]
- Slice as a function parameter
package main import "fmt" func main() { slice_1 := []int{1, 2, 3, 4, 5} fmt.Printf("main-->data:\t%#v\n", slice_1) fmt.Printf("main-->len:\t%#v\n", len(slice_1)) fmt.Printf("main-->cap:\t%#v\n", cap(slice_1)) test1(slice_1) fmt.Printf("main-->data:\t%#v\n", slice_1) test2(&slice_1) fmt.Printf("main-->data:\t%#v\n", slice_1) } func test1(slice_2 []int) { slice_2[1] = 6666 // 函数外的slice确实有被修改 slice_2 = append(slice_2, 8888) // 函数外的不变 fmt.Printf("test1-->data:\t%#v\n", slice_2) fmt.Printf("test1-->len:\t%#v\n", len(slice_2)) fmt.Printf("test1-->cap:\t%#v\n", cap(slice_2)) } func test2(slice_2 *[]int) { // 这样才能修改函数外的slice *slice_2 = append(*slice_2, 6666) }
Results:
main-->data: []int{1, 2, 3, 4, 5}main-->len: 5main-->cap: 5test1-->data: []int{1, 6666, 3, 4, 5, 8888}test1-->len: 6test1-->cap: 12main-->data: []int{1, 6666, 3, 4, 5}main-->data: []int{1, 6666, 3, 4, 5, 6666}
Note here, why slice as a value to pass parameters, the slice outside the function has also been changed? Why is append inside a function not to change the slice outside the function? To get back to da these questions you need to understand slice internal structure, see below for details.
The internal structure of the slice
In fact, slice in the run-time library of Go is a C language dynamic array implementation, in $goroot/src/pkg/runtime/runtime.h can see its definition:
struct Slice{ // must not move anything byte* array; // actual data uintgo len; // number of elements uintgo cap; // allocated number of elements};
This structure has 3 fields, the first field represents the pointer to the array, is a pointer to the real data (this must be noted), so it is often said that slice is an array of references, the second is to indicate the length of slice, the third is to represent the capacity of slice, note: Both Len and cap are not pointers .
Now it is possible to explain the previous example slice as a function argument: the function of the slice called Slice_1, the parameters of the function is called slice_2, when the function is passed slice_1, in fact, is actually the slice_1 parameter copy, so Slice_ 2 copied the slise_1, but note that the pointer to the array stored in slice_2, so when changing the contents of the array inside the function, the contents of the Slice_1 outside the function also change. When using append within a function, append automatically expands the capacity of the slice_2 in a multiplication manner, but the extension is only the length and capacity of slice_2 within the function, and the length and capacity of the slice_1 are unchanged, so it appears to be unchanged when printing outside the function.
The operating mechanism of append
The automatic expansion of the slice can be caused when the slice is append and so on. The size growth rule when expanding is:
- If the new slice size is more than twice times the current size, the size grows to the new size
- Otherwise, loop the following operation: If the current slice size is less than 1024, increase by twice times each time, otherwise increase by the current size 1/4 each time. Until the size of the growth exceeds or equals the new size.
- The implementation of append is simply a simple copy of the old slice*** in memory to the new slice***
As to why this is so, you have to look at the Golang source code to know: Https://github.com/golang/go/blob/master/src/runtime/slice.go
newcap := old.cap if newcap+newcap < cap { newcap = cap } else { for { if old.len < 1024 { newcap += newcap } else { newcap += newcap / 4 } if newcap >= cap { break } } }
Why not use a dynamic list to implement slice?
- First copy a broken continuous memory is very fast, if you do not want to copy, that is, with a dynamic list, then you do not have contiguous memory. At this time the random access cost would be: The list O (N), twice times the growth block chain O (logn), level Two table A constant large O (1). The problem is not only the algorithm overhead, but also the memory location is scattered and the cache height is not friendly, these problems I in the continuous memory scheme does not exist. Unless your application is crazy append and then read it only once, otherwise optimizing writes and sacrificing reading is completely non-sense. And even if your application is strictly sequential, the cache hit rate will often make your overall efficiency lower than the copy-and-replace memory.
- For small slice, the cost of continuous append is not more in memmove, but in allocating a new space for the memory allocator and after the GC pressure (which is more detrimental to the list). So, when you can roughly know the maximum space you need (which is most of the time), it's good to reserve the appropriate cap for make. If the maximum space required is large and the amount of space used is uncertain, then you have to waste memory and CPU consumption on the allocator + GC.
- The cost of Go in append and copy is predictable + controllable, and the application of simple tuning has a good effect. There is no free, dynamically growing memory in the world, and various implementations have a design tradeoff.
When should I use slice?
In the go language, slice is very flexible, most of the situation can be very good, but there are special circumstances. When the program requires slice capacity and needs to change the content of slice frequently, it should not use slice, instead of using list more appropriate.