浅谈Go数组比切片好在哪

目录
  • 数组是什么
  • 切片是什么
  • 数组的优势
    • 可比较
    • 编译安全
    • 长度是类型
    • 规划内存布局
    • 访问速度
  • 总结
  • 参考

前段时间有播放一条快讯,就是 Go1.17 会正式支持切片(Slice)转换到数据(Array),不再需要用以前那种骚办法了,安全了许多。

但是也有同学提出了新的疑惑,在 Go 语言中,数组其实是用的相对较少的,甚至会有同学认为在 Go 里可以把数组给去掉。

数组相较切片到底有什么优势,我们又应该在什么场景下使用呢?

这是一个我们需要深究的问题,因此今天就跟大家一起来一探究竟,本文会先简单介绍数组和切片是什么,再进一步对数组的使用场景剖析。

一起愉快地开始吸鱼之路。

数组是什么

Go 语言中有一种基本数据类型,叫数组。其格式为:[n]T。是一个包含 N 个类型 T 的值的数组。

基本声明格式为:

var a [10]int

代表的是声明了一个变量 a 是一个包含 10 个整数的数组。数组的长度是其类型的一部分,所以数组不能被随意调整大小。

在使用例子上:

func main() {
 var a [2]string
 a[0] = "脑子进"
 a[1] = "煎鱼了"
 fmt.Println(a[0], a[1])
 fmt.Println(a)

 primes := [6]int{2, 3, 5, 7, 11, 13}
 fmt.Println(primes)
}

输出结果:

脑子进 煎鱼了
[脑子进 煎鱼了]
[2 3 5 7 11 13]

在赋值和访问上,数组可以针对不同的索引,进行单独操作。在内存布局上,数组的索引 0 和 1...是会在相邻区域,可直接访问。

切片是什么

为什么数组在业务代码似乎用的很少。因为 Go 语言有一个切片的数据类型:

基本声明格式为:

var a []T

代表的是变量 a 是带有类型元素的切片T。通过指定两个索引(下限和上限)并用冒号隔开来形成切片:

a[low : high]

在使用例子上:

func main() {
 primes := [3]string{"煎鱼", "搞", "Go"}

 var s []string = primes[1:3]
 fmt.Println(s)
}

输出结果:

[搞 Go]

切片支持动态的扩缩容,不需要用户侧去关注,非常便利。更重要的一点是,切片的底层数据结构中本身就包含了数组:

type slice struct {
 array unsafe.Pointer
 len   int
 cap   int
}

也就很多人笑称:在 Go 语言中数组已经可以下岗了,用切片就完事了...

你怎么看待这个说法的呢,快速思考你心中的答案。

数组的优势

在风尘仆仆介绍完数组和切片的基本场景后,在数组的优势方面,先了解一下官方的自述:

Arrays are useful when planning the detailed layout of memory and sometimes can help avoid allocation, but primarily they are a building block for slices.

非常粗暴间接:在规划内存的详细布局时,数组是很有用的,有时可以帮助避免分配,但主要是它们是分片的构建块。

我们再进一步解读,看看官方这股 “密文” 具体指的是什么,我们将该密文解读为以下内容进行讲解:

  • 可比较。
  • 编译安全。
  • 长度是类型。
  • 规划内存布局。
  • 访问速度。

可比较

数组是固定长度的,它们之间是可以进行比较的,数组是值对象(不是引用或指针类型),你不会遇到 interface 等比较的误判:

func main() {
 a1 := [3]string{"脑子", "进", "煎鱼了"}
 a2 := [3]string{"煎鱼", "进", "脑子了"}
 a3 := [3]string{"脑子", "进", "煎鱼了"}

 fmt.Println(a1 == a2, a1 == a3)
}

输出结果:

false true

另一方面,切片不可以直接比较,也不能用于判断:

func main() {
 a1 := []string{"脑子", "进", "煎鱼了"}
 a2 := []string{"煎鱼", "进", "脑子了"}
 a3 := []string{"脑子", "进", "煎鱼了"}

 fmt.Println(a1 == a2, a1 == a3)
}

输出结果:

# command-line-arguments
./main.go:10:17: invalid operation: a1 == a2 (slice can only be compared to nil)
./main.go:10:27: invalid operation: a1 == a3 (slice can only be compared to nil)

同时数组可以作为 map 的 k(键),而切片不行,切片并没有实现平等运算符(equality operator),需要考虑的问题有非常多,例如:

  • 涉及浅层与深层比较。
  • 指针与值比较。
  • 如何处理递归类型。

平等是为结构体和数组定义的,所以这类类型可以作为 map 键使用。切片没有平等的定义,有着非常根本的差距。

数组的可比较和平等,切片做不到。

编译安全

数组可以提供更高的编译时安全,可以在编译时检查索引范围。如下:

s := make([]int, 3)
s[3] = 3 // "Only" a runtime panic: runtime error: index out of range

a := [3]int{}
a[3] = 3 // Compile-time error: invalid array index 3 (out of bounds for 3-element array)

这个编译检查的帮助虽 “小”,但其实非常有意义。我是日常看到各大切片越界的告警,感觉都能背下来了...

万一这个越界是在 hot path 上,影响大量用户,分分钟背个事故,再来个 3.25,岂不梦中惊醒?

数组的编译安全,切片做不到。

长度是类型

数组的长度是数组类型声明的一部分,因此长度不同的数组是不同的类型,两个就不是一个 “东西”。

当然,这是一把双刃剑。其优势在于:可用于显式指定所需数组的长度。

例如:你在业务代码中想编写一个使用 IPv4 地址的函数。可以声明 type [4]byte。使用数组有以下意识:

  • 有了编译时的保证,也就是达到传递给你的函数的值将恰好具有4个字节,不多也不少的效果。
  • 如果长度不对,也就可以认为是无效的 IPv4 地址,非常方便。

同时数组的长度,也可以用做记录目的:

  • MD5 类型,在 crypto/md5包中,md5.Sum 方法返回类型为的值,[Size]byte 其中 md5.Size 一个常量为16:MD5 校验和的长度。
  • IPv4 类型,所声明的 [4]byte 正确记录了有 4 个字节。
  • RGB 类型,所声明的 [3]byte 告诉有对每个颜色成分 1 个字节。

在特定业务场景上,使用数组更好。

规划内存布局

数组可以更好地控制内存布局,因为不能直接在带有切片的结构中分配空间,所以可以使用数组来解决。

例如:

type Foo struct {
    buf [64]byte
}

不知道你是否有在一些 Go 图形库上见过这种不明所以的操作,例子如下:

type TGIHeader struct {
    _        uint16 // Reserved
    _        uint16 // Reserved
    Width    uint32
    Height   uint32
    _        [15]uint32 // 15 "don't care" dwords
    SaveTime int64
}

因为业务需求,我们需要实现一个格式,其中格式是 "TGI"(理论上的Go Image),头包含这样的字段:

  • 有 2 个保留字(每个16位)。
  • 有 1 个字的图像宽度。
  • 有 1 个字的图像高度。
  • 有 15 个业务 "不在乎 "的字节。
  • 有 1 个保存时间,图像的保存时间为8字节,是自1970年1月1日UTC以来的纳秒数。

这么一看,也就不难理解数组的在这个场景下的优势了。定长,可控的内存,在计划内存布局时非常有用。

访问速度

使用数组时,其访问(单个)数组元素比访问切片元素更高效,时间复杂度是 O(1)。例如:

 var a [2]string
 a[0] = "脑子进"
 a[1] = "煎鱼了"
 fmt.Println(a[0], a[1])

切片就没那么方便了,访问某个位置上的索引值,需要:

 var a []int{0, 1, 2, 3, 4, 5}
  number := numbers[1:3]

相对复杂些的,删除指定索引位上的值,可能还有小伙伴纠结半天,甚至在找第三方开源库想快速实现。

无论在访问速度和开发效率上,数组都占一定的优势,这是切片所无法直接对比的。

总结

经过一轮的探讨,我们对 Go 语言的数组有了更深入的理解。总结如下:

  • 数组是值对象,可以进行比较,可以将数组用作 map 的映射键。而这些,切片都不可以,不能比较,无法作为 map 的映射键。
  • 数组有编译安全的检查,可以在早起就避免越界行为。切片是在运行时会出现越界的 panic,阶段不同。
  • 数组可以更好地控制内存布局,若拿切片替换,会发现不能直接在带有切片的结构中分配空间,数组可以。
  • 数组在访问单个元素时,性能比切片好。
  • 数组的长度,是类型的一部分。在特定场景下具有一定的意义。
  • 数组是切片的基础,每个数组都可以是一个切片,但并非每个切片都可以是一个数组。如果值是固定大小,可以通过使用数组来获得较小的性能提升(至少节省 slice 头占用的空间)。

参考

In GO programming language what are the benefits of using Arrays over Slices?
Why have arrays in Go?

到此这篇关于浅谈Go数组比切片好在哪的文章就介绍到这了,更多相关Go数组切片内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • go特性之数组与切片的问题

    数组: 复制传递(不要按照c/c++的方式去理解,c/c++中数组是引用传递),定长 切片: 引用传递,底层实现是3个字段 array(数组) + len(长度) +cap(容量) go/src/runtime/slice.go slice结构定义: type slice struct { array unsafe.Pointer len int cap int } 要特别注意的是,切片的引用传递指的是切片传递时,切片的array字段是引用传递的,len和cap字段依然是赋值传递. 写个伪代码:

  • 深入理解Go语言中的数组和切片

    一.类型 数组是值类型,将一个数组赋值给另一个数组时,传递的是一份拷贝. 切片是引用类型,切片包装的数组称为该切片的底层数组. 我们来看一段代码 //a是一个数组,注意数组是一个固定长度的,初始化时候必须要指定长度,不指定长度的话就是切片了 a := [3]int{1, 2, 3} //b是数组,是a的一份拷贝 b := a //c是切片,是引用类型,底层数组是a c := a[:] for i := 0; i < len(a); i++ { a[i] = a[i] + 1 } //改变a的值后

  • GO语言数组和切片实例详解

    本文实例讲述了GO语言数组和切片的用法.分享给大家供大家参考.具体分析如下: 一.数组 与其他大多数语言类似,Go语言的数组也是一个元素类型相同的定长的序列. (1)数组的创建. 数组有3种创建方式:[length]Type .[N]Type{value1, value2, ... , valueN}.[...]Type{value1, value2, ... , valueN} 如下: 复制代码 代码如下: func test5() {     var iarray1 [5]int32    

  • 理解Golang中的数组(array)、切片(slice)和map

    我比较喜欢先给出代码,然后得出结论 数组 复制代码 代码如下: package main import (     "fmt" ) func main() {     arr := [...]int{1, 2, 3}     //打印初始的指针     fmt.Printf("the pointer is : %p \n", &arr)     printPointer(arr) } func printPointer(any interface{}) {

  • 简单聊一聊Go语言中的数组和切片

    目录 1. 数组 2. 切片(Slice) append 函数 总结 1. 数组 数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成.因为数组的长度是固定的,因此在 Go 语言中很少直接使用数组.和数组对应的类型是 Slice(切片),它是可以增长和收缩的动态序列,slice 功能也更灵活. 数组的每个元素可以通过索引下标来访问,索引下标的范围是从 0 开始到数组长度减 1 的位置.内置的 len 函数将返回数组中元素的个数. var a [3]int // arra

  • 浅谈Go数组比切片好在哪

    目录 数组是什么 切片是什么 数组的优势 可比较 编译安全 长度是类型 规划内存布局 访问速度 总结 参考 前段时间有播放一条快讯,就是 Go1.17 会正式支持切片(Slice)转换到数据(Array),不再需要用以前那种骚办法了,安全了许多. 但是也有同学提出了新的疑惑,在 Go 语言中,数组其实是用的相对较少的,甚至会有同学认为在 Go 里可以把数组给去掉. 数组相较切片到底有什么优势,我们又应该在什么场景下使用呢? 这是一个我们需要深究的问题,因此今天就跟大家一起来一探究竟,本文会先简单

  • 浅谈$_FILES数组为空的原因

    今天做上传的文件时候,打印$_files总是为空,查阅了下资料. 发现是 max_file_uploads=0 知道了原因 file_uploads = On upload_max_filesize = 20M max_file_uploads = 20 以上这篇浅谈$_FILES数组为空的原因就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持我们.

  • 浅谈js数组和splice的用法

    首先添加一个splice函数: splice:该方法的作用就是从数组中删除一个元素 array.splice(index,count,value....); index:表示从哪一个下标开始, count:表示删除元素的个数 value:代表增加的元素 example: 1.var array = new Array(1,2,3,4,5,6); array.splice(0,1,2) result:2,2,3,4,5 2.var array = new Array(1,2,3,4,5,6); a

  • 浅谈php数组array_change_key_case() 函数和array_chunk()函数

    如下所示: <?php /* array_change_key_case() 返回其键均为大写或小写的数组. array array_change_key_case(array input[,int case]) 参数描述:array是要转换键值的数组 case有两个选项:CASE_LOWER,默认选项,以小写字母返回数组的键 CASE_UPPER,以大写字母返回数组的键 */ $input_array = array('a'=>'Java', 'B'=>'Php', 'c'=>'

  • 浅谈对象数组或list排序及Collections排序原理

    常需要对list进行排序,小到List<String>,大到对自定义的类进行排序.不需要自行归并或堆排序.简单实现一个接口即可. 本文先会介绍利用Collections对List<String>进行排序,继而讲到Collections.sort的原理, 再讲到如何对自定义类进行排序, 最后会介绍利用Collections sort对自定义对象进行排序的另外一种方法,并将两种排序进行了简单的性能比较. 1.对List<String>排序及Collections.sort的

  • 浅谈Java数组的一些使用方法及堆栈存储

    数组 用于存储一组同一数据类型数据的容器 数组会对放入其中的数据自动编号,编号是从0开始的---下标 定义格式 数据类型[] 数组名 = new 数据类型[数组的大小];---可以先声明再初始化 int[] arr = new int[5];---定义了一个最多能存储5的整数的数组 arr[3] = 4; arr[3]---通过数组名[下标]的形式来获取数组元素或者给对应的位置赋值 数据类型[] 数组名 = new 数据类型[]{元素1,元素2--}; int[] arr = new int[]

  • 浅谈numpy数组的几种排序方式

    简单介绍 NumPy系统是Python的一种开源的数组计算扩展.这种工具可用来存储和处理大型矩阵,比Python自身的嵌套列表(nested list structure)结构要高效的多(该结构也可以用来表示矩阵(matrix)). 创建数组 创建1维数组: data = np.array([1,3,4,8]) 查看数组维度 data.shape 查看数组类型 data.dtype 通过索引获取或修改数组元素 data[1] 获取元素 data[1] = 'a' 修改元素 创建二维数组 data

  • 浅谈numpy数组中冒号和负号的含义

    在实际使用numpy时,我们常常会使用numpy数组的-1维度和":"用以调用numpy数组中的元素.也经常因为数组的维度而感到困惑. 总体来说,":"用以表示当前维度的所有子模块 "-1"用以表示当前维度所有子模块最后一个,"负号用以表示从后往前数的元素" 测试代码 import numpy as np b = np.arange(start=0, stop=24, dtype=int) print('b.shape', b

  • 浅谈shell数组的定义及循环

    shell中数组的定义及遍历,先直接看示例: #!/bin/sh #定义方法一 数组定义为空格分割 arrayWen=(a b c d e f) #定义方法二 arrayXue[0]="m" arrayXue[1]="n" arrayXue[2]="o" arrayXue[3]="p" arrayXue[4]="q" arrayXue[5]="r" #打印数组长度 echo ${#arr

  • 浅谈js数组splice删除某个元素爬坑

    先来看下几个概念: // splice:返回从原始数组中删除的项(如果没有任何删除,则返回空数组) // 当指定2个参数时,表示删除 // 当指定3个参数,且第2个参数为0时表示插入 // 当指定3个参数,且第2个参数为1时表示替换 本次就拿删除举例,本身我们想删除数组中的某个指定元素,我们需要知道它所在数组中的下标,我们可以用 数组.indexOf获取它所在的下标,然后拿splice删除这个元素. 本身是没问题 代码如下: var arr = ["张三","李四"

随机推荐