Go 灵活多变的切片Slice
我们知道数组定义好之后其长度就无法再修改,但是,在实际开发过程中,有时候我们并不知道需要多大的数组,我们期望数组的长度是可变的,
在 Go 中有一种数据结构切片(Slice) 解决了这个问题,它是可变长的,可以随时向Slice 里面添加数据。
1 什么是切片(Slice)
在 Go 源码中是这样定义切片的,源码地址:https://github.com/golang/go/blob/master/src/runtime/slice.go
type slice struct {
array unsafe.Pointer
len int
cap int
}
从源码中我们可以看到 Slice 也是一种结构体,这个结构体的名字是:Slice,这个结构体包含三个属性:array、len、cap。
第1个属性是指向底层数组的指针(Pointer),指向数组中 Slice 开始的位置;
第2个属性是切片中元素的个数(len),也就是这个 Slice 的长度;
第3个属性是这个切片的容量(cap),也就是 Slice 从开始位置到底层数组最后位置的长度;
2 切片的创建
2.1 切片的创建方式有很多种,一个比较通用的创建方式,使用 Go 的内置函数 make() 创建
package main import "fmt" func main() {
var s1 []int = make([]int,5,8)
var s2 []int = make([]int,8) fmt.Println(s1, s2)
}
输出结果
[root@VM_81_181_centos golang]# go run slice01.go
[0 0 0 0 0] [0 0 0 0 0 0 0 0]
[root@VM_81_181_centos golang]#
make() 函数创建切片,需要提供三个参数,切片的类型、切片的长度、切片的容量。其中第3个参数是可选的,如果第三个参数不提供的话,
则代表创建的是满容切片,也就是长度和容量相等。另外切片也可以通过类型自动推导,省去类型定义和 var 关键字。比如:
package main import "fmt" func main() {
var s1 []int = make([]int, 5, 8)
s2 := make([]int, 8) fmt.Println(s1, s2)
}
另外,我们可以使用 len()、cap() 函数获取切片的长度和容量
package main import "fmt" func main() {
numbers := make([]int,3,5) printSlice(numbers)
} func printSlice(x []int) {
fmt.Printf("len=%d cap=%d slice=%v\n",len(x), cap(x),x)
}
输出结果
[root@VM_81_181_centos golang]# go run slice01.go
len=3 cap=5 slice=[0 0 0]
[root@VM_81_181_centos golang]#
2.2 用已有的数组生成切片
package main import "fmt" func main() {
// 1.通过数组生成切片
// 定义一个数组
arr1 := [8]int{1,2,3,4,5,6,7,8}
fmt.Println(arr1) // 定义一个切片
s1 := arr1[1:4]
fmt.Println(s1)
}
输出结果
[root@VM_81_181_centos golang]# go run slice01.go
[1 2 3 4 5 6 7 8]
[2 3 4]
[root@VM_81_181_centos golang]#
2.3 用已有的切片生成切片
package main import "fmt" func main() {
s := []int{1,2,3}
s1 := s[1:3] // s1 为 [2,3]
s2 := s1[1:2]
fmt.Println(s2)
}
输出结果
[root@VM_81_181_centos golang]# go run slice01.go
[3]
[root@VM_81_181_centos golang]#
3 切片的初始化
使用 make() 函数创建的切片是零值切片,Go 语言还提供了另外一种创建切片的方法,允许我们给它赋初值,使用这种方式创建的切片
是满容的。
3.1 通过数组初始化切片:
s := []int{1,2,3,4,5}
直接初始化切片s
s := arr[:]
初始化切片s,是数组 arr 的引用
s := arr[startIndex:endIndex]
从已有数组中创建一个新的切片,新切片元素是从数组 arr 下标 startIndex 到 endIndex-1
s := [startIndex:]
缺省 endIndex 表示一直取到数组的最后一个元素
s :=arr[:endIndex]
缺省 startIndex 表示从数组的第一个元素开始取值
3.2 通过切片初始化切片
s1 := s[startIndex:endIndex]
在这里我们把原切片称之为父切片,新切片称之为子切片,子切片在语法上要提供起始位置和结束位置,这两个位置都是可选值,
如果不提供起始位置则默认为是从父切片的初始位置开始;如果不提供结束位置则默认到父切片尾部结束,如下
package main import "fmt" func main() {
s1 := []int{1,2,3,4,5,6,7}
// 不提供起始位置
s2 := s1[:5]
// 不提供结束位置
s3 := s1[3:]
// 提供起始位置和结束位置
s4 := s1[1:5]
// 起始位置和结束位置都不提供
s5 := s1[:]
fmt.Println(s1,len(s1),cap(s1))
fmt.Println(s2,len(s2),cap(s2))
fmt.Println(s3,len(s3),cap(s3))
fmt.Println(s4,len(s4),cap(s4))
fmt.Println(s5,len(s5),cap(s5))
}
输出结果
[root@VM_81_181_centos golang]# go run test01.go
[1 2 3 4 5 6 7] 7 7
[1 2 3 4 5] 5 7
[4 5 6 7] 4 4
[2 3 4 5] 4 6
[1 2 3 4 5 6 7] 7 7
[root@VM_81_181_centos golang]#
我们发现当我们没有提供起始位置时子切片 s1 和父切片 s2 的容量一致;不提供结束位置的时候子切片的长度和容量都是到
父切片结束位置;起始位置和结束位置都提供的时候,子切片的容量是从起始位置到父切片结束位置;综合这几种情况可以说
明父子切片共享底层数组。
再看一下子切片 s5 ,起始位置和结束位置都没有提供并且输出结果和父切片 s1 是一致的,给人的感觉好像是切片的赋值一样,
那这种形式和切片的赋值有什么区别呢?看一个例子
package main import "fmt" func main() {
// 定义切片s
s := []int{1,2,3,4,5,6,7} // 使用切片赋值的形式将s赋值给s1
s1 := s
// 使用[:]
s2 := s[:]
fmt.Println(s,len(s),cap(s))
fmt.Println(s1,len(s1),cap(s1))
fmt.Println(s2,len(s2),cap(s2)) // 修改父切片s
s[0] = 10
fmt.Println(s,len(s),cap(s))
fmt.Println(s1,len(s1),cap(s1))
fmt.Println(s2,len(s2),cap(s2))
}
输出结果
[root@VM_81_181_centos golang]# go run test01.go
[1 2 3 4 5 6 7] 7 7
[1 2 3 4 5 6 7] 7 7
[1 2 3 4 5 6 7] 7 7
[10 2 3 4 5 6 7] 7 7
[10 2 3 4 5 6 7] 7 7
[10 2 3 4 5 6 7] 7 7
[root@VM_81_181_centos golang]#
事实证明这两种形式没有区别,从这个地方也可以证实切片拷贝前后共享底层数组,修改其中一个会影响另一个切片的内容,
这一点和数组值拷贝是不一样的。
4 切片的值拷贝
切片作为参数传递给函数的时候,虽然也是通过值拷贝的形式传递,但是依然引用的是同一个底层数组。所以,当切片作为参
数传递给函数的时候,在函数内对切片的内容更改也会在函数外可见。如下
package main import "fmt" func main() {
s1 := []int{6,7,8}
// 打印切片s1
fmt.Println(s1) subtactone(s1)
fmt.Println(s1)
} func subtactone(s []int) {
for i := range s{
s[i] -= 2
}
}
输出结果
[root@VM_81_181_centos golang]# go run slice03.go
[6 7 8]
[4 5 6]
[root@VM_81_181_centos golang]#
函数 subtactone 对传入的切片循环并将切片中的每个元素减去 2 。在函数调用后输出切片的值,发现
函数外切片的值也发生了变化,这一点是不同于数组的,数组作为参数传递给函数时在函数内部对数组
修改,在函数外是不可见的。
5 切片的遍历
切片在遍历上的方式和数组是一样的,支持 for 和 使用 range 关键字遍历
package main import "fmt" func main() {
s := []int{1,2,3,4,5} // for 遍历
for i := 0;i < len(s);i++ {
fmt.Println(s[i])
} // 使用 range 关键字
for index,value := range s {
fmt.Println(index, value)
}
}
输出结果
[root@VM_81_181_centos golang]# go run slice01.go
1
2
3
4
5
0 1
1 2
2 3
3 4
4 5
[root@VM_81_181_centos golang]#
6 切片的扩容(追加)
我们知道切片的长度是可变化的,这个可变其实就是追加操作(append)导致的,我们使用 append 操作追加元素到切片时,如果容
量不够,则会创建新的切片,意味着内存地址也会发生变化,如果底层数组没有扩容,那么追加前后的两个切片共享底层数组,当底
层数组是共享的,一个切片的内容变化会影响到另一个切片的内容。如果底层数组扩容了,那么追加前后的两个切片是不共享底层数组
的。
package main import "fmt" func main() {
// 定义切片s1 且切片s1是满容的
s1 := []int{1,2,3,4,5}
// 打印切片s1
fmt.Printf("内存地址:%p \t\t长度:%v \t\t容量%v \t\t包含的元素:%v\n",s1,len(s1),cap(s1),s1) // 对满容的切片追加元素
s2 := append(s1,6)
// 打印切片s2
fmt.Printf("内存地址:%p \t\t长度:%v \t\t容量%v \t\t包含的元素:%v\n",s2,len(s2),cap(s2),s2)
// 修改s2的值
s2[1] = 10
fmt.Println(s1,s2) // 对没有满容的切片追加元素
s3 := append(s2,7)
// 打印切片s3
fmt.Printf("内存地址:%p \t\t长度:%v \t\t容量%v \t\t包含的元素:%v\n",s3,len(s3),cap(s3),s3)
// 修改s3的值
s3[1] = 20
fmt.Println(s2,s3)
}
输出结果
[root@VM_81_181_centos golang]# go run slice01.go
内存地址:0xc420010150 长度:5 容量5 包含的元素:[1 2 3 4 5]
内存地址:0xc420042050 长度:6 容量10 包含的元素:[1 2 3 4 5 6]
[1 2 3 4 5] [1 10 3 4 5 6]
内存地址:0xc420042050 长度:7 容量10 包含的元素:[1 10 3 4 5 6 7]
[1 20 3 4 5 6] [1 20 3 4 5 6 7]
[root@VM_81_181_centos golang]#
我们可以看到输出结果中 s1、s2 的内存地址发生了变化,是因为我们将元素 6 追加至切片 s1 中,超过了切片的最大容量 5 ,会创
建一个新的切片并将 s1 原有的元素拷贝一份至新的切片中,并且我们修改 s2 的值,发现 s1 并没有发生变化,说明s1、s2不在共享底
层数组。
扩容后的切片 s2 容量为 10 ,我们再向 s2 追加元素 7 后,没有超过 s2 的最大容量,且s2、s3 的内存地址一致,并且修改 s3 的值,
s2也会发生变化,说明s2、s3 共享底层数组。
所以,初始化切片的时候给出了足够的容量,append 操作的时候不会创建新的切片。
这里可能还会有一个疑问,为什么追加一个元素后容量由原来的 5 变成了 10?这里牵涉到 Slice 的扩容机制,可以参考这篇文章写得非
常详细:http://blog.realjf.com/archives/217 (点击无法访问可以将这个链接粘贴至地址栏访问)
7 copy 函数
func copy(dst, src []Type) int
用于将源slice的数据(第二个数据),复制到目标slice(第一个参数)
返回值为拷贝了的数据个数,是 len(dst)和len(src)中的最小值
通过一段代码看下实例
package main import "fmt" func main() {
// 切片a、切片s
a := []int{0,1,2,3,4,5,6,7}
s := make([]int,6) // 源长度8 目标长度6
// 只会将a前6个复制
n1 := copy(s,a)
fmt.Println("s -",s)
fmt.Println("n1 -",n1) // a[1:]长度7 s长度6
// 只会将a[1:]前6个复制
n2 := copy(s,a[1:])
fmt.Println("s -",s)
fmt.Println("n2 -",n2) // a[5:]长度3 s长度6
// 只会将a[5:]前3个复制
n3 := copy(s,a[5:])
fmt.Println("s -",s)
fmt.Println("n3 -",n3) n4 := copy(s[3:],a[5:])
fmt.Println("s - ", s)
fmt.Println("n4 - ", n4)
}
8 空切片和 nil 切片的区别
nil 切片和空切片,两者的区别在于前者指针为nil,后者指针指向一个地址
var slice [] int // nil 切片
slice := make([]int,0) // 使用make创建空切片
slice := []int{} // 使用切片字面量创建空切片
空切片在底层数组包含0个元素,也没有分配任何存储空间。想表示空集合时空切片很有用。不管是nil切片还是空切片,对其调用内置函数 append, len 和 cap 的效果是一样的
判断一个切片是否为空使用的是 len(s) == 0 而不是 s == nil
参考文章:https://juejin.im/post/5be8e0b1f265da614d08b45a
Go 灵活多变的切片Slice的更多相关文章
- go 数组(array)、切片(slice)、map、结构体(struct)
一 数组(array) go语言中的数组是固定长度的.使用前必须指定数组长度. go语言中数组是值类型.如果将数组赋值给另一个数组或者方法中参数使用都是复制一份,方法中使用可以使用指针传递地址. 声明 ...
- golang切片slice
切片slice是引用类型 len()函数获取元素的个数 cap()获取数组的容量 1.申明方式 (1)var a []int 与数组不同的是他不申明长度(2)s2 := make([]int, 3, ...
- 在python&numpy中切片(slice)
在python&numpy中切片(slice) 上文说到了,词频的统计在数据挖掘中使用的频率很高,而切片的操作同样是如此.在从文本文件或数据库中读取数据后,需要对数据进行预处理的操作.此时就 ...
- Golang 入门 : 切片(slice)
切片(slice)是 Golang 中一种比较特殊的数据结构,这种数据结构更便于使用和管理数据集合.切片是围绕动态数组的概念构建的,可以按需自动增长和缩小.切片的动态增长是通过内置函数 append( ...
- 7 切片slice
include 切片 切片的日常用法 切片slice 其本身并不是数组,它指向底层的数组 作为变长数组的替代方案,可以关联底层数组的局部或者 为引用类型. 可以直接创建或从底层数组获取生成. 使用le ...
- go递归函数如何传递数组切片slice
数组切片slice这个东西看起来很美好,真正用起来会发现有诸多的不爽. 第一,数组.数组切片混淆不清,使用方式完全一样,有时候一些特性又完全不一样,搞不清原理很容易误使用. 第二,数组切片的appen ...
- [PY3]——内置数据结构(9)——线性结构与切片/命名切片slice()
线性结构的总结 列表list 元组tuple 字符串str bytes bytearray的共同点: 都是顺序存储.顺序访问的: 都是可迭代对象: 都可以通过索引访问 线性结构的特征: 可迭代 ...
- Go语言【第十二篇】:Go数据结构之:切片(Slice)、范围(Range)、集合(Map)
Go语言切片(Slice) Go语言切片是对数组的抽象,Go数组的长度不可改变,在特定场景中这样的集合就不太适用,Go中提供了一种灵活,功能强悍的内置类型切片("动态数组"),与数 ...
- go中的数据结构切片-slice
1.部分基本类型 go中的类型与c的相似,常用类型有一个特例:byte类型,即字节类型,长度为,默认值是0: bytes = []btye{'h', 'e', 'l', 'l', 'o'} 变量byt ...
随机推荐
- nginx tomcat https
.首先确保机器上安装了openssl和openssl-devel #yum install openssl #yum install openssl-devel . server { listen s ...
- Eclipse在Debug模式下经常进入ThreadPoolExecutor解决办法
1.进入Window-->搜索:debug
- redmine3.2 的部署
安装libyaml [root@ ~]#wget http://pyyaml.org/download/libyaml/yaml-0.1.4.tar.gz -O /dist/dist/yaml-0.1 ...
- mysql库、表、索引
创建和删除数据库,同一个数据库下的不同表可以采用不同的引擎 mysql> create database oldboy default character set utf8 collate ut ...
- VMware Workstation Pro14安装
1. 下载VMware Workstation Pro14,注意,这个链接支持win7 64及以上系统 2. 点击进入安装 3. 接受许可协议 4. 选择安装目录,是否选择增强型键盘驱动程序 5. ...
- What is probabilistic programming? | 中文翻译
What is probabilistic programming? | 中文翻译 Probabilistic languages can free developers from the compl ...
- 理解 neutron
之前大师发个结构图. understanding_neutron.pdf 自己走读了代码: 1. get_extensions_path() # 在/opt/stack/neutron/neutro ...
- Codeforces Round #479 (Div. 3)题解
CF首次推出div3给我这种辣鸡做,当然得写份博客纪念下 A. Wrong Subtraction time limit per test 1 second memory limit per test ...
- Codeforces Round #424 (Div. 2, rated, based on VK Cup Finals) Problem E (Codeforces 831E) - 线段树 - 树状数组
Vasily has a deck of cards consisting of n cards. There is an integer on each of the cards, this int ...
- bzoj 3993 星际战争 - 二分答案 - 最大流
3333年,在银河系的某星球上,X军团和Y军团正在激烈地作战.在战斗的某一阶段,Y军团一共派遣了N个巨型机器人进攻X军团的阵地,其中第i个巨型机器人的装甲值为Ai.当一个巨型机器人的装甲值减少到0或者 ...