range
循环是迭代各种数据结构的便捷方式。我们不必处理索引和终止状态。Go 开发人员可能会忘记或不知道 range
循环如何分配值,从而导致常见错误。首先,让我们提醒我们如何使用 range
循环;接下来,我们将深入研究如何分配值。
range
循环允许迭代不同的数据结构:
- 字符串(String)
- 数组(Array)
- 数组指针
- 切片(Slice)
- 映射(Map)
- 接收通道(Receiving channel)
与经典的 for
循环相比,range
循环是一种方便的方式来迭代,其中一个数据结构的所有元素,这要归功于它的简洁性句法。此外,它更不容易出错,因为我们不必手动处理条件表达式和迭代变量,这可以避免诸如"差一个"(off-by-one errors)错误之类的错误。这是一个对字符串切片进行迭代的示例:
s := []string{"a", "b", "c"}
for i, v := range s {
fmt.Printf("index=%d, value=%s\n", i, v)
}
此代码循环切片的每个元素。在每次迭代中,当我们迭代切片时,range
会产生一对值:一个索引和一个元素值,分别分配给 i
和 v
。通常,range
为每个数据结构生成两个值,但接收通道只需要接收生成单个元素,即值。
在某些情况下,我们可能只对元素值感兴趣,而不对索引感兴趣。由于不使用局部变量会导致编译错误,我们可以改为使用空白标识符来替换索引变量,如下所示:
s := []string{"a", "b", "c"}
for _, v := range s {
fmt.Printf("value=%s\n", v)
}
由于空白标识符,我们通过忽略索引并仅将元素值分配给 v
来遍历每个元素。
如果我们对值不感兴趣,我们可以省略第二个元素,如下所示:
for i := range s {}
现在我们对使用 range
循环有了新的认识,让我们看看在迭代过程中返回了什么样的值。
了解在每次迭代期间如何处理值对于有效使用 range
循环至关重要。让我们用一个具体的例子来看看它是如何工作的。
我们将创建一个包含单个 balance
字段的 account
结构:
type account struct {
balance float32
}
接下来,我们将创建一个 account
结构切片,并使用 range
循环遍历每个元素。 在每次迭代中,我们将增加每个 account
的余额:
accounts := []account{
{balance: 100.},
{balance: 200.},
{balance: 300.},
}
for _, a := range accounts {
a.balance += 1000
}
在您看来,按照这段代码,切片的内容应该是什么?
[{100} {200} {300}]
[{1100} {1200} {1300}]
答案是前者:[{100} {200} {300}]
在此示例中, range
循环不会影响切片的内容。让我们了解一下为什么。
在 Go 中,我们分配的所有内容都是一个副本。例如,如果我们分配一个函数返回的结果:
- 一个结构,它将执行这个结构的副本
- 一个指针,它将执行内存地址的复制(在64位架构上,地址是64位长)
记住这一点对于避免常见错误至关重要,包括与 range
循环相关的错误。实际上,当范围循环遍历数据结构时,它会将每个元素复制到值变量(第二项)。
回到我们的示例,当我们迭代每个 account
元素时,它会导致分配给值变量 a
的结构副本。 因此,当我们使用 account.balance += 1000
增加余额时,它只改变了 value 变量 (a
),而不是切片中的元素。
那么如果我们想要更新切片元素呢?有两个主要选项。
第一个选项是使用切片索引访问元素。它可以通过经典的 for
循环或使用索引而不是值变量的 range
循环来实现:
for i := range accounts {
accounts[i].balance += 1000
}
for i := 0; i < len(accounts); i++ {
accounts[i].balance += 1000
}
在前面的示例中,两次迭代具有相同的效果:更新 accounts
切片内的元素。
我们应该支持哪一个?这取决于上下文。如果我们想遍历每个元素,那么第一个循环的读写时间会更短。然而,如果我们需要控制我们想要更新哪个元素(例如,二选一),我们应该使用第二个循环。
Note 另一种选择可能是继续使用
range
循环并访问值,但将切片类型修改为account
指针切片:
accounts := []*account{
{balance: 100.},
{balance: 200.},
{balance: 300.},
}
for _, a := range accounts {
a.balance += 1000
}
在这种情况下,正如我们所提到的,
a
变量是存储在切片中的account
指针的副本。但是,由于两个指针都引用同一个结构,所以a.balance += 1000
语句将更新切片元素本身。 然而,这样的选择有两个主要缺点。首先,它需要更新切片类型,这可能并不总是可行的。其次,如果性能很重要,我们应该注意,由于缺乏可预测性,迭代指针切片对 CPU 的效率可能较低(我们将在不了解 CPU 缓存中深入研究这一点)。
一般来说,我们应该记住 range
循环中的值元素是一个副本。因此,如果值是我们需要改变的结构体,我们只会更新副本,而不是元素本身,除非我们修改的值或字段是指针。首选选项是使用 range
循环或经典 for
循环通过索引访问元素。
在下一节中,我们将继续使用 range
循环,并了解如何评估提供的表达式。