前端学后端,golang切片的云里雾里

416 阅读2分钟

一门技术,当你不熟悉他的时候,他的一切对你来说都是未知,担忧,退缩,甚至逃避都可能发生,于是你的眼前就形成了一片片的云雾缭绕。解决方案其实很简单——剥开天空中的乌云,像蓝丝绒一样美丽。

                    ————大叔你好

切片的三种创建方式

根据go的基础知识,创建切片的三种方式:

直接定义切片
s := []int{} //空切片
或  
s := []int{1, 2, 3} //默认赋值
通过make定义一个切片
  • s := make([]int, 0, 0
  • make 参数的意义分别为:类型,长度len,容量cap
  • 长度与容量都是0的时候,上面写法可以简写成 s := make([]int,0) * make出来的只是一个空切片,需要append操作才能增加内容
从现有的切片创建
  •  s1 := []int{1, 2, 3}
     s2 := s1[1:2]
    
  • 语法: [startIndex:endIndex]
  • 取值范围: startIndex<= index < endIndex
  • 左闭右开的半开区间,即取值包括startIndex,不包括endIndex

切片的疑惑

问题就出现在第三种方式上

案例(一):
a := []int{1, 2, 3, 4}
fmt.Printf("a: %v\n", a) //[1 2 3 4]

b := a[1:3]
fmt.Printf("b: %v\n", b) //[2 3]
fmt.Printf("len(a): %v\n", len(a)) //4
fmt.Printf("len(b): %v\n", len(b)) //2

b = append(b, 5)
fmt.Printf("b: %v\n", b) //[2 3 5]
fmt.Printf("len(b): %v\n", len(b))//3

fmt.Printf("a: %v\n", a)           //[1 2 3 5]
fmt.Printf("len(a): %v\n", len(a)) //4

变量b从切片a里通过开始索引与结束索引得到从第1项到第3项【以前】的内容,这一点与js里的slice用法几乎一样,不同的是,go里的切片截取索引不支持负数。

解惑

代码中,b拿到的值为 [2, 3],然后通过append增加了元素 5,b变了 [2, 3, 5] 是没问题了,但是为什么a变了 [1, 2, 3, 5] 了呢?这里就得先了解一下切片的基本原理

  • 可以把切片理解成是数组的高级管理者
  • 切片对数据的管理依然通过它的底层数组
  • 切片的组成部分:
    • len长度
    • cap容量
    • 指针

如图所示

切片示意图.jpg

图中的结论就是,

  • 切片 a与b指向了相同的底层数组
  • 当执行b = a[1:3]时,a的底层数组的指针指向了第3位
  • 当执行 b = b.append(b, 5)时,a的底层数组从第3位开始增加5,于是4被5覆盖
案例(二)

如果上面的内容你懂了,那么我们再看案例(二)

a := []int{1, 2, 3, 4}
fmt.Printf("a: %v\n", a) //[1 2 3 4]

b := a[1:3] //[2 3]
fmt.Printf("b: %v\n", b)
fmt.Printf("len(a): %v\n", len(a)) //4
fmt.Printf("len(b): %v\n", len(b)) //2

b = append(b, 5, 6) 🍎🍎🍎🍎🍎🍎
fmt.Printf("b: %v\n", b)           //[2 3 5 6]
fmt.Printf("len(b): %v\n", len(b)) //4

fmt.Printf("a: %v\n", a)           //[1 2 3 4]
fmt.Printf("len(a): %v\n", len(a)) //4

唯一的区别就是b = b.append(b, 5, 6) ,比案例(一)多了一个元素。为什么a切片并没有受到影响?

解惑
  • 首先,在b.append的时候,会优先与a使用相同的底层数组。
  • 在b=a[1:3]时,a的底层数组的指针移动到第3位时,因为要即将一次性增加2个元素(覆盖一个,增加一个),期望容量为5个(5替换4 + 额外增加6),超出了a切片的起始容量4
  • 如果还共用相同底层数组的话,就需要【扩容】,因此,golang就会新建一个底层数组给b,同时把刚才b=a[1:3]所得的内容复制到这个新的底层数组里,再执行 b = append(b, 5, 6)
  • 最终实现了 b与a的底层数组相互独立,实现了 「断开」

切片结论

根据案例(一)与案例(二)得出结论

  • 切片只不过是底层数组的上层管理
  • 一个底层数组可能对应多个切片
  • 对于案例中的a 与 b,b=a[1:3]生成新切片后,对b进行append操作,如果:
    • 他们共用的底层数组不需要扩容,则复用同一个底层数组
    • 如果不得不扩容,那么就新建一个底层数组与a的底层数组彻底断开联系

不知道朋友是否能彻底理解 golang的切片了,是否像蓝丝绒一样美丽了?🌸

业务中的烦恼

想实现js里的[].map方法

别开心的太早,原理明白了,接下来,就是实际上写代码的痛点来了。在js业务中,官方有很多种方法来帮助开发者实现数组的管理,比如 map()方法, golang中,我们需要这么去实现

//为了不改变上述切片原理导致的影响,我们使用make创建新切片
arr := []int{1, 2, 3, 4}
newArr := make([]int, 0)//用[]int{}也可以
arrLen := len(arr)
for i := 0; i < arrLen; i++ {
        newArr = append(newArr, arr[i])
}
fmt.Printf("arr: %v\n", arr) //[1 2 3 4] 互不影响
fmt.Printf("newArr: %v\n", newArr)//[1 2 3 4] 互不影响

是,我们可以这样去实现,但是每次需要的时候都用5行代码(甚至更多)来解决业务操作,是不是很麻烦?

想实现js里的[].filter方法

我们再看js里的filter方法如何使用

//js里
const a = [1,2,3,4]
const b = a.filter(item => item > 3) //b 的结果为[4]

如果用golang

//golang
arr := []int{1, 2, 3, 4}
newArr := make([]int, 0)
arrLen := len(arr)
for i := 0; i < arrLen; i++ {
    if arr[i] > 3 {
        newArr = append(newArr, arr[i])
    }
}
fmt.Printf("arr: %v\n", arr)       //[1 2 3 4]
fmt.Printf("newArr: %v\n", newArr) //[4]

虽然实现了,但是不是很麻烦,业务中,我们对数组的操作其实还是很频繁的。每次都这么写,是不是降低了工作效率? js中那些shift unshift pop splice every some find findLast findIndex findLastIndex 怎么办?

大叔的轮子

于是大叔帮大伙造了一个轮子,模拟js里的上述方法在golang里实现了一套 《javascript 味的 golang 数组》

github.com/butoften/ar…

my.png

结束语

  • javescript味golang,有兴趣的朋友可以直接使用。节省时间,提高开发效率。
  • 关于切片的云里雾里就先介绍到这里,理解它些,开发者就不再害怕什么时候会彼此影响什么时候不影响了。
  • 同时,golang相比js,以切片为例,他使开发者更多的了解了golang内存复用手法。也为我们对内存优化上,增加了一点思路。