六、Go语言快速入门之数组和切片

文章目录

    • 数组和切片
      • 数组
        • [:one: 数组初始化](#:one: 数组初始化)
        • [:two: 数组的遍历](#:two: 数组的遍历)
        • [:three: 多维数组](#:three: 多维数组)
        • [:four: 将数组传递给函数](#:four: 将数组传递给函数)
      • 切片(Slice)
        • [:one: 切片的初始化](#:one: 切片的初始化)
          • [:star: `new`和`make`区别](#:star: newmake区别)
        • [:two: 切片的使用](#:two: 切片的使用)
        • [:three: 将切片传递给函数](#:three: 将切片传递给函数)
        • [:four: 多维切片](#:four: 多维切片)
        • [:four: Bytes包](#:four: Bytes包)
        • [:four: 切片和垃圾回收](#:four: 切片和垃圾回收)

📅 2024年4月28日

📦 使用版本为1.21.5

数组和切片

⭐️ 在go语言中数组和切片看起来几乎一模一样,区别在于数组是不可变扩容的,切片是可变可伸缩(在Java中想数组和列表(ArrayList))


数组

⭐️ 在go中数组只能是基本类型,不能是引用类型(在Java中两者都可以)

⭐️ 由于数组是固定大小的,如果你知道了你要存放数据的长度,且以后不会有扩容了,就可以考虑使用数组

⭐️ Go语言的数组是一种值类型,不是指针类型

1️⃣ 数组初始化

⭐️ 由于数组在声明后大小就不能变,声明大小时可以使用常量来替换

go 复制代码
func main() {
	var a = 1;
	const b = 1;
	
	var aa [a]int; //报错a是变量
	var bb [b]int;
}

⭐️ 当然和其他语言一样它还可以声明并赋值,下标也是从0开始的,可以通过下标来范围和其他语言类似

go 复制代码
func main() {
	const b = 3
	var bb = [b]int{1,2,3}
	println(bb[0])
}

⭐️ 可以通过len来访问数组中元素的数量,还可以通过cap来查看数组的容量

⭐️ 如果想要创建一个指针类型的数组就可以用到new

  1. 数值类型数组赋值,改变另外一个数组不会影响赋值的数组
go 复制代码
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. 指针类型相反
go 复制代码
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]

⭐️ 还可以通过切割来访问数组的某一段数值,和其他语言差不多

go 复制代码
arr[1:3]
2️⃣ 数组的遍历

⭐️ 可以使用forfor-range来遍历数组

go 复制代码
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️⃣ 多维数组

⭐️ 想像成阵列就好了

go 复制代码
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语言那样使用数组的指
  • 也可以使用切片(学到切片再说)
go 复制代码
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

go 复制代码
func main() {
	arr1 := [10]int{1,2,3,4,5,6,7,8,9,10}
	var slice1 []int = arr1[0:5] //通过数组的切片创建
	fmt.Println(slice1) 
}

⭐️ 多个切片如果表示同一个数组的片段,它们可以共享数据;因此一个切片和相关数组的其他切片是共享存

储的,相反,不同的数组总是代表不同的存储。数组实际上是切片的构建块

go 复制代码
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 的原因

go 复制代码
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。

go 复制代码
func main() {
	var a = make([]int, 1, 10)
	println(a[0]) //输出0
}

⭐️ 切片的长度代表着切片中元素的个数,切片的容量代表着切片总共能装多少个元素,切片与数组最大的区别在于切片的容量会自动扩张,而数组不会

⭐️ 和数组一样也可以使用new来创建数组

go 复制代码
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

2️⃣ 切片的使用

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

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

  1. 在尾部添加元素
go 复制代码
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),用于将切片或数组展开为多个单独的参数。

go 复制代码
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. 从中间插入
go 复制代码
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. 删除元素
go 复制代码
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, ai+1:...)
  4. 切除切片 a 中从索引 i 至 j 位置的元素: a = append(a:i, aj:...)
  5. 为切片 a 扩展 j 个元素长度: a = append(a, make(\[\]T, j)...)
  6. 在索引 i 的位置插入元素 x: a = append(a:i, append(\[\]T{x}, ai:...)...)
  7. 在索引 i 的位置插入长度为 j 的新切片: a = append(a:i, append(make(\[\]T, j), ai:...)...)
  8. 在索引 i 的位置插入切片 b 的所有元素: a = append(a:i, append(b, ai:...)...)
  9. 取出位于切片 a 最末尾的元素 x: x, a = alen(a)-1, a:len(a)-1
  10. 将元素 x 追加到切片 a: a = append(a, x)
3️⃣ 将切片传递给函数

⭐️ 前面说过传递数组你可以使用切片来提高程序运行效率

⭐️ 如果你有一个函数需要对数组做操作,你可能总是需要把参数声明为切片。当你调用该函数时,把数组分

片,创建为一个切片引用并传递给该函数

var slice1 \[\]type = arr1: 那么 slice1 就等于完整的 arr1 数组(所以这种表示方式是

arr10:len(arr1) 的一种缩写)。另外一种表述方式是: slice1 = &arr1 。

arr12: 和 arr12:len(arr1) 相同,都包含了数组从第二个到最后的所有元素。

arr1:3 和 arr10:3 相同,包含了从第一个到第三个元素(不包括第三个)

go 复制代码
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 函数)

go 复制代码
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的功能

go 复制代码
//源代码
// 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好用,它不能使用那种流方法,只能另起一行或者是用循环输入

go 复制代码
func main() {
	var buffer bytes.Buffer
	buffer.WriteString("hello") //追加String
	buffer.WriteString("world") //追加String
	str := buffer.String() //获取String
	fmt.Println(str) //helloworld

}
go 复制代码
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️⃣ 切片和垃圾回收

⭐️ 切片的底层指向一个数组,该数组的实际体积可能要大于切片所定义的体积。只有在没有任何切片指向的

时候,底层的数组内层才会被释放,这种特性有时会导致程序占用多余的内存

相关推荐
长栎12 分钟前
写 for 循环写了十年,你却从没用过迭代器模式最狠的那一面
后端
LiaCode16 分钟前
Redis 在生产项目的使用
前端·后端
用户5598224812220 分钟前
Docker Compose Down 导致容器数据误删——ext4 日志恢复全记录
后端
LiaCode21 分钟前
一天学完 redis 的爽翻版核心知识总结
前端·后端
大刚测试开发实战22 分钟前
如何内网穿透访问本地私有化部署的TestHub
前端·后端·github
xiaodaoluanzha41 分钟前
迄今為止,最簡單的編程語言 Nolang
前端·后端
Csvn41 分钟前
Docker 容器管理入门 — 从镜像到容器编排
后端
用户762352425911 小时前
ShardingJDBC
后端
行者全栈架构师1 小时前
IDEA 中 Maven 项目的 15 个红色报错快速解决方法
java·后端
秋播1 小时前
国内本地WSL2编译rancher源码
云原生