Skip to content

Latest commit

 

History

History
122 lines (86 loc) · 5.59 KB

4-1-ignoring-that-elements-are.md

File metadata and controls

122 lines (86 loc) · 5.59 KB

4.1 在 range 循环中不被重视的元素副本

range 循环是迭代各种数据结构的便捷方式。我们不必处理索引和终止状态。Go 开发人员可能会忘记或不知道 range 循环如何分配值,从而导致常见错误。首先,让我们提醒我们如何使用 range 循环;接下来,我们将深入研究如何分配值。

4.1.1 概念

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 会产生一对值:一个索引和一个元素值,分别分配给 iv。通常,range 为每个数据结构生成两个值,但接收通道只需要接收生成单个元素,即值。

在某些情况下,我们可能只对元素值感兴趣,而不对索引感兴趣。由于不使用局部变量会导致编译错误,我们可以改为使用空白标识符来替换索引变量,如下所示:

s := []string{"a", "b", "c"}
for _, v := range s {
        fmt.Printf("value=%s\n", v)
}

由于空白标识符,我们通过忽略索引并仅将元素值分配给 v 来遍历每个元素。

如果我们对值不感兴趣,我们可以省略第二个元素,如下所示:

for i := range s {}

现在我们对使用 range 循环有了新的认识,让我们看看在迭代过程中返回了什么样的值。

4.1.2 值拷贝

了解在每次迭代期间如何处理值对于有效使用 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 循环,并了解如何评估提供的表达式。