乐趣区

关于golang:Go语言切片面试真题7连问

前言

哈喽,大家好,我是asong。最近没事在看八股文,总结了几道常考的切片八股文,以问答的形式总结进去,心愿对正在面试的你们有用~

本文题目不全,对于切片的面试真题还有哪些?欢送评论区补充~

01. 数组和切片有什么区别?

Go语言中数组是固定长度的,不能动静扩容,在编译期就会确定大小,申明形式如下:

var buffer [255]int
buffer := [255]int{0}

切片是对数组的形象,因为数组的长度是不可变的,在某些场景下应用起来就不是很不便,所以 Go 语言提供了一种灵便,性能强悍的内置类型切片(“ 动静数组 ”),与数组相比切片的长度是不固定的,能够追加元素。切片是一种数据结构,切片不是数组,切片形容的是一块数组,切片构造如下:

<img src=”https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/93958be1acdb4eb8867d307e92ad1d17~tplv-k3u1fbpfcp-zoom-1.image” style=”zoom:50%;” />

咱们能够间接申明一个未指定大小的数组来定义切片,也能够应用 make() 函数来创立切片,申明形式如下:

var slice []int // 间接申明
slice := []int{1,2,3,4,5} // 字面量形式
slice := make([]int, 5, 10) // make 创立
slice := array[1:5] // 截取下标的形式
slice := *new([]int) // new 一个

切片能够应用 append 追加元素,当 cap 有余是进行动静扩容。

02. 拷贝大切片肯定比拷贝小切片代价大吗?

这道题比拟有意思,原文地址:Are large slices more expensive than smaller ones?

这道题实质是考查对切片实质的了解,Go语言中只有值传递,所以咱们以传递切片为例子:

func main()  {param1 := make([]int, 100)
    param2 := make([]int, 100000000)
    smallSlice(param1)
    largeSlice(param2)
}

func smallSlice(params []int)  {// ....}

func largeSlice(params []int)  {// ....}

切片 param2 要比 param11000000个数量级,在进行值拷贝的时候,是否须要更低廉的操作呢?

实际上不会,因为切片实质内部结构如下:

type SliceHeader struct {
 Data uintptr
 Len  int
 Cap  int
}

切片中的第一个字是指向切片底层数组的指针,这是切片的存储空间,第二个字段是切片的长度,第三个字段是容量。将一个切片变量调配给另一个变量只会复制三个机器字,大切片跟小切片的区别无非就是 LenCap的值比小切片的这两个值大一些,如果产生拷贝,实质上就是拷贝下面的三个字段。

03. 切片的深浅拷贝

深浅拷贝都是进行复制,区别在于复制进去的新对象与原来的对象在它们产生扭转时,是否会相互影响,本质区别就是复制进去的对象与原对象是否会指向同一个地址。在 Go 语言,切片拷贝有三种形式:

  • 应用 = 操作符拷贝切片,这种就是浅拷贝
  • 应用 [:] 下标的形式复制切片,这种也是浅拷贝
  • 应用 Go 语言的内置函数 copy() 进行切片拷贝,这种就是深拷贝,

04. 零切片、空切片、nil 切片是什么

为什么问题中这么多种切片呢?因为在 Go 语言中切片的创立形式有五种,不同形式创立进去的切片也不一样;

  • 零切片

咱们把切片外部数组的元素都是零值或者底层数组的内容就全是 nil的切片叫做零切片,应用 make 创立的、长度、容量都不为 0 的切片就是零值切片:

slice := make([]int,5) // 0 0 0 0 0
slice := make([]*int,5) // nil nil nil nil nil
  • nil切片

nil切片的长度和容量都为 0,并且和nil 比拟的后果为 true,采纳间接创立切片的形式、new 创立切片的形式都能够创立 nil 切片:

var slice []int
var slice = *new([]int)
  • 空切片

空切片的长度和容量也都为 0,然而和nil 的比拟后果为 false,因为所有的空切片的数据指针都指向同一个地址 0xc42003bda0;应用字面量、make 能够创立空切片:

var slice = []int{}
var slice = make([]int, 0)

空切片指向的 zerobase 内存地址是一个神奇的地址,从 Go 语言的源代码中能够看到它的定义:

// 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. 切片的扩容策略

这个问题是一个高频考点,咱们通过源码来解析一下切片的扩容策略,切片的扩容都是调用 growslice 办法,不同版本,扩容机制也有轻微差距,截取 Go1.17 版本局部重要源代码:

// 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)
    }
}

通过源代码能够总结切片扩容策略:

切片在扩容时会进行内存对齐,这个和内存调配策略相干。进行内存对齐之后,新 slice 的容量是要 大于等于老 slice 容量的 2 倍 或者 1.25 倍,当新切片须要的容量大于两倍扩容的容量,则间接依照新切片须要的容量扩容,当原 slice 容量小于 1024 的时候,新 slice 容量变成原来的 2 倍;原 slice 容量超过 1024,新 slice 容量变成原来的1.25 倍。

下面的版本是 Go 语言 1.17 的版本,在 1.16 以前和 1024 比拟是 oldLen,在1.18 时,又改成不和 1024 比拟了,而是和 256 比拟,具体代码如下:

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}
  }
}

Go官网正文说这么做的目标是能更平滑的过渡小切片依照 2 倍扩容,大切片依照 1.25 倍扩容。

06. 参数传递切片和切片指针有什么区别?

咱们都晓得切片底层就是一个构造体,外面有三个元素:

type SliceHeader struct {
 Data uintptr
 Len  int
 Cap  int
}

别离示意切片底层数据的地址,切片长度,切片容量。

当切片作为参数传递时,其实就是一个构造体的传递,因为 Go 语言参数传递只有值传递,传递一个切片就会浅拷贝原切片,但因为底层数据的地址没有变,所以在函数内对切片的批改,也将会影响到函数外的切片,举例:

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]

不过这也有一个特例,先看一个例子:

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 梦工厂]

因为切片产生了扩容,函数外的切片指向了一个新的底层数组,所以函数内外不会相互影响,因而能够得出一个论断,当参数间接传递切片时,如果指向底层数组的指针被笼罩或者批改(copy、重调配、append 触发扩容),此时函数外部对数据的批改将不再影响到内部的切片,代表长度的 len 和容量 cap 也均不会被批改

参数传递切片指针就很容易了解了,如果你想批改切片中元素的值,并且更改切片的容量和底层数组,则应该按指针传递。

07. range遍历切片有什么要留神的?

Go语言提供了 range 关键字用于 for 循环中迭代数组 (array)、切片(slice)、通道(channel) 或汇合 (map) 的元素,有两种应用形式:

for k,v := range _ { }
for k := range _ {}

第一种是遍历下标和对应值,第二种是只遍历下标,应用 range 遍历切片时会先拷贝一份,而后在遍历拷贝数据:

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
  
}

不晓得这个知识点的状况下很容易踩坑,例如上面这个例子:

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}]

因为应用 range 遍历切片 u,变量v 是拷贝切片中的数据,批改拷贝数据不会对原切片有影响。

之前写了一个对 for-range 踩坑总结,能够读一下:面试官:go 中 for-range 应用过吗?这几个问题你能解释一下起因吗?

总结

本文总结了 7 道切片相干的面试真题,切片始终是面试中的重要考点,把本文这几个知识点弄会,应答面试官就会变的轻松自如。

对于切片的面试真题还有哪些?欢送评论区补充~

好啦,本文到这里就完结了,我是asong,咱们下期见。

欢送关注公众号:Golang 梦工厂

退出移动版