《Go程序设计语言》学习笔记之slice

一. 环境

  Centos8.5, go1.17.5 linux/amd64

二. 概念

1) slice 表示一个拥有相同类型元素的可变长度的序列。slice 通常写成 []T,其中元素的类型是T,它看上去像没有长度的数组类型。

slice 有三个属性:指针、长度、容量。

指针,指向数组的第一个可以从 slice 中访问的元素,这个元素并不一定是数组的第一个元素。

长度,slice 中元素的个数。

容量,从 slice 的起始元素到底层数组最后一个元素间元素的数量。

底层数组与 slice 是一对多的关系,一个底层数组可以对应多个 slice ,这些 slice 可以引用数组的任何位置,彼此之间的元素还可以重叠。

slice 操作符 s[i:j] (其中 0 <= i <= j <= cap(s)) 创建了一个新的 slice ,这个新的 slice 引用了序列 s 中从 i 到 j-1 索引位置的所有元素,这里的 s 既可以是数组或者指向数组的指针,也可以是 slice 。新 slice 的元素个数是 j-i 个。

s[i:j]

2) 零值

slice 类型的零值是 nil。申明一个 slice 类型变量 s ,通过与零值比较,可知它也是个零值。

 8 func main() {
9 var s []int
10 fmt.Println(nil == s)
11 fmt.Printf("s type: %T, len(s): %d, cap(s): %d\n", s, len(s), cap(s))
12 }

运行结果如下

三.申明/初始化

var s []int

1) slice s 引用数组。第9行代码申明了一个 slice s,它与数组的声明很像,但是 [] 中没有指定长度。结果是创建了一个指向数组的 slice。

  8 func main() {
9 s := []int{0, 1, 2, 3, 4, 5}
10 fmt.Printf("type: %T\n", s)
11 fmt.Printf("len: %d\n", len(s))
12 fmt.Printf("cap: %d\n", cap(s))
13 }

运行结果如下

2) slice 类型变量 s1 和 s 引用了同一个底层数组。

  8 func main() {
9 s := []int{0, 1, 2, 3, 4, 5}
10 s1 := s[:]
11 fmt.Println(s1)
12 }

运行结果如下

3) 内置函数 make 可以创建一个具有指定元素类型、长度和容量的 slice 。其中容量参数可以省略,此时,slice 的容量和长度相等。make 其实是创建了一个无名数组并返回了它的一个 slice,这个数组仅可能通过这个 slice 来访问。

示例代码如下

通过打印结果可知,slice 变量 s1 的容量没有指定,默认和其长度一致,两者均为5。slice 变量 s2 仅引用了数组前 5 个元素,但是它的容量是数组的长度,slice 的长度还有增长的空间。slice 变量 s3 与 s2 功能相同。

  8 func main() {
9 s1 := make([]int, 5)
10 s2 := make([]int, 5, 10)
11 s3 := make([]int, 10)[:5]
12
13 fmt.Printf("type: %T\n", s1)
14 fmt.Println("len: ", len(s1))
15 fmt.Println("cap: ", cap(s1))
16 fmt.Println("----------")
17 fmt.Printf("type: %T\n", s2)
18 fmt.Println("len: ", len(s2))
19 fmt.Println("cap: ", cap(s2))
20 fmt.Println("----------")
21 fmt.Printf("type: %T\n", s3)
22 fmt.Println("len: ", len(s3))
23 fmt.Println("cap: ", cap(s3))
24 }

运行结果如下

四. 访问

可通过顺序来访问元素,也可以通过索引来访问元素

  8 func main() {
9 s := []int{0, 1, 2, 3, 4, 5}
10 s1 := s[:len(s)-1]
11
12 for _, e := range s1 {
13 fmt.Printf("%d\t", e)
14 }
15
16 fmt.Println()
17 fmt.Println("----------")
18
19 for i := 0; i < len(s1); i++ {
20 fmt.Printf("%d\t", s1[i])
21 }
22 }

运行结果如下

五. 赋值

支持赋值

  8 func main() {
9 s := []int{0, 1, 2, 3, 4, 5}
10 s1 := s[:len(s)-1]
11 fmt.Printf("s type: %T, len(s): %d, cap(s): %d\n", s1, len(s1), cap(s1))
12
13 s2 := s1
14 fmt.Printf("s type: %T, len(s): %d, cap(s): %d\n", s2, len(s2), cap(s2))
15 fmt.Println(s2)
16 }

运行结果如下

六. 比较

1) slice 无法直接做比较,不能直接使用 == 来测试两个 slice 是否拥有相同的元素。标准库中有 bytes.Equal() 来比较两个字节 slice([]byte)。但对于其它类型的 slice ,需要自定义函数以实现比较。

  8 func main() {
9 s1 := []int{1, 2, 3}
10 s2 := []int{1, 2, 4}
11 fmt.Println(s1 == s2)
12 }

  编译报错,提示 slice 只能与 零值 nil 比较

invalid operation: s1 == s2 (slice can only be compared to nil)

原因有两个,一是 slice 的元素是非直接的,有可能 slice 可以包含它自身;二是底层数组元素有可能会改变,如果底层数组元素改变,同一个 slice 在不同的时间会拥有不同的元素。还需要在实践中去体会。

2) slice 唯一可直接比较的操作是和nil比较

示例代码如下

  8 func main() {
9 s1 := []int{1, 2, 3}
10 s2 := []int{1, 2, 4}
11 var s3 []int
12
13 fmt.Println(nil == s1)
14 fmt.Println(nil == s2)
15 fmt.Println(nil == s3)
16 }

运行结果如下

七. 判空

检查一个 slice 是否为空,请使用 len(s) == 0,而不是 s == nil。在 s != nil 的情况下,slice 也有可能为空。

第9行声明了 slice 类型变量 s1,它是零值,可以通过第21行的打印结果可以知道。此时,它的长度为0。第15行声明了 slice 类型变量 s2,它是一个空的 slice,并非零值  nil,通过第22行的打印结果可知。此时,它的长度也为0。

所以在检查 slice 是否为空时,使用 0 == len(s) 更为合理。示例代码如下

  8 func main() {
9 var s1 []int
10 fmt.Println(s1)
11 fmt.Println("len: ", len(s1))
12 fmt.Println("cap: ", cap(s1))
13
14 fmt.Println("----------")
15 s2 := []int{}
16 fmt.Println(s2)
17 fmt.Println("len: ", len(s2))
18 fmt.Println("cap: ", cap(s2))
19
20 fmt.Println("----------")
21 fmt.Println(s1 == nil)
22 fmt.Println(s2 == nil)
23 }

运行结果如下

八. 字节 slice 与字符串

对字节 slice([]byte) 做 slice 操作,求字符串子串操作,都写作 x[m:n],都返回原始字节的一个子序列,底层引用方式也相同。不同点在于,如果 x 是一字节 slice ,那么 x[m:n] 返回的结果是字节 slice;如果 x 是字符串,那么 x[m:n] 返回的是一个字符串。

九. 修改

引用书中去除空字符串的例子

  8 func nonempty(strings []string) []string {
9 i := 0
10 for _, e := range strings {
11 if "" != e {
12 strings[i] = e
13 i++
14 }
15 }
16
17 return strings[:i]
18 }
19
20 func main() {
21 data := []string{"one", "", "three"}
22 fmt.Printf("%q\n", nonempty(data))
23 fmt.Printf("%q\n", data)
24 }

运行结果如下

十. 使用

1) 直接使用

存储元素

2) 作函数参数

slice 包含了指向数组元素的指针,所以将一个 slice 传递给函数的时候,可以在函数内部修改底层数组的元素。即,创建一个数组的 slice 等于为数组创建了一个别名。

第10行代码,声明了一个 slice t,第20行代码,定义了一个逆序的函数,函数接收的参数是 slice 类型。slice t 中包含了指针属性,所以调用函数,可以直接对 slice t 进行修改。

第15行代码,声明了一个 整型数组 a,调用 reverse() 函数时,需要传入的是 slice 类型,实参写成了 a[:],表示对整个数组 a 的引用。 若实参直接写在 a,即 reverse(a),则会报错 "cannot use a (type [5]int) as type []int in argument ",实参类型与形参类型不一致。

  8 func main() {
9 // slice t
10 t := []int{1, 2, 3, 4, 5}
11 reverse(t)
12 fmt.Println(t)
13
14 // array a
15 a := [...]int{1, 2, 3, 4, 5}
16 reverse(a[:])
17 fmt.Println(a)
18 }
19
20 func reverse(s []int) {
21 for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
22 s[i], s[j] = s[j], s[i]
23 }
24 }

运行结果如下

十一. 增长

内置的append函数的增长策略,还没去翻源码,先不说了。

只要有可能改变 slice 的指针、长度或容量,都需要更新 slice 变量。

runes = append(runes, r)

引用书中的例子。示例代码中,每一次添加元素,均更新了 slice 变量 x。

  8 func main() {
9 var x []int
10 x = append(x, 1)
11 x = append(x, 2, 3)
12 x = append(x, 4, 5, 6)
13 x = append(x, x...)
14 fmt.Println(x)
15 }

运行结果如下

十二. 注意

1) 越界

如果 slice 的引用超过了被引用对象的容量,即 cap(s),那么会导致程序宕机。如果 slice 的引用超出了被引用对象的长度,即 len(s),但没有超过被引用对象的容量,那么最终 slice 会比原 slice 长。

声明了两个 slice 变量 Q2 和 summer。slice Q2 引用了数组 months 中 4: "April", 5: "May", 6: "June" 三个元素,长度为3,容量为9。 slice summer 引用了数组 months 中 6: "June", 7: "July", 8: "August" 三个元素,长度为3,容量为7。

第28行代码,声明了一个 slice 变量 endlessSummer ,它引用了 slice 变量 summer,起始位置省略了,endlessSummer 的的起始位置默认即是 summer 的起始位置( 6: "June"),长度为 (5-0),容量为7,endlessSummer 它指向的最后一个元素是  "October"。

第35行代码,打印 从slice 变量 Q2 起始位置开始的 20 个元素,但是编译报错 , “slice 界限超出了容量范围9”。Q2 的起始位置指向原始数组中的元素 “April”,slice Q2 的容量为9,从 “April” 至结束共有9个元素 。打印20个元素,超出了容量。

  8 func main() {
9 months := [...]string{1: "January", 2: "February", 3: "March",
10 4: "April", 5: "May", 6: "June",
11 7: "July", 8: "August", 9: "September",
12 10: "October", 11: "November", 12: "December"}
13
14 Q2 := months[4:7]
15 fmt.Println(Q2)
16 fmt.Printf("type: %T\n", Q2)
17 fmt.Println(len(Q2))
18 fmt.Println(cap(Q2))
19 fmt.Println("----------")
20
21 summer := months[6:9]
22 fmt.Println(summer)
23 fmt.Printf("type: %T\n", summer)
24 fmt.Println(len(summer))
25 fmt.Println(cap(summer))
26 fmt.Println("----------")
27
28 endlessSummer := summer[:5]
29 fmt.Println(endlessSummer)
30 fmt.Printf("type: %T\n", endlessSummer)
31 fmt.Println(len(endlessSummer))
32 fmt.Println(cap(endlessSummer))
33 fmt.Println("----------")
34
35 fmt.Println(Q2[:20])
36 }

运行结果如

《Go程序设计语言》学习笔记之slice的更多相关文章

  1. C程序设计语言学习笔记

    在Windows下运行C语言程序 Windows下的编程工具使用 VC 6.0,下面讲解如何在VC 6.0下运行上节的"Hello, world"程序. 1) 新建Win32 Co ...

  2. Go语言学习笔记十一: 切片(slice)

    Go语言学习笔记十一: 切片(slice) 切片这个概念我是从python语言中学到的,当时感觉这个东西真的比较好用.不像java语言写起来就比较繁琐.不过我觉得未来java语法也会支持的. 定义切片 ...

  3. 2017-04-21周C语言学习笔记

    C语言学习笔记:... --------------------------------- C语言学习笔记:学习程度的高低取决于.自学能力的高低.有的时候生活就是这样的.聪明的人有时候需要.用笨的方法 ...

  4. 2017-05-4-C语言学习笔记

    C语言学习笔记... ------------------------------------ Hello C语言:什么是程序:程序是指:完成某件事的既定方式和过程.计算机中的程序是指:为了让计算机执 ...

  5. GO语言学习笔记(一)

    GO语言学习笔记 1.数组切片slice:可动态增长的数组 2.错误处理流程关键字:defer panic recover 3.变量的初始化:以下效果一样 `var a int = 10` `var ...

  6. Go语言学习笔记六: 循环语句

    Go语言学习笔记六: 循环语句 今天学了一个格式化代码的命令:gofmt -w chapter6.go for循环 for循环有3种形式: for init; condition; increment ...

  7. HTML语言学习笔记(会更新)

    # HTML语言学习笔记(会更新) 一个html文件是由一系列的元素和标签组成的. 标签: 1.<html></html> 表示该文件为超文本标记语言(HTML)编写的.成对出 ...

  8. Haskell语言学习笔记(88)语言扩展(1)

    ExistentialQuantification {-# LANGUAGE ExistentialQuantification #-} 存在类型专用的语言扩展 Haskell语言学习笔记(73)Ex ...

  9. Go语言学习笔记十三: Map集合

    Go语言学习笔记十三: Map集合 Map在每种语言中基本都有,Java中是属于集合类Map,其包括HashMap, TreeMap等.而Python语言直接就属于一种类型,写法上比Java还简单. ...

  10. Go语言学习笔记十二: 范围(Range)

    Go语言学习笔记十二: 范围(Range) rang这个关键字主要用来遍历数组,切片,通道或Map.在数组和切片中返回索引值,在Map中返回key. 这个特别像python的方式.不过写法上比较怪异使 ...

随机推荐

  1. UVALive7146 Defeat the Enemy

    题目链接 题目 见链接. 题解 知识点:贪心,STL. 首先要保证我方军队能消灭对方军队才行,因此只要我们按攻击力从大到小排,对方按防御力从大到小排,从大到小遍历,用我方所有攻击力大于敌方目前防御力军 ...

  2. NC15688 Operating System

    题目链接 题目 题目描述 在学习Operating System的过程中,Glory遇到了这样一个问题,现在有一个大小为可以容纳N个页面的内存,硬盘内的内容被分成M个页面,用1~M来标识,一开始内存里 ...

  3. 从零开始手写 mybatis (三)jdbc pool 从零实现数据库连接池

    前景回顾 第一节 从零开始手写 mybatis(一)MVP 版本 中我们实现了一个最基本的可以运行的 mybatis. 第二节 从零开始手写 mybatis(二)mybatis interceptor ...

  4. The Network Adapter could not establish the connection errorCode 17002, state 08006

    问题说明 今天在centos7虚拟机中运行java程序,程序启动需要连接主机的oracle,报错: The Network Adapter could not establish the connec ...

  5. SIP协议解析

    起始行(start-line) INVITE sip:34020000001320000001@202.102.11.27:35611 SIP/2.0 请求消息的起始行包括三个参数,格式:Reques ...

  6. 【Azure 事件中心】Spring Cloud Stream Event Hubs Binder 发送Event Hub消息遇见 Spec. Rule 1.3 - onSubscribe, onNext, onError and onComplete signaled to a Subscriber MUST be signaled serially 异常

    问题描述 开发Java Spring Cloud应用,需要发送消息到Azure Event Hub中.使用 Spring Cloud Stream Event Hubs Binder 依赖,应用执行一 ...

  7. 容器与 Pod

    现在 Docker 的流行程度越来越高,越来越多的公司使用 Docker 打包和部署项目.但是也有很多公司只是追求新技术,将以前的单体应用直接打包为镜像,代码.配置方式等各方面保持不变,使用 Dock ...

  8. require和import的区别以及相互使用的方式

    Node.js 里可分为 CommonJS 模块和 ECMAScript 模块(ESM)两种不同的模块系统. CommonJS 模块是 Node.js 最初支持的模块系统,它使用 require() ...

  9. 小程序开发:app.vue检测更新时判断是否是朋友圈进入

    因为如果从朋友圈点进小程序来的,有些功能就用不了,所以需要判断下是否从朋友圈点进来的. 检查代码如下: checkScene() { // 判断场景值 如果是从分享到朋友圈再打开 就会有一些功能无法使 ...

  10. 14 CodeTON Round 5 (Div. 1 + Div. 2, Rated, Prizes!)C. Tenzing and Balls(dp+前缀最大值优化)

    思路: dp还是挺明显的,思路可以参考最长上升子序列 有点dp的感觉 \(f[i]\)表示考虑前\(i\)个数,的最大值 当前数有两种删或不删 不删:\(f[i]=f[i-1]\); 删:\(f[i] ...