0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

10种go语言编成中可能导致性能下降的坏实践

马哥Linux运维 来源:Teiva Harsanyi 作者:Teiva Harsanyi 2021-09-24 16:55 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

本文总结了10种 go 语言编成中可能导致性能下降的坏实践。有代码洁癖的同学来自我检查吧!

这篇文章主要讲述了我在 Go 项目中见到过的常见错误清单,顺序无关。

未知的Enum

来看个简单的例子

typeStatusuint32

const(
StatusOpenStatus=iota
StatusClose
StatusUnknown
)

在上面的代码中,使用iota创建了一个enum类型,分别代指下面的状态信息:

StatusOpen=0
StatusClose=1
StatusUnknown=2

现在,我们假设Status是一个 JSON 请求中被Marshalled / Unmarshalled的一个属性,我们可以设计出下面的数据结构:

typeRequeststruct{
IDint`json:"Id"`
Timestampint`json:"Timestamp"`
StatusStatus`json:"Status"`
}

然后,假设收到的Request 的接口返回值为:

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

到目前为止,没有什么特殊的表达,Status将会被反序列化为StatusOpen,是吧?

好的,我们来看一个未设置status返回值的请求(不管是出于什么原因吧)。

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

在这个例子中,Request结构体的Status字段将会被初始化为默认零值zeroed value, 对于 uint32 类型来说,值就是0。因此,StatusOpen就替换掉了原本值应该是StatusUnknown

对于这类场景,把unknown value设置为枚举类型0应该比较合适,如下:

typeStatusuint32

const(
StatusUnknownStatus=iota
StatusOpen
StatusClose
)

这样,即时返回的 JSON 请求中没有Status属性,结构体RequestStatus属性也会按我们预期的,被初始化为StatusUnknown

性能测试

正确地进行性能测试很困难,因为过程中有太多的因素会影响测试结果了。

其中一个最常见的错误就是被一些编译器优化参数糊弄,让我们以teivah/bitvector库中的一个真实案例来进行阐述:

funcclear(nuint64,i,juint8)uint64{
return(math.MaxUint64<1<< i) - 1))&n
}

这个函数会清理给定长度n的二进制位,对这个函数进行性能测试的话,我们可能会写出下面的代码:

funcBenchmarkWrong(b*testing.B){
fori:=0;i< b.N; i++ {
  clear(1221892080809121,10,63)
}
}

在这个性能测试中,编译器发现clear函数是并没有调用其他函数,因此编译器就会进行inline处理。除此之外,编译器还发现这个函数中也没有side-effects。因此,clear就会被删除,不去计算它的耗时,因此这就会导致测试结果的不准确。

一个建议是设置全局变量,如下:

varresultuint64

funcBenchmarkCorrect(b*testing.B){
varruint64
fori:=0;i< b.N; i++ {
  r = clear(1221892080809121,10,63)
}
result=r
}

这样的话,编译器就不知道clear函数是否会造成side-effect了,因此,性能测试的结果就会变得更加准确。

拓展阅读

指针,到处都是指针!

值传递的时候,会创建一个同值变量;而指针传递的时候,只是将变量地址进行拷贝。

因此,指针传递总是会很快,是不?

如果你觉得是这样,可以看一下这个例子。在这个性能测试中,一个大小为0.3K的数据结构分别以值传递和指针传递进行测试。0.3K 不大,但是也不能和大部分我们日常用到的场景中的数据结构大小相差甚远,接近即可。

当我在自己的本地环境中执行这个性能测试代码的时候,值传递比指针传递快了4 倍还多,是不是感觉有悖常理?

关于这个现象的解释涉及到了 Go 中的内存管理,我没法解释得像 William Kennedy 解释的那样精炼,一起来整理总结下吧:

变量可以被分配到heapstack上,粗略解释为:

  • 栈包含哪些分配给了goroutine的随时消失的变量,一旦函数返回,变量就会从栈中弹出
  • 堆包含共享变量,比如全局变量等

一起通过一个简单的例子来测试下:

funcgetFooValue()foo{
varresultfoo
//Dosomething
returnresult
}

result被当前 goroutine 创建,这个变量就会被压入当前运行栈。一旦函数返回,调用方就会收到与此变量的一份拷贝,二者值相同,但是变量地址不同。变量本身会被弹出,此时变量并不会被立即销毁,直到它的内存地址被另一个变量覆盖或者被擦除,这个时候它才是真的再也不会被访问到了。

与此相对,看一个一个指针传递的例子:

funcgetFooPointer()*foo{
varresultfoo
//Dosomething
return&result
}

result依旧是被当前goroutine所创建,但是调用方收到的会是一个指针(指向变量的内存地址)。如果result被栈弹出,那么调用方不可能访问到此变量。

在这个场景下,GO 的编译器会把result放置到可以被共享的变量空间:heap。

下面来看另一个场景,比如:

funcmain(){
p:=&foo{}
f(p)
}

f的调用方与f所属为同一个goroutine,变量p不会被转换,它只是被简单放回到栈中,因此子函数依旧可以访问到。

举例来说,io.Reader中的Read方法接收指针,而不是返回一个,因为返回一个切片就会被转换到堆中。

为什么栈会这么快?这里有两个主要的原因:

  • 栈不需要垃圾收集。正如我们所说,一个变量创建时被压入栈,函数返回时从栈中弹出。根本不需要复杂的处理来回收未使用的变量。
  • 一个栈隶属于一个 goroutine,与堆中变量相比,不需要同步处理,这同样会使得栈很快。

总结一下,当我们创建一个函数的时候,我们应该使用值传递而不是指针传递。只有我们期待某个变量被共享使用时,才使用指针传递适用。

当我们下次遇到性能优化的问题时,一个可能的优化方向就是检查在某些场景下,指针传递是否真的会有所帮助。一个需要了解的常识是:当使用go build -gcflags "-m -m"时,编译器会默认将一个变量转换到堆中。

再强调下,在日常开发中,应该总是首先考虑值传递。

拓展阅读 Language Mechanics On Stacks And Pointers

干掉for/switch或者for/select

如果f函数返回了 true,会发生什么?

for{
switchf(){
casetrue:
break
casefalse:
//dosomething
}
}

break语句会被调用,这会导致switch语句退出,而不是 loop 退出。再看一个类似问题:

for{
select{
case<-ch:
        //dosomething
case<-ctx.Done():
        break
}
}

break同样只是退出select语句,而不是 for 循环。

一个可能的解决方案是使用labeled break标签,例如:

loop:
for{
select{
case<-ch:
            //dosomething
case<-ctx.Done():
            breakloop
}
}

错误管理

Go 中的错误处理机制还是有点简单,或许到了 Go2.0,它会变得好一点。

当前标准库只提供创建错误类型数据结构的方法,具体可查看 pkg/errors。

这个库很好的展示了一些本该被遵守却经常不被遵守的规则的好例子。

一个错误只应该被处理一次。把错误打印到日志中也是在处理错误。所以一个错误要么被打日志,要么被传到调用方。

当前的标准库,如果我们想分层化或者在错误中添加上下文信息是非常困难的。接下来,我们一起看个期待使用 REST 形式调用而导致 DB 出问题的例子:

unabletoserveHTTPPOSTrequestforcustomer1234
|_unabletoinsertcustomercontractabcd
|_unabletocommittransaction

如果我们使用pkg/errors库,我们可能会这么做:

funcpostHandler(customerCustomer)Status{
err:=insert(customer.Contract)
iferr!=nil{
log.WithError(err).Errorf("unabletoserveHTTPPOSTrequestforcustomer%s",customer.ID)
returnStatus{ok:false}
}
returnStatus{ok:true}
}

funcinsert(contractContract)error{
err:=dbQuery(contract)
iferr!=nil{
returnerrors.Wrapf(err,"unabletoinsertcustomercontract%s",contract.ID)
}
returnnil
}

funcdbQuery(contractContract)error{
//Dosomethingthenfail
returnerrors.New("unabletocommittransaction")
}

需要我们使用errors.New来初始化错误信息(如果内部方法调用没有返回 error 的话)。中间调用层insert, 仅仅是通过添加更多上下文信息来包装了错误。然后insert的调用方通过日志进行了打印,每一层要么返回错误,要么处理错误。

有些时候,我们可能会检查错误以便于做重试处理。假如我们有一个叫db的处理数据库的外部的包,这个库可能会返回db.DBError 这种临时错误。到底要不要做重试处理,就看错误是不是符合预期, 比如处理代码:

funcpostHandler(customerCustomer)Status{
err:=insert(customer.Contract)
iferr!=nil{
switcherrors.Cause(err).(type){
default:
log.WithError(err).Errorf("unabletoserveHTTPPOSTrequestforcustomer%s",customer.ID)
returnStatus{ok:false}
case*db.DBError:
returnretry(customer)
}

}
returnStatus{ok:true}
}

funcinsert(contractContract)error{
err:=db.dbQuery(contract)
iferr!=nil{
returnerrors.Wrapf(err,"unabletoinsertcustomercontract%s",contract.ID)
}
returnnil
}

借助pkg/errors中的errors.Cause,便可以进行实现。

一个常见的错误就是独立使用pkg/errors,比如:

switcherr.(type){
default:
log.WithError(err).Errorf("unabletoserveHTTPPOSTrequestforcustomer%s",customer.ID)
returnStatus{ok:false}
case*db.DBError:
returnretry(customer)
}

上面例子中,如果db.DBError被包装了,那么重试机制将永远不会触发。

切片初始化

有时候我们知道切片的最终长度,比如:将切片Foo转换成切片Bar,这意味着两个切片的长度会是一致的。

我经常见到有人这么初始化切片:

varbar[]Bar

bars:=make([]Bar,0)

切片不是魔术结构,实际上当空间不足时,Go来动态的维护切片的长度。在这个场景下,一个新的更大容量的数组会自动被创建,然后将旧的数组元素一个个的拷贝到新数组中。

现在,假设我们要多次数以千计的增加[]Foo,插入的时间复杂度可不是O(1),毕竟内部重复了多次拷贝。

因此,如果我们知道切片最终长度的话,可以采用以下策略:

  • 使用预定义长度
funcconvert(foos[]Foo)[]Bar{
bars:=make([]Bar,len(foos))
fori,foo:=rangefoos{
bars[i]=fooToBar(foo)
}
returnbars
}
  • 使用 0 长度,并且给一个预定义容量
funcconvert(foos[]Foo)[]Bar{
bars:=make([]Bar,0,len(foos))
for_,foo:=rangefoos{
bars=append(bars,fooToBar(foo))
}
returnbars
}

那么,这俩方法哪个更好呢?

第一个更快一点点,而第二个更符合编码预期:不考虑初始长度,每次只通过append往尾部追加数据。

上下文管理

context.Context经常被开发者所误解,下面看下官方的解释:

上下文以 API 边界形式,可携带截止时间、取消信号以及其他值。

这段描述通常让人疑惑这玩意儿有啥用,咋用啊?

我们举几个例子,看看它到底能携带什么数据:

  • 截止日期不管是遇到250 ms还是遇到2019-01-08 0100格式的时间,必须立刻终止执行(执行的内容可能是 I/O 请求,等待 channel 输入等)
  • 取消信号类似于上面,一旦接收到信号,就需要立刻终止执行后续处理。例如:接收两个请求,一个是插入数据,另一个是取消第一个的插入,这个场景就可以借助在第一个请求中加入一个可取消的上下文来实现。
  • 其他值Key-Value形式,即便都是 interface{}类型。

context 是可组合的,因此可以添加截止时间和其他 key-value 类型数据;另外,多个协程可共享同一个上下文,因此取消信号可以阻止多个执行流程。

回到正题,继续来说说错误问题。

一个 基于 urface/cli (一个用于制作命令行应用的库)Go 应用,一旦启动,开发者继承了一串上下文,使用 context 的终止信号来终止所有的执行。当我意识到请求一个 gRPC 终端的时候,context 只是直接被传递了下去。这不是我想看到的。

相反,我们想让 gRPC 库在收到终止信号或者超过 100ms 处理时间时进行取消处理。为了达到这个目标,我们可以创建一个简单的组合上下文,如果parent是应用上下文的名字(通过 urfave/cli 创建),然后我们就可以写出下面的代码:

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

上下文不难理解,而且在我眼中,它是Go 语言中最棒的特色之一。

不要使用-race选项

我经常见的一个错误就是在测试时使用-race选项。

“即使 Go 是被设计成让并发更容易,更少错误的语言”, 我们仍然经受着很多并发问题的折磨。

显而易见的是,Go 语言中的 race 探查器对独立的并发问题而言并无帮助。不过,当测试我们的应用时开启它也是很有价值的。

使用文件名作为输入

另一个常见问题就是把文件名作为函数的参数。加入我们要实现一个统计文件中空行数量的函数,最自然的实现方式可能就是这样的:

funccount(filenamestring)(int,error){
file,err:=os.Open(filename)
iferr!=nil{
return0,errors.Wrapf(err,"unabletoopen%s",filename)
}
deferfile.Close()

scanner:=bufio.NewScanner(file)
count:=0
forscanner.Scan(){
ifscanner.Text()==""{
count++
}
}
returncount,nil
}

filename作为函数输入,然后我们打开文件,再实现后续的逻辑,对不?

接下来,在此函数的基础上写单测,测试使用的变量分别代表:常规文件,空文件,使用不同编码的文件等等。很快它就会变得难以管理。

同样,当我们想以同样的逻辑来处理 HTTP 响应体,我们就不得不重新写一个新函数了,因为这个函数只接受文件名。

GO 语言中有两个很棒的抽象:io.Readerio.Writer。与直接传递文件名不同的是,我们可以简单的传入一个io.Reader来抽象化数据源。

它是文件还是 HTTP 的响应体,或者是一个字节缓冲区?都不重要了,我们只需要使用Read方法就都可以搞定。在下面的例子中,我们甚至可以一行一行地读入数据。

funccount(reader*bufio.Reader)(int,error){
count:=0
for{
line,_,err:=reader.ReadLine()
iferr!=nil{
switcherr{
default:
return0,errors.Wrapf(err,"unabletoread")
caseio.EOF:
returncount,nil
}
}
iflen(line)==0{
count++
}
}
}

打开一个文件的职责交给count的调用方去代理就好了,如下:

file,err:=os.Open(filename)
iferr!=nil{
returnerrors.Wrapf(err,"unabletoopen%s",filename)
}
deferfile.Close()
count,err:=count(bufio.NewReader(file))

在第二种的实现中,数据源已经不重要了,并且单测也可以很方便的进行编写,比如使用字符串来创建一个bufio.Reader作为数据源:

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

协程与循环变量

最后一个常见的错误就是在循环结构中使用协程。

下面例子中的输出是什么?

ints:=[]int{1,2,3}
for_,i:=rangeints{
gofunc(){
fmt.Println("%v
",i)
}()
}

你是不是以为会是按顺序输出1 2 3?并不是哦。在这个例子中,每一个协程都会共享同一个变量实例,因此它最终大概率会输出3 3 3

有两种解决方案来解决类似问题,第一个就是把循环遍历当做参数传给闭包,比如:

ints:=[]int{1,2,3}
for_,i:=rangeints{
gofunc(iint){
fmt.Printf("%v
",i)
}(i)
}

另一种方式就是在循环内部的作用域中创建临时变量,比如:

ints:=[]int{1,2,3}
for_,i:=rangeints{
i:=i
gofunc(){
fmt.Printf("%v
",i)
}()
}

虽然看着i := i很奇怪,但是它真的有效。一个循环内部意味着在另一个作用域中,因此i := i就创建了一个新的变量实例,称之为i。当然,为了可读性我们也可以定义成一个别的名字。

转自:

guoruibiao.blog.csdn.net/article/details/108054295

编辑:jq
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • HTTP
    +关注

    关注

    0

    文章

    538

    浏览量

    35552
  • 数据源
    +关注

    关注

    1

    文章

    66

    浏览量

    10100
  • go语言
    +关注

    关注

    1

    文章

    159

    浏览量

    9848

原文标题:Go 项目中常见的 10 种错误

文章出处:【微信号:magedu-Linux,微信公众号:马哥Linux运维】欢迎添加关注!文章转载请注明出处。

收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    基于低噪声电源管理架构的射频采样系统设计方案

    本期为大家带来的是《雷达应用射频转换器的无杂波电源(第 1 部分)》,介绍了一基于低噪声电源管理架构的射频采样系统设计方案,以解决相控阵雷达和 5G 通信应用因开关电源噪声耦合导致
    的头像 发表于 03-25 08:10 3793次阅读
    一<b class='flag-5'>种</b>基于低噪声电源管理架构的射频采样系统设计方案

    风华功率电感出现异响,可能是哪些原因导致的?

    风华功率电感出现异响,可能由振动与共振、电感饱和、PCB布局与结构问题、设计参数不当、间歇工作模式、磁性体相互作用、漏磁通影响、电感品质或兼容性问题等多种因素导致,以下是具体分析: 1、振动与共
    的头像 发表于 03-18 16:38 192次阅读
    风华功率电感出现异响,<b class='flag-5'>可能</b>是哪些原因<b class='flag-5'>导致</b>的?

    AI辅助编程设计之道:从Spec到Code工程实践

    完成,只是设计成果的呈现方式发生了变化。 在传统的详细设计,开发者可能会用UML图、伪代码、接口定义语言来描述模块的行为。在AI辅助的开发模式下,这些设计可以用结构化的Markdown文档来承载
    发表于 03-16 13:33

    手机主板散热导热胶薄层涂布最佳实践 |铬锐特实业

    铬锐特实业|东莞厂家|详解手机主板导热胶薄层涂布最佳实践:推荐80-150μm厚度范围,热阻可降低40-50%,芯片温度下降5-10℃。掌握精准点胶、压力组装与材料选择,实现高效散热与性能
    的头像 发表于 03-02 01:54 239次阅读
    手机主板散热导热胶薄层涂布最佳<b class='flag-5'>实践</b> |铬锐特实业

    Go 语言高并发服务设计与性能调优实战:从万级到百万级并发的演进之路

    在2026年的今天,Go 语言已成为高并发后端服务的首选语言。根据 Stack Overflow 最新开发者调查: 指标 数据 Go 语言
    发表于 02-18 19:19

    内核配置项引发网络性能下降的深度剖析

    、CONFIG_PREEMPT_TRACER、CONFIG_SCHED_TRACER )的启用,竟导致网络性能下降10% ,关闭后借助 iperf3 测试丢包问题消失。本文将深入剖
    的头像 发表于 02-01 16:48 1810次阅读
    内核配置项引发网络<b class='flag-5'>性能</b><b class='flag-5'>下降</b>的深度剖析

    低成本TLI4971/TLE4971电流传感器评估套件——MS2Go与S2Go

    低成本TLI4971/TLE4971电流传感器评估套件——MS2Go与S2Go 在电子工程师的日常工作,电流传感器的评估和应用是一个重要的环节。今天我们要介绍的是英飞凌(Infineon
    的头像 发表于 12-19 16:50 1116次阅读

    单片机开发功能安全编译器

    ”的代码路径。高级语言,特别是C和C ++,包含数量众多的功能,这些功能的行为不是代码所遵循的语言规范所规定的。这种不确定的行为可能导致意外的结果和潜在的灾难性后果,而这在功能安全的应
    发表于 12-01 06:44

    为什么ADA4530-1运放总是

    这个运放的时候没注意到GRD是做保护环用的,所以直接接了地,但是这应该只会导致没有屏蔽漏电流的效果,不会道址运放总是吧,不知道是什么原因,我用这个运放的时候是处在一个激光周围,因为我要把激光打在
    发表于 11-28 16:15

    如何预防射频模块的性能下降

    预防射频模块(用于干扰发生类仪器,如射频信号发生器)性能下降,需围绕其核心失效诱因(散热不良、环境侵蚀、操作不当、部件老化、负载异常),从 “环境控制、规范操作、定期维护、硬件保护、校准溯源” 五大维度建立全生命周期预防体系,延缓部件老化、避免不可逆损伤。
    的头像 发表于 10-18 10:46 1036次阅读

    国巨电容出现漏液现象,可能是哪些原因导致的?

    焊接不佳,或绝缘子与外壳、引线焊接不佳,都可能导致密封性能下降,从而引发漏液。 密封材料老化 :长期使用后,密封材料(如橡胶塞)可能发生硬化、龟裂,失去密封性,
    的头像 发表于 09-29 14:21 769次阅读
    国巨电容出现漏液现象,<b class='flag-5'>可能</b>是哪些原因<b class='flag-5'>导致</b>的?

    数明半导体SiLM27531H栅极驱动器在碳化硅器件的应用

    碳化硅 MOSFET 凭借显著的开关性能优势,在许多大功率应用得到青睐。然而它的特性要求栅极驱动电路有较高要求,以优化碳化硅器件的开关性能。尽管碳化硅 MOSFET 并非难以驱动,但许多常见的驱动器
    的头像 发表于 09-03 17:54 4851次阅读
    数明半导体SiLM27531H栅极驱动器在碳化硅器件<b class='flag-5'>中</b>的应用

    CAN总线电容过大?三解决方案来了

    在新能源汽车路试,CAN总线传输异常是一个常见问题。本期我们将探讨由于总线电容过大导致下降沿过缓问题,并介绍三有效的解决方案。CAN总线下降
    的头像 发表于 07-22 11:36 841次阅读
    CAN总线电容过大?三<b class='flag-5'>种</b>解决方案来了

    从 Java 到 Go:面向对象的巨人与云原生的轻骑兵

    Go 语言在 2009 年被 Google 推出,在创建之初便明确提出了“少即是多(Less is more)”的设计原则,强调“以工程效率为核心,用极简规则解决复杂问题”。它与 Java 语言生态
    的头像 发表于 04-25 11:13 746次阅读

    如何成为一名合格的KaihongOS北向应用开发工程师

    基础知识 编程语言:学习至少一编程语言,如 JavaScript和TypeScript,这些语言是北向应用开发必备的基础
    发表于 04-23 06:46