Go语言的100个错误使用场景(30-40)|数据类型与字符串使用

前言

大家好,这里是白泽。 《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
  • 数组
  • 数组的指针
  • 切片
  • Map
  • channel 中取值
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)
}

image-20240206171231403

因为真正参与 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

image-20240206173439064

按照 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 将得到
key=1 value=&main.Customer{ID: "3", Balance: 0}
key=1 value=&main.Customer{ID: "3", Balance: 0}
key=1 value=&main.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份内容。

image-20240206181409086

🌟 修正版本:

// 方法一
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 任何时候都不能在使用时假设它的顺序

image-20240206184927915

即使 map 已经被创建好了,两次不同的遍历都会出现不同的结果。这是设计者设计用于警示开发者:任何时候都不要依赖 map 的顺序。

  • 在迭代的过程中插入 map
m := map[int]bool {0: true,1: false,2: true,
}for k, v := range m {if v {m[k+10] = 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[k+10] = 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 字符集中,使用 U+6C49 这个 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)

image-20240207140017000

针对这张图片中的字符串(第二个字符占用两个字节),尝试遍历:

// 方式一
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)
}

🌟 结果展示:

image-20240207140633410

方式一:range 遍历的是 s 的长度,每次遍历增加一个 code point 的长度,遍历的是每个码点的起始索引,因此1之后就是3了。因此打印 s[1] 无法对应字符串中第二个完整的 code point,而是打印出了底层字符数组的对应内容。

方式二:通过 range 可以直接遍历 code point(rune)。

方式三:先将字符串转换成 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%,再接再厉。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://xiahunao.cn/news/2777479.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

【Rust日报】2024-02-08 Loungy:使用 Rust 和 GPUI 开发的 MacOS 启动器

Mira Screenshare&#xff1a;基于 Rust 和 WebRTC 的高性能屏幕分享工具 一群大学生宣布推出了他们的期末项目&#xff1a;Mira Screenshare&#xff0c;一个开源、高性能的屏幕共享工具&#xff0c;由 Rust 和 WebRTC 构建。此项目支持 4k 60 FPS 和 110ms 端到端延迟的屏幕…

这种学习单片机的顺序是否合理?

这种学习单片机的顺序是否合理&#xff1f; 在开始前我有一些资料&#xff0c;是我根据网友给的问题精心整理了一份「单片机的资料从专业入门到高级教程」&#xff0c; 点个关注在评论区回复“888”之后私信回复“888”&#xff0c;全部无偿共享给大家&#xff01;&#xff01…

图像处理之《鲁棒图像隐写术:隐藏频率系数中的信息》论文精读

一、文章摘要 隐写术是一种将秘密信息隐藏到公共多媒体对象中而不会引起第三方怀疑的技术。然而&#xff0c;大多数现有的工作不能提供良好的抗有损JPEG压缩鲁棒性&#xff0c;同时保持相对较大的嵌入容量。提出了一种基于可逆神经网络的端到端鲁棒隐写系统。该方法将秘密信息…

【开源】基于JAVA+Vue+SpringBoot的智慧社区业务综合平台

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 业务类型模块2.2 基础业务模块2.3 预约业务模块2.4 反馈管理模块2.5 社区新闻模块 三、系统设计3.1 用例设计3.2 数据库设计3.2.1 业务类型表3.2.2 基础业务表3.2.3 预约业务表3.2.4 反馈表3.2.5 社区新闻表 四、系统展…

【Python中Selenium元素定位的各种方法】

1、元素定位操作&#xff1a; 2、创建浏览器驱动操作&#xff0c;导入By模块&#xff1a; from selenium import webdriver # 用于界面与浏览器互动 from selenium.webdriver.common.by import By # 用于元素定位 driver webdriver.Chrome() # 调用Chrome类&#xff0c;创…

知识图谱与图神经网络融合:构建智能应用的新前沿

目录 前言1 知识图谱表示学习1.1 典型模型1.2 下游任务 2 图神经网络与知识图谱表示学习2.1 Compgcn&#xff1a;合成图卷积模型2.2 知识图谱嵌入在归纳设置下的推进 3 图神经网络与知识图谱构建3.1 关系抽取的进阶应用3.2 结构信息补全与知识图谱的完整性 4 图神经网络与知识图…

基于css-vars-ponyfill实现换肤

文章目录 一、换肤二、换肤调研2.1、ElementUI2.2、ant.design 三、换肤痛点和思考四、换肤架构五、换肤技术选型和实现5.1、该方案的亮点和规则5.2、核心原理5.3、色组 & 色值平台设计5.4、获取在当前主题自定义变量颜色 六、总结七、最后 一、换肤 网站或者应用一键切换…

【大厂AI课学习笔记】【1.5 AI技术领域】(8)文本分类

8,9,10&#xff0c;将分别讨论自然语言处理领域的3个重要场景。 自然语言处理&#xff0c;Natual Language Processing&#xff0c;NLP&#xff0c;包括自然语言识别和自然语言生成。 用途是从非结构化的文本数据中&#xff0c;发掘洞见&#xff0c;并访问这些信息&#xff0…

Flume安装部署

安装部署 安装包连接&#xff1a;链接&#xff1a;https://pan.baidu.com/s/1m0d5O3Q2eH14BpWsGGfbLw?pwd6666 &#xff08;1&#xff09;将apache-flume-1.10.1-bin.tar.gz上传到linux的/opt/software目录下 &#xff08;2&#xff09;解压apache-flume-1.10.1-bin.tar.gz…

【Spring框架】Spring事务同步

目录 一、什么是Spring事务同步 二、 事务同步管理器 2.1 TransactionSynchronizationManager事务同步管理器 2.1.1 资源同步 2.1.2 事务同步 2.1.3 总结 三、事务同步管理器保障事务的原理 四、spring事务为何使用TransactionSynchronizationManager spring源码实现 …

书生·浦语大模型第三课作业

基础作业&#xff1a; 复现课程知识库助手搭建过程 (截图) 进阶作业&#xff1a; 选择一个垂直领域&#xff0c;收集该领域的专业资料构建专业知识库&#xff0c;并搭建专业问答助手&#xff0c;并在 OpenXLab 上成功部署&#xff08;截图&#xff0c;并提供应用地址&#x…

【Web】基于Mybatis的SQL注入漏洞利用点学习笔记

目录 MyBatis传参占位符区别 不能直接用#{}的情况 in多参数值查询 like %%模糊查询 order by列名参数化 MyBatis传参占位符区别 在 MyBatis 中&#xff0c;#{} 和 ${} 都是用于传参的占位符&#xff0c;但它们之间有很大的区别&#xff0c;主要体现在两个方面&#xff1a…

计算机网络基础 第四章——介质访问控制子层 知识点(上)

4.1局域网技术的发展与演变 1.访问控制的基本概念 介质访问控制(MAC)是所有“共享介质"类型的局域网都必须解决的共性问题。理解 介质访问控制方法的基本概念,需要注意以下两个问题。 (1)对术语“共享介质”、“多路访问”与“冲突"的理解 由于“共享介质”与“多…

如何开发一个游戏平台?

随着科技的进步和互联网的普及&#xff0c;游戏行业正在迅速发展。游戏平台的开发已成为游戏行业的一个重要组成部分。开发一个游戏平台需要深入了解游戏行业&#xff0c;掌握相关技术&#xff0c;并具备创新思维。以下是一些关于如何开发一个游戏平台的建议&#xff1a; 市场调…

STL - map 和 set

1、关联式容器 vector、list、deque、 forward_list(C11)等&#xff0c;这些容器统称为序列式容器&#xff0c;因为其底层为线性序列的数据结构&#xff0c;里面 存储的是元素本身 关联式容器也是用来存储数据的&#xff0c;与序列式容器不同的是&#xff0c;其里面存储的是<…

Nginx 配置 SSL证书

成功配置SSL证书后&#xff0c;您将能够通过HTTPS加密通道安全访问Nginx服务器。 一、准备材料 SSL证书绑定的域名已完成DNS解析&#xff0c;即您的域名与主机IP地址相互映射。您可以通过DNS验证证书工具&#xff0c;检测域名DNS解析是否生效。具体操作&#xff1a; 【1】登录…

Three.js学习8:基础贴图

一、贴图 贴图&#xff08;Texture Mapping&#xff09;&#xff0c;也翻译为纹理映射&#xff0c;“贴图”这个翻译更直观。 贴图&#xff0c;就是把图片贴在 3D 物体材质的表面&#xff0c;让它具有一定的纹理&#xff0c;来为 3D 物体添加细节的一种方法。这使我们能够添加…

numpy 查漏补缺

1. iterating 2. 3. 4. 5. 6. 7. 8. 9.

Blender教程(基础)--试图的显示模式-22

一、透视模式&#xff08;AltZ&#xff09; 透视模式下可以实现选中透视的物体信息 发现选中了透视区的所有顶点 二、试图着色模式-显示网格边框 三、试图着色模式-显示实体 三、试图着色模式-材质预览 四、试图着色模式-显示渲染预览

test222

欢迎关注博主 Mindtechnist 或加入【Linux C/C/Python社区】一起探讨和分享Linux C/C/Python/Shell编程、机器人技术、机器学习、机器视觉、嵌入式AI相关领域的知识和技术。 磁盘满的本质分析 专栏&#xff1a;《Linux从小白到大神》 | 系统学习Linux开发、VIM/GCC/GDB/Make工具…