序章
为了更好的理解这篇文章中展现的内容,你应该先阅读以下文章:
四篇文章的索引:
值的概念和指针的语义在 Go 中无处不在。正如之前的文章中说的那样,语义一致性对于完整性和可读性至关重要。它让开发人员,随着代码库的增长,保持一个强大的代码库的心理模型。它可以尽可能减少错误,副作用和未知的表现。
引言
这这篇文章中,我将会探索在 Go 中 for range 语法块同时提供值和指针两种语义形式。我会教你语义的使用并展示给你这个语义如何衍生至更深层次。然后,我会使用一个简单的例子,来展示使用这些语义会出现的错误。
语言技巧
用这段代码来展示 for range 循环的 value 语义格式。
https://play.golang.org/p/_CWCAF6ge3
Listing1
1 | 01 package main |
在图 1 中,程序声明了 user 类型,创建了四个 user 值并且列出每一个 user 的信息。18 行的 for range 循环使用 value 语义。因为在每一个迭代中,创建切片中的原始的 user 值的拷贝,并在循环中操作这个拷贝值。实际上,调用 Println 创建了循环体中的第二个拷贝。如果要将值语义用于 user 值,这就是你想要的。
如果你想要使用指针,那么 for range 循环将会是这样。
Listing 2
1 | 18 for i := range users { |
现在循环已经修改成指针语义。循环中的代码不再操作它自己的复制,而是操作存在切片中的原始的 user 值。但是调用 Println 仍然使用值语义,传递的是一个拷贝。
为了修正这个需求,我们最后修改一下。
Listing 3
1 | 18 for i := range users { |
现在就是始终使用的 user 数据的指针技巧了。
Listing4 一步步展示了值和指针语义作为参考
Listing 4
1 | // Value semantics. // Pointer semantics. |
更深层次的技巧
比这更深层次的语言技巧。让我们看一下下面 listing5 的程序。这段程序初始化了字符串数组,迭代这些字符串并且每次迭代的时候都改变 index 为 1 的字符串的值。
Listing 5
1 | 01 package main |
这段程序将会输出什么呢?
Listing 6
1 | Bfr[Betty] : Aft[Jack] |
正如你所希望的那样,第 10 行代码已经改变了 index 为 1 的字符串的值,你可以从输出的结果中看到。这段代码使用了 for range 的指针语义版本。下面的代码将会使用 for range 循环的值语义版本。
https://play.golang.org/p/opSsIGtNU1
Listing 7
1 | 01 package main |
在每一个循环的迭代中,代码再次改变了 index 1 的字符串的值,但是这次输出的结果却不一样。
Listing 8
1 | Bfr[Betty] : v[Betty] |
你可以看到这次 for range 确实使用了值语义。for range 迭代了它自己的对于数组的拷贝。这就是为什么改变的内容在输出的结果中看不到。
当使用值语义格式迭代切片的时候,就会得到切片的头部的拷贝。这就是为什么我 listing 9 不会产生 panic 的原因。
https://play.golang.org/p/OXhdsneBec
Listing 9
1 | 01 package main |
如果你想到 09 行你就会知道,虽然切片在循环中已经被缩短到长度为 2,但是循环值操作属于它自己的切片的拷贝。这让循环可以使用原先长度来迭代因为备份的数组仍是完整的。
如果代码使用 for range 的指针语义格式,那么这个代码会产生 panic。
https://play.golang.org/p/k5a73PHaka
Listing 10
1 | 01 package main |
for range 在迭代前就获取了切片的长度,但是在循环的时候长度改变了。在第三次迭代的时候,循环体尝试访问与切片长度关联不上元素。
混合语义
下面展示一个完整的错误例子。这段代码混合使用 user 类型的语义,并引起错误。
https://play.golang.org/p/L_WmUkDYFJ
Listing 11
1 | 01 package main |
这个例子不是很做作。在 05 行声明了 user 类型,选择指针语义实现 user 类型的方法集。在 main 程序中,for range 循环体使用了值语义,并且每一个 user 都使用了 addlock()方法。第二个循环体用来 notify 每一个 user,同样使用了值语义。
Listing 12
1 | bill has 0 likes |
输出表明没有 like 增加。我一直强调你应该为给定类型选择一个语义,并坚持使用该类型的数据做所有事情。
下面是代码如何注意与 user 类型的指针语义保持一致的方法。
https://play.golang.org/p/GwAnyBNqPz
Listing 13
1 | 01 package main |
总结
值和指针语义是 Go 语言编程的很大一部分,正如我所展示的,整合进 for range 循环中。当使用 for range 循环的时候,确定你使用了你迭代的给定类型的正确的格式。尽量不要混合使用语义,如果你不注意的话,使用 for range 的时候很容易这么做。
语言赋予你这种选择语义的能力,并能始终如一地工作。这是你想要充分利用的东西。我想让你决定每种类型使用什么语义并保持一致。你对一个数据的语义越一致,您的代码库就会越好。如果您有一个很好的理由来改变语义,那么就将其广泛地记录下来。