2.4 Go语言基础之切片
本文主要介绍Go语言中切片(slice)及它的基本使用。
一、引子
因为数组的长度是固定的并且数组长度属于类型的一部分,所以数组有很多的局限性。 例如:
func arraySum(x [3]int) int{sum := 0for _, v := range x{sum = sum + v}return sum
}
这个求和函数只能接受[3]int
类型,其他的都不支持。 再比如,
a := [3]int{1, 2, 3}
数组a中已经有三个元素了,我们不能再继续往数组a中添加新元素了。
二、切片
2.1 切片知多少
切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。它非常灵活,支持自动扩容。
切片与数组相比,不需要设定长度,在[]中不用设定值,相对来说比较自由。
slice本身没有数据(切片并不存储任何元素而只是对现有数组的引用。),是对底层array的一个view对slice所做的任何修改都将反映在底层数组中。
切片是什么?一种数据结构,类似数组,围绕动态数组的概念而设计,可按需自动改变大小。
切片内部实现?切片基于数组实现,底层是数组(故底层的内存是连续分配,可根据索引获取数据,可迭代以及垃圾回收),自身非常小(切片对象只有3个字段数据结构即:指向底层数组的指针ptr(内存地址)、切片长度len、切片容量cap),可看作对底层数组的抽象。
切片是一个引用类型,它的内部结构包含地址
、长度
和容量
。切片一般用于快速地操作一块数据集合。
切片三要素:
- 地址(切片中第一个元素指向的内存空间)
- 大小(切片中目前元素的个数)
len()
- 容量(底层数组最大能存放的元素的个数)
cap()
2.2 切片定义
定义声明切片类型的基本语法如下:
var name []T
其中,
- name:表示变量名
- T:表示切片中的元素类型
2.3 切片初始化
2.3.1 为什么要初始化?
简单地说,定义声明只是告诉我的样子(类型) 初始化就是告诉我在哪里(分配内存) 要想找到我 仅仅知道我的样子是没用的 得知道我的地址
2.3.2 方法1 定义声明切片变量时初始化
我们可以直接在声明定义切片变量时对其直接进行初始化
例1:
func main() {// 声明切片类型var a []string //声明一个字符串切片var b = []int{} //声明一个整型切片并初始化var c = []bool{false, true} //声明一个布尔切片并初始化//var d = []bool{false, true} //声明一个布尔切片并初始化fmt.Println(a) //[]fmt.Println(b) //[]fmt.Println(c) //[false true]fmt.Println(a == nil) //truefmt.Println(b == nil) //falsefmt.Println(c == nil) //false// fmt.Println(c == d) //切片是引用类型,不支持直接比较,只能和nil比较
}
2.3.3 方法2 基于数组定义切片
由于切片的底层就是一个数组,所以我们可以基于数组定义切片。
func main() {// 基于数组定义切片a := [5]int{55, 56, 57, 58, 59}b := a[1:4] //基于数组a创建切片,包括元素a[1],a[2],a[3]fmt.Println(b) //[56 57 58]fmt.Printf("type of b:%T\n", b) //type of b:[]int
}
还支持如下方式:
c := a[1:] //[56 57 58 59]
d := a[:4] //[55 56 57]
e := a[:] //[55 56 57 58 59]
总结如下:
a) arr[start:end]:包括start到end-1(包括end-1)之间的所有元素
b) arr[start:]:包括start到arr最后一个元素(包括最后一个元素)之间的所有元素
c) arr[:end]:包括0到end-1(包括end-1)之间的所有元素
d) arr[:]:包括整个数组的所有元素
2.3.4 方法3 基于切片再切片
除了基于数组得到切片,我们还可以通过切片来得到切片。
func main() {//切片再切片a := [...]string{"北京", "上海", "广州", "深圳", "成都", "重庆"}fmt.Printf("a:%v type:%T len:%d cap:%d\n", a, a, len(a), cap(a))b := a[1:3]fmt.Printf("b:%v type:%T len:%d cap:%d\n", b, b, len(b), cap(b))c := b[1:5]fmt.Printf("c:%v type:%T len:%d cap:%d\n", c, c, len(c), cap(c))
}
输出:
a:[北京 上海 广州 深圳 成都 重庆] type:[6]string len:6 cap:6
b:[上海 广州] type:[]string len:2 cap:5
c:[广州 深圳 成都 重庆] type:[]string len:4 cap:4
注意: 对切片进行再切片时,索引不能超过原数组的长度,否则会出现索引越界的错误。
2.3.5 方法4 使用make函数构造切片
我们上面都是基于数组来创建的切片,如果需要动态的创建一个切片,我们就需要使用内置的make()
函数,格式如下:
make([]T, size, cap)
其中:
- T:切片的元素类型
- size:切片中元素的数量
- cap:切片的容量
举个例子:
func main() {a := make([]int, 2, 10)fmt.Println(a) //[0 0]fmt.Println(len(a)) //2fmt.Println(cap(a)) //10
}
上面代码中a
的内部存储空间已经分配了10个,但实际上只用了2个。 容量并不会影响当前元素的个数,所以len(a)
返回2,cap(a)
则返回该切片的容量。
补充:
make(make是一个内置函数)创建切片(其也是基于数组,只不过是底层已经创建好了,我们看不见)比较推荐这种,应用范围也比较广。切片元素的默认值为0。
make在这里做的就是在底层做了一个数组并且分配内存空间,切片然后去引用这个数组。
2.3.6 注意
空切片必须初始化,或者用append才能使用,否则就会panic。
例1:
package mainimport "fmt"func main() {var a []stringif a == nil {fmt.Println("a是空切片")}a[0] = "paul" //不初始化直接赋值操作fmt.Println(a) }
执行结果:
例2:
package mainimport "fmt"func main() {var a []stringif a == nil {fmt.Println("a是空切片")}a = append(a, "harden") //不初始化但是可以进行append操作,append会自动对这个空切片做自动扩容。fmt.Println(a) }
执行结果:
2.4切片的本质
切片的本质就是对底层数组的封装,它包含了三个信息:底层数组的指针、切片的长度(len)和切片的容量(cap)。
举个例子,现在有一个数组a := [8]int{0, 1, 2, 3, 4, 5, 6, 7}
,切片s1 := a[:5]
,相应示意图如下。
切片s2 := a[3:6]
,相应示意图如下:
2.5 nil切片和空切片区别
nil切片(比如: var nilSlice []int )指向底层数组的指针为nil,nil切片表示不存在的切片。
空切片(比如: slice := []int{} )对应的指针是个地址,空切片表示一个空集合。
nil切片和空切片区别:
有无初始化(就是有无分配内存地址),nil切片无初始化,空切片初始化了;
打印出来内容虽然都是空,但是nil切片打印为空内存地址,空切片打印为特殊的内存地址(所有类型空切片都存放在这个特殊内存地址)
nil切片和空切片都可以进行append操作,因为append操作会对该切片做自动初始化扩容操作;
关于nil切片和空切片区别:http://blog.itpub.net/31561269/viewspace-2220962/
例1:
package mainimport "fmt"func main() {//空切片b := []int{}fmt.Printf("b为:%#v\n", b) //%#v表示以go语言语法形式显示值fmt.Printf("b的内存地址:%p\n", b)b = append(b, 2)fmt.Printf("After:b为:%#v\n", b)fmt.Printf("After:b的内存地址:%p\n", b)c := []string{}fmt.Printf("c为:%#v\n", c)fmt.Printf("c的内存地址:%p\n", c)c = append(c, "MVP")fmt.Printf("After:c为:%#v\n", c)fmt.Printf("After:c的内存地址:%p\n", c)//nil切片var d []intfmt.Printf("d为:%#v\n", d)fmt.Printf("d的内存地址:%p\n", d)d = append(d, 1)fmt.Printf("After:d为:%#v\n", d)fmt.Printf("After:d的内存地址:%p\n", d)}
执行结果:
2.6 切片不能直接比较
切片之间是不能比较的,我们不能使用==
操作符来判断两个切片是否含有全部相等元素。 切片唯一合法的比较操作是和nil
比较。 一个nil
值的切片并没有底层数组,一个nil
值的切片的长度和容量都是0。但是我们不能说一个长度和容量都是0的切片一定是nil
,例如下面的示例:
var s1 []int //len(s1)=0;cap(s1)=0;s1==nil
s2 := []int{} //len(s2)=0;cap(s2)=0;s2!=nil
s3 := make([]int, 0) //len(s3)=0;cap(s3)=0;s3!=nil
2.7 如何使用切片?
类似使用数组,通过索引获取切片对应元素的值,也可修改对应元素的值。
切片只能访问到长度内的元素,长度外的如果访问会运行时异常。就会panic(报越界(panic: runtime error: index out of range)错误)
slice := []int{1,2,3,4,5}
fmt.Println(slice[2]) //3
slice[2] = 10
fmt.Println(slice[2]) //10
2.8 切片的赋值拷贝
下面的代码中演示了拷贝前后两个变量共享底层数组,对一个切片的修改会影响另一个切片的内容,这点需要特别注意。
func main() {s1 := make([]int, 3) //[0 0 0]s2 := s1 //将s1直接赋值给s2,s1和s2共用一个底层数组s2[0] = 100fmt.Println(s1) //[100 0 0]fmt.Println(s2) //[100 0 0]
}
2.9 切片遍历
切片的遍历方式和数组是一致的,支持索引遍历和for range
遍历。
func main() {s := []int{1, 3, 5}for i := 0; i < len(s); i++ {fmt.Println(i, s[i])}for index, value := range s {fmt.Println(index, value)}
}
2.10 append()方法为切片添加元素
Go语言的内建函数append()
可以为切片动态添加元素。 每个切片会指向一个底层数组,这个数组能容纳一定数量的元素。当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行“扩容”,此时该切片指向的底层数组就会更换。“扩容”操作往往发生在append()
函数调用时。 举个例子:
func main() {//append()添加元素和切片扩容var numSlice []intfor i := 0; i < 10; i++ {numSlice = append(numSlice, i)fmt.Printf("%v len:%d cap:%d ptr:%p\n", numSlice, len(numSlice), cap(numSlice), numSlice)}
}
输出:
[0] len:1 cap:1 ptr:0xc0000a8000
[0 1] len:2 cap:2 ptr:0xc0000a8040
[0 1 2] len:3 cap:4 ptr:0xc0000b2020
[0 1 2 3] len:4 cap:4 ptr:0xc0000b2020
[0 1 2 3 4] len:5 cap:8 ptr:0xc0000b6000
[0 1 2 3 4 5] len:6 cap:8 ptr:0xc0000b6000
[0 1 2 3 4 5 6] len:7 cap:8 ptr:0xc0000b6000
[0 1 2 3 4 5 6 7] len:8 cap:8 ptr:0xc0000b6000
[0 1 2 3 4 5 6 7 8] len:9 cap:16 ptr:0xc0000b8000
[0 1 2 3 4 5 6 7 8 9] len:10 cap:16 ptr:0xc0000b8000
从上面的结果可以看出:
append()
函数将元素追加到切片的最后并返回该切片。- 切片numSlice的容量按照1,2,4,8,16这样的规则自动进行扩容,每次扩容后都是扩容前的2倍。
append()函数还支持一次性追加多个元素。 例如:
var citySlice []string
// 追加一个元素
citySlice = append(citySlice, "北京")
// 追加多个元素
citySlice = append(citySlice, "上海", "广州", "深圳")
// 追加切片
a := []string{"成都", "重庆"}
citySlice = append(citySlice, a...)
fmt.Println(citySlice) //[北京 上海 广州 深圳 成都 重庆]
2.11 切片的扩容策略
可以通过查看$GOROOT/src/runtime/slice.go
源码,其中扩容相关代码如下:
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {newcap = cap
} else {if old.len < 1024 {newcap = doublecap} else {// 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}}
}
从上面的代码可以看出以下内容:
- 首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)。
- 否则判断,如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap),
- 否则判断,如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap)
- 如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)。
需要注意的是,切片扩容还会根据切片中元素的类型不同而做不同的处理,比如int
和string
类型的处理方式就不一样。
2.12 使用copy()函数复制切片
首先我们来看一个问题:
func main() {a := []int{1, 2, 3, 4, 5}b := afmt.Println(a) //[1 2 3 4 5]fmt.Println(b) //[1 2 3 4 5]b[0] = 1000fmt.Println(a) //[1000 2 3 4 5]fmt.Println(b) //[1000 2 3 4 5]
}
由于切片是引用类型,所以a和b其实都指向了同一块内存地址。修改b的同时a的值也会发生变化。
Go语言内建的copy()
函数可以迅速地将一个切片的数据复制到另外一个切片空间中,copy()
函数的使用格式如下:
copy(destSlice, srcSlice []T)
其中:
- srcSlice: 数据来源切片
- destSlice: 目标切片
举个例子:
func main() {// copy()复制切片a := []int{1, 2, 3, 4, 5}c := make([]int, 5, 5)copy(c, a) //使用copy()函数将切片a中的元素复制到切片cfmt.Println(a) //[1 2 3 4 5]fmt.Println(c) //[1 2 3 4 5]c[0] = 1000fmt.Println(a) //[1 2 3 4 5]fmt.Println(c) //[1000 2 3 4 5]
}
2.13 从切片中删除元素
Go语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素。 代码如下:
func main() {// 从切片中删除元素a := []int{30, 31, 32, 33, 34, 35, 36, 37}// 要删除索引为2的元素a = append(a[:2], a[3:]...)fmt.Println(a) //[30 31 33 34 35 36 37]
}
总结一下就是:要从切片a中删除索引为index
的元素,操作方法是a = append(a[:index], a[index+1:]...)
转载于:https://www.cnblogs.com/forever521Lee/p/10770349.html
2.4 Go语言基础之切片相关推荐
- 【搞定Go语言】第2天6:Go语言基础之切片
引子 因为数组的长度是固定的并且数组长度属于类型的一部分,所以数组有很多的局限性. 例如: func arraySum(x [3]int) int{sum := 0for _, v := range ...
- go语言基础语法-切片
文章目录 1. 概念 2. 切片的基本操作 1) 切片生成方式 a. 从数组或者切片上切的生成方式 b. 直接声明切片 : var name []Type 2)make()函数构造切片 3)切片扩容a ...
- go语言基础数据结构学习---- 数组, 列表(list)和切片(slice)
go语言基础数据结构学习–> 数组, 列表(list)和切片(slice) go 语言中的 数组是类型相同的元素的集合, 列表是双链表的容器, 可以添加不同类型的数据切片是对现有数组的引用, 比 ...
- 【python语言基础】疑难点整理2
[python语言基础]疑难点整理1 第五章 在python语法中,循环体中的语句没有做限制,因此,可以是任何合法语句,当然也可以是循环语句.这样就形成了循环语句的嵌套. while循环语句和for循 ...
- Go语言基础之数据类型
Go语言基础之数据类型 Go语言中有丰富的数据类型,除了基本的整型.浮点型.布尔型.字符串外,还有数组.切片.结构体.函数.map.通道(channel)等.Go 语言的基本类型和其他语言大同小异. ...
- 没有c语言基础可以学python吗-没有c语言基础,能不能学好python?
学习C语言对学好python有帮助吗? 答案是有. 学好python需不需要有C语言基础? 答案是不一定. 实际上,所有的编程语言都是在做一件事情--告诉计算机,将你输入的数据A如何转化成需要输出的数 ...
- ostream作为函数返回值_GO语言基础函数
GO语言基础–函数 函数是组织好的.可重复使用的.用于执行指定任务的代码块.本文介绍了Go语言中函数的相关内容. 函数定义 Go语言中定义函数使用func关键字,具体格式如下: func 函数名(参数 ...
- [GO语言基础] 三.变量声明、数据类型、标识符及编程练习12题
作为网络安全初学者,会遇到采用Go语言开发的恶意样本.因此从今天开始从零讲解Golang编程语言,一方面是督促自己不断前行且学习新知识:另一方面是分享与读者,希望大家一起进步.前文介绍了Go的编译运行 ...
- D04——C语言基础学PYTHON
C语言基础学习PYTHON--基础学习D04 20180810内容纲要: 1 内置函数 2 装饰器 3 生成器 4 迭代器 5 软件目录结构规范 6 小结 1 内置函数 内置函数方法详 ...
最新文章
- [2017.02.23] Java8 函数式编程
- Error in ggplot(df, aes(x = x, y = y)) : could not find function “ggplot“
- 【 MATLAB 】DFT的性质讨论(二)序列的循环移位及其 MATLAB 实现(时域方法)
- 函数的递归调用(专题)
- 统计学中p值计算公式_不得不学的统计学基础知识(二)
- 马斯克:特斯拉FSD测试版V9将再推迟一周
- Docker学习总结(40)——Docker常见应用场景再总结
- 超火爆的6 个必学持续集成工具,测试人的福音
- 一键杀死最近打开APP
- asp.net C#实现下载文件的六种方法实例
- Docker 基础 ( 二十 ) 部署Redis集群,问题记录
- netperf 学习笔记
- jquery实现点击图片放大功能
- vue通过识别字符串中的分号进行换行
- Linux关闭防火墙并设置开机启动/不启动
- 工业计算机控制系统的分类,工业自动化系统组成和产品分类介绍
- App Store2016年最新审核规则
- 简易的学生社团管理(大二课设)
- 15ubuntu20.04离线安装openssh8.9.p1
- Excel 防止一列重复输入
热门文章
- Linux下DB2数据库安装教程
- android button imagebutton 区别,Android 开发入门篇
- java字节对齐 32 64_【C语言】字节对齐问题(以32位系统为例)
- lisp坐标一键生成_联排建筑一键生成?你的SU有外挂吧!
- 2021计算机专业课考研大纲,2021考研计算机大纲原文:操作系统
- eclipse lombok插件安装_Eclipse-安装Freemarker插件
- 多线程处理缓慢_华为昇腾,AI推理性能超越对手一倍:软件挖掘处理器全部潜力...
- inline函数返回值_C++知识补充-指针,const,函数指针,指针数组,运算符重载
- 量子计算机科学原理,1000字看懂IBM量子计算机原理
- 利用mysql建立随机森林_随机森林算法实例 - osc_4imme0wh的个人空间 - OSCHINA - 中文开源技术交流社区...