Go语言的100个错误使用场景(40-47)|字符串函数方法

前言

大家好,这里是白泽。 《Go语言的100个错误以及如何避免》 是最近朋友推荐我阅读的书籍,我初步浏览之后,大为惊喜。就像这书中第一章的标题说到的:“Go: Simple to learn but hard to master”,整本书通过分析100个错误使用 Go 语言的场景,带你深入理解 Go 语言。

我的愿景是以这套文章,在保持权威性的基础上,脱离对原文的依赖,对这100个场景进行篇幅合适的中文讲解。所涉内容较多,总计约 8w 字,这是该系列的第五篇文章,对应书中第40-47个错误场景。

🌟 当然,如果您是一位 Go 学习的新手,您可以在我开源的学习仓库中,找到针对 《Go 程序设计语言》 英文书籍的配套笔记,其他所有文章也会整理收集在其中。

📺 B站:白泽talk,公众号【白泽talk】,聊天交流群:622383022,原书电子版可以加群获取。

前文链接:

  • 《Go语言的100个错误使用场景(1-10)|代码和项目组织》
  • 《Go语言的100个错误使用场景(11-20)|项目组织和数据类型》
  • 《Go语言的100个错误使用场景(21-29)|数据类型》
  • 《Go语言的100个错误使用场景(30-40)|数据类型与字符串使用》

5. 字符串

🌟 章节概述:

  • 了解 rune 的概念
  • 避免常见的字符串遍历和截取造成的错误
  • 避免由于字符串拼接和转换造成的低效代码
  • 避免获取子字符串造成的内存泄漏

5.5 无用的字符串转换(#40)

错误示例:

func getBytes(reader io.Reader) ([]byte, error) {b, err := io.ReadAll(reader)if err != nil {return nil, err}// 去除首尾空格return []byte(sanitize(string(b))), nil
}func sanitize(s string) string {return strings.TrimSpace(s)
}

正确示例:

func getBytes(reader io.Reader) ([]byte, error) {b, err := io.ReadAll(reader)if err != nil {return nil, err}// 去除首尾空格return sanitize(b), nil
}func sanitize(b []byte) []byte {return bytes.TrimSpace(b)
}

通常来说 bytes 库提供了与 strings 库相同功能的方法,而且大多数 IO 相关的函数的输入输出都是 []byte,而不是 string,错误示例中,将字符切片转换成字符串,再转换成字符切片,需要额外承担两次内存分配的开销。

5.6 获取子字符串操作和内存泄漏(#41)

假设有许多个 string 类型的 log 需要存储(假设一个log有1000字节),但是只需要存放 log 的前36字节,不恰当的子字符串截取函数,会导致内存泄漏。

示例代码:

// 方式一
func (s store) handleLog(log string) error {if len(log) < 36 {return errors.New("log is not correctly formatted")}uuid := log[:36]s.store(uuid)// Do something
}
// 方式二
func (s store) handleLog(log string) error {if len(log) < 36 {return errors.New("log is not correctly formatted")}uuid := string([]byte(log[:36]))s.store(uuid)// Do something
}
// 方式三
func (s store) handleLog(log string) error {if len(log) < 36 {return errors.New("log is not correctly formatted")}uuid := strings.Clone(log[:36])s.store(uuid)// Do something
}
  1. 和(#26)提到的子切片获取造成的内存泄漏一样,获取子字符串操作执行后,其底层依旧依赖原来的整个字符数组,因此1000个字节内存依旧占用,不会只有36个。
  2. 通过将字符串转换为字节数组,再转换为字符串,虽然消耗了2次长度为36字节的内存分配,但是释放了底层1000字节的原字节数组的依赖。有些 IDE 如 Goland 会提示语法错误,因为本质来说,将 string 转 []byte 再转 string 是一个累赘的操作。
  3. go1.18之后,提供了一步到位的 strings.Clone 方法,可以避免内存泄漏。

6. 函数和方法

🌟 章节概述:

  • 什么时候使用值或者指针类型的接受者
  • 什么时候命名的返回值,以及其副作用
  • 避免返回 nil 接受者时的常见错误
  • 函数接受一个文件名,并不是最佳实践
  • 处理 defer 的参数

6.1 不知道选择哪种类型的方法接受者(#42)

值接受者:

type customer struct {balance float64
}func (c customer) add(operation float64) {c.balance += operation
}func main() {c := customer{balance: 100.0}c.add(50.0)fmt.Printf("%.2f\n", c.balance) // 结果为 100.00
}

指针接受者:

type customer struct {balance float64
}func (c *customer) add(operation float64) {c.balance += operation
}func main() {c := customer{balance: 100.0}c.add(50.0)fmt.Printf("%.2f\n", c.balance) // 结果为 150.00
}

值接受者在方法内修改自身结构的值,不会对调用方造成实际影响。

🌟 一些实践的建议:

  • 必须使用指针接受者的场景:

    • 如果方法需要修改原始的接受者。
    • 如果方法的接受者包含不可以被拷贝的字段。
  • 建议使用指针接受者的场景:

    • 如果接受者是一个巨大的对象,使用指针接受者可以更加高效,避免了拷贝内存。
  • 必须使用值接受者的场景:

    • 如果我们必须确保接受者是不变的。
    • 如果接受者是一个 map, function, channel,否则会出现编译错误。
  • 建议使用值接受者的场景:

    • 如果接受者是一个切片,且不会被修改。
    • 如果接受者是一个小的数组或者结构体,不含有易变的字段。
    • 如果接受者是基本类型如:int, float64, string。

特殊情况:

type customer struct {data *data
}type data struct {balance float64
}func (c customer) add(operation float64) {c.data.balance += operation
}func main() {c := customer{data: &data {balance: 100.0}}c.add(50.0)fmt.Printf("%.2f\n", c.data.balance) // 150.00
}

在这种情况下,即使方法接受者 c 不是指针类型,但是修改依旧可以生效。

但是为了清楚起见,通常还是将 c 声明成指针类型,如果它是可操作的。

6.2 从来不使用命名的返回值(#43)

如果使用命名返回值:

func f(a int) (b int) {b = areturn
}

推荐使用命名返回值的场景举例:

// 场景一
type locator interface {getCoordinates(address string) (lat, lng float32, err error)
}
// 场景二
func ReadFull(r io.Reader, buf []byte) (n int, err error) {// 两个返回值被初始化为对应类型的零值:0和nilfor len(buf) > 0 && err == nil {var nr intnr, err = r.Read(buf)n += nrbuf = buf[nr:]}return
}

场景一:通过命名返回值提高接口的可读性

场景二:通过命名返回值节省编码量

🌟 最佳实践:需要权衡使用命名返回值是否能带来收益,如果可以就果断使用吧!

6.3 使用命名返回值造成的意外副作用(#44)

🌟 注意:使用命名返回值的方法,并不意味着必须返回单个 return,有时可以只为了函数签名清晰而使用命名返回值。

错误场景:

func (l loc) getCoordinates(ctx content.Content, address string) (lat, lng float32, err error) {isValid := l.validateAddress(address)if !isValid {return 0, 0, errors.New("invalid address")}if ctx.Err() != nil {return 0, 0, err}// Do something and return
}

此时,由于 ctx.Err() != nil 成立时,并没有为 err 赋值,因此返回的 err 永远都是 nil。

修正方案:

func (l loc) getCoordinates(ctx content.Content, address string) (lat, lng float32, err error) {isValid := l.validateAddress(address)if !isValid {return 0, 0, errors.New("invalid address")}if err = ctx.Err(); err != nil {// 这里原则上可以返回单个return,但是最好保持风格统一return 0, 0, err}// Do something and return
}

6.4 返回一个 nil 接受者(#45)

🔔 提示:在 Go 语言当中,方法就像是函数的语法糖一样,相当于函数的第一个参数是方法的接受者,nil 可以作为参数,因此 nil 接受者可以触发方法,因此不同于纯粹的 nil interface。

type Foo struct {}func (foo *Foo) Bar() string {return "bar"
}func main() {var foo *Foofmt.Println(foo.Bar()) // 虽然 foo 动态值是 nil,但动态类型不是nil,是可以打印出 bar
}

错误示例:

type MultiError struct {errs []string
}func (m *MultiError) Add(err error) {m.errs = append(m.errs, err.Error())
}func (m *MultiError) Error() string {return stirngs.Join(m.errs, ";")
}func (c Customer) Validate() error {var m *MultiErrorif c.Age < 0 {m = &MultiError{}m.Add(errors.New("age is negative"))}if c.Name == "" {if m == nil {m = &MultiError{}}m.Add(errors.New("age is nil"))}return m
}func main() {// 传入的两个参数都不会触发 Validate 的 err 校验customer := Customer{Age: 33, Name: "John"}if err := customer.Validate(); err != nil {// 但是无论如何都会打印这行语句,err != nil 永远成立!log.Fatalf("customer is invalid: %v", err)}
}

🔔 提示:Go 语言的接口,有动态类型和动态值两个概念,

image-20240214163507390

上述错误示例中,即使通过了两个验证,Validate 返回了 m,此时这个接口承载的动态类型是 *MultiError,它的动态值是 nil,但是通过 == 判断一个 err 为 nil,或者说一个接口为 nil,要求其底层类型和值都是 nil 才会成立。

正确方案:

func (c Customer) Validate() error {var m *MultiErrorif c.Age < 0 {m = &MultiError{}m.Add(errors.New("age is negative"))}if c.Name == "" {if m == nil {m = &MultiError{}}m.Add(errors.New("age is nil"))}if m != nil {return m}return nil
}

此时返回的是一个 nil interface,是存粹的。而不是一个非 nil 动态类型的 interfere 返回值。

6.5 使用文件名作为函数的输入(#46)

编写一个从文件中按行读取内容的函数。

错误示例:

func countEmptyLinesInFile(filename string) (int, error) {file, err := os.Open(filename)if err != nil {return 0, err}scanner := bufio.NewScanner(file)for scanner.Scan() {// ...}
}

弊端:

  1. 每当需要做不同功能的单元测试,需要单独创建一个文件。
  2. 这个函数将无法被复用,因为它依赖于一个具体的文件名,如果是从其他输入源读取将需要重新编写函数。

🌟 修正方案:

func countEmptyLines(reader io.Reader) (int, error) {scanner := bufio.NewScanner(reader)for scanner.Scan() {// ...}
}func TestCountEmptyLines(t *testing.T) {emptyLines, err := countEmptyLines(strings.NewReader(`foobarbaz`))// 测试逻辑
}

通过这种方式,可以将输入源进行抽象,从而满足来自任何输入的读取(文件,字符串,HTTP Request,gRPC Request等),编写单元测试也十分便利。

6.6 不理解 defer 参数和接收者是如何确定的(#47)

  • defer 声明的函数的参数值,在声明时确定:
const (StatusSuccess = "success"StatusErrorFoo = "error_foo"StatusErrorBar = "error_bar"
)func f() error {var status stringdefer notify(status)defer incrementCounter(status)if err := foo(); err != nil {status = StatusErrorFooreturn err}if err := bar(); err != nil {status = StatusErrorBarreturn err}status = StatusSuccessreturn nil
}

🌟 上述示例中,无论是否会在 foobar 函数的调用后返回 errstatus 的值传递给 notifyincrementCount 函数的都是空字符串,因为 defer 声明的函数的参数值,在声明时确定。

修正方案1:

func f() error {var status string// 修改为传递地址defer notify(&status)defer incrementCounter(&status)if err := foo(); err != nil {status = StatusErrorFooreturn err}if err := bar(); err != nil {status = StatusErrorBarreturn err}status = StatusSuccessreturn nil
}

因为地址一开始确定,所以无论后续如何为 status 赋值,都可以通过地址获取到最新的值。这种方式的缺点是需要修改 notify 和 incrementCounter 两个函数的传参形式。

🌟 defer 声明一个闭包,则闭包内使用的外部变量的值,将在闭包执行的时候确定。

func main() {i := 0j := 0defer func(i int) {fmt.Println(i, j)}(i)i++j++
}

因为 i 作为匿名函数的参数传入,因此值在一开始确定,而 j 是闭包内使用外部的变量,因此在 return 之前确定值。最后打印结果 i = 0, j = 1。

修正方案2:

func f() error {var status stringdefer func() {notify(status)incrementCounter(status)}()
}

通过使用闭包将 notify 和 incrementCounter 函数包裹,则 status 的值使用闭包外侧的变量 status,因此 status 的值会在闭包执行的时候确定,这种修改方式也无需修改两个函数的签名,更为推荐。

  • 指针和值接收者:

值接收者:

func main() {s := Struct{id: "foo"}defer s.print()s.id = "bar"
}type Struct struct {id string
}func (s Struct) print() {fmt.Println(s.id)
}

打印的结果是 foo,因为 defer 后声明的 s.print() 的接收者 s 将在一开始获得一个拷贝,foo 作为 id 已经固定。

指针接收者:

func main() {s := &Struct{id: "foo"}defer s.print()s.id = "bar"
}type Struct struct {id string
}func (s *Struct) print() {fmt.Println(s.id)
}

打印结果是 bar,defer 后声明的 s.print() 的接收者 s 将在一开始获得一份拷贝,因为是地址的拷贝,所以对 return 之前的改动有感知。

小结

已完成《Go语言的100个错误》全书学习进度47%,欢迎追更。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

《Java 简易速速上手小册》第3章:Java 数据结构(2024 最新版)

文章目录 3.1 数组和字符串 - 数据的基本营地3.1.1 基础知识3.1.2 重点案例&#xff1a;统计文本中的单词频率3.1.3 拓展案例 1&#xff1a;寻找数组中的最大元素3.1.4 拓展案例 2&#xff1a;反转字符串 3.2 集合框架概述 - 数据小队的训练场3.2.1 基础知识3.2.2 重点案例&…

Intelij Terminal中文乱码解决

第一&#xff1a; &#xff08;重启Intelij生效&#xff09; -Dfile.encodingUTF-8 第二&#xff1a; &#xff08;重启Intelij生效&#xff09; 如果还不行&#xff0c;第三&#xff1a; 测试结果很ok&#xff1a;

w28pikachu-csrf实例

pikachu-csrf实例 get级别 这里需要同时修改性别、手机、住址、邮箱。 写一个简单的html文件&#xff0c;里面伪装修改密码的文字&#xff0c;代码如下&#xff1a; <html><body><a href"http://pikachu:7002/vul/csrf/csrfget/csrf_get_edit.php?sex…

论文阅读_用模型模拟记忆过程

英文名称: A generative model of memory construction and consolidation 中文名称: 记忆构建和巩固的生成模型 文章: https://www.nature.com/articles/s41562-023-01799-z 代码: https://github.com/ellie-as/generative-memory 作者: Eleanor Spens, Neil Burgess&#xff…

智慧供应链控制塔大数据解决方案

一、供应链控制塔的概念定义 (1) Gartner 的定义: “控制塔是一个物理或虚拟仪表板,提供准确的、及时的、完整的物流事件和数据,从组织和服务的内部和跨组织运作供应链,以协调所有相关活动。”、“供应链控制塔…提供供应链端到端整体可见性和近实时信息和决策的概念……

深入解析Mybatis-Plus框架:简化Java持久层开发(二)

&#x1f340; 前言 博客地址&#xff1a; CSDN&#xff1a;https://blog.csdn.net/powerbiubiu &#x1f44b; 简介 本章节开始从实际的应用场景&#xff0c;来讲解Mybatis-Plus常用的一些操作&#xff0c;根据业务场景来进行增删改查的功能&#xff0c;首先先搭建一个项目…

内容检索(2024.02.17)

随着创作数量的增加&#xff0c;博客文章所涉及的内容越来越庞杂&#xff0c;为了更为方便地阅读&#xff0c;后续更新发布的文章将陆续在此汇总并附上原文链接&#xff0c;感兴趣的小伙伴们可持续关注文章发布动态&#xff01; 本期更新内容&#xff1a; 1. 信号完整性理论与…

印度基金低风险套利回顾

2024年1月19日当天&#xff0c;印度基金(164824)开放申购&#xff0c;限额申购100元&#xff0c;当天溢价率13%左右&#xff0c;这个溢价率已经非常可观了&#xff0c;当然要祭出一拖七大法搞它一把&#xff01; 一拖七套利原理简介 详细的原理和方法可自行在雪球搜索&#…

ACM题解Day4 | Boring Non-Palindrome ,Mental Rotation ,so Easy

学习目标&#xff1a; 博主介绍: 27dCnc 专题 : 数据结构帮助小白快速入门算法 &#x1f44d;&#x1f44d;&#x1f44d;&#x1f44d;&#x1f44d;&#x1f44d;&#x1f44d;&#x1f44d;&#x1f44d;&#x1f44d;&#x1f44d;&#x1f44d; ☆*: .&#xff61;. o(≧▽…

时间序列预测模型:ARIMA模型

1. ARIMA模型原理介绍 ARIMA模型&#xff0c;全称为自回归积分滑动平均模型&#xff08;Autoregressive Integrated Moving Average Model&#xff09;&#xff0c;是一种常用的时间序列预测方法。ARIMA模型通过对时间序列数据的差分化处理&#xff0c;使非平稳时间序列数据变…

鸿蒙系统优缺点,能否作为开发者选择

凡是都有对立面&#xff0c;就直接说说鸿蒙的优缺点吧。 鸿蒙的缺点&#xff1a; 鸿蒙是从2019年开始做出来的&#xff0c;那时候是套壳Android大家都知晓。从而导致大家不看鸿蒙系统&#xff0c;套壳Android就是多次一举。现在鸿蒙星河版已经是纯血鸿蒙&#xff0c;但是它的…

点亮代码之灯,程序员的夜与电脑

在科技的海洋里&#xff0c;程序员是那些驾驶着代码船只&#xff0c;穿梭于虚拟世界的探险家。他们手中的键盘是航行的舵&#xff0c;而那台始终不愿关闭的电脑&#xff0c;便是他们眼中永不熄灭的灯塔。有人说&#xff0c;程序员不喜欢关电脑&#xff0c;这究竟是为什么呢&…

SSM框架,spring-aop的学习

代理模式 二十三种设计模式中的一种&#xff0c;属于结构型模式。它的作用就是通过提供一个代理类&#xff0c;让我们在调用目标方法的时候&#xff0c;不再是直接对目标方法进行调用&#xff0c;而是通过代理类间接调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来…

PPT导出PDF时保持图像高清的方法

问题: 我们经常会发现&#xff0c;在PPT中插入的图片非常高清&#xff0c;但是通过PPT转换为PDF之后&#xff0c;图片就会出现不同程度的失真。 问题产生的原因: 这是因为Acrobat的PDF Maker在将PPT转换为PDF的时候&#xff0c;对PPT中的图片进行了压缩 Solution: 在PPT的…

博途PLC数值积分器(矩形梯形积分自由切换)

数值积分器的相关介绍,大家可以也可以参看下面几篇文章,链接如下: PLC算法系列数值积分器 https://rxxw-control.blog.csdn.net/article/details/128562853https://rxxw-control.blog.csdn.net/article/details/128562853SMART PLC 梯形和矩形积分 https://rxxw-control.…

【数据结构】18 二叉搜索树(查找,插入,删除)

定义 二叉搜索树也叫二叉排序树或者二叉查找树。它是一种对排序和查找都很有用的特殊二叉树。 一个二叉搜索树可以为空&#xff0c;如果它不为空&#xff0c;它将满足以下性质&#xff1a; 非空左子树的所有键值小于其根节点的键值非空右子树的所有键值都大于其根结点的键值左…

2.16学习总结

1.邮递员送信&#xff08;dijkstra 不只是从起到到目标点&#xff0c;还要走回去&#xff09; 2.炸铁路(并查集) 3.统计方形&#xff08;数据加强版&#xff09;&#xff08;排列组合&#xff09; 4.滑雪&#xff08;记忆化&#xff09; 5.小车问题&#xff08;数学问题&#x…

无人机系统组装与调试,多旋翼无人机组装与调试技术详解,无人机飞控系统原理

多旋翼无人机飞控系统的组装 在开始组装前&#xff0c;确保您已准备好所有必要的工具和材料。这包括螺丝刀、电烙铁、焊台、杜邦线、飞控板、GPS模块、电机、桨叶等。 飞控安装 安全开关安装&#xff0c;将安全开关固定在机架上。将安全开关的线插到飞控SWITCH插口上。 电调…

【半监督图像分割 2023 】BHPC

【半监督图像分割 2023 】BHPC 论文题目&#xff1a;Semi-supervised medical image segmentation via hard positives oriented contrastive learning 中文题目&#xff1a;通过面向硬阳性的对比学习进行半监督医学图像分割 论文链接&#xff1a; 论文代码&#xff1a;https:/…

亚马逊、国际站、速卖通新店怎么销量破冰?自养号测评爆款打造思路

亚马逊作为全球最大的电子商务平台之一&#xff0c;吸引了众多卖家进驻其平台。对于新店铺来说&#xff0c;如何在竞争激烈的市场中突破销量瓶颈&#xff0c;成为卖家们关注的焦点。 一、亚马逊新店怎么销量破冰&#xff1f; 优化商品信息&#xff1a;在亚马逊平台上&#xff…