本文主要介绍Go语言中切片(slice)及它的基本使用。

一、引子

因为数组的长度是固定的并且数组长度属于类型的一部分,所以数组有很多的局限性。 例如:

func arraySum(x [3]int) int{
sum := 0
for _, 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) //true
fmt.Println(b == nil) //false
fmt.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)) //2
fmt.Println(cap(a)) //10
}

上面代码中a的内部存储空间已经分配了10个,但实际上只用了2个。 容量并不会影响当前元素的个数,所以len(a)返回2,cap(a)则返回该切片的容量。

补充:

make(make是一个内置函数)创建切片(其也是基于数组,只不过是底层已经创建好了,我们看不见)比较推荐这种,应用范围也比较广。切片元素的默认值为0。

make在这里做的就是在底层做了一个数组并且分配内存空间,切片然后去引用这个数组。

2.3.6 注意

  1. 空切片必须初始化,或者用append才能使用,否则就会panic。

    例1:

    package main
    
    import "fmt"
    
    func main() {
    var a []string
    if a == nil {
    fmt.Println("a是空切片")
    }
    a[0] = "paul" //不初始化直接赋值操作
    fmt.Println(a)
    }

    执行结果:

    例2:

    package main
    
    import "fmt"
    
    func main() {
    var a []string
    if 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切片和空切片区别:

  1. 有无初始化(就是有无分配内存地址),nil切片无初始化,空切片初始化了;

  2. 打印出来内容虽然都是空,但是nil切片打印为空内存地址,空切片打印为特殊的内存地址(所有类型空切片都存放在这个特殊内存地址)

  3. nil切片和空切片都可以进行append操作,因为append操作会对该切片做自动初始化扩容操作;

关于nil切片和空切片区别:http://blog.itpub.net/31561269/viewspace-2220962/

例1:

package main

import "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 []int
fmt.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] = 100
fmt.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 []int
for 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

从上面的结果可以看出:

  1. append()函数将元素追加到切片的最后并返回该切片。
  2. 切片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 := a
fmt.Println(a) //[1 2 3 4 5]
fmt.Println(b) //[1 2 3 4 5]
b[0] = 1000
fmt.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中的元素复制到切片c
fmt.Println(a) //[1 2 3 4 5]
fmt.Println(c) //[1 2 3 4 5]
c[0] = 1000
fmt.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:]...)

2.4 Go语言基础之切片的更多相关文章

  1. Go语言基础之切片

    Go语言基础之切片 本文主要介绍Go语言中切片(slice)及它的基本使用. 引子 因为数组的长度是固定的并且数组长度属于类型的一部分,所以数组有很多的局限性. 例如: func arraySum(x ...

  2. GO学习-(9) Go语言基础之切片

    Go语言基础之切片 本文主要介绍Go语言中切片(slice)及它的基本使用. 引子 因为数组的长度是固定的并且数组长度属于类型的一部分,所以数组有很多的局限性. 例如: func arraySum(x ...

  3. go语言基础之切片做函数参数

    1.切片做函数参数 (备注:用了冒泡排序) 示例: package main //必须有个main包 import "fmt" import "math/rand&quo ...

  4. go语言基础之切片和底层数组关系

    1.切片和底层数组关系 示例: package main //必须有个main包 import "fmt" func main() { a := []int{0, 1, 2, 3, ...

  5. go语言基础之切片的创建和截取

    1.切片的创建 示例: package main //必须有个main包 import "fmt" func main() { //切片和数组的区别 //数组[]里面的长度时固定的 ...

  6. Go语言基础之5--数组(array)和切片(slince)

    一.数组(array) 1.1 数组定义 1)含义: 数组是同一类型的元素集合. 数组是具有固定长度并拥有零个或者多个相同数据类型元素的序列. 2)定义一个数组的方法: var 变量名[len] ty ...

  7. 【GoLang】GO语言系列--002.GO语言基础

    002.GO语言基础 1 参考资料 1.1 http://www.cnblogs.com/vimsk/archive/2012/11/03/2736179.html 1.2 https://githu ...

  8. Go语言基础(二)

    Go语言基础(二) 跟着上篇,继续看Go基础 一.变量作用域 与C类似,有全局变量.局部变量.形参之分 package main import "fmt" // 全局变量 var ...

  9. Go语言基础之反射

    Go语言基础之反射 本文介绍了Go语言反射的意义和基本使用. 变量的内在机制 Go语言中的变量是分为两部分的: 类型信息:预先定义好的元信息. 值信息:程序运行过程中可动态变化的. 反射介绍 反射是指 ...

随机推荐

  1. sql server存储过程返回数据只有一个字符

    SqlParameter[] param = { new SqlParameter("@shopId",shopId), new SqlParameter("@newSh ...

  2. 浏览器获取当前ip

    function findIP(callback) { var myPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConn ...

  3. No PostCSS Config found in报错解决

    前情提要]日前本人将本地项目上传GitHub之后,然后再clone到本地,运行是报错:Error: No PostCSS Config found in... 项目在本地打包运行的时候不报错,上传到 ...

  4. [HNOI2007]分裂游戏 SG打表博弈

    结论:其实每一个巧克力都是一堆石子 它的石子数就是它到队尾的距离 打一个SG表即可 #include<bits/stdc++.h> using namespace std; typedef ...

  5. web页面长时间未操作自动退出登录

    var lastTime = new Date().getTime(); var currentTime = new Date().getTime(); * * ; //设置超时时间: 10分 $(f ...

  6. B-Tree目录和Hash索引的区别

    Hash 索引结构的特殊性,其检索效率非常高,索引的检索可以一次定位,不像B-Tree 索引需要从根节点到枝节点,最后才能访问到页节点这样多次的IO访问,所以 Hash 索引的查询效率要远高于 B-T ...

  7. rabbitmq 一些属性

    消息属性 消息有14个属性,最常用的几种: deliveryMode:持久化属性 contentType:编码 replyTo:指定一个回调队列 correlationId:消息id mandator ...

  8. [Javascript] Create an Image with JavaScript Using Fetch and URL.createObjectURL

    Most developers are familiar with using img tags and assigning the src inside of HTML. It is also po ...

  9. 边学边体验django--模型

    步骤: 1.选定数据库,然后修改 settings.py 中的 DATABASES配置 实验过了sqlite3. 应该是这个样子的: 2. 创建app, 建立数据表模型. python manage. ...

  10. 对象(Object)相关

    详情参考 1.对象的表示方法 js原生提供Object构造函数.js中所有的对象都是Object的实例. 定义一个对象最简单的就是var obj = {}; ES6属性和方法允许简写.对象的super ...