为什么Go语言需要数组?

34

我理解 Go 语言中数组和切片的区别。但是我不理解为什么拥有数组是有帮助的。为什么数组类型定义需要指定长度和元素类型?为什么我们不能把每个使用的“数组”都当做切片来使用呢?


2
我认为我找到了一个可能的重复问题:为什么要使用数组而不是切片? - icza
4个回答

48

数组arrays不仅仅是固定长度,它们还可以进行比较,并且它们是值类型(而不是引用或指针类型)。

在某些情况下,数组比切片具有无数优势,所有这些优势加起来足以证明数组的存在(以及切片)。让我们看看它们。(我甚至没有计算数组作为切片构建块的情况。)


1. 可比性意味着你可以将数组用作映射的键,但不能使用切片。是的,现在你可能会问为什么不让切片具有可比性,这样就不足以证明两者的存在了。切片上的相等性没有很好地定义。FAQ:为什么映射不允许使用切片作为键?

它们没有实现相等性,因为这些类型上的相等性没有很好地定义;涉及浅层比较与深度比较、指针比较与值比较、如何处理递归类型等多个考虑因素。

2. 数组还可以提供更高的编译时安全性,因为索引边界可以在编译时进行检查(数组长度必须计算为非负常量,该常量可由类型为int的值表示):

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)

3. 同样地,传递或分配数组值会隐式地复制整个数组,因此它将与原始值"分离"。如果你传递一个切片,它仍然会复制但只是切片的头部,而切片值(头部)将指向同一后备数组。这可能是你想要的,也可能不是。如果你想要从"原始"切片中"分离"出一个切片,你必须显式地复制内容,例如使用内置的 copy() 函数到一个新的切片。

a := [2]int{1, 2}
b := a
b[0] = 10 // This only affects b, a will remain {1, 2}

sa := []int{1, 2}
sb := sa
sb[0] = 10 // Affects both sb and sa
4. 由于数组长度是数组类型的一部分,长度不同的数组是不同的类型。这可能会带来一些麻烦(例如,你编写了一个函数,它需要一个类型为[4]int的参数,你不能使用该函数来处理类型为[5]int的数组),但这也可能是一个优点:可以用它来显式地指定期望的数组长度。例如,你想编写一个函数,它需要一个IPv4地址作为参数,它可以使用类型[4]byte来建模。现在,你在编译时就可以保证传递给函数的值恰好有4个字节,不多不少(否则就是无效的IPv4地址)。

5. 与之前的相关,数组长度也可以用于文档目的。类型[4]byte适当记录了IPv4有4个字节。类型为[3]bytergb变量告诉我们每个颜色分量有1个字节。在某些情况下,它甚至被取出并单独记录;例如在crypto/md5包中: md5.Sum()返回类型为[Size]byte的值,其中md5.Size是一个常量,为16:MD5校验和的长度。

6.planning memory layout of struct types时,它们也非常有用,请参见JimB的答案,以及this answer in greater detail and real-life example

7. 由于切片是头部,它们(几乎)总是原样传递(不使用指针),因此语言规范对于指向切片的指针比指向数组的指针更加限制。例如,规范提供了多个缩写操作指向数组的指针,而在切片的情况下会导致编译时错误(因为很少使用指向切片的指针,如果您仍然想要/必须这样做,您必须明确地处理它;请在this answer中阅读更多信息)。

这样的例子有:

  • 将指向数组的指针 p 切片: p[low:high](*p)[low:high] 的简写。如果 p 是指向切片的指针,则会出现编译时错误 (spec: Slice expressions)。

  • 对指向数组的指针 p 进行索引: p[i](*p)[i] 的简写。如果 p 是指向切片的指针,则会出现编译时错误 (spec: Index expressions)。

示例:

pa := &[2]int{1, 2}
fmt.Println(pa[1:1]) // OK
fmt.Println(pa[1])   // OK

ps := &[]int{3, 4}
println(ps[1:1]) // Error: cannot slice ps (type *[]int)
println(ps[1])   // Error: invalid operation: ps[1] (type *[]int does not support indexing)
8. 访问(单个)数组元素比访问切片元素更有效率,因为在切片的情况下,运行时必须经过隐式指针解引用。此外,“如果s的类型是数组或指向数组的指针,则表达式len(s)和cap(s)是常量”。

可能会让人惊讶,但您甚至可以编写:

type IP [4]byte

const x = len(IP{}) // x will be 4

这是有效的,并且在编译时进行评估,即使IP{}不是常量表达式,所以例如const i = IP{}将导致编译时错误!在此之后,甚至以下内容也能正常工作:
const x2 = len((*IP)(nil)) // x2 will also be 4

注意:在遍历完整数组与完整切片时,可能根本没有性能差异,因为很明显可以进行优化,使得只有一次解引用切片头部的指针。有关详细信息/示例,请参见 Array vs Slice: accessing speed

请查看相关问题,了解什么情况下使用数组比切片更合适:

为什么要使用数组而不是切片?

为什么Go语言中切片不能像数组一样被用作map的键?

使用数组类型作为哈希表的键

如何优雅地检查三个值的相等性?

如何对传递的切片指针进行切片操作?

这只是出于好奇: 一个切片可以包含自己,而数组则不能。(实际上,这个属性使得比较更容易,因为你不必处理递归数据结构)

必读博客:

Go Slices:使用和内部机制

Arrays、Slices(和Strings):“append”的工作原理


3
在我看来,除了68之外,这些都不是“基本”优势(即不特定于Go中所做的糟糕设计选择)。我相信Go的设计者只是想留下更精确的内存管理可能性,因此我们有了数组。 - gavv

4

数组是值,而且通常使用值而不是指针更加有用。

值可以进行比较,因此您可以使用数组作为映射的键。

值始终被初始化,因此您不需要像对待切片一样进行初始化或者make操作。

数组让您更好地控制内存布局,在结构体中无法直接分配切片空间,但可以使用数组:

type Foo struct {
    buf [64]byte
}

在这里,一个Foo值将包含64字节的值,而不是需要单独初始化的切片标头。在与C代码进行交互时,数组还用于填充结构以匹配对齐,并防止错误共享以提高缓存性能。
另一个改进性能的方面是,您可以比使用切片更好地定义内存布局,因为数据局部性对内存密集型计算有很大影响。与正在执行的操作相比,解引用指针可能需要相当长的时间,并且复制小于缓存行的值几乎没有成本,因此对于那些对性能至关重要的代码,仅出于这个原因就经常使用数组。

1

数组在节省空间方面更加高效。如果您从未更新切片的大小(即以预定义的大小开始,并且永远不会超过它),则实际上没有太大的性能差异。但是,切片存在额外的空间开销,因为切片只是一个包含其核心数组的包装器。在上下文中,这也提高了清晰度,因为它使变量的预期用途更加明显。


实际上,性能差异可能非常大。使用指针(通过切片)可能会影响数据局部性,并产生额外的开销来查找内存中的数据。 - JimB

0
每个数组都可以是切片,但并非每个切片都可以是数组。如果您有一个固定的集合大小,使用数组可以获得轻微的性能改进。至少,您将节省由切片头占用的空间。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接