go语言:数组、切片

本文详细介绍了Go语言中的数组和切片的区别与使用。数组是定长的,不可变长度,不同长度的数组不能相互赋值。切片是动态数组,支持append操作,可以改变长度。切片的拷贝是浅拷贝,而数组拷贝是深拷贝,只有长度相同的数组间才能相互赋值。此外,文章还讨论了数组和切片的访问、遍历、越界检查、拷贝、追加、切割以及扩容策略等细节,并对比了空切片与nil切片的差异。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

数组的拷贝是 深拷贝(只有长度相同的数组之间才能相互拷贝),切片的拷贝是 浅拷贝。

数组与切片在内存形式上的区别:
数组只有“体”,切片除了“体”之外,还有“头”部。切片的头部和内容体是分离的,使用指针关联起来。

1. 数组:

Go语言里面的数组(array)其实很不常用,这是因为数组是定长的、静态的,一旦定义好长度就无法更改,而且不同长度的数组属于不同的类型,之间不能相互转换相互赋值,用起来多有不方便之处。

切片(slice)是动态的数组,是可以扩充内容增加长度的数组。
当长度不变时,它用起来和普通数组一样。当长度不同时,它们也属于相同的类型,之间可以相互赋值。
这就决定了数组的应用领域都广泛的被切片取代了。

切片是数组的一个包装,数组是切片的底层实现,切片的特殊语法隐藏了内部的细节,让用户不能直接看到内部隐藏的数组。

1.1 数组的定义:

package main
import "fmt"

func main() {
    var a [9]int		//默认初始化为零值
    fmt.Println(a)
}

------
[0 0 0 0 0 0 0 0 0]

数组的另外三种定义方式:

func main() {
    var a [9]int{1, 2, 3, 4, 5, 6, 7, 8, 9}
    var b [9]int = [9]int{1, 2, 3, 4, 5, 6, 7, 8, 9}
    c := [9]int{1, 2, 3, 4, 5, 6, 7, 8, 9}
}

1.2 数组的访问:(通过 数组下标 访问)

package main
import "fmt"

func main() {
    var squares [9]int
    for i := 0; i < len(squares); i++ {
		squares[i] = i + 1
	}
	fmt.Println(squares)
}

------
[1 2 3 4 5 6 7 8 9]

1.3 数组的下标越界检查:

package main
import "fmt"

func main() {
	var a = [5]int{1, 2, 3, 4, 5}
	a[101] = 255
	fmt.Println(a)
}

------
> go run test.go

# command-line-arguments
./2test.go:6:3: invalid array index 101 (out of bounds for 5-element array)

上述示例程序在编译时会报错 数组越界,说明Go语言会对数组访问下标越界进行编译器检查。

如果数组下标是常量,则在编译期间即可检查出数组越界;但如果数组下标是变量,则需要在执行器才会检查数据越界,这种情况下Go会在 编译后 的代码中插入下标检查的逻辑,所以数组的下标访问效率是要打折扣的,比不得C语言的数据访问性能。

package main
import "fmt"

func main() {
        var a = [5]int{1, 2, 3, 4, 5}
        var b int = 101
        a[b] = 255
        fmt.Println(a)
}

------
go build test.go		//没问题,顺利编译通过
./test					//执行,则会报错

panic: runtime error: index out of range [101] with length 5

goroutine 1 [running]:
main.main()
	/Users/xuesong/Documents/TEST/test.go:7 +0x1d

1.4 数组拷贝:

只有长度相同的数组之间才能相互赋值(深拷贝);长度不同的数组之间是禁止相互赋值的,因为它们属于不同的类型。

package main
import "fmt"

func main() {
	var a [9]int = [9]int{1, 2, 3, 4, 5, 6, 7, 8, 9}
	var b [9]int
	b = a
	a[0] = 12345
	fmt.Println(a)
	fmt.Println(b)
}

------
12345 2 3 4 5 6 7 8 9]
[1 2 3 4 5 6 7 8 9]

1.5 数组的遍历:(range)

除了可以使用下标对数组进行遍历之外,还可以使用 range 关键字来遍历。
range有两种使用形式:

package main
import "fmt"

func main() {
    var a [5]int = [5]int{1, 2, 3, 4, 5}
    for index := range a {
		fmt.Println(index, a[index])
	}
	for index, value := range a {
		fmt.Println(index, value)
	}
}

2. 切片:

一个切片变量包含三个域,分别是:
底层数组的指针、切片的长度(length)、切片的容量(capacity)。

切片支持append操作 可以将新的内容追加到底层数组,即下图中灰色的各自(尚未被填充的切片容量空间)。如果格子满了,即切片的空间已全被使用时,底层的数组就会更换。
在这里插入图片描述

2.1 切片的创建:(make函数)

package main
import "fmt"

func main() {
    var s1 []int = make([]int, 5, 8)
    var s2 []int = make([]int, 8)		//满容切片

    var s3 []int = []int{1, 2, 3, 4, 5}	//不使用make函数,此时创建的切片是满容的

	fmt.Println(s1, len(s1), cap(s1))
	fmt.Println(s2, len(s2), cap(s2))
	fmt.Println(s3, len(s3), cap(s3))
}

------
[0 0 0 0 0] 5 8
[0 0 0 0 0 0 0 0] 8 8
[1 2 3 4 5] 5 5

make函数创建切片,需要提供三个参数,分别是:切片的类型、切片的长度、切片的容量。
其中,第三个参数可选,如果不提供第三个参数,则长度和容量相等,即切片是满容的。

make的作用在于可以创建 非满容 的切片,即给切片预留一部分空间,避免扩容重新申请内存、数组拷贝造成的性能影响。 如果不使用make 则第二种方式创建的切片只能是满容的。

Go语言提供了内置函数 len()cap() 可以直接获得切片的 长度 和 容量 属性。

2.3 切片的复制:

切片的复制是 浅拷贝的操作。
切片的底层是一个数组,切片的表层是一个包含三个变量的结构体,当我们将一个切片赋值给另一个切片时,本质上是对切片表层结构体的浅拷贝。

结构体中的第一个变量是一个指针,指向底层的数组,另外两个变量分别是切片的长度和容量。

type slice struct {
	array 		unsafe.Pointer
	length 		int
	capacity 	int
}

2.4 切片的追加:

切片是动态的数组,长度不固定,可以对其进行“追加”操作。
如果追加后底层数组没有扩容,那么追加前后的两个切片变量共享底层数组;
如果追加后底层数组发生扩容,那么会导致数组的分离,即新建一个数组,追加前后的两个切片变量的底层数组 不同享。

package main
import "fmt"

func main() {
    var s1 = []int{1,2,3,4,5}
    fmt.Println(s1, len(s1), cap(s1))

    var s2 = append(s1, 6)      //对满容切片s1追加新元素,会导致底层的数组分离,
即新建一个数组,s2与s1分别指向两个独立的底层数组
    fmt.Println(s1, len(s1), cap(s1))
    fmt.Println(s2, len(s2), cap(s2))   //s2的容量是10

    var s3 = append(s2, 7)      //对非满容切片s2追加新元素,不会导致底层数组分离
,s2与s3指向同一个底层数组
    fmt.Println(s2, len(s2), cap(s2))
    fmt.Println(s3, len(s3), cap(s3))
}

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

注意:
追加操作需要将append函数的返回值重新赋给原切片,否则追加操作不会生效:

func main() {
    var s1 []int = []int{1,2,3}
    s1 = append(s1, 4)			//正确

	append(s1, 5)		//错误!编译报错:
						//append(s1, 5) evaluated but not used
}

2.5 切片的切割:

既然是叫“切片”,自然是要支持切割操作。
切片的切割可以类比字符串的子串,子切片只是母切片的一个片段,子切片与母切片共享底层数组

package main
import "fmt"

func main() {
    var s1 []int = []int{1,2,3,4,5,6}

    var s2 []int = s1[0:2]
    var s3 []int = s1[:]
    var s4 []int = s1[2:4]

    fmt.Println(s1, len(s1), cap(s1))
    fmt.Println(s2, len(s2), cap(s2))
    fmt.Println(s3, len(s3), cap(s3))
    fmt.Println(s4, len(s4), cap(s4))
}

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

关于切片的切割有几点需要注意:
(1)s1[:] 与普通的切片复制效果相同,同样都是共享底层数组,同样都是浅拷贝;
(2)虽然子切片与母切片共享底层数组,但是二者的capacity容量可能不同:
s1[0:2] : 如果子切片从母切片的起始位置开始,则二者容量是相同的,与子切片指定的结束位置无关;
s1[2:4] : 如果子切片的起始位置非0,比如从2开始,则子切片底层数组的指针指向2的位置,二者容量不同,如下图所示:

请添加图片描述

2.6 数组转切片:

对数组进行切割可以转换成切片,切片将原数组作为内部底层数组。也就是说修改了原数组会影响到新切片,对切片的修改也会影响到原数组。

package main
import "fmt"

func main() {
    var s1 [6]int = [6]int{1, 2, 3, 4, 5, 6}
    var s2 []int = s1[2:4]

    fmt.Println(s1, len(s1))    //6
    fmt.Println(s2, len(s2), cap(s2)) //2 4
}

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

2.7 切片的扩容点:

当比较短的切片扩容时,系统会多分配 100% 的空间,即扩容后的切片容量是原切片的 2倍
当切片的长度超过 1024 时,扩容策略调整为多分配 25%(并不是一个严格的数字,需要看具体的运行环境) 的空间,这是为了避免空间的过多浪费。

package main
import "fmt"

func main() {
    s1 := make([]int, 6)
    s2 := make([]int, 1024)

    s1 = append(s1, 1)
    s2 = append(s2, 1)

    fmt.Println(len(s1), cap(s1))
    fmt.Println(len(s2), cap(s2))
}

------
7 12
1025 1280

2.8 空切片:

package main
import (
    "fmt"
    "unsafe"
)

func main() {
    var s1 []int
    var s2 []int = []int{}
    var s3 []int = make([]int, 0)
    var s4 []int = *new([]int)

    fmt.Println(s1, s2, s3, s4)
    fmt.Println(len(s1), len(s2), len(s3), len(s4))
    fmt.Println(cap(s1), cap(s2), cap(s3), cap(s4))


    var a1 [3]int = *(*[3]int)(unsafe.Pointer(&s1)) //*[3]int: 切片类型相当于是一个含有3个元素的结构体类型,可以将这个结构体看成长度为3的整型
数组[3]int,所以指向s1的指针类型是 *[3]int
    var a2 [3]int = *(*[3]int)(unsafe.Pointer(&s2))
    var a3 [3]int = *(*[3]int)(unsafe.Pointer(&s3))
    var a4 [3]int = *(*[3]int)(unsafe.Pointer(&s4))

    fmt.Println(a1)
    fmt.Println(a2)
    fmt.Println(a3)
    fmt.Println(a4)
}

------
[] [] [] []
0 0 0 0
0 0 0 0
[0 0 0]
[18400112 0 0]
[18400112 0 0]
[0 0 0]

s1 与 s4 这两种形式创建的切片是 “nil切片”, s2 与 s3 这两种形式创建的切片是 “空切片”。
切片类型结构体中的第一个元素是切片底层数组的地址,18400112 这个值(另说值为824634199592)是一个特殊的内存地址,所有类型的 空切片 都共享这一内存地址。

用图来表示“空切片”和“nil切片”如下:
请添加图片描述

nil切片 和 空切片 在使用上的区别在于类型判断上,为了避免出现异常错误,最好的办法是不要创建 空切片,这是官方的标准建议:

The former declares a nil slice value, while the latter is non-nil but zero-length.
They are functionally equivalant -- their len and cap are both zero 
-- but the nil slices is the preferred style.
package main
import "fmt"

func main() {
    var s1 []int                //nil
    var s2 []int = []int{}      //空

    fmt.Println(s1 == nil)
    fmt.Println(s2 == nil)

    fmt.Printf("%#v\n", s1)
    fmt.Printf("%#v\n", s2)
}

------
true
false
[]int(nil)
[]int{}

参考内容:
https://zhuanlan.zhihu.com/p/48927056
https://zhuanlan.zhihu.com/p/49415852
https://zhuanlan.zhihu.com/p/49529590

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值