我们知道数组定义好之后其长度就无法再修改,但是,在实际开发过程中,有时候我们并不知道需要多大的数组,我们期望数组的长度是可变的,

在 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的更多相关文章

  1. go 数组(array)、切片(slice)、map、结构体(struct)

    一 数组(array) go语言中的数组是固定长度的.使用前必须指定数组长度. go语言中数组是值类型.如果将数组赋值给另一个数组或者方法中参数使用都是复制一份,方法中使用可以使用指针传递地址. 声明 ...

  2. golang切片slice

    切片slice是引用类型 len()函数获取元素的个数 cap()获取数组的容量 1.申明方式 (1)var a []int 与数组不同的是他不申明长度(2)s2 := make([]int, 3, ...

  3. 在python&numpy中切片(slice)

     在python&numpy中切片(slice) 上文说到了,词频的统计在数据挖掘中使用的频率很高,而切片的操作同样是如此.在从文本文件或数据库中读取数据后,需要对数据进行预处理的操作.此时就 ...

  4. Golang 入门 : 切片(slice)

    切片(slice)是 Golang 中一种比较特殊的数据结构,这种数据结构更便于使用和管理数据集合.切片是围绕动态数组的概念构建的,可以按需自动增长和缩小.切片的动态增长是通过内置函数 append( ...

  5. 7 切片slice

    include 切片 切片的日常用法 切片slice 其本身并不是数组,它指向底层的数组 作为变长数组的替代方案,可以关联底层数组的局部或者 为引用类型. 可以直接创建或从底层数组获取生成. 使用le ...

  6. go递归函数如何传递数组切片slice

    数组切片slice这个东西看起来很美好,真正用起来会发现有诸多的不爽. 第一,数组.数组切片混淆不清,使用方式完全一样,有时候一些特性又完全不一样,搞不清原理很容易误使用. 第二,数组切片的appen ...

  7. [PY3]——内置数据结构(9)——线性结构与切片/命名切片slice()

    线性结构的总结 列表list  元组tuple  字符串str  bytes  bytearray的共同点: 都是顺序存储.顺序访问的: 都是可迭代对象: 都可以通过索引访问 线性结构的特征: 可迭代 ...

  8. Go语言【第十二篇】:Go数据结构之:切片(Slice)、范围(Range)、集合(Map)

    Go语言切片(Slice) Go语言切片是对数组的抽象,Go数组的长度不可改变,在特定场景中这样的集合就不太适用,Go中提供了一种灵活,功能强悍的内置类型切片("动态数组"),与数 ...

  9. go中的数据结构切片-slice

    1.部分基本类型 go中的类型与c的相似,常用类型有一个特例:byte类型,即字节类型,长度为,默认值是0: bytes = []btye{'h', 'e', 'l', 'l', 'o'} 变量byt ...

随机推荐

  1. Hive 数仓中常见的日期转换操作

    (1)Hive 数仓中一些常用的dt与日期的转换操作 下面总结了自己工作中经常用到的一些日期转换,这类日期转换经常用于报表的时间粒度和统计周期的控制中 日期变换: (1)dt转日期 to_date(f ...

  2. MongoDB遇到的疑似数据丢失的问题。不要用InsertMany!

    最近做数据备份的时候发现了有个很严重的问题,那就是数据丢失(最后证明没丢,是别的问题造成的). 问题如下: 我通过两种方式在两个mongoDB集群中,对一组collection进行备份,最后2个备份数 ...

  3. java == 与 equals 相同与不同点

    java中与很多有意思又值得深究的点. 写这篇文章呢,是由于在百度知道中看到一个问题:怎样比较两个对象是否相同.这又使我想到了另外一个问题,== 和 equals有什么不同?写了几行代码,看了几篇文章 ...

  4. 处理springmvc的post和get提交参数乱码问题

    1,post 配置CharacterEncodingFilter拦截器 2,get 在tomcat的Connect 上配置uri 编码

  5. java提供的线程池的使用

    应用场景,比如你有个业务模块,非常耗时,并且还需要重复调用5次. 如果你写个for循环调用5次,调用一次3秒,那么5次就15秒,不是很友好. 这时,如果你用线程池就方便了,多线程跑,都跑完,收集到结果 ...

  6. Docker学习笔记之编写 Docker Compose 项目

    0x00 概述 通过阅读之前的小节,相信大家对 Docker 在开发中的应用已经有了一定的了解.作为一款实用的软件,我们必须回归到实践中来,这样才能更好地理解 Docker 的实用逻辑和背后的原理.在 ...

  7. P3813 [FJOI2017]矩阵填数(组合数学)

    P3813 [FJOI2017]矩阵填数 shadowice1984说:看到计数想容斥........ 这题中,我们把图分成若干块,每块的最大值域不同 蓝后根据乘法原理把每块的方案数(互不相干)相乘. ...

  8. Builgen 插件——IntelliJ IDEA和Eclipse Java Bean Builder模式代码生成器-比lombok更符合需求

    builder模式在越来越多的项目中使用,类似于alibaba fastjson JSONObject.fluentPut(),调用一个方法后返回这个对象本身,特别适合构建一些参数超级多的对象,代码优 ...

  9. 【题解】Luogu CF915E Physical Education Lessons

    原题传送门:CF915E Physical Education Lessons 前置芝士:珂朵莉树 窝博客里对珂朵莉树的介绍 没什么好说的自己看看吧 这道题很简单啊 每个操作就是区间赋值,顺带把总和修 ...

  10. Debian\CentOS Linux配置管理

    CentOS 6 安装 gcc-4.8 以支持 C++11 1.下载 repo 文件 wget http://people.centos.org/tru/devtools-2/devtools-2.r ...