Golang线性表-数组、切片
线性表-数组、切片
一、数据结构
1、数值处理
取整
// 整除
fmt.Println(3/2, -1/2, -3/2)
// 向上取整
fmt.Println(math.Ceil(2.01), math.Ceil(2.5), math.Ceil(2.9))
fmt.Println(math.Ceil(-2.01), math.Ceil(-2.5), math.Ceil(-2.9))
// 向下取整
fmt.Println(math.Floor(2.31), math.Floor(2.54), math.Floor(2.99))
fmt.Println(math.Floor(-2.31), math.Floor(-2.54), math.Floor(-2.99))
// 四舍五入
fmt.Println(math.Round(3.44449), math.Round(3.9), math.Round(3.555))
fmt.Println(math.Round(-3.44449), math.Round(-3.9), math.Round(-3.555))
// 输出结果为:
1 0 -1
3 3 3
-2 -2 -2
~~~~~~~~~~~~~~~~
2 2 2
-3 -3 -3
~~~~~~~~~~~~~~~~
3 4 4
-3 -4 -4
~~~~~~~~~~~~~~~~
- /整除除法,截取整数部分
- math.Ceil向上取整
- math.Floor向下取整
- math.Round 四舍五入
其他数值处理
fmt.Println(math.Abs(-2.7)) // 绝对值
fmt.Println(math.E, math.Pi) // 常数
fmt.Println(math.MaxInt16, math.MinInt16) // 常量,极值
fmt.Println(math.Log10(100), math.Log2(8)) // 对数
fmt.Println(math.Max(1, 2), math.Min(-2, 3)) // 最大值、最小值
fmt.Println(math.Pow(2, 3), math.Pow10(3)) // 幂
fmt.Println(math.Mod(5, 2), 5%2) // 取模
fmt.Println(math.Sqrt(2), math.Sqrt(3), math.Pow(2, 0.5)) // 开方
标准输入
Scan:空表字符分割,回车提交。换行当做空白字符
package main
import (
"fmt"
)
func main() {
var n int
var err error
var word1, word2 string
fmt.Print("Plz input two words: ")
n, err = fmt.Scan(&word1, &word2) // 控制台输入时,单词之间空白字符分割
if err != nil {
panic(err)
}
fmt.Println(n)
fmt.Printf("%T %s, %T %s\n", word1, word1, word2, word2)
fmt.Println("~~~~~~~~~~~~~~~~~~~~~~~~~~~")
var i1, i2 int
fmt.Println("Plz input two ints: ")
n, err = fmt.Scan(&i1, &i2)
if err != nil {
panic(err)
}
fmt.Println(n)
fmt.Printf("%T %[1]d, %T %[2]d", i1, i2) }
如果少一个数据,Scan就会阻塞;如果输入数据多了,等下回Scan读取。例如,一次性输入a b 1 2看看效果。
Scanf:读取输入,按照格式匹配解析。如果解析失败,立即报错,那么就会影响后面的Scanf。
package main
import (
"fmt"
)
func main() {
var n int
var err error
var name string
var age int
fmt.Print("Plz input your name and age: ")
n, err = fmt.Scanf("%s %d\n", &name, &age) // 这里要有\n以匹配回车
if err != nil {
panic(err)
}
fmt.Println(n, name, age)
var weight, height int
fmt.Print("weight and height: ")
_, err = fmt.Scanf("%d %d", &weight, &height)
if err != nil {
panic(err)
}
fmt.Printf("%T %[1]d, %T %[2]d", weight, height) }
fmt.Scanf(“%s,%d”, &name, &age) 中%s会和后面的非空白字符分不清楚,用 abc,20 是匹配不上的,因为除空白字符外,都可以看做是字符串。所以,建议格式字符串中,一律使用空格等空白字符分割。
二、线性数据结构
线性表:
- 线性表(简称表):是一种抽象的概念,是一组元素的序列的抽象,它由又穷个元素组成(0个或任意个)
物理实现有两种方式:
- 顺序表:使用一大块连续的内存顺序存储表中的元素,这样的表称为顺序表,或成为连续表(go语言中的数组)
- 在顺序表中,元素的关系使用顺序表的存储顺序自然的表示
- 顺序表需要开辟连续的内存空间,变量指向顺序表的第一个元素地址
- 变量指向顺序表A=[1,2,3,4,5] A这个标识符指向数组的首地址,每个元素占用空间一样大
从CRUD的方面来分析:
- C:create
- 容器元素个数+1
- append如同排队,在队尾增加
- insert
- 中间增加,把当前位置与其后所有元素向后移动
- 尾部增加,就相当于append
- 开头增加,所有元素向后移动
- 挪动数据是要消耗时间,挪动的元素越多(规模越大),代价也就越大
- D:delete
- 容器元素个数-1
- 队尾移除pop,影响最小
- 中间移除remove
- 如果是队尾,就相当于pop
- 中间移除,后面的数据都向前移动
- 开头删除,所有元素向前移动
- U:update
- 元素个数len不变
- 首先定位需要修改的元素
- 更新内容
- R:read
- 定位元素:首地址 + 该类型的字节数 * 偏移量
- 偏移,所以定位要用索引计算得到元素的内存地址,不用遍历,效率极高。如果使用内容定位,内容比较,就需要遍历的方式查找,效率低下
- 获取内容:使用索引定位直接该位置,拿走内容
- 遍历:容器的元素,不管有没有顺序,都需要不重复的将元素挨个摸一遍,首地址开始挨个偏移取内容
前提:要看数据规模,如果数据规模小,随意使用,顺序表适合在尾部增删,且有扩容问题。
- 链接表:每一个元素存储在内存中,但是元素并不是连续的的存储在内存中,散落在内存的不同位置,前一个元素指向下一个元素(链接),head首部元素,tail尾部元素
- 链表是个容器,可以放元素,不需要实现开辟内存
- 单项链表:前一个元素指向下一个元素
- 双向链表:前一个元素指向下一个元素,下一个元素也指向上一个元素
- 列表(List)往往都是链表实现。python例外
从CRUD的方面来分析:
- C:create
- 容器元素个数+1
- 尾部追加,原来的尾巴指向新尾巴,容器改tail指向,没有数据挪动,速度很快
- 中间插入,断开原来的链接,分别和新元素拉手,没有数据挪动,代价不大
- 首部插入,原来的头和新头互指,当前head的下一个元素,成为新head,删除旧head元素
- D:delete
- 容器元素个数 -1
- 尾部删除:用tail定位尾巴,删除当前尾巴,当前尾巴的前一个称为新tail,删除旧tail元素
- 中间删除:遍历定位,当前元素删除,前驱、后继拉手即可
- 首部删除:用head定位首部,当前head的下一个元素,成为新head,删除旧head元素
- R:read
- 定位元素:
- 有索引,使用索引定位,由于元素散落在内存中,不能使用顺序表的公式来定位,找到开头,依次编索引,按照遍历元素的方式来找的,但是取的每一个节点中保存的下一个地址,使用该地址定位下一个元素,和顺序表比起来,略慢,但是由于都是使用地址,效率还不错。
- 使用内容定位,需要遍历查找,效率低下
- 获取内容:定位到了,取走内容即可
- 定位元素:
- U:update
- 元素个数len不变
- 定位问题
- 更新内容
链接表在规模大时,如果增删发生在中间或头部(可以包括尾部),链接表比较适合,因为他不需要扩容,找到一个内存中能放当前元素即可,然后修改指向(链接地址)
数组等类型,如同地铁站排好的队伍,有序,可以插队、离队,可以索引。
链表,如同操场上手拉手的小朋友,有序,但排列随意,或者可以想象成一串带线的珠子,随意盘方在桌上。也可以离队、插队,也可以索引。
三、数组
数组是由顺序表实现,容器不可变,可以索引,是值类型,容器的元素个数定义后就不能变了。数组必须给出长度,以后数组这个容器就不能增删元素,不能扩容了。
数组的内存结构:
数组的地址就是数组内第一个元素的内存地址
每个元素占用的空间看元素类型,int动态类型,看cpu架构,32位就是4字节,64位就是8字节。
一个元素占用几个字节和类型有关,第一个元素后面一定是第二个元素的存储单元,顺序表
字符串字面常来那个,一旦定义就不可改变,但是不同的字符串长度不一,数组采用string为元素,元素的存储空间间隔一样,都是16字节(字符串在go语言中也是个结构体,指向字符串的指针为byte类型也就是uint8,占用8字节,字符串长度为int,也占用8字节),说明字符串更复杂。(16字节中其实是放的指向字符串的指针,而并不是字符串本身!)
定义
// 注意下面2种区别
var a0 [3]int // 零值初始化3个元素的数组
var a1 = [3]int{} // 零值初始化3个元素的数组
// [3]int是类型,[3]int{} 是字面量值
var a2 [3]int = [3]int{1, 3, 5} // 声明且初始化,不推荐,啰嗦
var a3 = [3]int{1, 3, 5} // 声明且初始化,推荐
count := 3
a4 := [count] int{1,3,5} // 错误的长度类型,必须是常量,换成const
fmt.Println(a2, a3)
const count = 3
a4 := [count]int{1, 3, 5} // 正确
fmt.Println(a2, a3, a4)
a5 := [...]int {10, 30, 50} // ...让编译器确定当前数组大小
a6 := [5]int{100, 200} // 顺序初始化前面的,其余用零值填充
a7 := [5]int{1: 300, 3: 400} // 指定索引位置初始化,其余用零值填充
// 二维数组
a8 := [2][3]int{{100}} // 两行三列 [[100 0 0] [0 0 0]]
// [[10 0 0] [11 12 0] [13 14 15] [16 0 0]]
// 多维数组,只有第一维才能用...推测
// 第一维有4个,第二维有3个。可以看做4行3列的表
a9 := [...][3]int{{10}, {11, 12}, {13, 14, 15}, {16}}
长度和容量
- cap即capacity,容量,表示给数组分配的内存空间可以容纳多少个元素
- len即length,长度,指的是容器中目前有几个元素
由于数组创建时就必须确定元素的个数,且不能改变长度,所以不需要预留多余的内存空间,因此cap和len对数组来说相等。
索引
Go语言不支持负索引,通过[index]来获取该位置上的值。索引范围就是[0,长度-1]
修改
a5 := [...]int{10, 30, 50}
a5[0] += 100
遍历
- 索引遍历
a1 := [...]int{10, 30, 50}
for i := 0; i < len(a1); i++ {
fmt.Println(i, a1[i])
}
- for-range遍历
for i, i2 := range a1 {
fmt.Println(i, i2)
}
内存模型
var a [3]int // 内存开辟空间存放长度为3的数组,零值填充
for i := 0; i < len(a); i++ {
fmt.Println(i, a[i], &a[i])
}
fmt.Printf("%p %p %v \n", &a, &a[0], a)
a[0] = 1000
fmt.Printf("%p %p %v \n", &a, &a[0], a)
//输出结果
0 0 0x1400001c090
1 0 0x1400001c098
2 0 0x1400001c0a0
0x1400001c090 0x1400001c090 [0 0 0]
0x1400001c090 0x1400001c090 [1000 0 0]
- 数组必须在编译时就确定大小,之后不能改变大小
- 数组第一个元素的地址就是数组地址
- 所有元素一个接一个顺序存储在内存中
- 元素的值可以以改变,但是元素地址不变
上面每个元素间隔8个字节,正好64位,符合int类型定义
如果数据元素是字符串类型呢?
var a [3]string // 内存开辟空间存放长度为3的数组
for i := 0; i < len(a); i++ {
fmt.Println(i, a[i], &a[i])
}
fmt.Printf("%p %p %v \n", &a, &a[0], a)
a[0] = "oooooooooo"
fmt.Printf("%p %p %v \n", &a, &a[0], a)
// 运行结果
0 0x1400006c180
1 0x1400006c190
2 0x1400006c1a0
0x1400006c180 0x1400006c180 [ ]
0x1400006c180 0x1400006c180 [ss ]
- 数组首地址就是数组地址
- 所有元素顺序存储在内存中
- 元素的值可以改变,但是元素地址不变
值类型
package main
import "fmt"
func showAddr(arr [3]int) [3]int {
fmt.Printf("arr = %v %p\n", arr, &arr)
return arr
}
func main() {
a1 := [...]int{10, 30, 50}
fmt.Printf("a1 = %v ,%p\n", a1, &a1)
a2 := a1
fmt.Printf("a2 = %v ,%p\n", a2, &a2)
fmt.Println("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
a3 := showAddr(a1)
fmt.Printf("a3 = %v ,%p\n", a3, &a3)
}
// 输出结果
a1 = [10 30 50] ,0x140000c0018
a2 = [10 30 50] ,0x140000c0048
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
arr = [10 30 50] 0x140000c0090
a3 = [10 30 50] ,0x140000c0078
可以看到a1、a2、a3、a4的地址都不一样,看最后a2 :=a1之后两个变量地址也不一样。
这说明go语言在这些地方对数组进行了值拷贝,都生成了一份副本。
四、切片
- 容量可变
- 内容可变
- 引用类型,和值类型有区别
- 底层基于数组,依赖于顺序表,表现得也像个可变容量和长度顺序表
####### 内存模型
- 组合结构
- 1、底层数组,数组容量不可变,元素内容可变
- 2、slice head标头值或descriptor描述符
切片本质是对底层数组一个连续片段的引用。此片段可以是整个底层数组,也可以是由起始和终止索引标识的一些项的子集
type slice struct {
array unsafe.Pointer //指针,指向低层数组点的首地址
len int // 当前切片的长度
cap int //当前切片的容量
}
上面三个结构体的属性都是小写,所以包外不可见。len函数取的就是len属性,cap取得就是cap属性
指针可以通过取底层数组的第一个元素的地址,即切片第一个元素的地址
a := []int{1, 3, 5, 6}
fmt.Printf("%v, %p %p", a, &a, &a[0])
//输出
[1 3 5 6], 0x1400000c030 0x1400001a020
&a 是切片结构体的地址,&a[0]是底层数组的地址。
####### 切片定义
var s1 []int // 长度、容量都为0的切片,零值
var s2 = []int{} // 长度、容量都为0的切片,字面量定义
var s3 = []int{1,3,5} //长度、容量都为3点的切片
var s4 = make([]int,0) // 长度、容量都为0的切片,make([]T,length)
var s5 = make([]int,1,5) // 长度为1,容量为5,底层数组长度为5,元素长度为1,所以显示[0]
切片删除(伪删除)
func Remove(slice []int, index int) []int {
return append(slice[:index], slice[index+1:]...)
}
func main() {
slice := []int{1, 2, 3, 4, 5}
index := 2
slice = Remove(slice, index)
fmt.Println(slice)
}
// 在上述代码中,Remove函数的参数包括一个整型切片“slice”和一个索引值“index”,表示需要删除的元素在切片中的位置。接着,我们使用“append”函数的特性来实现删除操作:首先将“slice”切片中的前半部分(从0到“index”-1)和后半部分(从“index”+1到切片的末尾)连接起来,返回一个新的切片作为最终结果。通过将这个新的切片赋值给“slice”,就实现了删除操作。
####### 追加
append:在切片的尾部追加元素,长度加1
增加元素后,有可能超过当前容量,导致切片扩容。
长度和容量
s1 := make([]int, 3, 5)
fmt.Printf("s1 %p %p len=%d cap=%d %v\n", &s1, &s1[0], len(s1), cap(s1), s1)
s2 := append(s1, 4)
fmt.Printf("s2 %p %p len=%d cap=%d %v\n", &s2, &s2[0], len(s2), cap(s2), s2)
fmt.Printf("s1 %p %p len=%d cap=%d %v\n", &s1, &s1[0], len(s1), cap(s1), s1)
目前还没有超过容量,底层共用一个数组,但是,对底层数组使用的片段不一样
s1 0x1400000c030 0x140000141b0 len=3 cap=5 [0 0 0]
s2 0x1400000c060 0x140000141b0 len=4 cap=5 [0 0 0 4]
s1 0x1400000c030 0x140000141b0 len=3 cap=5 [0 0 0]
s1 := make([]int, 3, 5)
fmt.Printf("s1 %p %p len=%d cap=%d %v\n", &s1, &s1[0], len(s1), cap(s1), s1)
s2 := append(s1, 4)
fmt.Printf("s2 %p %p len=%d cap=%d %v\n", &s2, &s2[0], len(s2), cap(s2), s2)
s3 := append(s1, -1)
// 因为没有超过最大容量,所以还是共用底层数组,那么s3基于s1增加元素就是在[0,0,0]上增加-1,底层数组的值就为[0,0,0,-1]
fmt.Printf("s3 %p %p len=%d cap=%d %v\n", &s3, &s3[0], len(s3), cap(s3), s3)
// 因为是共用底层数组所以上面的第四个元素值为-1,所以s2得第四个元素也被修改成-1
fmt.Printf("s2 %p %p len=%d cap=%d %v\n", &s2, &s2[0], len(s2), cap(s2), s2)
// 输出
s1 0x1400000c030 0x140000141b0 len=3 cap=5 [0 0 0]
s2 0x1400000c060 0x140000141b0 len=4 cap=5 [0 0 0 4]
s3 0x1400000c090 0x140000141b0 len=4 cap=5 [0 0 0 -1]
s2 0x1400000c060 0x140000141b0 len=4 cap=5 [0 0 0 -1]
s1 := make([]int, 3, 5)
fmt.Printf("s1 %p %p len=%d cap=%d %v\n", &s1, &s1[0], len(s1), cap(s1), s1)
s2 := append(s1, 4)
fmt.Printf("s2 %p %p len=%d cap=%d %v\n", &s2, &s2[0], len(s2), cap(s2), s2)
s3 := append(s1, -1)
fmt.Printf("s3 %p %p len=%d cap=%d %v\n", &s3, &s3[0], len(s3), cap(s3), s3)
fmt.Printf("s2 %p %p len=%d cap=%d %v\n", &s2, &s2[0], len(s2), cap(s2), s2)
s4 := append(s3, 6, 7, 8)
fmt.Printf("s4 %p %p len=%d cap=%d %v\n", &s4, &s4[0], len(s4), cap(s4), s4)
// 输出 可以看到,底层数组的地址经过扩容之后地址已经改变,说明已经是一个新的底层数组了
s1 切片地址:0x14000122018 底层数组地址:0x14000130030 len=3 cap=5 [0 0 0]
s2 切片地址:0x14000122048 底层数组地址:0x14000130030 len=4 cap=5 [0 0 0 4]
s3 切片地址:0x14000122078 底层数组地址:0x14000130030 len=4 cap=5 [0 0 0 -1]
s2 切片地址:0x14000122048 底层数组地址:0x14000130030 len=4 cap=5 [0 0 0 -1]
s4 切片地址:0x140001220c0 底层数组地址:0x14000138000 len=7 cap=10 [0 0 0 -1 6 7 8]
- append一定返回一个新的切片
- append可以增加若干元素
- 如果增加元素时,当前长度+新增个数 <=cap 则不扩容
- 原切片使用原来的底层数组,返回的新切片也使用这个底层数组
- 返回的新切片有新的长度
- 原切片长度不变
- 如果增加元素时,当前长度+新增个数 > cap 则需要扩容底层数组
- 生成新的底层数组,新生成的切片使用新数组,将旧元素复制到新数组,在其后增加新元素
- 原切片底层数组、长度、容量不变
- 如果增加元素时,当前长度+新增个数 <=cap 则不扩容
####### 底层库容策略
https://go.dev/src/runtime/slice.go
(老版本)实际上,当扩容后的cap<1024时,扩容是以翻倍的形式,容量变成之前的2倍,如之前容量是10,扩容后容量变成20;当cap>=1024时,变成之前的1.25倍,如之前容量是10,扩容后变成12.5
(新版本1.18+) 阈值变成了256,当扩容后的cap<256时,扩容翻倍,容量变成之前的2倍;当cap>=256时,newcap += (newcap + 3*threshold) / 4 计算后就是 newcap = newcap +newcap/4 + 192
,即1.25倍后再加192。
扩容是创建新的内部数组,把原内存数据拷贝到新内存空间,然后在新内存空间上执行元素追加操作。
切片频繁库容成本非常高,所以尽量估算出使用的大小,一次性给够,建议用make,常用make([]int,0,100)
引用类型
package main
import (
"fmt"
)
func showAddr(s []int) []int {
fmt.Printf("s %p, %p, %d, %d, %v\n", &s, &s[0], len(s), cap(s), s)
// 修改一个元素
if len(s) > 0 {
s[0] = 123
}
return s }
func main() {
s1 := []int{10, 20, 33}
fmt.Printf("s1 %p, %p, %d, %d, %v\n", &s1, &s1[0], len(s1), cap(s1), s1)
s2 := s1
fmt.Printf("s2 %p, %p, %d, %d, %v\n", &s2, &s2[0], len(s2), cap(s2), s2)
fmt.Println("~~~~~~~~~~~~~~~~~~~~~~~~~~~")
s3 := showAddr(s1)
fmt.Printf("s1 %p, %p, %d, %d, %v\n", &s1, &s1[0], len(s1), cap(s1), s1)
fmt.Printf("s2 %p, %p, %d, %d, %v\n", &s2, &s2[0], len(s2), cap(s2), s2)
fmt.Printf("s3 %p, %p, %d, %d, %v\n", &s3, &s3[0], len(s3), cap(s3), s3) }
运行结果
s1 0xc000008078, 0xc0000101b0, 3, 3, [10 20 33]
s2 0xc0000080a8, 0xc0000101b0, 3, 3, [10 20 33]
~~~~~~~~~~~~~~~~~~~~~~~~~~~
s 0xc0000080f0, 0xc0000101b0, 3, 3, [10 20 33]
s1 0xc000008078, 0xc0000101b0, 3, 3, [123 20 33]
s2 0xc0000080a8, 0xc0000101b0, 3, 3, [123 20 33]
s3 0xc0000080d8, 0xc0000101b0, 3, 3, [123 20 33]
这说明,底层数组是同一份,修改切片中某个已有元素,那么所有切片都能看到。
那如果在上面showAddr函数中对切片增加一个元素会怎么样?
package main
import (
"fmt"
)
func showAddr(arr []int) []int {
fmt.Printf("arr 切片地址:%p 底层数组地址:%p len=%d cap=%d %v\n", &arr, &arr[0], len(arr), cap(arr), arr)
arr = append(arr, 444, 222)
fmt.Printf("arr2 切片地址:%p 底层数组地址:%p len=%d cap=%d %v\n", &arr, &arr[0], len(arr), cap(arr), arr)
//if len(arr) > 0 {
// arr[0] = 22222
//}
return arr
}
func main() {
//a := []int{1, 3, 5, 6}
//fmt.Printf("%v, %p %p", a, &a, &a[0])
s1 := []int{10, 20, 33}
fmt.Printf("s1 切片地址:%p 底层数组地址:%p len=%d cap=%d %v\n", &s1, &s1[0], len(s1), cap(s1), s1)
s2 := s1
fmt.Printf("s2 切片地址:%p 底层数组地址:%p len=%d cap=%d %v\n", &s2, &s2[0], len(s2), cap(s2), s2)
fmt.Println("~~~~~~~~~~~~~~~~")
s3 := showAddr(s1)
fmt.Printf("s1 切片地址:%p 底层数组地址:%p len=%d cap=%d %v\n", &s1, &s1[0], len(s1), cap(s1), s1)
fmt.Printf("s2 切片地址:%p 底层数组地址:%p len=%d cap=%d %v\n", &s2, &s2[0], len(s2), cap(s2), s2)
fmt.Printf("s3 切片地址:%p 底层数组地址:%p len=%d cap=%d %v\n", &s3, &s3[0], len(s3), cap(s3), s3)
}
// 输出
s1 切片地址:0x14000116018 底层数组地址:0x1400012a018 len=3 cap=3 [10 20 33]
s2 切片地址:0x14000116048 底层数组地址:0x1400012a018 len=3 cap=3 [10 20 33]
~~~~~~~~~~~~~~~~
arr 切片地址:0x14000116090 底层数组地址:0x1400012a018 len=3 cap=3 [10 20 33]
arr2 切片地址:0x14000116090 底层数组地址:0x14000126060 len=5 cap=6 [10 20 33 444 222]
s1 切片地址:0x14000116018 底层数组地址:0x1400012a018 len=3 cap=3 [10 20 33]
s2 切片地址:0x14000116048 底层数组地址:0x1400012a018 len=3 cap=3 [10 20 33]
s3 切片地址:0x14000116078 底层数组地址:0x14000126060 len=5 cap=6 [10 20 33 444 222]
可以看到showAddr传入s1,但是返回的s3已经和s1不共用一个底层数组了。
其实这里还是值拷贝,不过拷贝的是切片的标头值(Header)。标头值内指针也被复制,刚复制完大家指向同一个底层数组罢了,但是一旦操作切片时扩容了,或另一个切片增加元素,那么就不能简单归结为“切片”是引用类型,拷贝了地址“这样来解释”
GO语言中全部都是值传递,整型、数组这样的类型的值是完全复制,slice、map、channel、interface、function这样的引用类型也是值拷贝,不过复制的是标头值。
截取子切片
切片可以通过指定索引区间获得一个子切片,格式为slice[start:end],规则就是前包后不包
package main
import (
"fmt"
)
func main() {
s1 := []int{10, 30, 50, 70, 90} // 容量、长度为5,索引0、1、2、3、4
for i := 0; i < len(s1); i++ {
fmt.Printf("%d : addr = %p\n", i, &s1[i])
}
fmt.Printf("s1 %p, %p, l=%-2d, c=%-2d, %v\n", &s1, &s1[0], len(s1), cap(s1), s1)
s2 := s1 // 和s1共用底层数组 [10,30,50,70,90]
fmt.Printf("s2 %p, %p, l=%-2d, c=%-2d, %v\n", &s2, &s2[0], len(s2), cap(s2), s2)
s3 := s1[:] // 和s1共用底层数组,从头到尾元素都要
fmt.Printf("s3 %p, %p, l=%-2d, c=%-2d, %v\n", &s3, &s3[0], len(s3), cap(s3), s3)
fmt.Println("~~~~~~~~~~~~~~~~~~~~~~~~~~~")
s4 := s1[1:] // 掐头,容量、长度都为4,首地址偏移1个元素,共用底层数组,[30,50,70,90]
fmt.Printf("s4 %p, %p, l=%-2d, c=%-2d, %v\n", &s4, &s4[0], len(s4), cap(s4), s4)
s5 := s1[1:4] // 掐头去尾,容量为4,长度为3,首地址偏移1个元素,共用底层数组,[30,50,70]
fmt.Printf("s5 %p, %p, l=%-2d, c=%-2d, %v\n", &s5, &s5[0], len(s5), cap(s5), s5)
s6 := s1[:4] // 去尾,容量为5,长度为4,首地址不变,共用底层数组 [10,30,50,70]
fmt.Printf("s6 %p, %p, l=%-2d, c=%-2d, %v\n", &s6, &s6[0], len(s6), cap(s6), s6)
fmt.Println("~~~~~~~~~~~~~~~~~~~~~~~~~~~")
s7 := s1[1:1] // 首地址偏移1个元素,长度为0,容量为4,共用底层数组 []
// fmt.Printf("s7 %p, %p, l=%-2d, c=%-2d, %v\n", &s7, &s7[0], len(s7), cap(s7), s7) // 由于长度为0,所以不能s7[0]报错
fmt.Printf("s7 %p, l=%-2d, c=%-2d, %v\n", &s7, len(s7), cap(s7), s7)
// s7 = append(s7, 111) // 请问,为s7增加一个元素,s1、s7分别是什么?
// fmt.Printf("s1 %p, %p, l=%-2d, c=%-2d, %v\n", &s1, &s1[0], len(s1), cap(s1), s1) [10,111,50,70,90]
// fmt.Printf("s7 %p, %p, l=%-2d, c=%-2d, %v\n", &s7, &s7[0], len(s7), cap(s7), s7) [111]
fmt.Println("~~~~~~~~~~~~~~~~~~~~~~~~~~~")
s8 := s1[4:4] // 首地址偏移4个元素,长度为0,容量为1,因为最后一个元素没在切片中,共用底层数组 []
fmt.Printf("s8 %p, l=%-2d, c=%-2d, %v\n", &s8, len(s8), cap(s8), s8)
s9 := s1[5:5] // 首地址偏移5个元素,长度为0,容量为0,共用底层数组
fmt.Printf("s9 %p, l=%-2d, c=%-2d, %v\n", &s9, len(s9), cap(s9), s9) // []
fmt.Println("~~~~~~~~~~~~~~~~~~~~~~~~~~~")
s9 = append(s9, 11) // 增加元素会怎么样?s1、s9分别是什么?
fmt.Printf("s1 %p, %p, l=%-2d, c=%-2d, %v\n", &s1, &s1[0], len(s1), cap(s1), s1) //[10,111,50,70,90]
fmt.Printf("s9 %p, %p, l=%-2d, c=%-2d, %v\n", &s9, &s9[0], len(s9), cap(s9), s9) } //[11] 这个是新数组
上面例子可以看出,切这个操作都是从同一个底层数组上取的段,所以子切片和原始切片共用同一个底层数组
- start默认为0,end默认为len(slice)即切片长度
- 通过指针确定底层数组从哪里开始共享
- 长度为end-start
- 容量是底层数组从便偏移的元素到结尾能容纳几个元素
子切片操作不可能构造出一个新的底层数组! 子切片本质上利用原来的切片的部分元素,共用一个底层数组,因此不会导致扩容。
数组也可以切片,会生成新的切片。
package main
import "fmt"
func main() {
s1 := [5]int{10, 30, 50, 70, 90}
for i := 0; i < len(s1); i++ {
fmt.Printf("%d : addr = %p\n", i, &s1[i])
}
s4 := s1[1:] // 掐头,容量、长度都为4,首地址偏移1个元素,共用底层数组
fmt.Printf("s4 %p, %p, l=%-2d, c=%-2d, %v\n", &s4, &s4[0], len(s4), cap(s4), s4)
s4[0] = 100 // s1、s4分别是什么?
fmt.Printf("s1 %p, %p, l=%-2d, c=%-2d, %v\n", &s1, &s1[0], len(s1), cap(s1), s1)
fmt.Printf("s4 %p, %p, l=%-2d, c=%-2d, %v\n", &s4, &s4[0], len(s4), cap(s4), s4)
s4 = append(s4, 11, 22) // 是否扩容?会怎样?
fmt.Printf("s1 %p, %p, l=%-2d, c=%-2d, %v\n", &s1, &s1[0], len(s1), cap(s1), s1)
fmt.Printf("s4 %p, %p, l=%-2d, c=%-2d, %v\n", &s4, &s4[0], len(s4), cap(s4), s4)
}
// 输出
0 : addr = 0x140000ae030
1 : addr = 0x140000ae038
2 : addr = 0x140000ae040
3 : addr = 0x140000ae048
4 : addr = 0x140000ae050
s4 0x140000a0018, 0x140000ae038, l=4 , c=4 , [30 50 70 90]
s1 0x140000ae030, 0x140000ae030, l=5 , c=5 , [10 100 50 70 90]
s4 0x140000a0018, 0x140000ae038, l=4 , c=4 , [100 50 70 90]
s1 0x140000ae030, 0x140000ae030, l=5 , c=5 , [10 100 50 70 90]
s4 0x140000a0018, 0x140000b0080, l=6 , c=8 , [100 50 70 90 11 22]
切片总结:
- 使用slice[start:end] 表示切片,切片长度为end-start,前包后不包
- start缺省,表示从索引0开始
- end缺省,表示取到末尾,包含最后一个元素,特别注意这个缺省值是len(slice) 即切片长度,不是容量
- a1[5:] 相当于 a1[5:len(a1)]
- start 和end都缺省,表示从头到尾
- start和end同时给出,要求end >=start
- start、end最大都不可以超过容量值cap
- 假设当前容量是8,长度为5,有以下情况
- a1[:] 共用底层数组,从头到尾所有元素都取,相当于对标头值的拷贝,也就是指针、长度、容量都一样
- a1[:8],可以,end最多写成8(因为后不包),a1[:9]不可以。该切片长度、容量都为8,这8个元素都是原序列的,一旦append就扩容
- a1[8:],不可以,end缺省为当前长度5,等价于a1[8:5]
- a1[8:8],可以,但这个切片容量和长度都为0了。注意和a1[:8]的区别
- a1[7:7],可以,但这个切片长度为0,容量为1
- a1[0:0],可以,但这个切片长度为0,容量为8
- a1[1:5],可以,这个切片长度为4,容量为7,相当于跳过了原序列第一个元素
- 切片刚产生时,和原序列(数组、切片)开始共用同一个底层数组,但是每一个切片都自己独立保存着指针、cap和len
- 一旦一个切片扩容,就和原来共用一个底层数组的序列分道扬镳。