GoLang切片相关问题梳理讲解

作者:~庞贝 时间:2024-04-29 13:06:11 

1.数组和切片有什么区别

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

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

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

GoLang切片相关问题梳理讲解

我们可以直接声明一个未指定大小的数组来定义切片,也可以使用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不足时进行动态扩容。

2.拷贝大切片一定比拷贝小切片代价大吗

这道题本质是考察对切片本质的理解,Go语言中只有值传递,所以我们以传递切片为例子:

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

切片param2要比param1大1000000个数量级,在进行值拷贝的时候,是否需要更昂贵的操作呢?

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

type SliceHeader struct {
Data uintptr
Len  int
Cap  int
}

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

3.切片的深浅拷贝

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

1.使用=操作符拷贝切片,这种就是浅拷贝

2.使用[:]下标的方式复制切片,这种也是浅拷贝

3.使用Go语言的内置函数copy()进行切片拷贝,这种就是深拷贝

4.零切片 空切片 nil切片是什么

4.1零切片

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

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

4.2nil切片

nil切片的长度和容量都为0,并且和nil比较的结果为true,采用直接创建切片的方式、new创建切片的方式都可以创建nil切片:

var slice []int
var slice = *new([]int)

4.3空切片

空切片的长度和容量也都为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)
}
 ...
}

5.切片的扩容策略

这个问题是一个高频考点,我们通过源码来解析一下切片的扩容策略,切片的扩容都是调用growslice方法,截取部分重要源代码:

1.17之前

代码的扩容策略可以简述为以下三个规则:

1.当期望容量 > 两倍的旧容量时,直接使用期望容量作为新切片的容量

2.如果旧容量 < 1024(注意这里单位是元素个数),那么直接翻倍旧容量

3.如果旧容量 > 1024,那么会进入一个循环,每次增加25%直到大于期望容量

可以看到,原来的go对于切片扩容后的容量判断有一个明显的magic number:1024,在1024之前,增长的系数是2,而1024之后则变为1.25。关于为什么会这么设计,社区的相关讨论1给出了几点理由:

1.如果只选择翻倍的扩容策略,那么对于较大的切片来说,现有的方法可以更好的节省内存。

2.如果只选择每次系数为1.25的扩容策略,那么对于较小的切片来说扩容会很低效。

3.之所以选择一个小于2的系数,在扩容时被释放的内存块会在下一次扩容时更容易被重新利用

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

1.18之后

到了Go1.18时,又改成不和1024比较了,而是和256比较;并且扩容的增量也有所变化,不再是每次扩容1/4,如下代码所示:

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

GoLang切片相关问题梳理讲解

在1.18中,优化了切片扩容的策略2,让底层数组大小的增长更加平滑:

通过减小阈值并固定增加一个常数,使得优化后的扩容的系数在阈值前后不再会出现从2到1.25的突变,该commit作者给出了几种原始容量下对应的&ldquo;扩容系数&rdquo;:

GoLang切片相关问题梳理讲解

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

我们都知道切片底层就是一个结构体,里面有三个元素:

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

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

因为切片发生了扩容,函数外的切片指向了一个新的底层数组,所以函数内外不会相互影响,因此可以得出一个结论,当参数直接传递切片时,如果指向底层数组的指针被覆盖或者修改(重分配、append触发扩容),此时函数内部对数据的修改将不再影响到外部的切片,代表长度的len和容量cap也均不会被修改

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

7.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]
 k := index_temp
 v := 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是拷贝切片中的数据,修改拷贝数据不会对原切片有影响。

来源:https://blog.csdn.net/qq_53267860/article/details/126772653

标签:GoLang,切片
0
投稿

猜你喜欢

  • Python中Timedelta转换为Int或Float方式

    2021-01-24 19:46:41
  • JavaScript使用math.js进行精确计算操作示例

    2024-04-10 10:54:41
  • django实现用户注册实例讲解

    2022-06-23 08:05:53
  • Numpy中Meshgrid函数基本用法及2种应用场景

    2023-05-11 20:13:21
  • logging level级别介绍

    2023-06-07 00:39:16
  • PHP实现的微信公众号扫码模拟登录功能示例

    2023-11-22 18:13:08
  • Python如何实现逐行读取文本文件

    2022-06-10 03:34:54
  • 我的“Orcas初览”讲座

    2007-09-23 12:53:00
  • 分享下GET和POST的真正区别

    2023-10-19 23:23:09
  • 用ASP实现Google在线文章翻译的功能

    2008-10-11 13:55:00
  • Python根据成绩分析系统浅析

    2023-08-02 20:25:07
  • 使用element-ui +Vue 解决 table 里包含表单验证的问题

    2024-05-28 16:00:02
  • MySQL数据库备份恢复的两个实用方法

    2008-12-31 15:09:00
  • Python程序控制语句用法实例分析

    2021-04-13 06:59:48
  • 对django中foreignkey的简单使用详解

    2021-09-04 21:04:20
  • Oracle中获取执行计划的几种方法分析

    2023-07-17 15:18:31
  • PHP取余函数介绍MOD(x,y)与x%y

    2023-10-02 20:15:55
  • Javascript 面试题随笔

    2024-04-29 13:44:53
  • Python教程之生产者消费者模式解析

    2021-03-07 17:55:53
  • python输出结果刷新及进度条的实现操作

    2022-09-24 15:13:15
  • asp之家 网络编程 m.aspxhome.com