当前位置:首页 > 短网址资讯

Golang Slice 的一些事

www.ft12.com9年前 (2017-07-21)短网址资讯1148

女主宣言

使用 Golang 编程时,常会使用到一个数据结构 —— Slice,这篇文带大家看看 Slice 具体的数据结构以及常用手法。

PS:丰富的一线技术、多元化的表现形式,尽在“HULK一线技术杂谈”,点关注哦!

1. Slice 数据结构



首先,直接从源码$YOUR_GO_DIR/src/runtime/slice.go(其中$YOUR_GO_DIR指你自己go源代码的根目录)中找到定义的slice结构,如下:

type slice struct {
   // 任意类型指针(类似C语言中的 void* ), 指向实际存储slice数据的数组    array unsafe.Pointer    len   int // length, 长度    cap   int // capacity, 容量 }

从结构很好看出,通过make()函数(比如:make([]int, 10, 20))创建出来的slice,其实就是由两部分组成:slice的"描述"(上面的结构体) + 存储数据的数组(指针指向的数组)

备注:后文将使用SliceHeader代替上面的结构体,SliceHeader是Rob PikeGolang/slices博文中暂用来指代的名词,这里我也借用一下。为了方便理解,把slice拆成 SliceHeader + 存储数据的数组 两部分。


2. 使用 Slice 须知 



2.1 值传递下的Slice

我们知道Go的参数是值传递,那么这里有个问题需要考虑: 当把一个slice变量通过参数传递给某函数时,传的是SliceHeader、还是整个SliceHeader+数据(存储数据的数组)都被复制过去?

比如,这样的代码:

s := make([]string, 10)  
saveSlice(s)

当我们在项目中某个slice有10万元素,如果传参数直接复制SliceHeader + 数据,那么这是一定不能接受的。

Rob Pike有这样一句话定义Slice: slice不是数组,它是对数组进行了描述(A slice is not an array. A slice describes a piece of an array)。 实际上,在上面的代码片段中saveSlice(s)接收到的是变量s的一个副本(就是一个值跟s一样,但是是全新的变量),这个副本跟变量s一样,有一个指向同个数组的指针、len和cap相同值。

为什么?因为Go是值传递,简单试验就知道。

实验一:如果slice变量参数传递,是复制了数据,那么在函数中操作"被复制过来的"数据,不会对原数据造成影响。

// 代码片段
data := make([]int, 10)
fmt.Println("处理前数据: ", data)
changeIndex0(data)
fmt.Println("处理后数据: ", data)

函数 changeIndex0(data []int)

// 替换第一个元素的值
func changeIndex0(data []int)  {
    data[0] = 99
}

实验结果

处理前数据:  [0 0 0 0 0 0 0 0 0 0]
处理后数据:  [99 0 0 0 0 0 0 0 0 0]

显然,从结果中看得出,原始数据被修改了,所以可以得出结论是:传递slice变量时,并不是复制真正存储数据的数组进行传递。

所以,在实际项目中,直接传递slice变量与传递slice变量的指针,对内存的消耗区别并不是很大。一个SliceHeader的大小是24字节,而指针大小8字节。

备注: SliceHeader 24字节计算方式:8字节(指针) + 8字节(整型int, len) + 8字节(整型int, cap),这是以我自己电脑为例(64位),指针大小8字节;整型int大小也跟编译器有关,但Golang中最少是32bit,我在本机使用unsafe.SizeOf()实测是8字节。

2.2 Slice截取和扩充

说到底,slice由SliceHeader和数组构成。涉及到数组,避不开的问题就是定长,也就是一旦数组长度确定了就无法改变。如果非要改变长度,那只能一个办法:重新分配一个新的数组。

对于slice也一样,如果一个slice已经确定了容量(capacity),那么如果要扩充该slice的容量,也必须重新分配一个存数据的数组。

备注:slice的容量在使用make([]byte, 10, 20)时,第三个参数已经确定;第三个参数就是容量(capacity),如果不指定,默认跟第二个参数(长度len)一样。


i. 截取子Slice


当基于原Slice进行截取子Slice时,实际上操作的还是原Slice的元素。也就是对子Slice的元素进行修改,都会在原Slice中体现。

实验 二:操作从原 Slice 截取而获得的子 Slice

// 代码片段
data := make([]int, 10)
fmt.Println("处理前数据: ", data, len(data), cap(data))
subSlice(data)
fmt.Println("处理后数据: ", data, len(data), cap(data))

函数 subSlice(data []int)

// 截取slice
func subSlice(data []int)  {
    data[0] = 99
    data = data[0:8]
    fmt.Println("函数中数据: ", data, len(data), cap(data))
}

实验结果

处理前数据:  [0 0 0 0 0 0 0 0 0 0] 10 10
函数中数据:  [99 0 0 0 0 0 0 0] 8 10
处理后数据:  [99 0 0 0 0 0 0 0 0 0] 10 10

从实验结果可以看出,在Slice的容量(capacity)范围内子Slice截取,是直接使用了原Slice的数组,并没有为该子Slice分配新的数组。

如果我需要截取一个子Slice并且希望该子Slice有新的数组,该怎么操作?这是可以使用copy()函数。

sub := make([]int, 2)
copy(sub, data[3:5])
ii. 扩充Slice


事实上,扩充Slice的操作就是:重新创建一个更大容量的Slice,然后把原Slice中的数据复制到新的Slice里面。

比如:常用操作append()

fmt.Printf("append()前: len: %d, cap: %d \n", len(data), cap(data))
data = append(data, 5)
fmt.Printf("append()后: len: %d, cap: %d \n", len(data), cap(data))

结果

append()前: len: 10, cap: 10 
append()后: len: 11, cap: 20

append()中的操作就是新建了一个容量为原来两倍的Slice,然后把原来的数据复制到新Slice并且把新的元素加上。

3. Slice 常用操作函数


Go提供了方便操作的语法糖,如: data[2:5],以此来获取第二到第四(包括第四)个元素。

备注: ':' 左右都可以不指定值。右边的值不可以超过该Slice的容量大小,否则会Panic。

3.1 copy() 复制Slice的值到另外一个Slice,上面例子也用到了,这函数会自动参考len更小的那个Slice,不会发生报slice bounds out of range的异常。

3.2 append()给某个Slice添加元素,也是常用的,上面的例子也有体现。

4. 其他


4.1 关于string

从源码包runtimestring.go中可以看到字符串的struct。

type stringStruct struct {
    str unsafe.Pointer
    len int
}

也就是,string实际上就是只读的byte切片(Slice),只是从Golang语言层面提供的语法支持而已。因为只能读,所以容量的存在与否都无济于事。

4.2 关于Slice nil

我们知道make()方法专门用来新建Slice、map、chan,但是我们也可以用new()来建Slice,但是两者有区别。

// 代码片段
nilSlice := new([]int)
fmt.Printf("nilSlice is nil: %v \n", *nilSlice == nil)
emptySlice := make([]int, 0)
fmt.Printf("emptySlice is nil: %v \n", emptySlice == nil)

结果打印

nilSlice is nil: true 
emptySlice is nil: false

也就是用new()创建后的Slice变量是零值,而make()创建一个0长度的Slice并不是nil。

为什么?因为new() 和 make()做的事情不一样。

new()做了两件事

  • 为该类型分配内存

  • 置零值(不同类型的零值不一样,比如: bool是false,整型是0...等)

make()也做了两件事

  • 为该类型分配内存

  • 初始化

以Slice为例,new([]int)得到的SliceHeader是:

sliceHeader {
    array: nil,
    len: 0,
    cap: 0,
}

make([]int, 0)得到的SliceHeader应该是:

sliceHeader {
    array: 0x8201d0140, // 指向0个元素的数组
    len: 0,
    cap: 0,
}

5. 结语


从Slice的实现、使用场景进行更加全面的了解,会对在项目中的使用有更大的帮助以及尽量避免因为不知道细节而错用。


参考:

扫描二维码推送至手机访问。

版权声明:本文由短链接发布,如需转载请注明出处。

本文链接:https://www.ft12.com/article_316.html

分享给朋友:

相关文章

好标题≠标题党,学会这5招,你也能打造刷爆朋友圈的新媒体标题

好标题≠标题党,学会这5招,你也能打造刷爆朋友圈的新媒体标题

奥美创始人大卫·奥格威有一句经典名言“如果你的标题没有吸引到受众的目光,相当于浪费了80%的广告费。对于做内容的人而言,你就浪费了80%的精力和时间”。标题对于一篇文章的重要性不言而喻。但是,好标题≠标题党,一个有传播性的新媒体标题不仅应该…

如何成为牛逼的产品经理?硅谷连续创业者这样说

如何成为牛逼的产品经理?硅谷连续创业者这样说

编者注:Jim Patterson,“大麻界的Uber”Eaze的首席执行官,深谙产品之道的企业家,曾联合创办包括DineOnMe、AudioCaseFiles、Zinc在内的多家公司,并曾担任过诸多企业的首席产品官。 五年前,Clove…

高并发秒杀系统架构设计 · 抢购、微信红包、短网址、一元夺宝

高并发秒杀系统架构设计 · 抢购、微信红包、短网址、一元夺宝

秒杀业务与难点秒杀业务在各业务中已然非常流行,这里我将互联网行业中的秒杀定义为:在非常短的时间内,将一件商品分成多份进行购买的行为。微信抢红包、短网址、一元夺宝、双11大促抢购等业务本质上都可视作秒杀业务。而最近大热的抢红包的难度在于这是和…

看了那么多SEO教程,为何你还是搞不定排名?

看了那么多SEO教程,为何你还是搞不定排名?

前言:本文独家原创首发卢松松博客,转载请保留作者联系方式与原文出处!前些天和一个朋友在讨论互联网营销的时候偶然提到了SEO优化,朋友说去年买了一套SEO教程自学,到目前为止,差不多将近一年了,排名还是非常的不好(几乎没有排名)。我问他既然看…

否认全盘接手,百度外卖或与顺丰成立合资公司

否认全盘接手,百度外卖或与顺丰成立合资公司

[ 亿欧导读 ] 现在顺丰控股收购baidu外卖的方向是,双方将按照5:5的出资份额,一起出资经过建立合资公司来运营,不过一切都未确定,还存在变数。一位接近交易的人士表示:“之所以不会全盘接手baidu外卖,主要是因为顺丰以为全资…

在线教育企业7成亏损,15%濒临倒闭,你如何突围?

在线教育企业7成亏损,15%濒临倒闭,你如何突围?

[ 亿欧导读 ] 四月暮春,又到了一年一度在线教育公司交作业的时期,无论是自己晒成绩,还是第三方机构出评估,我们看到报表里的在线教育行业都是欣欣向荣、前景喜人,但真实情况如何?在线教育市场规模增幅迅猛,2019年将达2692.6亿…

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。