线性表-数组、切片

一、数据结构

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]是底层数组的地址。

image

####### 切片定义

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 则需要扩容底层数组
      • 生成新的底层数组,新生成的切片使用新数组,将旧元素复制到新数组,在其后增加新元素
      • 原切片底层数组、长度、容量不变

####### 底层库容策略
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
  • 容量是底层数组从便偏移的元素到结尾能容纳几个元素

image

子切片操作不可能构造出一个新的底层数组! 子切片本质上利用原来的切片的部分元素,共用一个底层数组,因此不会导致扩容。

数组也可以切片,会生成新的切片。

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
  • 一旦一个切片扩容,就和原来共用一个底层数组的序列分道扬镳。