📅 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

  1. 数值类型数组赋值,改变另外一个数组不会影响赋值的数组
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]

  1. 指针类型相反
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️⃣ 数组的遍历

⭐️ 可以使用 forfor-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函数接收三个参数:类型,长度,容量;,容量一定是会大于或者等于长度的,就像一个桶子里面水的高度不会大于桶子的高度

Screenshot_2024_0429_230208

⭐️ 可以直接通过一个数组来创建切片 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)
}
⭐️ newmake区别
  • new(T) 为每个新的类型 T分配一片内存,初始化为 0 并且返回类型为 *T的内存地址:这种方法 返回一个指向类型为 T,值为 0 的地址的指针,它适用于值类型如数组和结构体;它相当于 &T{} 。

  • make(T) 返回一个类型为 T 的初始值,它只适用于3种内建的引用类型:切片mapchannel

    Screenshot_2024_0429_224147

2️⃣ 切片的使用

⭐️ 切片的基本使用与数组完全一致,区别只是切片可以动态变化长度

⭐️ 通过 append来对切片实行操作

  1. 在尾部添加元素
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

  1. 在头部插入元素

⭐️ ...操作符被称为扩展操作符(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. 从中间插入
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)) //输出内部元素个数和切片的容量
}
  1. 删除元素
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]


  1. 将切片 b 的元素追加到切片 a 之后: a = append(a, b...)
  2. 复制切片 a 的元素到新的切片 b 上:
    b = make([]T, len(a))
    copy(b, a)
  3. 删除位于索引 i 的元素: a = append(a[:i], a[i+1:]...)
  4. 切除切片 a 中从索引 i 至 j 位置的元素: a = append(a[:i], a[j:]...)
  5. 为切片 a 扩展 j 个元素长度: a = append(a, make([]T, j)...)
  6. 在索引 i 的位置插入元素 x: a = append(a[:i], append([]T{x}, a[i:]...)...)
  7. 在索引 i 的位置插入长度为 j 的新切片: a = append(a[:i], append(make([]T, j), a[i:]...)...)
  8. 在索引 i 的位置插入切片 b 的所有元素: a = append(a[:i], append(b, a[i:]...)...)
  9. 取出位于切片 a 最末尾的元素 x: x, a = a[len(a)-1], a[:len(a)-1]
  10. 将元素 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,它可以实现类似 JavaStringbuilder的功能

//源代码
// 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️⃣ 切片和垃圾回收

⭐️ 切片的底层指向一个数组,该数组的实际体积可能要大于切片所定义的体积。只有在没有任何切片指向的
时候,底层的数组内层才会被释放,这种特性有时会导致程序占用多余的内存