7

Go数据结构系列之 Array and Alice

 3 years ago
source link: https://studygolang.com/articles/32121
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Nj6fYvz.png!mobile

概述

在使用 Go 开发的时候,数组和切片经常被使用到,这篇文章来简单聊聊吧。

数组array

在 Go 中,有两种方式可以初始化数组

func main() {
  userId := [3]int{1, 2, 3}
  userName := [...]string{"wqq", "curry", "joke"}
}

一种是显式的定义数组的大小,另一种通过 [...] 声明数组,Go 会在编译期间推导出数组的大小。

既然使用了数组,少不了遍历,在 Go 中遍历数组一般也就两种方式。

func main() {
  userIds := [3]int{1, 2, 3}
  names := [...]string{"wqq", "curry", "joke"}

  for i := 0; i < len(names); i++ {
    fmt.Printf("user id is:%v,user name is:%v\n", userIds[i], names[i])
  }

  for index, item := range names {
    fmt.Printf("user id is:%v,user name is:%v\n", userIds[index], item)
  }
}

第一种就是你所认知的 for 循环。第二种可以使用 for/range 表达式,该表达式返回两个值,第一个值是索引,第二个值对应此索引的元素值。range 不单单能遍历数组,还能遍历 slice、map、channel 等集合结构。当然这些不在这篇文章的讨论范围内。

切片slice

切片本质上是动态数组,它的底层包含了对数组的引用。切片的长度是动态的,可以随意的对其进行 append 操作,在使用的过程中,如果容量不足,会自动进行扩容操作。我们可以从源码看看 slice 的结构。源码位于 src/runtime/slice.go ,更多底层知识可以自行查看源码。

type slice struct {
  array unsafe.Pointer // 底层数组的指针位置
  len   int // 切片当前长度
  cap   int //容量,当容量不够时,会触发动态扩容的机制
}

同理,初始化 slice 的方式也是多样的。

  • 使用 make 关键字
  • 和数组一样,使用字面量初始化
  • 通过下标的方式获取数组或者切片的一部分,生成 slice
func main() {
  // 字面量初始化
  userIds1 := []int{1, 2, 3}

  // make初始化slice的长度为5,容量为10
  userIds2 := make([]int, 5,10)

  // 通过下标的方式获取数组的一部分作为alice
  userArray := [5]string{"curry", "wqq", "lisa", "tony", "james"}

  // 获取从索引下标0开始,到下标3(不包括3)
  user := userArray[0:3]
  fmt.Printf("userIds1:%v,userIds2:%v,userSlice:%v\n", userIds1, userIds2, user)
}

这里就拿 make 初始化切片进行说明。

vEzaMv.png!mobile

注:图片来源 《Go编程专家》

这段初始化操作表示 slice 的长度是 5,容量是 10,array 字段存储的是引用数组的指针位置。因为长度是 5,我们可以使用下标 0-4 来操作此 slice。同时容量是 10,所以后续向 slice 添加新数据暂时不需要重新分配新内存。

那数组和切片有什么关联呢?

我们看看通过下标的方式获取数组数据,初始化切片的一种形式。

func main() {
  userArray := [4]string{"curry", "wqq", "lisa", "tony"}

  // 获取从索引下标0开始,到下标3(不包括3)
  userSlice := userArray[0:3]
  userSlice[0] = "zhangsan"
  fmt.Printf("userArray:%v,userSlice:%v\n", userArray, userSlice)
}

我们用数组创建了 userSlice 的切片,此时 userSlice 将和 userArray 共用一部分内存。因此在修改 userSlice 索引 0 处的值时,操作的是同一块数组内存地址,从结果中可以看出生效了。

UrYBrq6.png!mobile

然后我们开始往 userSlice 切片添加元素。

func main() {
  userArray := [4]string{"curry", "wqq", "lisa", "tony"}
  // 获取从索引下标0开始,到下标3(不包括3)
  userSlice := userArray[0:3]
  userSlice[0] = "zhangsan"
  fmt.Printf("userArray:%v,userSlice:%v\n", userArray, userSlice)
  
  userSlice = append(userSlice, "test1")
  fmt.Printf("userArray:%v,userSlice:%v\n", userArray, userSlice)
  
}

查看输出结果:

BR7BNfm.png!mobile

可以看到,再向 userSlice 增加一个元素后,打印结果,数组和切片值一样,操作之后 userSlice 的 len 是 4,数组的长度也是 4。操作 append 后 userSlice 底层数组和 userArray 指向的还是同一个内存地址,并不需要发生扩容。

这时候,userSlice 所引用的底层数组已经满了(底层数组的长度是4),我们继续向 userSlice 增加元素。

func main() {
  userArray := [4]string{"curry", "wqq", "lisa", "tony"}
  // 获取从索引下标0开始,到下标3(不包括3)
  userSlice := userArray[0:3]
  userSlice[0] = "zhangsan"
  fmt.Printf("userArray:%v,userSlice:%v\n", userArray, userSlice)

  userSlice = append(userSlice, "test1")
  fmt.Printf("userArray:%v,userSlice:%v\n", userArray, userSlice)

  userSlice = append(userSlice, "test2")
  fmt.Printf("userArray:%v,user:%v\n", userArray, userSlice)
 }

查看输出结果:

mm6ZVr7.png!mobile

可以看到,userArray 的元素未变,因为这时候 userSlice 切片的长度已经大于原指向的数组的长度了, userSlice 发生了扩容。

我们可以做个实验测试一下,我们修改数组 userArray 范围内的 userSlice 元素的值,查看数组的数据是否会跟着改变。

func main() {
  userArray := [4]string{"curry", "wqq", "lisa", "tony"}
  // 获取从索引下标0开始,到下标3(不包括3)
  userSlice := userArray[0:3]
  userSlice[0] = "zhangsan"
  fmt.Printf("userArray:%v,userSlice:%v\n", userArray, userSlice)

  userSlice = append(userSlice, "test1")
  fmt.Printf("userArray:%v,userSlice:%v\n", userArray, userSlice)

  userSlice = append(userSlice, "test2")
  fmt.Printf("userArray:%v,userSlice:%v\n", userArray, userSlice)
// 改变索引0处的值
  userSlice[0] = "only one"
  fmt.Printf("userArray:%v,userSlice:%v\n", userArray, userSlice)
}

rQVzmar.png!mobile

最后一行已经说明了一切。此时的 userSlice 发生了扩容,不再和 userArray 共用原数组空间了。因此对 userSlice 的改动不会影响到 userArray。

关于扩容

前面提到在向切片添加新元素时如果此时切片的容量不足,会自动发生扩容。所谓扩容,也就是为当前切片生成新的一块内存空间,然后根据一定规则,将原切片的元素全部拷贝到新的地址。扩容的规则在 src/runtime/slice.go 里的 growslice 方法。

AvMbqi7.png!mobile

这里截取了此方法中关于扩容规则的代码。

  • 如果期望的新容量 (cap) 大于当前容量的两倍,那么就直接使用期望的容量
  • 如果当前切片的长度 (len) 小于 1024,那么把当前容量翻倍
  • 如果当前切片的长度(len) 大于等于 1024,那么每次把当前容量增加 1/4,直到新容量值大于期望的的容量。

其实要写下去还有很多东西,比如,sliceCopy、底层编译逻辑......,有些东西我也没看过,学习的最好方式还是自己动手然后输出。

参考资料:

有疑问加站长微信联系(非本文作者)

eUjI7rn.png!mobile

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK