为什么在 Go 中有数组?

Why have arrays in Go?

我了解 Go 中数组和切片的区别。但我不明白的是为什么拥有数组是有帮助的。为什么数组类型定义指定长度和元素类型会有帮助?为什么我们使用的每个 "array" 不能是一个切片?

每个数组都可以是一个切片,但不是每个切片都可以是一个数组。如果你有一个固定的集合大小,你可以通过使用数组获得一个小的性能提升。至少你会保存切片头占用的space。

数组在保存方面效率更高space。如果您从不更新切片的大小(即从预定义的大小开始并且永远不会超过它),那么性能差异确实不大。但是 space 中有额外的开销,因为切片只是一个包含数组核心的包装器。在上下文中,它还提高了清晰度,因为它使变量的预期用途更加明显。

arrays than just the fixed length: they are comparable 还有更多内容,它们是 (不是引用或指针类型)。

在某些情况下,数组相对于切片有无数的优势,所有这些加起来足以证明数组(以及切片)的存在。让我们看看他们。 (我什至没有把数组算作切片的构建块。)


1. 具有可比性意味着您可以使用数组作为映射中的键,但不能使用切片。是的,你现在可以说为什么不让 slice 具有可比性,这样就不能单独证明两者的存在。切片上的平等定义不明确。 FAQ: Why don't maps allow slices as keys?

They don't implement equality because equality is not well defined on such types; there are multiple considerations involving shallow vs. deep comparison, pointer vs. value comparison, how to deal with recursive types, and so on.

2. 数组还可以给你更高的compile-time安全性,因为索引边界可以在编译时检查(数组长度必须评估为 non-negative constant,可由类型 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. 此外 传递或分配数组值将隐式地复制整个数组 ,因此它将是 "detached" 从原始值。如果你传递一个切片,它仍然会制作一个副本,但只是切片 header,但切片值(header)将指向相同的后备阵列。这可能是也可能不是您想要的。如果你想 "detach" 从 "original" 切片,你必须显式复制内容,例如使用内置 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. 另外由于数组长度是数组类型的一部分,不同长度的数组是不同的类型。一方面,这可能是一个 "pain in the ass"(例如,您编写了一个接受类型 [4]int 参数的函数,您不能使用该函数获取和处理类型 [5]int 的数组), 但这也可能是一个优势:这可以用来明确指定预期数组的长度。例如。你想写一个接受 IPv4 地址的函数,它可以用 [4]byte 类型建模。现在你有一个 compile-time 保证传递给你的函数的值将恰好有 4 个字节,不多也不少(无论如何这将是一个无效的 IPv4 地址)。

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

6.它们在规划结构类型的内存布局时也非常有用,参见JimB's answer here,以及this answer in greater detail and real-life example.

7. 此外,由于切片是 header 并且它们(几乎)总是在 as-is 周围传递(没有指针),语言规范对切片指针的限制比对数组指针的限制。例如,规范提供了多个 shorthands 用于使用指向数组的指针进行操作,而在切片的情况下同样给出 compile-time 错误(因为很少使用指向切片的指针,如果你仍然想要/必须做它,你必须明确地处理它;阅读 this answer).

中的更多信息

这样的例子是:

  • p 指针切片到数组:p[low:high](*p)[low:high] 的 shorthand。如果 p 是指向切片的指针,则这是 compile-time 错误 (spec: Slice expressions)。

  • 索引指向数组的 p 指针:p[i](*p)[i] 的 shorthand。如果 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. 访问(单个)数组元素比访问切片元素更高效;与切片的情况一样,运行时必须通过隐式指针取消引用。还有"the expressions len(s) and cap(s) are constants if the type of s is an array or pointer to an array".

可能会令人惊讶,但你甚至可以这样写:

type IP [4]byte

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

它是有效的,并且被评估并且 compile-time 即使 IP{} 不是 constant expression 所以例如const i = IP{} 将是 compile-time 错误!在此之后,以下内容也有效也就不足为奇了:

const x2 = len((*IP)(nil)) // x2 will also be 4

注意:当遍历整个数组与整个切片时,可能根本没有性能差异,因为显然它可能会被优化,因此切片中的指针 header 是只取消引用一次。有关详细信息/示例,请参阅 Array vs Slice: accessing speed.


查看相关问题,其中可以使用数组/比切片更有意义:

Why use arrays instead of slices?

Why can't Go slice be used as keys in Go maps pretty much the same way arrays can be used as keys?

Hash with key as an array type

How do I check the equality of three values elegantly?

Slicing a slice pointer passed as argument

这只是出于好奇: a slice can contain itself while an array can't。 (实际上,属性 使比较更容易,因为您不必处理递归数据结构)。

Must-read 博客:

Go Slices: usage and internals

Arrays, slices (and strings): The mechanics of 'append'

数组是值,使用值而不是指针通常很有用。

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

值总是被初始化,所以你不需要初始化,或者 make 它们就像你对切片所做的那样。

数组可以让您更好地控制内存布局,因为您不能直接在带有切片的结构中分配 space,您可以使用数组:

type Foo struct {
    buf [64]byte
}

这里,一个Foo值将包含一个64字节的值,而不是需要单独初始化的切片头。数组还用于填充结构以在与 C 代码互操作时匹配对齐,并防止错误共享以获得更好的缓存性能。

提高性能的另一个方面是您可以比使用切片更好地定义内存布局,因为数据局部性会对内存密集型计算产生非常大的影响。与对数据执行的操作相比,取消引用指针可能需要相当长的时间,并且复制小于缓存行的值产生的成本非常低,因此性能关键代码通常仅出于这个原因就使用数组。