Golang 语言中数组和切片的区别是什么?

简介: Golang 语言中数组和切片的区别是什么?

01

介绍

在很多编程语言中都有数组,而切片类型却不常见。实际上,Golang 语言中的切片的底层存储也是基于数组。因为数组是固定长度的,而切片比数组更加灵活,所以在 Golang 语言中,数组使用的并不多,切片使用更加广泛。

数组和切片的区别

  • 数组的零值是元素类型的零值,切片的零值是 nil;
  • 数组是固定长度,切片是可变长度;
  • 数组是值类型,切片是引用类型。

数组:

func main () {
    var arr1 [4]int
    fmt.Printf("arr1 val:%d arr1 len:%d arr1 cap:%d\n", arr1, len(arr1), cap(arr1))
    arr := [4]int{}
    fmt.Printf("val:%d len:%d cap:%d\n", arr, len(arr), cap(arr)) // val:[0 0 0 0] len:4 cap:4
    arr[0] = 1
    arr[1] = 2
    arr[2] = 3
    arr[3] = 4
    // arr[4] = 5 // invalid array index 4 (out of bounds for 4-element array)
    fmt.Printf("val:%d len:%d cap:%d\n", arr, len(arr), cap(arr)) // val:[1 2 3 4] len:4 cap:4
    arr2 := arr
 fmt.Printf("arr2 val:%d len:%d cap:%d ptr:%p\n", arr2, len(arr2), cap(arr2), &arr2) // arr2 val:[1 2 3 4] len:4 cap:4 ptr:0xc0001980a0
 fmt.Printf("arr val:%d len:%d cap:%d ptr:%p\n", arr, len(arr), cap(arr), &arr) // arr val:[1 2 3 4] len:4 cap:4 ptr:0xc000198040
 ss := arr[:]
 ssPtr := (*reflect.SliceHeader)(unsafe.Pointer(&ss)).Data
 fmt.Printf("ss val:%d len:%d cap:%d ptr:%v\n", ss, len(ss), cap(ss), ssPtr) // ss val:[1 2 3 4] len:4 cap:4 ptr:824635392064
 ss2 := arr[:]
 ss2Ptr := (*reflect.SliceHeader)(unsafe.Pointer(&ss2)).Data
 fmt.Printf("ss2 val:%d len:%d cap:%d ptr:%v\n", ss2, len(ss2), cap(ss2), ss2Ptr) // ss2 val:[1 2 3 4] len:4 cap:4 ptr:824635392064
}

切片:

func main () {
  var s []int
 if s == nil {
  fmt.Println("nil")
 }
 fmt.Printf("s val:%d len:%d cap:%d\n", s, len(s), cap(s)) // s val:[] len:0 cap:0
 s = append(s, 1)
 fmt.Printf("s val:%d len:%d cap:%d\n", s, len(s), cap(s)) // s val:[1] len:1 cap:1
 s = append(s, 2)
 fmt.Printf("s val:%d len:%d cap:%d\n", s, len(s), cap(s)) // s val:[1 2] len:2 cap:2
 s = append(s, 3)
 fmt.Printf("s val:%d len:%d cap:%d\n", s, len(s), cap(s)) // s val:[1 2 3] len:3 cap:4
}

阅读上面这两段代码,我们可以发现数组的零值是元素类型的零值,而切片的零值是 nil,同时,nil 也是唯一可以和切片类型作比较的值。

数组中元素超越边界会引发错误,切片中元素超越边界会自动扩容,切片的扩容规则将在 Part 03 介绍。

数组是值类型,切片是引用类型。arr2 和 arr 的内存地址不同,它们是两块不同的内存空间;ss 和 ss2 的内存地址相同,它们指向同一个底层数组。

在 Golang 语言中传递数组属于值拷贝,如果数组的元素个数比较多或者元素类型的大小比较大时,直接将数组作为函数参数会造成性能损耗,可能会有读者想到使用数组指针作为函数参数,这样是可以避免性能损耗,但是在 Golang 语言中,更流行使用切片,关于这块内容,阅读完 Part 04 的切片数据结构,会有更加深入的理解。

03

切片扩容规则

通过阅读 Part 02 关于切片的这段代码,我们还可以看出切片的扩容规则,当一个切片的容量无法存储更多元素时,切片会自动扩容,它会生成一个容量更大的新切片,然后把原切片的元素和新元素一起拷贝到新切片中。

在原切片长度小于 1024 时,新切片的容量会按照原切片的 2 倍扩容,否则,新切片的容量会按照原切片的 1.25 倍扩容,此时需要注意的是,如果新切片的容量按照原切片的 1.25 倍扩容一次仍然无法存储新元素时,将会不断按照原切片的 1.25 倍扩容,直到新切片的容量可以存储原切片的元素和新元素为止。一般最终扩容后的新切片,它的容量会大于或等于原切片的容量。

需要注意的是,当切片的零值是 nil 时,切片此时还没有指向底层数组。但是切片的零值是可用的,当使用 append 向零值切片追加元素时,将会先给切片分配一个底层数组。

切片扩容实际是创建一个新的底层数组,把原切片的元素和新元素一起拷贝到新切片的底层数组中,原切片的底层数组将会被垃圾回收。

注意:切片的容量可以根据元素的个数的增多自动扩容,但是不会根据元素的个数的减少自动缩容。

04

切片数据结构

在 Golang 语言中,切片实际是一个结构体,源码如下所示:

// /usr/local/go/src/runtime/slice.go
type slice struct {
 array unsafe.Pointer
 len   int
 cap   int
}

阅读源码,我们可以发现先,slice 结构体包含 3 个字段:

  • array - 指向底层数组
  • len - 切片的长度
  • cap - 切片的容量

在 Golang 语言运行时中,一个切片类型的变量实际上就是 runtime.slice 结构体的实例,其中 arrray 字段是指针类型,指向切片的底层数组,len 是切片的长度,cap 是切片的容量,当使用 make 函数创建切片时,如果不指定 cap 参数的值,cap 的值就等于 len 的值。

05

切片编程技巧

如果已经认真阅读完以上内容,我们应该已经知道切片在每次扩容时都会将原切片底层数组的元素和新元素一起拷贝到新切片的底层数组,这种操作在元素比较多或者元素的类型大小比较大时,内存分配和拷贝的代价还是比较大的。

为了降低或避免内存分配和拷贝的代价,我们通常会为新创建的切片指定 cap 参数的值,比如:

s := make([]T, 0, cap)

但是,这种使用方式的前提是,我们可以预估切片的元素个数。

for range 遍历切片

通过使用 for range 遍历切片,每次遍历操作实际上是对遍历元素的拷贝。而使用 for 遍历切片,每次遍历是通过索引访问切片元素,性能会远高于通过 for range 遍历。

因此想要优化使用 for range 遍历切片的性能,可以使用空白标识符 _ 省略每次遍历返回的切片元素,改为使用切片索引取访问切片的元素。

普通方式:

func main () {
    s := make([]int, 0, 10000)
    for k, v := range s {
        fmt.Println(s, v)
    }
}

优化方式:

func main () {
    s := make([]int, 0, 10000)
    for k, _ := range s {
        fmt.Println(k, s[k])
    }
}

07

总结

本文我们先是介绍了数组和切片的区别,然后还介绍了一些关于切片的扩容规则、数据结构和使用技巧等。文中代码比较多,建议读者将代码拷贝到编辑器中,查看运行结果,从而可以更加深刻理解文中的内容。如果想了解更多数组和切片的内容,请阅读推荐阅读列表中的相关文章。

推荐阅读:

Go 语言学习之数组

Go 语言学习之 slice

参考资料:

https://blog.golang.org/slices-intro

https://tour.golang.org/moretypes/12

https://www.godesignpatterns.com/2014/05/arrays-vs-slices.html


目录
相关文章
|
3月前
|
Go
Golang语言之管道channel快速入门篇
这篇文章是关于Go语言中管道(channel)的快速入门教程,涵盖了管道的基本使用、有缓冲和无缓冲管道的区别、管道的关闭、遍历、协程和管道的协同工作、单向通道的使用以及select多路复用的详细案例和解释。
140 4
Golang语言之管道channel快速入门篇
|
3月前
|
Go
Golang语言之gRPC程序设计示例
这篇文章是关于Golang语言使用gRPC进行程序设计的详细教程,涵盖了RPC协议的介绍、gRPC环境的搭建、Protocol Buffers的使用、gRPC服务的编写和通信示例。
112 3
Golang语言之gRPC程序设计示例
|
3月前
|
安全 Go
Golang语言goroutine协程并发安全及锁机制
这篇文章是关于Go语言中多协程操作同一数据问题、互斥锁Mutex和读写互斥锁RWMutex的详细介绍及使用案例,涵盖了如何使用这些同步原语来解决并发访问共享资源时的数据安全问题。
100 4
|
3月前
|
Prometheus Cloud Native Go
Golang语言之Prometheus的日志模块使用案例
这篇文章是关于如何在Golang语言项目中使用Prometheus的日志模块的案例,包括源代码编写、编译和测试步骤。
75 3
Golang语言之Prometheus的日志模块使用案例
|
2月前
|
前端开发 中间件 Go
实践Golang语言N层应用架构
【10月更文挑战第2天】本文介绍了如何在Go语言中使用Gin框架实现N层体系结构,借鉴了J2EE平台的多层分布式应用程序模型。文章首先概述了N层体系结构的基本概念,接着详细列出了Go语言中对应的构件名称,包括前端框架(如Vue.js、React)、Gin的处理函数和中间件、依赖注入和配置管理、会话管理和ORM库(如gorm或ent)。最后,提供了具体的代码示例,展示了如何实现HTTP请求处理、会话管理和数据库操作。
37 0
|
3月前
|
Go
Golang语言文件操作快速入门篇
这篇文章是关于Go语言文件操作快速入门的教程,涵盖了文件的读取、写入、复制操作以及使用标准库中的ioutil、bufio、os等包进行文件操作的详细案例。
71 4
Golang语言文件操作快速入门篇
|
3月前
|
Go
Golang语言错误处理机制
这篇文章是关于Golang语言错误处理机制的教程,介绍了使用defer结合recover捕获错误、基于errors.New自定义错误以及使用panic抛出自定义错误的方法。
55 3
|
3月前
|
Go 调度
Golang语言goroutine协程篇
这篇文章是关于Go语言goroutine协程的详细教程,涵盖了并发编程的常见术语、goroutine的创建和调度、使用sync.WaitGroup控制协程退出以及如何通过GOMAXPROCS设置程序并发时占用的CPU逻辑核心数。
72 4
Golang语言goroutine协程篇
|
3月前
|
Go
Golang语言之函数(func)进阶篇
这篇文章是关于Golang语言中函数高级用法的教程,涵盖了初始化函数、匿名函数、闭包函数、高阶函数、defer关键字以及系统函数的使用和案例。
71 3
Golang语言之函数(func)进阶篇
|
3月前
|
Go
Golang语言之函数(func)基础篇
这篇文章深入讲解了Golang语言中函数的定义和使用,包括函数的引入原因、使用细节、定义语法,并通过多个案例展示了如何定义不返回任何参数、返回一个或多个参数、返回值命名、可变参数的函数,同时探讨了函数默认值传递、指针传递、函数作为变量和参数、自定义数据类型以及返回值为切片类型的函数。
80 2
Golang语言之函数(func)基础篇