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
Define a 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.
Expansion of Slice:
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.
Slice's cutting:
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 mainimport "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 answer these questions you need to understand the internal structure of slice, 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 parameter:
Outside 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 replication, so slice_2 copied slise_1, but note that Slice_ A pointer to an array stored in 2, so when changing the contents of an array within a 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 append implementation simply replicates the old slice to the new slice in memory
As to why this is so, you need to look at Golang source code slice will know:
newcap := old.capif 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?
The
Slice in the go language is very flexible, and most of the situation can be very good, but there are special cases.
When a program requires slice to have a large capacity and needs to change slice content frequently, you should not use slice, which is more appropriate when you use a list.