golang:GO的ForRange语义设计
source link: https://ljlu1504.github.io/2019/04/07/interface-semantics-on-go
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
golang:GO的ForRange语义设计
https://www.ardanlabs.com/blog/2017/06/for-range-semantics.html
在阅读这篇文章前会好先阅读这四篇文章,它们将理解这篇文章。
四部分系列索引:
在Go编程语言中,值和指针语义的概念无处不在。 如前所述,语义一致性对于完整性和可读性至关重要。 它允许开发人员在代码库不断增长时保持强大的代码库设计思路。 它还有助于最大限度地减少错误,副作用和未知行为。
在这篇文章中,我将探讨Go中的for range语句如何提供值和指针语义形式。 我将教授语言机制,并向您展示这些语义的深层含义。 然后我将展示一个简单的例子,说明混合这些语义和可能导致的问题是多么容易。
GO语言机制(Language Mechanics)
从这段代码开始,该代码显示for range循环的值语义形式。 go play
01 package main
02
03 import "fmt"
04
05 type user struct {
06 name string
07 email string
08 }
09
10 func main() {
11 users := []user{
12 {"Bill", "[email protected]"},
13 {"Lisa", "[email protected]"},
14 {"Nancy", "[email protected]"},
15 {"Paul", "[email protected]"},
16 }
17
18 for i, u := range users {
19 fmt.Println(i, u)
20 }
21 }
在以上示例中,程序声明了一个名为user的类型,创建了四个用户值,然后显示有关每个用户的信息。 第18行的for range循环使用值语义。 这是因为在每次迭代时,来自slice的原始用户值的副本在循环内部进行并操作。 实际上,对Println的调用会创建循环副本的第二个副本。 如果要将值语义用于用户值,那么这就是您想要的。
如果您要使用指针语义,则for range循环将如下所示。
18 for i := range users {
19 fmt.Println(i, users[i])
20 }
现在循环已被修改为使用指针语义。 循环内的代码不再在其自己的副本上运行,而是在存储在slice内的原始user 值上运行。 但是,对Println的调用仍然使用值语义并且正在传递副本。 要解决此问题,需要进一步修改。
18 for i := range users {
19 fmt.Println(i, &users[i])
20 }
现在,用户数据一直使用指针机制。
作为参考,下面并排显示了值和指针语义。
// Value semantics. // Pointer semantics.
18 for i, u := range users { for i := range users {
19 fmt.Println(i, u) fmt.Println(i, &users[i])
20 } }
##Deeper Mechanics 让我们来看看比上面例子更深层次的Go语言机制。请看下面的这个程序。程序初始化一个字符串数组,迭代这些字符串,并在每次迭代时更改索引1处的字符串。 go play
01 package main
02
03 import "fmt"
04
05 func main() {
06 five := [5]string{"Annie", "Betty", "Charley", "Doug", "Edward"}
07 fmt.Printf("Bfr[%s] : ", five[1])
08
09 for i := range five {
10 five[1] = "Jack"
11
12 if i == 1 {
13 fmt.Printf("Aft[%s]\n", five[1])
14 }
15 }
16 }
这段程序预计的输出是什么?
Bfr[Betty] : Aft[Jack]
正如您所料,第10行的代码已更改索引1处的字符串,您可以在输出中看到该字符串。 该程序使用for range循环的指针
语义版本。 接下来,代码将使用for range循环的值
语义版本。
go play
01 package main
02
03 import "fmt"
04
05 func main() {
06 five := [5]string{"Annie", "Betty", "Charley", "Doug", "Edward"}
07 fmt.Printf("Bfr[%s] : ", five[1])
08
09 for i, v := range five {
10 five[1] = "Jack"
11
12 if i == 1 {
13 fmt.Printf("v[%s]\n", v)
14 }
15 }
16 }
在循环的每次迭代中,代码再次更改索引1处的字符串。这次当代码显示索引1处的值时,输出是不同的。
Bfr[Betty] : v[Betty]
您可以看到for range
的这种形式是真正使用值语义。 for range
是迭代它自己的数组副本
。 这就是输出中没有看到变化的原因。
当使用值语义形式在slice上进行ranging
时,for range
采用slice header
的副本。 这就是一下代码不会引起panic的原因。
go play
01 package main
02
03 import "fmt"
04
05 func main() {
06 five := []string{"Annie", "Betty", "Charley", "Doug", "Edward"}
07
08 for _, v := range five {
09 five = five[:2]
10 fmt.Printf("v[%s]\n", v)
11 }
12 }
Output:
v[Annie]
v[Betty]
v[Charley]
v[Doug]
v[Edward]
如果你看第09
行,切片值在循环内减少到2的长度,但是循环在它自己的slice
副本上运行。 这允许循环使用原始长度进行迭代而没有任何问题,因为底层Array
并没有被修改。
如果代码使用for range
的指针语义形式,则程序会发生panics
。
go play
01 package main
02
03 import "fmt"
04
05 func main() {
06 five := []string{"Annie", "Betty", "Charley", "Doug", "Edward"}
07
08 for i := range five {
09 five = five[:2]
10 fmt.Printf("v[%s]\n", five[i])
11 }
12 }
Output:
v[Annie]
v[Betty]
panic: runtime error: index out of range
goroutine 1 [running]:
main.main()
/tmp/sandbox688667612/main.go:10 +0x140
for range
在迭代之前使用slice的长度,但在循环期间,slice长度发生了变化。 在第三次迭代中,循环尝试访问不再与slice长度相关联的元素。
##Mixing Semantics 这是一个反例。 此代码混合了user类型的语义,并导致错误。
01 package main
02
03 import "fmt"
04
05 type user struct {
06 name string
07 likes int
08 }
09
10 func (u *user) notify() {
11 fmt.Printf("%s has %d likes\n", u.name, u.likes)
12 }
13
14 func (u *user) addLike() {
15 u.likes++
16 }
17
18 func main() {
19 users := []user{
20 {name: "bill"},
21 {name: "lisa"},
22 }
23
24 for _, u := range users {
25 u.addLike()
26 }
27
28 for _, u := range users {
29 u.notify()
30 }
31 }
这个例子并不是专门设计的。 在第05
行,声明user
类型,并选择指针语义来实现user
类型的方法集。 然后在main
程序中,在for range
循环中使用值语义来向每个用户添加like
。 然后使用第二个循环来再次使用值语义来通知每个user
。
bill has 0 likes
lisa has 0 likes
输出显示没有添加任何like
。 我不能强调你应该为给定类型选择一个语义,并坚持使用该类型的数据。
以下代码展示了user类型的指针语义保持一致的用法。 go play
01 package main
02
03 import "fmt"
04
05 type user struct {
06 name string
07 likes int
08 }
09
10 func (u *user) notify() {
11 fmt.Printf("%s has %d likes\n", u.name, u.likes)
12 }
13
14 func (u *user) addLike() {
15 u.likes++
16 }
17
18 func main() {
19 users := []user{
20 {name: "bill"},
21 {name: "lisa"},
22 }
23
24 for i := range users {
25 users[i].addLike()
26 }
27
28 for i := range users {
29 users[i].notify()
30 }
31 }
// Output:
bill has 1 likes
lisa has 1 likes
值和指针语义是Go
编程语言的重要组成部分,正如我所示,它集成到for range
循环中。 使用for range
时,确保您正在对给定迭代的类型使用正确的格式。最后想要说的是如果你没有加倍注意,用for range
很容易导致语义的混用。
Go
语言赋予您选择语义的能力,并且可以干净利落地使用它。 这是你想要充分利用的东西。 我希望您确定每种类型使用的语义并保持一致。 您对一段数据的语义越一致,您的代码库就越好。 如果您有充分的理由更改语义,请进行广泛的记录。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK