Go 数组、切片

本文详细介绍了Go语言中数组和切片的使用。包括数组的定义、操作、多维数组、作为函数参数以及指针数组与数组指针等内容。同时阐述了切片的定义、扩充与拼接,还分析了切片与数组的关系,提醒注意小切片引用大数组导致的内存浪费问题。

5.数组、切片

5.1 数组

5.1.1 如何定义数组

数组是具有相同类型且长度固定的一组连续数据。在go语言中我们可以使用如下几种方式来定义数组。

//方式一
var arr1 = [5]int{}
//方式二
var arr2 = [5]int{1,2,3,4,5}
//方式三
var arr3 = [5]int{3:10}

输出以上三个变量的值如下所示:

arr1 [0 0 0 0 0]
arr2 [1 2 3 4 5]
arr3 [0 0 0 10 0]
  • 方法一在声明时没有为其指定初值,所以数组内的值被初始化为类型的零值。
  • 方法二使用显示的方式为数组定义初值。
  • 方法三通过下标的方式为下标为3的位置赋上了初值10

而且在数组的定义是包含其长度的,也就是说[3]int与[4]int是两种不同的数据类型。

5.1.2 如何操作数据

在上面的arr1我们没有为其指定初值,那么之后我们可以通过循环的方式为其赋予初值

for i := 0; i < len(arr1); i++ {
		arr1[i] = i * 10
	}

此时我们再输出一下数组arr1,可以看到其值已经变化

arr1 update:  [0 10 20 30 40]

我们还可以通过循环的方式遍历数组,与上面不同的是,for循环提供这样的方式来为便利数组

for index, value := range arr1 {
		fmt.Printf("index: %d, value: %d\n", index, value)
	}

通过range来遍历数组会有两个返回值,其中第一个为数组的索引,第二个位置为对应的值,输出结果如下:

index: 0, value: 0
index: 1, value: 10
index: 2, value: 20
index: 3, value: 30
index: 4, value: 40

当然不想要索引值也是可以的,可以用_代替。

5.1.3 多维数组

与其他语言一样,go语言也可以定义多维数组,可以选择的定义方式如下:

var arr4 = [5][5]int{
		{1, 2, 3, 4, 5},
		{6, 7, 8, 9, 10},
	}

输出当前数组,可以看到数组内的值如下所示

[[1 2 3 4 5] [6 7 8 9 10] [0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0]]
5.1.4 数组作为函数参数

go语言在传递数组时会对其进行拷贝,所以如果传递的是大数组的话会非常占内存,所以一般情况下很少直接传递一个数组,避免这种情况我们可以使用以下两种方式:

  • 传递数组的指针
  • 传递切片(具体内容见下一小节)
5.1.5 指针数组与数组指针

对于指针数组和数组指针在c或c++中也经常被讨论,尤其对于初学者来说会分辨不清楚。其实在每个词中间添加一个“的“就很好理解了,指针的数组,数组的指针。

1.指针数组

对于指针数组来说,就是:一个数组里面装的都是指针,在go语言中数组默认是值传递的,所以如果我们在函数中修改传递过来的数组对原来的数组是没有影响的。

func main() {
	var a [5]int
	fmt.Println(a)
	test(a)
	fmt.Println(a)
}


func test(a [5]int) {
	a[1] = 2
	fmt.Println(a)
}

输出

[0 0 0 0 0]
[0 2 0 0 0]
[0 0 0 0 0]

但是如果我们一个函数传递的是指针数组,情况会有什么不一样呢?

func main() {
	var a [5]*int
	fmt.Println(a)
	for i := 0; i < 5; i++ {
		temp := i
		a[i] = &temp
	}
	for i := 0; i < 5; i++ {
		fmt.Print(" ", *a[i])
	}
	fmt.Println()
	test1(a)
	for i := 0; i < 5; i++ {
		fmt.Print(" ", *a[i])
	}
}


func test1(a [5]*int) {
	*a[1] = 2
	for i := 0; i < 5; i++ {
		fmt.Print(" ", *a[i])
	}
	fmt.Println()
}

我们先来看一下程序的运行结果

[<nil> <nil> <nil> <nil> <nil>]
 0 1 2 3 4
 0 2 2 3 4
 0 2 2 3 4

可以看到初始化值全是nil,也就验证了指针数组内部全都是一个一个指针,之后我们将其初始化,内部的每个指针指向一块内存空间。

注:在初始化的时候如果直接另a[i] = &i那么指针数组内部存储的全是同一个地址,所以输出结果也一定是相同的

然后我们将这个指针数组传递给test1函数,对于数组的参数传递仍然是复制的形式也就是值传递,但是因为数组中每个元素是一个指针,所以test1函数复制的新数组中的值仍然是这些指针指向的具体地址值,这时改变a[1]这块存储空间地址指向的值,那么原实参指向的值也会变为2,具体流程如下图所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dIkwKjn6-1608479312713)(./img/array1.png)]

2.数组指针

了解了指针数组之后,再来看一下数组指针,数组指针的全称应该叫做,指向数组的指针,在go语言中我们可以如下操作。

func main() {
	var a [5]int
	var aPtr *[5]int
	aPtr = &a
	//这样简短定义也可以aPtr := &a
	fmt.Println(aPtr)
	test(aPtr)
	fmt.Println(aPtr)
}


func test(aPtr *[5]int) {
	aPtr[1] = 5
	fmt.Println(aPtr)
}

我们先定义了一个数组a,然后定一个指向数组a的指针aPtr,然后将这个指针传入一个函数,在函数中我们改变了具体的值,程序的输出结果

&[0 0 0 0 0]
&[0 5 0 0 0]
&[0 5 0 0 0]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cjGNbU69-1608479312716)(./img/array2.png)]

通过上面的图我们可以看见虽然main和test函数中的aPtr是不同的指针,但是他们都指向同一块数组的内存空间,所以无论在main函数还是在test函数中对数组的操作都会直接改变数组。

5.2.切片

因为数组是固定长度的,所以在一些场合下就显得不够灵活,所以go语言提供了一种更为便捷的数据类型叫做切片。切片操作与数组类似,但是它的长度是不固定的,可以追加元素,如果以达到当前切片容量的上限会再自动扩容。

5.2.1 如何定义切片

可以通过以下几种方式定义切片

//方法一
var s1 = []int{}
//方法二
var s2 = []int{1, 2, 3}
//方法三
var s3 = make([]int, 5)
//方法四
var s4 = make([]int, 5, 10)

方法一声明了一个空切片,方法二声明了一个长度为3的切片,方法三声明了一个长度为5的空切片,方法四声明了一个长度为5容量为10的切片。可以看到切片的定义与数组类似,但是定义切片不需要为其指定长度。

我们可以通过len()cap()这两个函数来获取切片的长度和容量,下面就来依次看下上面各切片的长度以及容量。

s1 [] 0 0
s2 [1 2 3] 3 3
s3 [0 0 0 0 0] 5 5
s4 [0 0 0 0 0] 5 10

如果我们通过这种方式定义一个切片,那么他会被赋予切片的空值nil。

var s5 []int

切片操作

我们可以通过如下的方式在数组和切片上继续获取切片

func main() {
	arr := [5]int{1, 2, 3, 4, 5}
	s := []int{6, 7, 8, 9, 10}

	s1 := arr[2:4]
	s2 := arr[:3]
	s3 := arr[2:]
	s4 := s[1:3]

	fmt.Println("s1:", s1)
	fmt.Println("s2:", s2)
	fmt.Println("s3:", s3)
	fmt.Println("s4:", s4)
}

程序的输出结果如下

s1: [3 4]
s2: [1 2 3]
s3: [3 4 5]
s4: [7 8]

可以看到切片操作是“包左不包右”,例如arr[2:4]是选择arr数组内下标为2,3的两个元素。如果:左边没有起始位置则默认从头开始,同理如果右边没有终止位置则默认到末尾。

5.2.2 切片的扩充与拼接

我们之前介绍了切片的定义以及一些切片的操作,下面就来看一下如何对切片进行扩充。

func main() {
	a := []int{1, 2, 3}
	b := a[1:3]

	b = append(b, 4)
	b = append(b, 5)
	b = append(b, 6)
	b = append(b, 7)
	fmt.Println(a)
	fmt.Println(b)
}

程序输出结果如下

[1 2 3]
[2 3 4 5 6 7]

如果想要将两个切片进行拼接可以使用如下这种方式:

func main() {
	a := []int{1, 2, 3}
	b := a[1:3]

	fmt.Println(b)

	a = append(a, b...)
	fmt.Println(a)
}

如果我们想要将一个切片的值复制给另一个切片,go语言也提供了非常简便的方式。

func main() {
	a := []int{1, 2, 3}
	b := make([]int, 3)
	copy(b, a)
	fmt.Println(a)
	fmt.Println(b)
}

你不妨动手试一试下面几个操作结果会怎么样:

  • 声明b切片时,其长度比a切片长,复制结果是怎么样的?
  • 声明b切片时,其长度比a切片短,复制结果是怎么样的?
  • 声明b切片时,其长度被定义为0,那么调用copy函数会报错吗?

结合append与copy函数可以实现很多非常实用的功能,你也可以动手尝试一下😀

  • 删除处于索引i的元素
  • 在索引i的位置插入元素
  • 在索引i的位置插入长度为 j 的新切片
  • 等等
5.2.3 切片与数组的关系

对于任何一个切片来说,其都有一个底层数组与之对应,我们可以将切片看作是一个窗口,透过这个窗口可以看到底层数组的一部分元素,对应关系如下图所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tFJ9tvhA-1608479312719)(./img/array_slice.png)]

图片源自:《极客时间》https://time.geekbang.org/column/article/14106

下面有几个问题欢迎你自己动手尝试一下:

  • 编写程序看看切片的容量与数组的大小有什么关系呢?

  • 如果我们在切片上再做切片那么他们会指向相同的底层数组吗?修改其中一个切片会影响其他切片的值么?其中一个切片扩容到容量大小之后会更换底层数组,那么之前的其他切片也会指向新的底层数组吗?

既然切片是引用底层数组的,需要注意的就是小切片引用大数组的问题,如果底层的大数组一直有切片进行引用,那么垃圾回收机制就不会将其收回,造成内存的浪费,最有效的做法是copy需要的数据后再进行操作。
转存中…(img-tFJ9tvhA-1608479312719)]

图片源自:《极客时间》https://time.geekbang.org/column/article/14106

下面有几个问题欢迎你自己动手尝试一下:

  • 编写程序看看切片的容量与数组的大小有什么关系呢?

  • 如果我们在切片上再做切片那么他们会指向相同的底层数组吗?修改其中一个切片会影响其他切片的值么?其中一个切片扩容到容量大小之后会更换底层数组,那么之前的其他切片也会指向新的底层数组吗?

既然切片是引用底层数组的,需要注意的就是小切片引用大数组的问题,如果底层的大数组一直有切片进行引用,那么垃圾回收机制就不会将其收回,造成内存的浪费,最有效的做法是copy需要的数据后再进行操作。

### 数组切片的基本概念 在 Go 语言中,**数组**是一种固定长度的数据结构,用于存储相同类型的元素。数组的长度在声明时就必须确定,并且不能更改。例如: ```go var arr [5]int ``` 上述代码声明了一个长度为 5 的整型数组数组适用于存储固定数量的数据,且在访问元素时具有高效的性能[^1]。 与数组不同,**切片**(slice)是 Go 语言中一种更灵活的数据结构。切片是对数组的抽象,它提供了动态扩展的能力,允许在运行时根据需要调整其长度。切片的底层实际上仍然依赖于数组,但它通过三个要素来管理数据:指向底层数组的指针(`ptr`)、当前切片的长度(`len`)和容量(`cap`)[^4]。 ```go slice := arr[1:4] // 从数组 arr 中创建一个切片,包含索引 1 到 3 的元素 ``` ### 切片的创建方式 切片可以通过多种方式创建: 1. **从数组创建切片**:如上例所示,使用数组切片操作可以创建一个新的切片,该切片共享底层数组的数据。 2. **使用 `make` 函数创建切片**:`make` 函数允许指定切片的长度和容量,适用于需要预分配空间的场景。 ```go slice := make([]int, 3, 5) // 创建一个长度为 3,容量为 5 的切片 ``` 3. **使用字面量创建切片**:直接通过字面量的方式创建切片Go 会自动推导出其长度。 ```go slice := []int{1, 2, 3} ``` ### 切片的操作 切片支持多种操作,包括: - **访问元素**:通过索引访问切片中的元素,索引范围从 0 到 `len(slice)-1`。 ```go fmt.Println(slice[0]) // 输出第一个元素 ``` - **修改元素**:可以直接通过索引修改切片中的元素。 ```go slice[0] = 10 // 将第一个元素修改为 10 ``` - **追加元素**:使用 `append` 函数可以向切片追加元素。如果切片的容量不足以容纳新元素,Go 会自动分配一个新的底层数组,并将原有数据复制过去。 ```go slice = append(slice, 4) // 向切片末尾添加元素 4 ``` - **切片的切割**:可以通过切片操作来获取子切片,类似于从数组创建切片的方式。 ```go subSlice := slice[1:3] // 获取索引 1 到 2 的子切片 ``` ### 切片的注意事项 1. **空切片与 `nil` 切片**:空切片是一个长度为 0 的切片,而 `nil` 切片表示切片未被初始化。两者在某些情况下表现不同,例如使用 `len` 和 `cap` 函数时。 ```go var nilSlice []int // nil 切片 emptySlice := []int{} // 空切片 ``` 2. **切片的比较**:切片不能直接进行比较操作,只能与 `nil` 进行比较。 ```go if slice == nil { // 判断切片是否为 nil } ``` 3. **切片的扩容策略**:当使用 `append` 函数向切片追加元素时,如果当前切片的容量不足以容纳新元素,Go 会自动扩容。扩容策略通常是将容量翻倍,直到满足需求。 4. **删除元素**:切片本身没有内置的删除操作,但可以通过切片操作和 `append` 函数实现删除元素的功能。例如,删除索引 `i` 处的元素: ```go slice = append(slice[:i], slice[i+1:]...) ``` ### 切片的底层原理 切片的底层是一个数组,它通过 `ptr`、`len` 和 `cap` 三个要素来管理数据。`ptr` 指向底层数组的起始地址,`len` 表示当前切片的长度,而 `cap` 表示从 `ptr` 开始到底层数组末尾的总容量。这种结构使得切片可以在不重新分配内存的情况下进行扩展和收缩,从而提高了性能[^4]。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值