📅 2024年4月28日
📦 使用版本为 1.21.5
数组和切片
⭐️ 在 go
语言中数组和切片看起来几乎一模一样,区别在于数组是不可变扩容的,切片是可变可伸缩(在 Java
中想数组和列表(ArrayList
))
数组
⭐️ 在 go
中数组只能是基本类型,不能是引用类型(在 Java
中两者都可以)
⭐️ 由于数组是固定大小的,如果你知道了你要存放数据的长度,且以后不会有扩容了,就可以考虑使用数组
⭐️ Go
语言的数组是一种值类型,不是指针类型
1️⃣ 数组初始化
⭐️ 由于数组在声明后大小就不能变,声明大小时可以使用常量来替换
func main() {
var a = 1;
const b = 1;
var aa [a]int; //报错a是变量
var bb [b]int;
}
⭐️ 当然和其他语言一样它还可以声明并赋值,下标也是从0开始的,可以通过下标来范围和其他语言类似
func main() {
const b = 3
var bb = [b]int{1,2,3}
println(bb[0])
}
⭐️ 可以通过 len
来访问数组中元素的数量,还可以通过 cap
来查看数组的容量
⭐️ 如果想要创建一个指针类型的数组就可以用到 new
- 数值类型数组赋值,改变另外一个数组不会影响赋值的数组
func main() {
a := [5]int{1, 2, 3, 4, 5}
b := a
b[2] = 100
fmt.Println(a)
fmt.Println(b)
}
//输出
[1 2 3 4 5]
[1 2 100 4 5]
- 指针类型相反
func main() {
a := new([5]int)
b := a
b[2] = 100
fmt.Println(a)
fmt.Println(b)
}
//输出
&[0 0 100 0 0]
&[0 0 100 0 0]
⭐️ 还可以通过切割来访问数组的某一段数值,和其他语言差不多
arr[1:3]
2️⃣ 数组的遍历
⭐️ 可以使用 for
和 for-range
来遍历数组
func main() {
a := []int{1, 2, 3, 4, 5}
for i := 0; i <= len(a); i++ {
fmt.Printf("%d ", i)
}
println()
for i, v := range a {
fmt.Printf("%d %d\n", i, v)
}
}
3️⃣ 多维数组
⭐️ 想像成阵列就好了
func main() {
var a [10][4]int
for i := 0; i < len(a); i++ {
for j := 0; j < len(a[i]); j++ {
a[i][j] = i + j
}
}
}
⭐️ 还可以使用 for-range
func main() {
var a [10][4]int
for row := range a {
for column := range a[0] {
a[row][column] = 1;
}
}
}
4️⃣ 将数组传递给函数
第一个大数组传递给函数会消耗很多内存,使用下面的方法可以避免:
- 可以像C语言那样使用数组的指
- 也可以使用切片(学到切片再说)
package main
import "fmt"
func main() {
var a [10][4]int
funcName(&a)
}
func funcName(a *[10][4]int) {
for i := 0; i < len(a); i++ {
for j := 0; j < len(a[i]); j++ {
fmt.Printf("a[%d][%d] ", i, j)
}
}
}
切片(Slice)
⭐️ 切片在Go中使用的更加广泛,由于它可变成的特性,可以使用它来频繁的插入和删除元素
⭐️ 一个切片是对底层数组的一个连续片段的引用,因此它被视为引用类型。这种结构设计使得多个切片能够共享相同的底层数组内存,即使这些切片表达的是数组的不同部分。当通过一个切片对底层数组进行修改时,所有指向同一底层数组的其他切片都会看到这些更改。这是因为切片并没有复制底层数组的元素,而只是提供了一种访问和操作这些元素的方式,有点像 java
数组和 ArrayList
的关系,
⭐️ 一个切片由三个部分组成,指针、长度、容量,指针指向第一个切片元素对于的底层数组元素地址,但是并不一定就是数组的一个元素
⭐️ 优点 因为切片是引用,所以它们不需要使用额外的内存并且比使用数组更有效率,所以在 Go 代码中 切片
比数组更常用
1️⃣ 切片的初始化
⭐️ 切片的初始化和数组类似,但是切片可以使用 make
关键字来创建,也推荐使用 make
来创建切片,make
函数接收三个参数:类型,长度,容量;,容量一定是会大于或者等于长度的,就像一个桶子里面水的高度不会大于桶子的高度
⭐️ 可以直接通过一个数组来创建切片 Slice
arr1[0:5]
表示此数组从0到5(不包含5)被切成了一个切片5就是此切片的 len
如果是 arr1[0:5:5]
前两个数和之前提到的一样,但是最后这个(5-0)就表示 cap
func main() {
arr1 := [10]int{1,2,3,4,5,6,7,8,9,10}
var slice1 []int = arr1[0:5] //通过数组的切片创建
fmt.Println(slice1)
}
⭐️ 多个切片如果表示同一个数组的片段,它们可以共享数据;因此一个切片和相关数组的其他切片是共享存
储的,相反,不同的数组总是代表不同的存储。数组实际上是切片的构建块
func main() {
arr1 := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
var slice1 []int = arr1[0:5] //通过数组的切片创建
var slice2 []int = arr1[0:5]
slice2[0] = 100 //修改切片2的0下标元素
fmt.Println(slice1[0]) //发现切片1的也修改了
}
//输出:
100
此时如果你想要扩容,就可以使用切片的切片操作,来扩容
然后,当执行
slice1 = slice1[0:6]
时,Go 语言会检查新切片所需的容量是否超过了现有切片的容量。在这个例子中,新切片需要的容量是6,而现有切片的容量是5。由于6大于5,Go 语言会分配一个新的底层数组来容纳这6个元素,并将新切片的指针指向这个新数组。然后,它会复制原切片中的元素到新数组中,并更新新切片的长度和容量。因此,尽管最初的切片
slice1
的容量只有5,但是在执行slice1 = slice1[0:6]
之后,slice1
被重新分配了一个更大的底层数组,其容量至少为6,以容纳新的切片范围。这就是为什么slice1
可以扩展到0:6
的原因
func main() {
arr1 := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
var slice1 []int = arr1[0:5] //通过数组的切片创建
fmt.Println(slice1) //输出:[1 2 3 4 5]
slice1 = slice1[0:6] //切片的切片操作扩容
fmt.Println(slice1) //输出:[1 2 3 4 5 6]
}
⭐️ 通过 var nums []int
这种方式声明的切片,默认值为 nil
,所以不会为其分配内存,而在使用 make
进行初始化时,建议预分配一个足够的容量,可以有效减少后续扩容的内存消耗
//源代码
// make内置函数分配并初始化一个类型的对象
// slice、map或chan(仅限)和new一样,第一个参数是类型,而不是
/ /值。与new不同,make的返回值类型与its相同
//参数,而不是指针。结果的具体情况取决于
//类型:
//
// slice: size指定了长度。切片的容量为
//等于它的长度可以向提供第二个整数参数
//指定不同的容量;它必须不小于
/ /长度。例如,make([]int, 0, 10)会分配一个底层数组
//返回长度为0、容量为10的切片
//由底层数组支持。
// map:一个空的map被分配了足够的空间来容纳
//指定元素的数量在这种情况下,大小可以省略
//分配一个较小的起始长度。
// channel:使用指定的参数初始化channel的缓冲区
//缓存容量。如果为零,或大小省略,则通道为
/ /无缓冲的。
func make(t Type, size ...IntegerType) Type
⭐️ 你使用 make
函数创建一个切片时,如果指定了长度(第二个参数),那么切片将会被初始化为该长度,并且每个元素都会被初始化为该类型对应的零值。对于整数类型(如 int
),零值就是0。
func main() {
var a = make([]int, 1, 10)
println(a[0]) //输出0
}
⭐️ 切片的长度代表着切片中元素的个数,切片的容量代表着切片总共能装多少个元素,切片与数组最大的区别在于切片的容量会自动扩张,而数组不会
⭐️ 和数组一样也可以使用 new
来创建数组
func main() {
var a = make([]int, 10)
var b = new([10]int)[0:10]
fmt.Printf("%T", b)
fmt.Printf("%T", a)
}
⭐️ new
和 make
区别
-
new
(T) 为每个新的类型T
分配一片内存,初始化为 0 并且返回类型为*T
的内存地址:这种方法 返回一个指向类型为 T,值为 0 的地址的指针,它适用于值类型如数组和结构体;它相当于 &T{} 。 -
make
(T) 返回一个类型为T
的初始值,它只适用于3种内建的引用类型:切片
、map
和channel
2️⃣ 切片的使用
⭐️ 切片的基本使用与数组完全一致,区别只是切片可以动态变化长度
⭐️ 通过 append
来对切片实行操作
- 在尾部添加元素
func main() {
var a = make([]int, 0, 0) //创建一个空切片
a = append(a, 1, 2, 3, 4, 5, 6, 7) //添加元素
fmt.Println(len(a), cap(a)) //输出内部元素个数和切片的容量
}
//输出7,8
新
slice
预留的buffer
容量 大小是有一定规律的。 在golang1.18
版本更新之前网上大多数的文章都是这样描述slice
的扩容策略的: 当原slice
容量小于1024
的时候,新slice
容量变成原来的2
倍;原slice
容量超过1024
,新slice
容量变成原来的1.25
倍。 在1.18
版本更新之后,slice
的扩容策略变为了: 当原slice
容量(oldcap
)小于256
的时候,新slice
(newcap
)容量为原来的2倍;原slice
容量超过256,新slice
容量newcap
=oldcap
+(oldcap
+3*256)/4
- 在头部插入元素
⭐️ ...
操作符被称为扩展操作符(spread
operator
),用于将切片或数组展开为多个单独的参数。
func main() {
var a = make([]int, 0, 0) //创建一个空切片
a = append(a, 1, 2, 3, 4, 5, 6, 7) //添加元素
a = append([]int{-1, 'a'}, a...) //这个a会转换成unicode代码中的值97
fmt.Println(a) //输出数组 [-1 97 1 2 3 4 5 6 7]
fmt.Println(len(a), cap(a)) //输出内部元素个数和切片的容量
}
- 从中间插入
1.a[:i+1]: 这部分表示从切片a的开始到索引i+1处的元素,即保留原始切片的前i+1个元素。
2.[]int{999,888}: 这是一个包含两个整数999和888的切片,这是要插入的元素。
3.a[i+1:]...: 这部分表示从切片a的索引i+1处开始到末尾的所有元素,采用...的方式表示这些元素将被展开并追加到前面的切片组合之后。
4.append(...):将前面提到的三个切片部分组合起来,并执行append操作,将所有元素添加到一起,最终生成一个新的切片。
5.a = ...: 将新生成的切片赋值回原始切片a,完成插入操作并修改原始切片。
通过这种方式,切片a在索引i处被扩展,其中插入了两个新的整数值999和888。
func main() {
i := 3
var a = make([]int, 0, 0) //创建一个空切片
a = append(a, 1, 2, 3, 4, 5, 6, 7) //添加元素
a = append(a[:i+1], append([]int{999, 888}, a[i+1:]...)...) //使用i当索引,在索引中间开辟两个空间来插入来插入元素
fmt.Println(a) //输出数组
fmt.Println(len(a), cap(a)) //输出内部元素个数和切片的容量
}
- 删除元素
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
//从头部删除n个元素
nums = nums[n:]
fmt.Println(nums) //n=3 [4 5 6 7 8 9 10]
//从尾部删除n给元素
nums = nums[:len(nums)-n]
fmt.Println(nums) //n=3 [1 2 3 4 5 6 7]
//从中间指定下标删除
nums = append(nums[:i], nums[i+n:]...)
fmt.Println(nums)// i=2,n=3,[1 2 6 7 8 9 10]
- 将切片 b 的元素追加到切片 a 之后: a = append(a, b...)
- 复制切片 a 的元素到新的切片 b 上:
b = make([]T, len(a))
copy(b, a)- 删除位于索引 i 的元素: a = append(a[:i], a[i+1:]...)
- 切除切片 a 中从索引 i 至 j 位置的元素: a = append(a[:i], a[j:]...)
- 为切片 a 扩展 j 个元素长度: a = append(a, make([]T, j)...)
- 在索引 i 的位置插入元素 x: a = append(a[:i], append([]T{x}, a[i:]...)...)
- 在索引 i 的位置插入长度为 j 的新切片: a = append(a[:i], append(make([]T, j), a[i:]...)...)
- 在索引 i 的位置插入切片 b 的所有元素: a = append(a[:i], append(b, a[i:]...)...)
- 取出位于切片 a 最末尾的元素 x: x, a = a[len(a)-1], a[:len(a)-1]
- 将元素 x 追加到切片 a: a = append(a, x)
3️⃣ 将切片传递给函数
⭐️ 前面说过传递数组你可以使用切片来提高程序运行效率
⭐️ 如果你有一个函数需要对数组做操作,你可能总是需要把参数声明为切片。当你调用该函数时,把数组分
片,创建为一个切片引用并传递给该函数
var slice1 []type = arr1[:] 那么 slice1 就等于完整的 arr1 数组(所以这种表示方式是
arr1[0:len(arr1)] 的一种缩写)。另外一种表述方式是: slice1 = &arr1 。
arr1[2:] 和 arr1[2:len(arr1)] 相同,都包含了数组从第二个到最后的所有元素。
arr1[:3] 和 arr1[0:3] 相同,包含了从第一个到第三个元素(不包括第三个)
func main() {
var arr = [6]int{1, 2, 3, 4, 5, 6}
sum := sum(arr[:]) //数组的切片
fmt.Println(sum)
}
func sum(a []int) int {
sum := 0
for _, i := range a {
sum += i
}
return sum
}
4️⃣ 多维切片
⭐️和数组一样,切片通常也是一维的,但是也可以由一维组合成高维。通过分片的分片(或者切片的数
组),长度可以任意动态变化,所以 Go 语言的多维切片可以任意切分。而且,内层的切片必须单独分配
(通过 make 函数)
func main() {
slice := [][]int{{1, 2, 3}, {4, 5, 6}}
// slice2 := make([][]int, 2,10) 通过make
fmt.Println(slice)
}
4️⃣ Bytes包
⭐️ []bytes
这种切片类型很常见,包括使用的 string
类型,它底层就是使用的它
⭐️ 他提供一个非常有用的 Buffer
,它可以实现类似 Java
的 Stringbuilder
的功能
//源代码
// A Buffer is a variable-sized buffer of bytes with Read and Write methods.
// The zero value for Buffer is an empty buffer ready to use.
type Buffer struct {
buf []byte // contents are the bytes buf[off : len(buf)]
off int // read at &buf[off], write at &buf[len(buf)]
lastRead readOp // last read operation, so that Unread* can work correctly.
}
⭐️ 使用,感觉它没有 Stringbuilder
好用,它不能使用那种流方法,只能另起一行或者是用循环输入
func main() {
var buffer bytes.Buffer
buffer.WriteString("hello") //追加String
buffer.WriteString("world") //追加String
str := buffer.String() //获取String
fmt.Println(str) //helloworld
}
func main() {
var buffer bytes.Buffer
for {
var str string
fmt.Scanln(&str)
buffer.WriteString(str) //追加String
if str == "q" {
break
}
}
str := buffer.String() //获取String
fmt.Println(str) //helloworld
}
4️⃣ 切片和垃圾回收
⭐️ 切片的底层指向一个数组,该数组的实际体积可能要大于切片所定义的体积。只有在没有任何切片指向的
时候,底层的数组内层才会被释放,这种特性有时会导致程序占用多余的内存