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

    文章

    531

    浏览量

    34849
  • 数据源
    +关注

    关注

    1

    文章

    65

    浏览量

    10043
  • go语言
    +关注

    关注

    1

    文章

    159

    浏览量

    9625

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

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

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

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

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

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

    为什么ADA4530-1运放总是

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

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

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

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

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

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

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

    三一挖掘机一键启动开关易的原因及更换注意事项

    三一挖掘机一键启动开关易的原因虽然三一挖掘机的一键启动系统设计旨在提高便利性和安全性,但在实际使用可能会出现一些问题导致开关易。这些
    发表于 03-12 09:29

    语言模型的解码策略与关键优化总结

    本文系统性地阐述了大型语言模型(LargeLanguageModels,LLMs)的解码策略技术原理及其实践应用。通过深入分析各类解码算法的工作机制、性能特征和优化方法,为研究者和工
    的头像 发表于 02-18 12:00 1068次阅读
    大<b class='flag-5'>语言</b>模型的解码策略与关键优化总结

    OpenAI报告GPT-4o及4o-mini模型性能下降,正紧急调查

    ,自发现这一问题以来,公司已经迅速启动了内部调查机制,以尽快查明导致模型性能下降的具体原因。OpenAI强调,他们对此次事件高度重视,并将全力以赴解决这一问题,以确保用户能够继续享受到高质量的AI服务。 GPT-4o和4o-mi
    的头像 发表于 01-23 10:22 1105次阅读

    OpenAI:GPT-4o及4o-mini模型性能下降,正展开调查

    近期,OpenAI发布了一份事故报告,指出其GPT-4o及4o-mini模型遭遇了性能下降的问题。这一消息引起了业界的广泛关注和讨论。 据OpenAI官方透露,他们目前正在积极调查这一性能下降
    的头像 发表于 01-21 10:34 936次阅读

    光耦的使用环境对性能的影响

    导致光耦的传输效率下降。 光敏元件的灵敏度 :温度的变化也会影响光敏元件的灵敏度,过高或过低的温度都可能导致光敏元件性能
    的头像 发表于 01-14 16:51 1890次阅读

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

    自己的技能。 10. 获得认证 专业认证 :如果可能,获取相关的专业认证,这可以增加你的可信度和市场竞争力。 成为一名合格的北向应用开发工程师需要时间和努力,通过不断学习和实践,你将能够掌握所需的技能,并在这一领域取得成功。
    发表于 01-10 10:00

    ADS1278上电后并未发热,只是DRDY一直检测不到下降沿,为什么?

    第一次使用ADS1278,设计疏忽导致DVDD与IOVDD都给了3.3V,AVDD给5V,板子上电后并未发热,只是DRDY一直检测不到下降沿,请问DVDD(手册规定1.8V标准)
    发表于 01-08 08:20

    LED硫化物的来源及其对性能的影响:以硫为例

    LED电源与硫化现象的关系在LED显示屏制造行业,LED电源的质量对LED光源的性能有着不可忽视的影响。特别是LED光源的硫化现象,即LED光源因接触含硫物质而发生化学反应,导致性能
    的头像 发表于 01-02 14:04 800次阅读
    LED<b class='flag-5'>中</b>硫化物的来源及其对<b class='flag-5'>性能</b>的影响:以硫为例

    影目科技发布全球首款同传翻译眼镜INMO GO2

    近日,搭载紫光展锐W517芯片平台的INMO GO2由影目科技正式推出。作为全球首款专为商务场景设计的智能翻译眼镜,INMO GO2 以“快、准、稳”三大核心优势,突破传统翻译产品局限,为全球商务人士带来高效、自然、稳定的跨语言
    的头像 发表于 12-11 10:00 1944次阅读

    导致ADS1278频繁的原因?

    表现为前2个通道损坏,我想问专家们,是不是这个上电顺序导致的ADS1278频繁的原因?非常感谢您的解答。
    发表于 12-09 07:19