Go 1.19.4 切片与子切片-Day 05

1. 切片

1.1 介绍

切片在Go中是一个引用类型,它包含三个组成部分:指向底层数组的指针(pointer)、切片的长度(length)以及切片的容量(capacity),这些信息共同构成了切片的“头(header)”。

切片是一个非常奇怪的集合体,它底层用的是数组,但它又能把数组值复制这个问题规避掉。
为啥底层是数组呢?因为它需要使用顺序表,因为使用索引访问,在顺序表中是最快的。

1.2 特点

它的特点如下:
(1)长度可以变,容量可变,长度和容量可以不一样,首次定义时,长度和容量相同。

长度:表示当前元素的数量
容量:表示最多可以定义多少个元素。
如切片长度3,容量5,含义为我切片中最多可以放5个元素,但当前只用了3个,还剩2个元素可以放置。
我把它理解为k8s中的request和limit。

(2)引用类型
切片之间引用(复制)的是header,并不是直接引用内存地址。

(3)底层基于数组

1.3 定义方式

1.3.1 方式一:字面量赋值定义

该方式适合小批量的定义,如果切片元素过多,就不太适合了。

package main

import "fmt"

func main() {
    // 错误的声明方式
    // var s0 = []int
    
	// 这就是定义一个切片,如果在[]中加上数字或者...,那就是一个数组
    // 这里的int可以是go中支持的任意数据类型,但元素类型必须一致
	var s0 = []int{1, 2, 3} // 该切片长度为3,容量为3
	fmt.Printf("%v\n%[1]T", s0)
}
=========调试结果=========
[1 2 3] // 光从输出结果来看,是无法分辨数组和切片
[]int // 打印值类型就可以,[]中为空,就表示切片

1.3.2 方式二:声明空切片(不推荐)

package main

import "fmt"

func main() {
	// 定义一个长度为0,容量为0的切片
	var s1 []int
	fmt.Printf("%T %[1]v %d %d", s1, len(s1), cap(s1))
}
=========调试结果=========
[]int [] 0 0

1.3.3 方式三:make(推荐)

make可以给内建容器开辟内存空间,比较适合用于多元素定义的场景。
并且make还能指定初始容量大小,减少频繁扩容。
但是注意,不同的数据类型使用make,参数含义是不一样的。


语法:make([]int len, cap)

  • int:表示切片的数据类型
  • len:表示切片的长度。
  • cpa:表示切片的容量。当未指定cap时(make([]int len)),len也等于cap。
package main

import "fmt"

func main() {
	// 0,表示长度为0,目前由于没有元素,所以容量也为0。
	// 切片使用make,()中的第二个参数表示长度
	var s3 = make([]int, 0)
	fmt.Println(s3, len(s3), cap(s3))

	// 切片使用make,()中的第二个参数0表示长度,第三个参数5表示容量
	s4 := make([]string, 0, 5)
	fmt.Println(s4, len(s4), cap(s4))
}
=========调试结果=========
[] 0 0 // 长度为0,容量为0
[] 0 5 // 长度为0,容量为5

1.4 切片内存模型

切片的内存模型大致如下,还能称为切片的herdedr:
(1)pointer
存放的指向底层数组的指针。
这个指针指向切片实际引用的数组元素的起始位置。通过这个指针,切片能够访问和操作底层数组中的元素。

(2)len
存放当前切片的长度,这个长度决定了切片可以访问的底层数组元素的范围。

(3)cap
存放当前切片的容量,容量反映了切片可以增长元素的最大范围,即在不需要重新分配底层数组的情况下,可以向切片追加的元素数量。

由于切片需要使用顺序表,所以它的底层其实还是依赖数组的。
但是数组一旦定死它的长度是不可变的,而切片的长度和容量都可变,那数组的长度不够咋办呢?
切换底层数组,当切片需要扩容,但底层数组长度又不够的时候,go会废弃这个老的底层数组,再创建一个新的满足切片扩容长度的底层数组。
在这里插入图片描述

1.4.1 切片元素内存地址理解

package main

import "fmt"

func main() {
	var s0 = []int{1, 2, 3}
	fmt.Printf("%p %p\n", &s0, &s0[0])
    // &s0,表示的是当前这个结构体(切片)的内存地址(header地址)。
	// &s0[0],表示的是当前这个切片底层数组的第一个元素的内存地址,也是底层数组的首地址。
}
=========调试结果=========
0xc000008078 0xc000010168

1.4.2 追加内容到切片(append)

append内置函数,用于在切片的尾部追加元素,并且不会修改当前切片的header,因为它总是会返回一个新的header(至于header内容是否改变,取决于操作的切片是新还是旧)。
如果是基于老切片新增元素给新切片,则header可能会发生变化,也就是说pointer、len、cap都有可能会发生变化。
增加元素后,有可能超过当前切片容量,导致切片扩容(切片扩容容量为扩容前已存在元素的倍数)。
注意append只能用于切片。

package main

import "fmt"

func main() {
	var s0 = []int{1, 2, 3}
	fmt.Printf("%p %p\n", &s0, &s0[0])

	// append(s0, 11),表示对s0进行尾部元素追加,追加完毕后又写入到s0
	s0 = append(s0, 11)
	fmt.Println(s0, &s0[0])
}
=========调试结果=========
0xc000008078 0xc000010168
// 11就是追加的内容,并且追加后,底层数组的首地址也发生了改变
// 这是符合上面的推断的
[1 2 3 11] 0xc00000e3c0
1.4.2.1 切片长度与容量
package main

import "fmt"

func main() {
    // 切片长度为3,容量为5
	var s0 = make([]int, 3, 5)
	fmt.Printf("切片内存地址:%p\n底层数组首地址:%p\n切片元素数量:%d\n切片容量:%v\n切片元素:%v", &s0, &s0[0], len(s0), cap(s0), s0)
}
=========调试结果=========
切片内存地址:0xc000116060
底层数组首地址:0xc000142030
切片元素数量:3
切片容量:5
切片元素:[0 0 0]

基于老切片追加元素到新切片,观察新老切片的变化。

// 上面s0切片还是3个0值,下面我给他调整一下
package main

import "fmt"

func main() {
	var s0 = make([]int, 3, 5)
	fmt.Printf("切片内存地址:%p\n底层数组首地址:%p\n切片元素数量:%d\n切片容量:%v\n切片元素:%v\n", &s0, &s0[0], len(s0), cap(s0), s0)
	fmt.Println("----------------------------------")

    // 向s0追加两个元素,得到新的切片s1
	s1 := append(s0, 1, 2)
	fmt.Println(s0, len(s0), cap(s0))
	fmt.Println(s1, len(s1), cap(s1))
}
=========调试结果=========
切片内存地址:0xc0000aa060
底层数组首地址:0xc0000d8030
切片元素数量:3
切片容量:5
切片元素:[0 0 0]
----------------------------------
// 看这部分
[0 0 0] 3 5 // 这是s0
[0 0 0 1 2] 5 5 // 这是s1

为什么s0的长度和容量与s1不一样?
这就不得不再说下切片的herdedr了,首先最开始用make定义切片的时候,var s0 = make([]int, 3, 5),这个切片中只存储了3个0元素,但由于容量为5,实际上还能增加2个元素。
所以追加两个元素后(​​s0​​原本长度为3,追加后长度为5),总长度并没有超过原切片的容量(5),所以​​append​​操作是在原切片​​s0​​的底层数组上进行的,并且​​s1​​和​​s0​​共享同一个底层数组。但是,​​s1​​和​​s0​​是两个不同的切片头(header),因为它们有不同的长度。

那这里思考一个问题,s0和s1的底层数组是否相同?
看下面的代码:

package main

import "fmt"

func main() {
	// 定义一个长度为3,容量为5的切片
	var s0 = make([]int, 3, 5)
	fmt.Printf("切片内存地址:%p 底层数组首地址:%p 切片元素数量:%d 切片容量:%v 切片元素:%v\n", &s0, &s0[0], len(s0), cap(s0), s0)
	fmt.Println("----------------------------------")

	// 向s0追加两个元素,得到新的切片s1
	s1 := append(s0, 1, 2)
	// fmt.Println(s0, len(s0), cap(s0))
	// fmt.Println(s1, len(s1), cap(s1))

	fmt.Printf("切片内存地址:%p 底层数组首地址:%p 切片元素数量:%d 切片容量:%v 切片元素:%v\n", &s1, &s1[0], len(s1), cap(s1), s1)
}
=========调试结果=========
切片内存地址:0xc000080048 底层数组首地址:0xc0000aa030 切片元素数量:3 切片容量:5 切片元素:[0 0 0]
----------------------------------
切片内存地址:0xc000080078 底层数组首地址:0xc0000aa030 切片元素数量:5 切片容量:5 切片元素:[0 0 0 1 2]

通过上面的返回可以看到,s0切片和s1切片的header(内存地址)不同,但底层数组地址完全一样,究其原因就是因为底层数组的长度是满足元素新增的,所以实际上两个切片都是引用的同一个数组(数据是存在同一个内存空间中的)。

既然底层是同一个数组,为什么s0和s1显示的内容不同?
可以把切片的长度当成一个窗帘,底层数组实际上就是存储着00012,但由于s0受到长度3的限制,所以我们是看不到超过长度3的内容的。

为啥两个切片的header不同呢?
因为两个切片的元素数量不同,所以s1 := append(s0, 1, 2)插入元素后返回值给s1时,header中的len被更新了,所以header看着不一样,其实简单理解,s0和s1都是一个独立的切片,所以header肯定不一样,虽然它们底层引用的都是相同的数组。

1.4.2.2 切片容量溢出

这里主要讲一下,切片容量溢出后,底层到底是怎么做的。
主要看下面新增的s3切片:

package main

import "fmt"

func main() {
	// 定义一个长度为3,容量为5的切片
	var s0 = make([]int, 3, 5)
	fmt.Printf("s0 切片内存地址:%p 底层数组首地址:%p 切片元素数量:%d 切片容量:%v 切片元素:%v\n", &s0, &s0[0], len(s0), cap(s0), s0)
	fmt.Println("----------------------------------")

	// 向s0追加两个元素,得到新的切片s1。
	s1 := append(s0, 1, 2)
	fmt.Printf("s1 切片内存地址:%p 底层数组首地址:%p 切片元素数量:%d 切片容量:%v 切片元素:%v\n", &s1, &s1[0], len(s1), cap(s1), s1)
	fmt.Println("----------------------------------")

	s2 := append(s0, -1)
	fmt.Printf("s2 切片内存地址:%p 底层数组首地址:%p 切片元素数量:%d 切片容量:%v 切片元素:%v\n", &s2, &s2[0], len(s2), cap(s2), s2)
	fmt.Println("----------------------------------")
    
    // 向s2追加三个元素,得到新的切片s3
	s3 := append(s2, 3, 4, 5)
	fmt.Printf("s3 切片内存地址:%p 底层数组首地址:%p 切片元素数量:%d 切片容量:%v 切片元素:%v\n", &s3, &s3[0], len(s3), cap(s3), s3)
}
=========调试结果=========
s0 切片内存地址:0xc000008078 底层数组首地址:0xc00000e3c0 切片元素数量:3 切片容量:5 切片元素:[0 0 0]
----------------------------------
s1 切片内存地址:0xc0000080a8 底层数组首地址:0xc00000e3c0 切片元素数量:5 切片容量:5 切片元素:[0 0 0 1 2]
----------------------------------
s2 切片内存地址:0xc0000080d8 底层数组首地址:0xc00000e3c0 切片元素数量:4 切片容量:5 切片元素:[0 0 0 -1]
----------------------------------
s3 切片内存地址:0xc000008108 底层数组首地址:0xc000012230 切片元素数量:7 切片容量:10 切片元素:[0 0 0 -1 3 4 5]

上述代码中,通过向s2追加三个元素,得到新的切片s3。
具体的实现逻辑大概是这样:
s2底层数组容量为5,长度为4,append要新增3个,超了2个,触发扩容,于是向系统申请一块新的连续(顺序表)的内存空间,然后将s2底层数组中已有的数据复制过来,再把要追加的元素写入,最终得到一个新的底层数组,并且append还会返回一个全新的header给到s3,其中pointer指向新的底层数组、切片长度为7、切片容量为10(系统会自动冗余一些空间,后续讲扩容策略)。

1.5 切片的扩容机制

官方文档:​​https://go.dev/src/runtime/slice.go​​

(老版本)实际上,当扩容后的cap<1024时,扩容翻倍,容量变成之前的2倍;当cap>=1024时,变成之前的1.25倍(扩容前已存在元素的倍数)。
(新版本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) 。

header复制也会消耗资源,但是很少。
如:var s1 = s0,这种就是header结构体复制

思考一下:如果 s1 := make([]int, 3, 100) ,然后对s1进行append元素,会怎么样?
当追加的元素不超过切片容量时,只有切片长度会变,其他不变。
如果超过了容量,那么就会触发扩容。
在这里插入图片描述

1.6 引用类型

在Go语言中,引用类型(Reference Types)是指那些在赋值、作为函数参数传递或作为函数返回值时,传递的是指针(即内存地址)的类型,而不是值本身。
这意味着,当操作引用类型的变量时,实际上是在操作其指向的内存位置上的数据。
但严格意义上来说,复制的是header。
Go语言中的引用类型包括切片(slices)、映射(maps)、通道(channels)、接口(interfaces)、函数类型以及指向它们的指针。

1.6.1 思考以下代码切片之间是否发生了复制

package main

import "fmt"

func main() {
	var s0 = []int{1, 3, 5}
	fmt.Printf("s0 切片内存地址:%p 底层数组首地址:%p 切片元素数量:%d 切片容量:%v 切片元素:%v\n", &s0, &s0[0], len(s0), cap(s0), s0)

	s1 := s0
	fmt.Printf("s1 切片内存地址:%p 底层数组首地址:%p 切片元素数量:%d 切片容量:%v 切片元素:%v\n", &s1, &s1[0], len(s1), cap(s1), s1)
}
=========调试结果=========
s0 切片内存地址:0xc000008078 底层数组首地址:0xc000010168 切片元素数量:3 切片容量:3 切片元素:[1 3 5]
s1 切片内存地址:0xc0000080a8 底层数组首地址:0xc000010168 切片元素数量:3 切片容量:3 切片元素:[1 3 5]

通过返回结果可以得出,只是把切片赋值给另一个新切片,只有header地址会改变,header中的pointer、len、cap都不会变。

这说明什么?说明s0和s1之间,只复制了header结构体,但header中的pointer、len、cap都没变。

如果把s1切片的元素修改,s0切片会改变吗?

package main

import "fmt"

func main() {
	var s0 = []int{1, 3, 5}
	// fmt.Printf("s0 切片内存地址:%p 底层数组首地址:%p 切片元素数量:%d 切片容量:%v 切片元素:%v\n", &s0, &s0[0], len(s0), cap(s0), s0)

	s1 := s0
	// fmt.Printf("s1 切片内存地址:%p 底层数组首地址:%p 切片元素数量:%d 切片容量:%v 切片元素:%v\n", &s1, &s1[0], len(s1), cap(s1), s1)

	s1[0] = 100
	fmt.Println(s0, s1)
}
=========调试结果=========
[100 3 5] [100 3 5]

表面上看,操作s1就好像在操作s0,有点类似复制了切片的内存地址,通过地址操作两个切片一起变,但实际上还是因为两个切片共用同一个底层数组。

1.6.2 使用函数传参是否会发生复制

package main

import "fmt"

func showAddr(s2 []int) { // 新增函数
	fmt.Printf("s2 切片内存地址:%p 底层数组首地址:%p 切片元素数量:%d 切片容量:%v 切片元素:%v\n", &s2, &s2[0], len(s2), cap(s2), s2)
}

func main() {
	var s0 = []int{1, 3, 5}
	fmt.Printf("s0 切片内存地址:%p 底层数组首地址:%p 切片元素数量:%d 切片容量:%v 切片元素:%v\n", &s0, &s0[0], len(s0), cap(s0), s0)

	s1 := s0
	fmt.Printf("s1 切片内存地址:%p 底层数组首地址:%p 切片元素数量:%d 切片容量:%v 切片元素:%v\n", &s1, &s1[0], len(s1), cap(s1), s1)

	s1[0] = 100
	// fmt.Println(s0, s1)

	showAddr(s0) // 函数传参
}
=========调试结果=========
s0 切片内存地址:0xc000008078 底层数组首地址:0xc000010168 切片元素数量:3 切片容量:3 切片元素:[1 3 5]
s1 切片内存地址:0xc0000080a8 底层数组首地址:0xc000010168 切片元素数量:3 切片容量:3 切片元素:[1 3 5]
s2 切片内存地址:0xc0000080d8 底层数组首地址:0xc000010168 切片元素数量:3 切片容量:3 切片元素:[100 3 5]

通过结果得出,只有header结构体发生了复制,但header中存储的pointer、len、cap不变。

1.7 总结

Go语言中全都是值拷贝(复制),如整型、数组这样的类型的值是完全复制,slice、map、channel、interface、function这样的引用类型也是值拷贝,不过复制的是标头值。

1.8 小练习

1.8.1 切片去重

package main

import (
	"fmt"
	"math/rand"
	"time"
)

// 随机生成一个整数切片,统计元素重复次数,并返回一个没有重复元素的切片。
func a() {
	// (1)自定义整数切片
	a := make([]int, 0, 10)
	r := rand.New(rand.NewSource(time.Now().UnixNano()))
	for i := 0; i < 10; i++ {
		a = append(a, r.Intn(5))
	}
	fmt.Println("原始的切片:", a)

	// (2)定义一个map,方便统计元素的重复次数(map中不允许出现重复元素)
	m := make(map[int]int, 10)

	// (3)遍历map中的元素,并统计重复次数
	for _, v := range a {
		// map中,不指定value时,value的默认值为0
		// 索引元素首次出现,value为0,重复出现一次,value就+1
		m[v]++
	}
	// 打印map中的元素
	fmt.Println("包含重复元素次数的map:", m)

	// 定义一个空切片,等下把map中的key遍历到这个切片中
	// b := []int{}
	b := make([]int, 0, 10)
	for k, _ := range m {
		b = append(b, k)
	}
	fmt.Println("去重后的切片:", b)
}

func main() {
	a()
}
===========调试结果===========
原始的切片: [4 1 1 0 3 3 1 0 0 2]
包含重复元素次数的mapmap[0:3 1:3 2:1 3:2 4:1]
去重后的切片: [3 4 1 0 2]

1.8.2 切片反转

package main

import (
	"fmt"
	"math/rand"
	"time"
)

// 随机生成一个整数切片,并返回它的反转版本。
func a() {
	// a := make([]int, 5)
	a := make([]int, 0, 5)
	r := rand.New(rand.NewSource(time.Now().UnixNano()))

	for i := 0; i < cap(a); i++ {
		a = append(a, r.Intn(10))
	}
	fmt.Println(a)

	b := make([]int, 5)
	// 首次循环,i=0,j=4,后续每次循环完毕都执行i, j = i+1, j-1
	for i, j := 0, len(a)-1; i < cap(a); i, j = i+1, j-1 {
		// b = append(b, a[j])
		b[j] = a[i]
	}
	fmt.Println(b)
}

func main() {
	a()
}
===========调试结果===========
[4 1 8 7 9]
[9 7 8 1 4]

1.8.3 切片搜索

package main

import (
	"fmt"
	"math/rand"
	"time"
)

// 切片搜索练习
// 在切片中寻找指定目标值,返回它的索引,否则返回-1。
func a(nums int) {
	a := make([]int, 0, 5)
	r := rand.New(rand.NewSource(time.Now().UnixNano()))

	for i := 0; i < cap(a); i++ {
		a = append(a, r.Intn(10))
	}
	fmt.Println(a)

	var Exist int
	Index := make(map[string]int)
	for i, v := range a {
		if nums == v {
			fmt.Println(nums, "Index is ", i)
			Exist++
		} else {
			Index["Not exits"] = -1
		}
	}

	if Exist < 1 {
		for k, v := range Index {
			fmt.Println(nums, k, v)
		}
	}

}

func main() {
	a(5)
}
===========调试结果===========
[5 5 7 1 6]
5 Index is  0
5 Index is  1

2 . 子切片

2.1 介绍

切片可以通过指定索引区间获得一个子切片,格式为slice[start:end],规则就是前包后不包,对应元素的索引。

2.2 子切片特点

子切片(slice)是基于底层数组的一个视图或者窗口。
当从一个已有的切片中创建子切片时,实际上是在共享同一个底层数组,而不是创建一个新的、独立的数组。因此,子切片的创建本身不会导致底层数组的扩容。
但是,如果使用append追加,则是有可能触发扩容的。

2.3 子切片语法

slice[start:end]
start:不写默认为0。
end:不写话,默认为切片长度。
注意:指定start和end时,不能超过切片的容量。

2.4 子切片示例

2.4.1 示例一:完全复制header

package main

import "fmt"

func main() {
	// 声明并初始化一个长度和容量都为5的切片
	s1 := []int{10, 30, 50, 70, 90} // 索引范围[0,4] 0到4
	fmt.Printf("s1的内存地址:%p|s1的底层数组首地址:%p|s1的长度:%d|s1的容量:%d|s1的元素:%v\n", &s1, &s1[0], len(s1), cap(s1), s1)

	// 把s1切片赋值给s2
	s2 := s1 // 本质上就是在复制header
	fmt.Printf("s2的内存地址:%p|s2的底层数组首地址:%p|s2的长度:%d|s2的容量:%d|s2的元素:%v\n", &s2, &s2[0], len(s2), cap(s2), s2)

	// 开始子切片
	s3 := s1[:] //构建一个新的header,但不会新建数组
	fmt.Printf("s3的内存地址:%p|s3的底层数组首地址:%p|s3的长度:%d|s3的容量:%d|s3的元素:%v\n", &s3, &s3[0], len(s3), cap(s3), s3)
}
===========调试结果===========
s1的内存地址:0xc0000aa060|s1的底层数组首地址:0xc0000d8030|s1的长度:5|s1的容量:5|s1的元素:[10 30 50 70 90]
s2的内存地址:0xc0000aa090|s2的底层数组首地址:0xc0000d8030|s2的长度:5|s2的容量:5|s2的元素:[10 30 50 70 90]
s3的内存地址:0xc0000aa0c0|s3的底层数组首地址:0xc0000d8030|s3的长度:5|s3的容量:5|s3的元素:[10 30 50 70 90]

通过上面的代码,可以看到s3子切片后,结果和之前的相同,说明了什么?
子切片和原来的切片使用的底层数组也是同一个。

2.4.2 示例二:偏移切片

package main

import "fmt"

func main() {
	// 声明并初始化一个长度和容量都为5的切片
	s1 := []int{10, 30, 50, 70, 90} // 索引范围[0,4] 0到4
	fmt.Printf("s1的内存地址:%p|s1的底层数组首地址:%p|s1的长度:%d|s1的容量:%d|s1的元素:%v\n", &s1, &s1[0], len(s1), cap(s1), s1)
    
    // 首地址发生变化,切偏移一个元素,最终的长度和容量都-1
	s4 := s1[1:]
	fmt.Printf("s4的内存地址:%p|s4的底层数组首地址:%p|s4的长度:%d|s4的容量:%d|s4的元素:%v\n", &s4, &s4[0], len(s4), cap(s4), s4)

}
===========调试结果===========
s1的内存地址:0xc000008078|s1的底层数组首地址:0xc00000e3c0|s1的长度:5|s1的容量:5|s1的元素:[10 30 50 70 90]
s4的内存地址:0xc0000080a8|s4的底层数组首地址:0xc00000e3c8|s4的长度:4|s4的容量:4|s4的元素:[30 50 70 90]

看结果:
s1的底层数组首地址:0xc00000e3c0
s4的底层数组首地址:0xc00000e3c8
是不是以为底层数组变了?错,子切片过程中,只要没有append操作,底层数组依然还是同一个。
之所以一个首地址是3c0,一个是3c8,是因为int类型就占用8个字节。
并且s4 := s1[1:],意思是偏移了一个元素(把第一个元素挡住了,看不到了),所以此时的首地址就变成了第二个元素的内存地址。
并且由于偏移了一个元素,所以子切片的容量就为4,长度呢?长度没有指定,所以就从偏移处直到末尾,为4。

2.4.3 示例三:指定start和end

package main

import "fmt"

func main() {
	// 声明并初始化一个长度和容量都为5的切片
	s1 := []int{10, 30, 50, 70, 90} // 索引范围[0,4] 0到4
	fmt.Printf("s1的内存地址:%p|s1的底层数组首地址:%p|s1的长度:%d|s1的容量:%d|s1的元素:%v\n", &s1, &s1[0], len(s1), cap(s1), s1)

    // s1[1:4],展示元素索引1,2,3的元素。
	s5 := s1[1:4]
	fmt.Printf("s5的内存地址:%p|s5的底层数组首地址:%p|s5的长度:%d|s5的容量:%d|s5的元素:%v\n", &s5, &s5[0], len(s5), cap(s5), s5)

}
===========调试结果===========
s1的内存地址:0xc00009a060|s1的底层数组首地址:0xc0000c8030|s1的长度:5|s1的容量:5|s1的元素:[10 30 50 70 90]
s5的内存地址:0xc00009a090|s5的底层数组首地址:0xc0000c8038|s5的长度:3|s5的容量:4|s5的元素:[30 50 70]

s5此处的切片长度为:3
s5此处的切片容量为:4
那这个长度和容量是怎么计算出来的?
子切片长度计算方式:end减去start
子切片容量计算方式:从偏移量(start索引)开始到切片底层数组的最后一个元素。

2.4.4 示例四:start和end相同

package main

import "fmt"

func main() {
	// 声明并初始化一个长度和容量都为5的切片
	s1 := []int{10, 30, 50, 70, 90} // 索引范围[0,4] 0到4
	fmt.Printf("s1的内存地址:%p|s1的底层数组首地址:%p|s1的长度:%d|s1的容量:%d|s1的元素:%v\n", &s1, &s1[0], len(s1), cap(s1), s1)

    // 该子切片会复制一个新的header,偏移一个元素,子切片长度为0,容量为4
	s7 := s1[1:1] // 子切片元素超界了,这里是不能显示的
	fmt.Printf("s7的内存地址:%p|s7的底层数组首地址:%p|s7的长度:%d|s7的容量:%d|s7的元素:%v\n", &s7, &s7[0], len(s7), cap(s7), s7)

}

注意看s1[1:1],这里实际上已经超界了,长度为0,容量为4,如下图,并且执行的时候会报错。
在这里插入图片描述

然后基于现在的代码,对s7进行append操作,看看会发生什么。

package main

import "fmt"

func main() {
	// 声明并初始化一个长度和容量都为5的切片
	s1 := []int{10, 30, 50, 70, 90} // 索引范围[0,4] 0到4
	fmt.Printf("s1的长度:%d|s1的容量:%d|s1的元素:%v\n", len(s1), cap(s1), s1)

	s7 := s1[1:1]
    fmt.Printf("s7的长度:%d|s7的容量:%d|s7的元素:%v\n", len(s7), cap(s7), s7)
	s7 = append(s7, 300, 400)
	fmt.Printf("s1的长度:%d|s1的容量:%d|s1的元素:%v\n", len(s1), cap(s1), s1)
	fmt.Printf("s7的长度:%d|s7的容量:%d|s7的元素:%v\n", len(s7), cap(s7), s7)

}
===========调试结果===========
s1的长度:5|s1的容量:5|s1的元素:[10 30 50 70 90]
s7的长度:0|s7的容量:4|s7的元素:[]
s1的长度:5|s1的容量:5|s1的元素:[10 300 400 70 90]
s7的长度:2|s7的容量:4|s7的元素:[300 400]

可以看到,最开始s7长度为0(啥也看不到了),容量为4,append后长度变成了2,容量不变。
并且由于s7和s1共享同一个底层数组,所以对应s1切片中索引1和2的元素也被改变了。
为什么是索引1和2?
因为最开始s7 := s1[1:1],这里start是从1开始的,对应的就是s1切片元素中的索引1。
在这里插入图片描述

再来看一个特殊示例

package main

import "fmt"

func main() {
	// 声明并初始化一个长度和容量都为5的切片
	s1 := []int{10, 30, 50, 70, 90} // 索引范围[0,4] 0到4
	fmt.Printf("s1的长度:%d|s1的容量:%d|s1的元素:%v\n", len(s1), cap(s1), s1)

    s9 := s1[5:5] //长度为0,容量为0,类似[]int{}定义方式
	fmt.Printf("s9的长度:%d|s9的容量:%d|s9的元素:%v\n", len(s9), cap(s9), s9)
}
===========调试结果===========
s1的长度:5|s1的容量:5|s1的元素:[10 30 50 70 90]
s9的长度:0|s9的容量:0|s9的元素:[]

为什么还能写成s9 := s1[5:5]?按索引来算不是超界了吗?
注意:指定start和end时,除了能使用元素对应的索引,还能够使用的最大值是切片的容量,s1切片的容量是5。
在这里插入图片描述

2.4.5 子切片总结

可以看出,上面所有示例操作都是从同一个底层数组上取的段,所以子切片和原始切片共用同一个底层数组。

  • start默认为0,end默认为len(slice)即切片长度,明确定义时可以使用的最大值为切片的容量。
  • 通过指针(切片内存地址)确定底层数组从哪里开始共享。
  • 切片长度计算方法是end - start。
  • 切片容量计算方式是底层数组从偏移的元素(start)到结尾还有几个元素。

2.5 切片总结

  1. 使用slice[start:end]表示切片,切片长度为end-start,前包后不包。
  2. start缺省(不写),表示从索引0开始。
  3. end缺省(不写),表示直接取到末尾,包含最后一个元素,特别注意这个值是len(slice)即切片长度,不是容量,如a1[5:]相当于a1[5:len(a1)]
  4. start和end都缺省,表示从头到尾。
  5. start和end同时给出,要求end >= start。
  6. start、end最大都不可以超过容量值。
  7. 假设当前容量是8,长度为5,有以下情况:
    a1[:8],可以,end最多写成8(因为后不包),a1[:9]不可以。
    a1[8:],不可以,end缺省为5,等价于a1[8:5]。
    a1[8:8],可以,但这个切片容量和长度都为0了。
    a1[7:7],可以,但这个切片长度为0,容量为1。
    a1[0:0],可以,但这个切片长度为0,容量为8。
    a1[:8],可以,这个切片长度为8,容量为8,这8个元素都是原序列的。
    a1[1:5],可以,这个切片长度为4,容量为7,相当于跳过了原序列第一个元素。
  8. 切片刚产生时,和原序列(数组、切片)开始共用同一个底层数组,但是每一个切片都自己独立保存着指针、cap和len。
  9. 一旦一个切片扩容,就和原来共用一个底层数组的序列分道扬镳,从此陌路。

3. 对数组进行切片

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

package main

import "fmt"

func main() {
	// 在[]中加个5,就变成了长度和容量都为5的数组
	s1 := [5]int{10, 30, 50, 70, 90}
	fmt.Printf("s1的内存地址:%p|s1的底层数组地址:%p|s1的长度:%d|s1的容量:%d|s1的元素:%v\n", &s1, &s1[0], len(s1), cap(s1), s1)

	// 数组拷贝,多一个副本出来,元素完全相同
	s2 := s1
	fmt.Printf("s2的内存地址:%p|s2的底层数组地址:%p|s2的长度:%d|s2的容量:%d|s2的元素:%v\n", &s2, &s2[0], len(s2), cap(s2), s2)

	s3 := s1[:]//这个切片操作,会产生一个新的底层数组吗?
	fmt.Printf("s3的内存地址:%p|s3的底层数组地址:%p|s3的长度:%d|s3的容量:%d|s3的元素:%v\n", &s3, &s3[0], len(s3), cap(s3), s3)
}
===========调试结果===========
s1的内存地址:0xc0000d8030|s1的底层数组地址:0xc0000d8030|s1的长度:5|s1的容量:5|s1的元素:[10 30 50 70 90]
s2的内存地址:0xc0000d80c0|s2的底层数组地址:0xc0000d80c0|s2的长度:5|s2的容量:5|s2的元素:[10 30 50 70 90]
s3的内存地址:0xc0000aa060|s3的底层数组地址:0xc0000d8030|s3的长度:5|s3的容量:5|s3的元素:[10 30 50 70 90]

可与看到,对数组进行切片后,切片的底层数组其实就是s1数组,说明对数组切片,不会诞生一个新的底层数组。

  • 12
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值