4
头图

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 the Go 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


asong
605 声望906 粉丝