民政 门户网站 建设,模板网站建设服务商,品划网络做营销型网站,个人网站如何在工信部备案前言
大家好#xff0c;这里是白泽。 《Go语言的100个错误以及如何避免》 是最近朋友推荐我阅读的书籍#xff0c;我初步浏览之后#xff0c;大为惊喜。就像这书中第一章的标题说到的#xff1a;“Go: Simple to learn but hard to master”#xff0c;整本书通过分析100…前言
大家好这里是白泽。 《Go语言的100个错误以及如何避免》 是最近朋友推荐我阅读的书籍我初步浏览之后大为惊喜。就像这书中第一章的标题说到的“Go: Simple to learn but hard to master”整本书通过分析100个错误使用 Go 语言的场景带你深入理解 Go 语言。
我的愿景是以这套文章在保持权威性的基础上脱离对原文的依赖对这100个场景进行篇幅合适的中文讲解。所涉内容较多总计约 8w 字这是该系列的第四篇文章对应书中第30-39个错误场景。 当然如果您是一位 Go 学习的新手您可以在我开源的学习仓库中找到针对 《Go 程序设计语言》 英文书籍的配套笔记其他所有文章也会整理收集在其中。 B站白泽talk公众号【白泽talk】聊天交流群622383022原书电子版可以加群获取。 前文链接
《Go语言的100个错误使用场景1-10代码和项目组织》《Go语言的100个错误使用场景11-20项目组织和数据类型》《Go语言的100个错误使用场景21-29数据类型》
4. 控制结构 章节概述 range 循环如何赋值处理 range 循环和引用避免遍历 map 导致问题在循环内使用 defer 4.1 忽视元素在range循环中是拷贝#30
range 循环允许遍历的数据类型
String数组数组的指针切片Mapchannel 中取值
s : []string{a, b, c}
// 保留值
for _, v range s {fmt.Println(value%s\n, v)
}
// 保留索引
for i : range s {fmt.Println(index%d\n, i)
}值拷贝
type account struct {balance float32
}
accounts : []account{{balance: 100.},{balance: 200.},{balance: 300.},
}
for _, a : range accounts {a.balance 1000
}
// 打印accounts切片得到的结果为 [{100}, {200}, {300}]赋值没有影响到切片
[{100}, {200}, {300}]Go 语言当中所有的赋值都是拷贝
如果函数返回一个结构体表示这个结构体的拷贝。如果函数返回一个结构体的指针表示一个内存地址的拷贝。
因此上述 range 循环赋值的过程只是将 1000 添加到了 a 这个拷贝的变量上。 修正方案
for i : range accounts {accounts[i].balance 1000
}
for i : 0; i len(accounts); i {accounts[i].balance 1000
}如果业务逻辑简单则推荐第一种因为编码更少如果逻辑复杂则推荐第二种因为可能需要对 i 的大小进行逻辑判断。
特殊情况
accounts : []*account{{balance: 100.},{balance: 200.},{balance: 300.},
}
for _, a : range accounts {a.balance 1000
}
// 打印accounts切片
[{1100}, {1200}, {1300}]遍历指针类型的 accounts 可以将修改在切片上生效因为指向的内存是同一份。但是性能会比直接修改 struct 更低这一点将在#91讲 CPU 缓存时着重讲解。
4.2 忽略在 range 循环中如何评估表达式#31
range 循环使用需要一个表达式例如 for i in range exp表达式只会在 range 循环前确定循环中不会变更。
s : []int{0, 1, 2}
// 这个循环只会执行3次而不会永无止境
for range s {s append(s, 10)
}因为真正参与 range 遍历的是 s 切片的一个拷贝指向同一份底层数组因此遍历结束后s 切片确实增加了3的长度。
反例
s : []int{0, 1, 2}
// 这个循环只会执行3次而不会永无止境
for i : 0; i len(s); i {s append(s, 10)
}使用传统方式遍历切片不断追加10会导致循环永远无法结束因为表达式 len(s) 每次循环都会重新确定一次值。range 只会在循环开始前确定一次。
⚠️ 注意range 的行为与具体表达式部分的数据类型也有关系下面分析 channel 和 array。
channel
ch1 : make(chan int, 3)
go func() {ch1 - 0ch1 - 1ch1 - 2close(ch1)
}()
ch2 : make(chan int, 3)
go func() {ch2 - 10ch2 - 11ch2 - 12close(ch2)
}()
ch : ch1
for v : range ch {fmt.Println(v)ch ch2
}
// 结果输出
0 1 2与上面提到的 range 表达式值确定规则一样这里只会在 range 开始前将一个 ch 的拷贝变量参与到 range 循环当中循环内部 ch ch2 确实修改了外部 ch 的指向所以如果有代码执行 close(ch) 则会关闭 ch2。
Array
a : [3]int{0, 1, 2}
for i, v : range a {a[2] 10if i 2 {fmt.Println(v)}
}
// 输出结果2而不是10按照 range 的表达式渲染规则循环之前会有一个长度为3的数组的拷贝创建用于参与循环因为赋值操作针对的是 a 数组所以对拷贝的数组没有影响因为只会在循环前确定一次值。
a : [3]int{0, 1, 2}
for i, v : range a {a[2] 10if i 2 {fmt.Println(v)}
}通过获取数组 a 的地址则即使发生一次拷贝其指向的还是原来的 a 数组地址所以 v 变量打印就是10。
4.3 忽略在 range 中使用指针元素的影响#32
错误示例
type Customer struct {ID stringBalance float64
}
type Sotre struct {m map[string]*Customer
}
func (s *Store) storeCustomers(customers []Customer) {for _, customer : range customers {s.m[customer.ID] customer}
}
--------------------------------------------
// 假设以如下代码运行
s.storeCustomers([]Customer{{ID: 1, Balance: 10},{ID: 2, Balance: -10},{ID: 3, Balance: 0},
})
// 打印这个 map 将得到
key1 valuemain.Customer{ID: 3, Balance: 0}
key1 valuemain.Customer{ID: 3, Balance: 0}
key1 valuemain.Customer{ID: 3, Balance: 0}因为在 range 循环的时候循环内的 customer 创建一次是一个固定的地址它不断被 range 表达式的拷贝变量赋值。
func (s *Store) storeCustomers(customers []Customer) {for _, customer : range customers {fmt.Printf(%p\n, customer)s.m[customer.ID] customer}
}
// 输出结果
0xc000096029
0xc000096029
0xc000096029因此循环结束之后三个 key 指向的 value 都是同一个地址自然也得到重复的3份内容。 修正版本
// 方法一
func (s *Store) storeCustomers(customers []Customer) {for _, customer : range customers {current : customer // 创建新的临时变量确保地址唯一s.m[current.ID] current}
}
// 方法二
func (s *Store) storeCustomers(customers []Customer) {for i : range customers {customer : customers[i] // 通过索引获取不同地址的 customer因此也能确保地址唯一s.m[customer.ID] customer}
}4.4 对 map 遍历的错误假设#33
Map 任何时候都不能在使用时假设它的顺序 即使 map 已经被创建好了两次不同的遍历都会出现不同的结果。这是设计者设计用于警示开发者任何时候都不要依赖 map 的顺序。
在迭代的过程中插入 map
m : map[int]bool {0: true,1: false,2: true,
}
for k, v : range m {if v {m[k10] true}
}
fmt.Println(m)
// 运行3次得到不同答案
map[0: true 1:false 2:true 10: true 12:true 20:true 22:true 30:true]
map[0: true 1:false 2:true 10: true 12:true 20:true 22:true 30:true 32:true]
map[0: true 1:false 2:true 10: true 12:true 20:true]结果不同的原因在遍历 map 的时候向 map 插入元素可能会成功可能会被忽略不可预计。
修正方案
m : map[int]bool {0: true,1: false,2: true,
}
m2 : copyMap(m)
for k, v : range m {m2[k] vif v {m2[k10] true}
}
fmt.Println(m2)
// 结果
map[0: true 1:false 2:true 10: true 12:true]通过拷贝一个新的 m2将新增的元素添加到新的 map 上即可。
4.5 忽略 break 的作用#34
概念break 会结束 for、switch、select 的循环。
错误示例
for i : 0; i 5; i {fmt.Printf(%d , i)switch i {default:case 2: // 当索引是2的时候结束循环break}
}上述代码无法在索引为2的时候终止循环因为 break 只会结束 switch 的逻辑不会影响到外部的 for。
修正方案
loop:for i : 0; i 5; i {fmt.Printf(%d , i)switch i {default:case 2: // 当索引是2的时候结束循环break loop}
}这种携带标签 loop 的 break 与 goto 不同之处在于loop 可以替换成其他名称使开发者可读性更友好是 Go 中的地道用法。
针对 select 的示例
loop:for {select {case -ch:// Do somethingcase -ctx.Done():break loop}}4.6 在循环中使用 defer#35
错误示例
func readFiles(ch -chan string) error {for path : range ch {file, err : os.Open(path)if err ! nil {return err}defer file.Close()// 处理 file}return nil
}上述代码 file.Close() 需要等到 readFiles 函数 return 返回之前执行如果不 return则所有的文件描述符 file 将一直保持 Open 状态不断以栈的方式堆积后进先出 造成内存泄漏。
修正方案
func readFiles(ch -chan string) error {for path : range ch {if err : readFile(path); err ! nil {return err}}return nil
}
func readFile(path string) error {file, err : os.Open(path)if err ! nil {return err}defer file.Close()// 处理 filereturn nil
}通过将读取文件处理的步骤封装成一个函数则可以在文件处理完成之后在函数返回前单独调用 defer关闭 file。
通过必包实现
func readFiles(ch -chan string) error {for path : range ch {err : func() error {// ...defer file.Close()// ...}()if err ! nil {return err}}return nil
}使用闭包的本质是一样的前一个方式更加清晰也方便添加单测。
5. 字符串 章节概述 了解 rune 的概念避免常见的字符串遍历和截取造成的错误避免由于字符串拼接和转换造成的低效代码避免获取子字符串造成的内存泄漏 5.1 不理解 rune 的概念#36
在 Go 语言当中一个 rune 是一个 Unicode 的码点code point比如说“汉”这个字符在 Unicode 字符集中使用 U6C49 这个 code point 定义在 UTF-8 编码当中使用0xE60xB10x89 三个字节表示。
UTF-8 编码格式将字符用1-4个字节表示最多32位因此 Go 语言当中一个 rune 是 int32 的别名。
type rune int32打印字符串的长度
s : 汉
fmt.Println(len(s))
// 结果3因为 Go 语言内置的 len 函数获取字符串的长度计算的是这个字符串底层字节数组的字节数量。
5.2 不准确的字符串迭代#37 针对这张图片中的字符串第二个字符占用两个字节尝试遍历
// 方式一
for i : range s {fmt.Printf(position %d: %c\n, i, s[i])
}
fmt.Printf(len%d\n, len(s))
// 方式二
for i, v : range s {fmt.Printf(position %d: %c\n, i, v)
}
// 方式三
runes : []rune(s)
for i, v : range runes {fmt.Printf(position %d: %c\n, i, v)
}结果展示 方式一range 遍历的是 s 的长度每次遍历增加一个 code point 的长度遍历的是每个码点的起始索引因此1之后就是3了。因此打印 s[1] 无法对应字符串中第二个完整的 code point而是打印出了底层字符数组的对应内容。
方式二通过 range 可以直接遍历 code pointrune。
方式三先将字符串转换成 rune 切片此时遍历则会一一对应。但是转换成 rune 切片会有额外 O(N) 的空间和时间开销如果只是希望遍历 rune则方式二即可如果是希望获取 rune 的索引编号则再使用方式三。
5.3 误用裁剪函数#38
fmt.Println(strings.TrimRight(123oxo, xo)) // 123
fmt.Println(strings.TrimSuffix(123xoxo, xo)) // 123xo
fmt.Println(strings.TrimLeft(xox123, xo)) // 123
fmt.Println(strings.TrimPrefix(xoxo123, xo)) // xo123
fmt.Println(strings.Trim(oxo123oxo, xo)) // 123代码一从右边开始截取出现在 xo 字符集合中的 rune直到不存在。
代码二从右边开始截取一次不会重复操作。
代码三从左边截取出现在 xo 字符集合中的 rune直到不存在。
代码四从左边开始截取一次匹配到才会裁剪同样不会重复操作。
代码五从整个字符串中去匹配出现在 xo 字符集合中的 rune全部裁剪。
5.4 优化不足的字符串拼接#39
错误示例
func concat1(values []string) string {s : for _, value : range values {s value}return s
}由于 Go 语言的字符串是不可变的因此上述循环中会不断的重新分配内存去存储拼接后的字符串性能较低。
修正方案
func concat2(values []string) string {sb : strings.Builder{}for _, value : range values {// 关于 error 的处理和忽略将在#53讲解_, _ sb.WriteString(value)}return sb.String()
}strings.Builder{} 在底层会通过字符切片存放字符串并且不断的通过 append 方法追加字符切片。
注意点
不可以并发调用注意性能问题
func concat3(values []string) string {tatal : 0for i : 0; i len(values); i {total len(values[i])}sb : strings.Builder{}sb.Grow(total)for _, value : range values {// 关于 error 的处理和忽略将在#53讲解_, _ sb.WriteString(value)}return sb.String()
}在拼接字符串之前调用 Grow 方法为底层字符切片分配 total 的长度空间这样可以避免字符切片扩容造成的开销。
拼接1000个字符串每个1000字节性能比较banchmark
concat1 16 72291485 ns/op
concat2 1188 878962 ns/op
concat3 5922 190340 ns/op最佳实践
在字符串个数超过5个的时候使用 strings.Builder{}并且在总长度可以预计的情况下优先使用 Grow 方法预分配空间。
小结
你已完成全书学习进度40%再接再厉。