文章目录

  • 一、枚举默认值和json反序列化
  • 二、BenchMarking和内联
  • 三、每次传参都应该用指针吗?
  • 四、break和条件控制语句
  • 五、错误管理
  • 六、数组初始化
    • 6.1 len 和 cap
    • 6.2 设置len 还是 cap 的效率高
  • 七、context 管理
    • 7.1 什么是context
    • 7.2 什么时候应该用context
  • 八、从来不用 -race 参数
  • 九、使用文件名作为输入(方法设计不满足SOLID原则)
    • 9.1 从问题出发
    • 9.2 SOLID 原则
    • 9.3 优化版本
  • 十、协程和循环中的局部变量
    • 10.1 协程共用循环的局部变量
    • 10.2 避免直接使用循环中的局部变量

原文链接
前言:这里提到的错误,并不是那种“致命错误”,而是业务中的使用习惯的问题。如果不够了解语言的设计方式,导致使用习惯不当,可能就会引入一些设计不够好的代码。因此学习这些前人对使用方式的总结是很有帮助的。
话不多说,一起来看看都有哪些常见易犯的错误:

一、枚举默认值和json反序列化

先来看一段枚举的定义:

type Status uint32const (StatusOpen Status = iotaStatusClosedStatusUnknown
)

然后业务结构体 Request 引用了这个枚举

type Request struct {ID        int    `json:"Id"`Timestamp int    `json:"Timestamp"`Status    Status `json:"Status"`
}

最后就是常见的接口之后的反序列化过程了,如果是正常的接口返回,如下:

{"Id": 1234,"Timestamp": 1563362390,"Status": 0
}

那么反序列化之后应该也是很正常的,调用方拿到了下游返回的状态信息,状态也都对得上。
但是如果下游有问题,没有返回这个状态:

{"Id": 1235,"Timestamp": 1563362390
}

这个时候后台拿到的状态是什么?又应该是什么?可以直接写段代码测试一下。

最终:一个更健壮的枚举定义:

type Status uint32const (StatusUnknown Status = iotaStatusOpenStatusClosed
)

别看是一个小问题,影响可不小,如果结构体设计阶段没有考虑到这个问题,需要发版之后再修复,可能要改的还有下游的结构体定义,而如果结构体是放在公共的pb 文件中,要改pb ,那么要影响到的服务可能就更多了。
所以元数据的定义永远是基础,牵一发而动全身。设计的时候还是要更考虑周全一些。需要从 业务逻辑转换成编程思维,考虑到更多的细节。
参考测试代码-enum_test.go

二、BenchMarking和内联

参考博客-详解Go内联优化

性能测试相关的代码,往往需要重复执行,如果写法不当,就很容易导致内联的问题:

func clear(n uint64, i, j uint8) uint64 {return (math.MaxUint64<<j | ((1 << i) - 1)) & n
}func BenchmarkCleanBit(b *testing.B) {for i := 0; i < b.N; i++ {clear(1221892080809121, 10, 63)}
}

这里先说明一下 testing.B 这个类的功能:它表示基准测试,在测试结束之后将会输出一段性能测试的结果
测试方法必须是 BenchMark 开头,另外执行测试需要带上 bench 参数:

go test -bench=. benchmark_test.go

测试结果:

goos: windows
goarch: amd64
pkg: github.com/smiecj/go_common_mistake
BenchmarkCleanBit
BenchmarkCleanBit-8 1000000000 0.339 ns/op
PASS

但是接下来要说到问题了:由于 clear 方法没有执行其他方法的调用,没有边际效应,所以会被内联,再加上其返回值也没有被外层接收,所以又会被进一步优化掉,直接不会执行。所以其实测试结果是不准的。
怎么确认 clear 方法被内联了呢?可以通过编译参数确认:

go test -gcflags="-m" -bench=. benchmark_test.go

-gcflags="-m": 打印编译过程中 golang 解析产生内联的详细过程

所以验证的方式也很简单,只要避免内联就可以了。结合这个性能测试的示例,大概有两种方式:
① 在 BenchMark 中设置一个局部变量去接收返回值

② clear 方法最上面设置取消内联

//go:noinline
func clear(n uint64, i, j uint8) uint64 {return (math.MaxUint64<<j | ((1 << i) - 1)) & n
}

新的测试结果:
goos: windows
goarch: amd64
pkg: github.com/smiecj/go_common_mistake
BenchmarkCleanBit
BenchmarkCleanBit-8 426727659 2.96 ns/op
PASS

③ 设置编译参数 -l 禁止内联

go test -gcflags="-N -l -m" -bench=. benchmark_test.go

-N:禁止编译优化
-l:禁止内联

测试结果:
goos: windows
goarch: amd64
BenchmarkCleanBit-8 376172835 3.13 ns/op
PASS
ok command-line-arguments 2.361s

扩展阅读:
High Performance Go Workshop

三、每次传参都应该用指针吗?

首先,就传递数据量来说,指针毫无疑问,在大多数时候还是更省空间的。(64位系统中是8个字节)

看起来似乎指针总比传值更好,对吧?其实不是的,我们可能只关注了参数本身的空间开销,却忽略了指针和值分别在栈和堆上的存储开销。
先从方法的返回值去理解返回参数和返回指针的区别,来看个例子:

func getFooValue() foo {var result foo// Do somethingreturn result
}

方法内部新建了result对象,这个对象只可能被方法内部访问,所以这个对象分配的空间就在栈上,不会在堆上。
然后,方法直接返回了值本身,这个动作会生成一份result的拷贝,存储在调用方的栈上,原result因为不会再被访问,将等待被GC回收。

再来看返回指针的情况:

func main()  {p := &foo{}f(p)
}

Go只有传值,所以对于指针p来说,它的空间申请和传递,都是和上一个例子一样的。但是对于foo对象本身,申请的时候必然不会在栈上申请,而会在堆上申请。这样才能让作用域扩大到调用方。

栈比堆更快的两个原因:

  • 栈上对象不需要GC,从上面的例子可以看到,除非返回指针,否则栈内的一切对象都跟调用方没有任何关系,都是拷贝后返回,因此可以在方法结束后直接被标记。
  • 栈上对象只会在当前routine被使用,不需要和其他协程同步,也就不会在堆上记录任何状态信息

总结来说,就是不管是传参还是返回,只要非共享的场景(当然,复合数据结构如map一般都是需要共享的),都建议传value,只有一定要传指针的时候才去传指针。

扩展阅读
Language Mechanics On Stacks And Pointers

四、break和条件控制语句

如下面这段代码,break 真的能够跳出循环吗?

for {switch f() {case true:breakcase false:// Do something}
}

答案:break 其实是跳出 switch 的循环。但是golang 的switch 执行完成一个分支之后其他分支也不会执行的,所以 switch 的 break 其实没有什么意义

但是select 的break 就有意义了。所以下面这种情况也是要特别注意的,break 跳出的也不是循环
for {
select {
case <-ch:
// Do something
case <-ctx.Done():
break
}
}

常见的退出循环+switch的方式:break + 代码块名称

OuterLoop:for i = 0; i < n; i++ {for j = 0; j < m; j++ {switch a[i][j] {case nil:state = Errorbreak OuterLoopcase item:state = Foundbreak OuterLoop}}}

五、错误管理

error的处理一般满足两个原则:处理了就不要再向上继续抛出,必须给上层返回不一样的信息;没处理就一定要继续向上抛出

而go1.13之前提供的error 管理方法其实很少,所以这里我们使用 pkg/errors 这个工具来帮我们更好地管理自定义错误:

import "github.com/pkg/errors"
......
func postHandler(customer Customer) Status {err := insert(customer.Contract)if err != nil {switch errors.Cause(err).(type) {default:log.WithError(err).Errorf("unable to serve HTTP POST request for customer %s", customer.ID)return Status{ok: false}case *db.DBError:return retry(customer)}}return Status{ok: true}
}func insert(contract Contract) error {err := db.dbQuery(contract)if err != nil {return errors.Wrapf(err, "unable to insert customer contract %s", contract.ID)}return nil
}

注意到判断错误类型使用对象的type判断就可以了,CauseWrapf需要配套使用

六、数组初始化

6.1 len 和 cap

我们知道数组有两个初始化参数,分别表示len和cap,分别表示长度和初始化长度。
比如初始化一个空数组:
var bars []Bar
bars := make([]Bar, 0, 0)

和Java不同的是,go把 cap 设置也半交给用户了(当不配置cap 的时候,len 就是 cap)。但是这也
比如当我们把 cap 设置成负数,或者小于 len 的时候,会发生什么呢?
直接测试一下:

可以看到编译期 就已经直接报错了,不会让你能够执行这样的代码。我们可以从types/expr.go 中找到具体报错信息打印的地方。

6.2 设置len 还是 cap 的效率高

来看一种比较常见的场景:需要把数据库的对象转换成对外接口传递的对象。对象数量是确定的,需要怎么做呢?
有两种实现方式:

func convert(foos []Foo) []Bar {bars := make([]Bar, len(foos))for i, foo := range foos {bars[i] = fooToBar(foo)}return bars
}
func convert(foos []Foo) []Bar {bars := make([]Bar, 0, len(foos))for _, foo := range foos {bars = append(bars, fooToBar(foo))}return bars
}

其实两种实现方式都可以,但是前者效率显然高一些,因为空间是已经分配好的,而后者虽然cap 设定了,但是随着 不断append 元素,底层也是要不断地进行数组的拷贝的。

译者:文章这里基本没有从源码说明效率高的原因,后续考虑新开一篇,从makeslice 方法去分析两种方式真正的差异

七、context 管理

7.1 什么是context

官方概念:
A Context carries a deadline, a cancelation signal, and other values across API boundaries.

这里说明了context可以带的三类信息:deadline(超时配置)、cancelation(终止动作)和values(键值对)

7.2 什么时候应该用context

前两个信息是context最常用的信息和功能,最常用的场景就是rpc调用,来看看一个grpc使用示例:

ctx, cancel := context.WithTimeout(parent, 100 * time.Millisecond)
response, err := grpcClient.Send(ctx, request)

WithTimeout 方法内部就是设置了 deadline,context 将会在超时时间到来的时候触发 Done 对应的channel close。这样我们可以通过 <- context.Done) 来做一些提前结束的操作,比如释放资源,避免超时请求一直阻塞其他正常请求。

总结一下,凡是涉及到上下游关系的都应该用context来处理调用关系,下游不应该忽略上游传下来的context。

扩展阅读:
Understanding the context package in golang

八、从来不用 -race 参数

根据 报告-Understanding real-world concurrency bugs in Go ,尽管go 的设计初衷是“更少错误的高并发”,但是现实中我们依然会遇到并发带来的问题
尽管 race 检测器不一定可以检测出每一种并发错误,但是它依然是有价值的,在测试程序的过程中我们应该始终打开它。

相对其余9个错误来说,竞态条件是能直接导致程序崩溃的,所以这一节应该是最重要的一部分,建议gopher 在平时开发中都尽量留意这一点,测试和调试工作要做好。
但是 开启race 也不代表 冲突能够马上检查出来,也是要有冲突的时候,才会有Warning信息。所以建议采用线上环境留一个节点用来开启竞态检查的方式。

扩展阅读:
Understanding real-world concurrency bugs in Go
Does the Go race detector catch all data race bugs?
自己写的示例-git-race_test.go

九、使用文件名作为输入(方法设计不满足SOLID原则)

9.1 从问题出发

来看一个常见的go 工具类开发需求:需要开发一个通用的读取文件行数的方法。项目中肯定会把这个方法封装到公共包的。
一种比较直接的思路,就是设置文件名作为传参,如下:

func count(filename string) (int, error) {file, err := os.Open(filename)if err != nil {return 0, errors.Wrapf(err, "unable to open %s", filename)}defer file.Close()scanner := bufio.NewScanner(file)count := 0for scanner.Scan() {if scanner.Text() == "" {count++}}return count, nil
}

这种方式看上去功能没有任何问题,但是忽略了具体使用场景。如:

  • 文件编码:当然你可以让方法增加一个传参,但是不符合接下来说到的开闭原则
  • 单元测试:测试读取一个空文件场景。那么单测可能还需要先在本地创建一个空文件

这些细节,都会导致这个方法看上去完美,实际使用起来限制却很多。

9.2 SOLID 原则

SOLID 是面向对象编程中很重要的原则,由 总结而来。

  • S 表示 Single Responsibility (单一原则):一个方法只做一件事
  • O 表示 open-close principle (开闭原则):方法对扩展开放,对修改封闭

从这个例子就是很好的说明:S 和 O 它实际都不满足,方法做了读取文件和扫描文件行数两件事、方法可能还需要因为文件编码做格式 做适配修改

9.3 优化版本

借鉴 go 对 io.Reader 和 io.Writer 的实现思路,我们可以将传参改成这样:

func count(reader *bufio.Reader) (int, error) {count := 0for {line, _, err := reader.ReadLine()if err != nil {switch err {default:return 0, errors.Wrapf(err, "unable to read")case io.EOF:return count, nil}}if len(line) == 0 {count++}}
}

这样不仅满足和 S 和 O,方法的扩展性其实也加强了:可以读取文件流或者 http 流等的输入

调用端:

file, err := os.Open(filename)
if err != nil {return errors.Wrapf(err, "unable to open %s", filename)
}
defer file.Close()
count, err := count(bufio.NewReader(file))

单测:读取一行字符串流

count, err := count(bufio.NewReader(strings.NewReader("input")))

因此,设计思想也非常重要,尽管代码规范之类的问题并不会直接导致程序运行问题,但是显然它的影响更为深远。

十、协程和循环中的局部变量

10.1 协程共用循环的局部变量

下面这段示例,会输出什么?

func TestRoutineRace(t *testing.T) {ints := []int{1, 2, 3}waitGroup := sync.WaitGroup{}waitGroup.Add(len(ints))for _, i := range ints {go func() {fmt.Printf("%v\n", i)waitGroup.Done()}()}waitGroup.Wait()
}

显然目的是想打印 1、2、3的,但是结果却都是3
这是因为 子协程中,打印用的都是同一个局部变量i,这个i 在循环结束之后会变成3,所以最终打印的结果就都是3 了(大部分时候)

利用刚才学的race,这种使用协程的错误方式也可以通过 -race 参数 提前检测出来。
go test -v -race routine_test.go
检测结果:

WARNING: DATA RACE
Read at 0x00c000116140 by goroutine 8:
command-line-arguments.TestRoutine.func1()
D:/coding/golang/go_common_mistake/routine_test.go:16 +0x44

Previous write at 0x00c000116140 by goroutine 7:
command-line-arguments.TestRoutine()
D:/coding/golang/go_common_mistake/routine_test.go:14 +0x104
testing.tRunner()
G:/Program Files/Go/src/testing/testing.go:1127 +0x202
……

从错误信息可以看到,省略的部分还有其他协程,同样的警告信息。仔细分析下来就可以得到协程用的都是同一个局部变量的结论了。
怎么样,马上就体验到 -race 参数的作用了,是不是很妙

10.2 避免直接使用循环中的局部变量

对于这种情况有两种解决方法:
1)go func 加上入参

for _, i := range ints {go func(i int) {fmt.Printf("%v\n", i)waitGroup.Done()}(i)}

2)循环内使用单独的局部变量

注意虽然这里的I 依然是局部变量,但是对每个开启的协程来说已经不是同一个了,每次进入循环的I 都是不一样的。
但是这里我更推荐第一种写法,逻辑更加清楚

golang-文章翻译-go常见的10种错误相关推荐

  1. 两千年是不是闰年 输出错误_干货!AMOS常见的10种错误及解决办法!

    临近毕业的小伙伴,是不是经常遇到这样令人崩溃的时候,好不容易求爹爹告奶奶的收集了无比珍贵的数据.正准备大干一场, 导入数据,建好模型,AMOS却出现各种各样报错. 话不多说,直接上干货! 1 检查数据 ...

  2. 计算机病毒常见病状,电脑中病毒常见的10种症状

    电脑中病毒常见的10种症状 计算机中了病毒有什么症状呢?下面是小编收集的资料,希望大家喜欢! 电脑中病毒的症状(一)文件或文件夹无故消失: 当发现电脑中的部分文件或文件夹无缘无故消失,就可以确定电脑已 ...

  3. 压力表负值表示什么_压力表常见的10种故障分析和修理方法

    压力表常见的 10 种故障分析和修理方法 一.压力表误差成比例地增加或减少如何调整 故障原因:主要是传动比发生了变化. 只要移动示值调节螺钉, 就能解决. 压力表误差逐渐增大时, 将示值调节螺钉往右( ...

  4. 最常见的 10种网络安全攻击类型

    网络攻击是指旨在针对计算机或计算机化信息系统的任何元素更改.破坏或窃取数据以及利用或损害网络的行为.随着近年来越来越流行的业务数字化,网络攻击一直在增加.虽然有几十种不同类型的攻击,但网络攻击列表包括 ...

  5. 使用Kubernetes最常见的10个错误

    使用 Kubernetes,大家都会遇到哪些错误?本文分享了作者多年来使用 Kubernetes 最常见的 10 个错误. 使用 kubernetes 这么多年以来,我们见过的集群不计其数(包括托管的 ...

  6. c语言数组常见错误,C语言数组在使用中常见的几种错误.pdf

    S C i 卜NOL & TE Q 月N创,)<白Y INI: 口书明A T I仁别 T 技 术 C 语言数组在使用 中常见 的几种错误 刘莉娜 (包头农牧学校 0 14030) 摘 要 ...

  7. C#编程中最常见的10个错误

    C#编程中最常见的10个错误 英文原文链接:https://www.toptal.com/c-sharp/top-10-mistakes-that-c-sharp-programmers-make 原 ...

  8. 电气设计软件中最常见的10个错误

    设计早期的设备名称重复,组件之间交叉引用的断开链接以及耗时的组件生成,这些只是在低质量CAD软件中生成电气设计时发生的一些令人头疼的错误.这些花费大量时间修复的错误只是冰山一角.本文将深入探讨所有电气 ...

  9. mysql 3种报错_MySQL读取Binlog日志常见的3种错误-阿里云开发者社区

    MySQL读取Binlog日志常见的3种错误 mysqlbinlog: [ERROR] unknown variable 'default-character-set=utf8mb4' 当我们在my. ...

最新文章

  1. 京东方拟收购法国零售物联网领域公司50.1%以上股份
  2. 知名美国服务器租用商介绍
  3. C#多线程学习(三) 生产者和消费者
  4. 八皇后问题求解动态图形演示
  5. 现实世界 机器学习_公司沟通分析简介现实世界的机器学习方法
  6. mysql check table_mysql check table
  7. (数据库系统概论|王珊)第九章关系查询与关系优化:习题
  8. CCF201812-3 CIDR合并(100分)【位运算+文本】
  9. bootstrap-table 树形结构
  10. Centos 6.5 Tengine 安装流程
  11. 修改Unity中Lua文件的默认打开程序
  12. 机器学习和深度学习资料合集
  13. MathType求和符号中的黑三角该如何消除
  14. table 手机 滑动_移动端touch事件滚动
  15. 20 个短小精悍的 pandas 骚操作
  16. 【《Real-Time Rendering 3rd》 提炼总结】(一) 全书知识点总览
  17. 为什么国产操作系统要基于linux,解说国产Linux操作系统为什么都模仿Windows桌面...
  18. 穷人和富人的差别在哪里
  19. i5 13600K和i5 12600k差距
  20. No converter found for return value of type错误解决以及消息转化器简单分析

热门文章

  1. PyCharm设置中文(官方插件版)
  2. CentOS-7部署安装Zabbix 3.0
  3. CSS画矩形、圆、半圆、弧形、半圆、小三角、疑问框
  4. 计算机办公室听什么音乐,盘点办公室里最适合听的九首歌!
  5. 尚硅谷智慧校园-SpringBoot最佳入手级项目
  6. 黑色沙漠首发五职业PVP强度排行
  7. php slik转wav_node.js安装wx-voice转换silk和mp3
  8. android实现忘记密码功能,手机忘记密码如可解决 安卓手机重置密码教程【详解】...
  9. sprint 1 总结
  10. Apple iBeacons