foreword
Hello, everyone, my name is
asong
. I have been reading the eight-part essay for nothing recently, and I have summarized a few sliced eight-part essays that are often tested, and summed it up in a question-and-answer format. I hope it will be useful to you who are interviewing~The title of this article is not complete. What are the real interview questions about slicing? Welcome to the comment area to add~
01. What is the difference between an array and a slice?
Arrays in Go
language are of fixed length and cannot be dynamically expanded. The size will be determined at compile time. The declaration method is as follows:
var buffer [255]int
buffer := [255]int{0}
Slice is an abstraction of array, because the length of array is immutable, it is not very convenient to use in some scenarios, so Go
language provides a flexible and powerful built-in type slice ("dynamic array"), In contrast to arrays, the length of slices is not fixed, and elements can be appended. A slice is a data structure. A slice is not an array. A slice describes an array. The slice structure is as follows:
<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/93958be1acdb4eb8867d307e92ad1d17~tplv-k3u1fbpfcp-zoom-1.image" style="zoom:50%;" />
We can directly declare an array of unspecified size to define a slice, or we can use the make()
function to create a slice. The declaration method is as follows:
var slice []int // 直接声明
slice := []int{1,2,3,4,5} // 字面量方式
slice := make([]int, 5, 10) // make创建
slice := array[1:5] // 截取下标的方式
slice := *new([]int) // new一个
Slices can use append
append elements, and dynamically expand when cap
is insufficient.
02. Is copying a large slice necessarily more expensive than copying a small slice?
This question is more interesting, the original address: Are large slices more expensive than smaller ones?
The essence of this question is to examine the understanding of the essence of slices. In the Go
language, there is only value passing, so we take passing slices as an example:
func main() {
param1 := make([]int, 100)
param2 := make([]int, 100000000)
smallSlice(param1)
largeSlice(param2)
}
func smallSlice(params []int) {
// ....
}
func largeSlice(params []int) {
// ....
}
The slice param2
is 1000000
orders of magnitude larger than param1
. Does it require a more expensive operation when copying the value?
Actually not, because the internal structure of slices is as follows:
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
The first word in the slice is a pointer to the underlying array of the slice, which is the storage space for the slice, the second field is the length of the slice, and the third field is the capacity. Assigning a slice variable to another variable will only copy three machine words. The difference between a large slice and a small slice is that the values of Len
and Cap
are larger than those of the small slice. If a copy occurs, it is essentially a copy the three fields above.
03. Light and dark copies of slices
Both deep and shallow copies are copied. The difference is whether the copied new object and the original object will affect each other when they change. The essential difference is whether the copied object and the original object will point to the same address. In the Go
language, there are three ways to copy slices:
- Use the
=
operator to copy the slice, this is a shallow copy - Use the
[:]
subscript to copy the slice, which is also a shallow copy - Use the built-in function
copy()
of theGo
language for slice copying, this is a deep copy,
04. What are zero slices, empty slices, and nil slices
Why so many slices in the question? Because there are five ways to create slices in the Go
language, the slices created by different methods are different;
- zero slice
We call a slice where the elements of the internal array of the slice are all zero-valued or the contents of the underlying array are all nil
as a zero-valued slice, and a slice created with make
and whose length and capacity are neither 0 is a zero-valued slice:
slice := make([]int,5) // 0 0 0 0 0
slice := make([]*int,5) // nil nil nil nil nil
nil
slice
The length and capacity of the nil
slice are both 0
, and the result compared with nil
is true
. The nil
slice can be created by directly creating slices or new
:
var slice []int
var slice = *new([]int)
- empty slice
The length and capacity of the empty slice are also 0
, but the comparison result with nil
is false
, because the data pointers of all empty slices point to the same address 0xc42003bda0
; using the literal, make
can create an empty slice:
var slice = []int{}
var slice = make([]int, 0)
The zerobase memory address pointed to by the empty slice is a magic address whose definition can be seen from the source code of the Go language:
// base address for all 0-byte allocations
var zerobase uintptr
// 分配对象内存
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
...
if size == 0 {
return unsafe.Pointer(&zerobase)
}
...
}
05. Slice expansion strategy
This question is a high-frequency test site. Let's analyze the expansion strategy of slices through the source code. The expansion of slices is to call the growslice
method. Different versions have slight differences in the expansion mechanism. Some important source codes of the Go1.17
version are intercepted:
// runtime/slice.go
// et:表示slice的一个元素;old:表示旧的slice; cap:表示新切片需要的容量;
func growslice(et *_type, old slice, cap int) slice {
if cap < old.cap {
panic(errorString("growslice: cap out of range"))
}
if et.size == 0 {
// append should not create a slice with nil pointer but non-zero len.
// We assume that append doesn't need to preserve old.array in this case.
return slice{unsafe.Pointer(&zerobase), old.len, cap}
}
newcap := old.cap
// 两倍扩容
doublecap := newcap + newcap
// 新切片需要的容量大于两倍扩容的容量,则直接按照新切片需要的容量扩容
if cap > doublecap {
newcap = cap
} else {
// 原 slice 容量小于 1024 的时候,新 slice 容量按2倍扩容
if old.cap < 1024 {
newcap = doublecap
} else { // 原 slice 容量超过 1024,新 slice 容量变成原来的1.25倍。
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
// 后半部分还对 newcap 作了一个内存对齐,这个和内存分配策略相关。进行内存对齐之后,新 slice 的容量是要 大于等于 老 slice 容量的 2倍或者1.25倍。
var overflow bool
var lenmem, newlenmem, capmem uintptr
// Specialize for common values of et.size.
// For 1 we don't need any division/multiplication.
// For sys.PtrSize, compiler will optimize division/multiplication into a shift by a constant.
// For powers of 2, use a variable shift.
switch {
case et.size == 1:
lenmem = uintptr(old.len)
newlenmem = uintptr(cap)
capmem = roundupsize(uintptr(newcap))
overflow = uintptr(newcap) > maxAlloc
newcap = int(capmem)
case et.size == sys.PtrSize:
lenmem = uintptr(old.len) * sys.PtrSize
newlenmem = uintptr(cap) * sys.PtrSize
capmem = roundupsize(uintptr(newcap) * sys.PtrSize)
overflow = uintptr(newcap) > maxAlloc/sys.PtrSize
newcap = int(capmem / sys.PtrSize)
case isPowerOfTwo(et.size):
var shift uintptr
if sys.PtrSize == 8 {
// Mask shift for better code generation.
shift = uintptr(sys.Ctz64(uint64(et.size))) & 63
} else {
shift = uintptr(sys.Ctz32(uint32(et.size))) & 31
}
lenmem = uintptr(old.len) << shift
newlenmem = uintptr(cap) << shift
capmem = roundupsize(uintptr(newcap) << shift)
overflow = uintptr(newcap) > (maxAlloc >> shift)
newcap = int(capmem >> shift)
default:
lenmem = uintptr(old.len) * et.size
newlenmem = uintptr(cap) * et.size
capmem, overflow = math.MulUintptr(et.size, uintptr(newcap))
capmem = roundupsize(capmem)
newcap = int(capmem / et.size)
}
}
The slice expansion strategy can be summarized through the source code:
When the slice is expanded, it will perform memory alignment, which is related to the memory allocation strategy. After memory alignment, the capacity of the new slice should be greater than or equal to twice or 1.25 times the old slice
capacity. When the capacity of slice
is smaller than that of 1024
, the capacity of the new slice
becomes 74 times the capacity of the original 2
; the capacity of the original slice
exceeds 1024
, and the capacity of the new slice
becomes 1.25
times the capacity of the original.
The above version is the version of Go language 1.17
. Before 1.16
, compared with 1024
, it was oldLen
. When 1.18
, it was changed to not compare with 1024
, but with 256
. The detailed code is as follows:
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
const threshold = 256
if old.cap < threshold {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
// Transition from growing 2x for small slices
// to growing 1.25x for large slices. This formula
// gives a smooth-ish transition between the two.
newcap += (newcap + 3*threshold) / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
The official comment of Go
said that the purpose of this is to make the transition more smoothly. Small slices can be expanded by 7 times of 2
, and large slices can be expanded by 8 times of 1.25
.
06. What is the difference between parameter passing slice and slice pointer?
We all know that the bottom layer of a slice is a structure with three elements:
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
Indicates the address of the underlying data of the slice, the slice length, and the slice capacity, respectively.
When a slice is passed as a parameter, it is actually the transmission of a structure, because the Go
language parameter is passed only by value, and passing a slice will shallowly copy the original slice, but because the address of the underlying data does not change, so the function of the slice is not changed. Modifications will also affect slices outside the function, for example:
func modifySlice(s []string) {
s[0] = "song"
s[1] = "Golang"
fmt.Println("out slice: ", s)
}
func main() {
s := []string{"asong", "Golang梦工厂"}
modifySlice(s)
fmt.Println("inner slice: ", s)
}
// 运行结果
out slice: [song Golang]
inner slice: [song Golang]
However, there is also a special case, let's look at an example:
func appendSlice(s []string) {
s = append(s, "快关注!!")
fmt.Println("out slice: ", s)
}
func main() {
s := []string{"asong", "Golang梦工厂"}
appendSlice(s)
fmt.Println("inner slice: ", s)
}
// 运行结果
out slice: [asong Golang梦工厂 快关注!!]
inner slice: [asong Golang梦工厂]
Because the slice has been expanded, the slice outside the function points to a new underlying array, so the inside and outside of the function will not affect each other, so it can be concluded that when the parameter directly passes the slice, If the pointer to the underlying array is overwritten or Modification (copy, reallocation, append triggers expansion), at this time, the modification of data inside the function will no longer affect the external slice, and neither the len representing the length nor the capacity cap will be modified .
Passing a slice pointer as a parameter is easy to understand. If you want to modify the value of the elements in the slice, and change the capacity and underlying array of the slice, you should pass it by pointer.
07. range
What should I pay attention to when traversing slices?
Go
language provides the range
keyword for iterating over elements of an array, slice, channel or map in a for loop. There are two ways to use it:
for k,v := range _ { }
for k := range _ { }
The first is to traverse the subscript and the corresponding value. The second is to traverse only the subscript. When using range
traverse the slice, one copy will be copied first, and then the copied data will be traversed:
s := []int{1, 2}
for k, v := range s {
}
会被编译器认为是
for_temp := s
len_temp := len(for_temp)
for index_temp := 0; index_temp < len_temp; index_temp++ {
value_temp := for_temp[index_temp]
_ = index_temp
value := value_temp
}
It is easy to step on the pit without knowing this knowledge point, such as the following example:
package main
import (
"fmt"
)
type user struct {
name string
age uint64
}
func main() {
u := []user{
{"asong",23},
{"song",19},
{"asong2020",18},
}
for _,v := range u{
if v.age != 18{
v.age = 20
}
}
fmt.Println(u)
}
// 运行结果
[{asong 23} {song 19} {asong2020 18}]
Because range
is used to traverse the slice u
, and the variable v
is the data in the copied slice, modifying the copied data will not affect the original slice.
I wrote a summary of for-range
stepping on the pit before, you can read it: Interviewer: Have you used for-range in go? Can you explain the reasons for these questions?
Summarize
This article summarizes the real interview questions related to 7
slicing. Slicing has always been an important test point in the interview. If you understand these knowledge points in this article, you will become more comfortable with the interviewer.
What other real interview questions about slicing? Welcome to the comment area to add~
Well, this article ends here, I am asong , see you in the next issue.
Welcome to the public account: Golang DreamWorks
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。