这是我参与「第三届青训营 -后端场」笔记创作活动的第3篇笔记。
《高质量编程与性能调优实战》课程由张雷老师讲授,根据张老师讲解的课程内容,我总结梳理出了如下笔记内容。
见微知著——课程重点一览
步步为营——知识点详细剖析
高质量编程
通过前两节课程的学习,我们已经对Go语言编程有了一定的基础,同时也实际动手编写了部分应用程序。那么在实际的项目开发过程中,还有一些编程习惯和技巧是我们需要了解和掌握的。
高质量编程简介
通过编程实现功能是最基本的要求,而编写高质量的代码则是我们应该追求的目标。如何才能够编写出高质量的代码,编写代码的过程中需要遵循哪些规范,以及如何对编写的代码进行优化是我们在编程之路成长过程中必备的技能。
高质量代码
首先了解一下什么是高质量代码:编写的代码能够达到正确可靠、简洁清晰的目标就可以称为高质量代码。高质量代码一般具有正确性、可靠性、简洁和清晰的特点:
- 正确性:覆盖各种边界条件,对错误的调用能够进行处理
- 可靠性:针对异常情况和错误有明确的处理策略,能够处理依赖的服务发生的异常,提供稳定性的保障
- 简洁:逻辑简单,支持后续功能调整和添加的快速实现
- 清晰:代码易读易维护,重构或修改功能时不必担心出现不可控的问题
高质量代码编写原则
编写高质量代码实际上可以遵循一个统一的原则:保证代码的简单性、可读性,提高团队生产力。
- 简单性:消除“多余的复杂性”,用最简单清晰的逻辑编写代码。
- 可读性:代码是写给人看的,需要人去维护。编写一段可维护的代码的第一步就是要确保代码的可读性。
- 生产力:编程更多的是团队合作,因此团队整体的工作效率非常重要。
编码规范
了解了高质量代码编写的原则,下一步就是遵守高质量代码的编写规范进行编程了。Google和大规模采用Go语言编程的公司都有开源的编码规范文档可以参考。下面重点说明一些通用的编码规范。
代码格式
在代码格式方面,推荐使用gofmt自动格式化代码,保证所有的Go语言代码与官方推荐的格式保持一致。另外,可以使用goimports来对依赖包进行管理,自动增删依赖的包的引用,按字母排序并进行分类。
注释
注释也是高质量代码所必备的一个编码规范。一个好的代码有很多注释,一个不好的代码需要很多注释。常见的注释主要有4类:
- 解释代码作用:这类注释适合说明公共符号,但要注意不要对显而易见的内容进行过度说明
- 解释代码如何做的:这类注释适合说明实现过程,它会对代码中复杂的、不明显的逻辑进行说明,同样需要注意不要过度说明和直译代码
- 解释代码实现的原因:这类注释用来解释代码的外部因素,提供额外的上下文信息
- 解释代码什么情况会出错:这类注释用来解释代码的限制条件或无法处理的情况
公共符号始终需要进行注释。包中声明的每个公共符号都需要添加注释;既不明显也不简短的公共功能必须予以注释;无论长度或复杂度如何,对库中的任何函数都必须进行注释。但需要注意的是:不要对实现接口的方法进行注释,更不要在注释中说明跳转查看的文档。
代码其实是最好的注释,注释应该提供代码未表达出来的上下文信息。简洁清晰的代码对流程注释没有要求,但代码的相关背景可以通过注释来进行补充,从而提供有效的信息。
命名规范
写代码时最常见的约定就是命名上的约定。关于命名可以分为变量的命名、函数的命名以及包的命名。
- 变量的命名:
对于变量的命名要遵循简洁胜于冗长的约定。当涉及到缩略词时,一般情况下需要将缩略词全大写;当缩略词位于变量开头且不需要导出时,需要将缩略词全小写。另外,变量距离其被使用的地方越远,需要携带的信息就需要越多。
- 函数的命名:
函数名不必携带包名的上下文信息,函数名需要尽可能地简短。当函数返回类型与包名相同时,可以省略类型信息。
- 包的命名:
包名仅由小写字母组成,包名简短且包含一定的上下文信息。此外,包名还需要遵循如下规则:
-
- 不与标准库重名
-
- 不使用常用变量名做包名
-
- 包名使用单数形式
-
- 包名需谨慎使用缩写
命名的核心在于降低阅读代码的成本,命名时要重点考虑上下文信息,设计简洁清晰的名称。
控制流程
在控制流程上也要遵循一些约定俗成的规则:
- 线性原理,尽可能地避免嵌套,保持正常流程清晰:
条件分支语句的else语句通常为正常流程语句,如果if条件语句中含有return语句,则可以去除冗余的else使正常流程更加清晰。
- 对于多条件的条件分支语句,尽量保持正常代码路径为最小缩进:
我们应该优先处理错误情况和特殊情况,尽早让函数返回或继续循环来减少代码的嵌套关系。
系统故障往往存在于复杂的条件语句和循环语句中,我们应尽可能地提升我们的代码的可维护性和可读性,使得代码逻辑变得清晰。
错误和异常处理
在对错误和异常的处理上也有一些可以参考的规范可以去学习。
error
error是系统中较为常见的错误,对error的处理应该尽可能提供简明的上下文信息链,方便问题的定位。关于error的处理,有如下几点需要了解:
- 简单错误
简单错误一般是指仅出现一次的错误,且在其他地方不需要捕获。对于简单错误的处理应优先使用errors.New来创建匿名变量来直接表示错误。如有格式化的需求,则使用fmt.Errorf进行处理。
- 错误链
对于复杂的错误有时并不能简单地进行描述,错误的包装提供了error嵌套另一个error的能力,从而生成一个error的追踪链,同时结合错误的判定方法来确认调用链中是否有关注的错误出现。错误的包装可以使每一层的调用方将自己对应的上下文补充进去,方便追踪排查问题。另外,在fmt.Errorf中使用%w可以将一个错误打包至错误链中。
- 错误的判定
对错误的判定有三种方法:
-
- 直接使用==进行判定:判断错误是否与指定错误类型匹配
-
- 使用errors.Is方法:既可以判断一个错误是否为特定错误,还能判断错误链上是否含有特定错误
-
- 使用errors.As方法:该方法可以从错误链中提取出特定种类的错误,方便后续的处理
panic
在Go语言中,比error更为严重的错误是panic,它的出现表示程序无法正常工作。
在编写代码的过程中,不建议在业务代码中使用panic。当panic发生后,程序会向上传播至调用栈顶,如果当前协程中所有defer函数都不包含recover,就会导致整个程序崩溃。若问题可以被屏蔽或解决,建议使用error来代替panic。特殊地,当程序启动阶段发生不可逆转的错误时,可以在init或main函数中使用panic。
recover
recover是用来处理panic的一种机制,它只能在当前协程中被defer的函数中使用,如果发生嵌套则会导致失效。如果程序需要更多的上下文信息,可以在recover后在log中记录当前的调用栈。
性能优化建议
高质量的代码仅能够保证完成程序的基本功能,但在大规模程序部署的场景下,仅仅支持正常功能的实现是远远不够的,我们还需要尽可能地提升性能、节约资源的成本。
性能优化是建立在代码满足正确可靠、简洁清晰等质量因素前提下的,它是对代码质量的综合评估。有时,在做性能优化的过程中时间效率和空间效率是可能对立的,这时就需要我们根据实际情况来做出适当的折衷。
针对Go语言本身来说,可以从语言层面上来对系统的性能做出优化。
Benchmark
Go语言自带了性能评估工具benchmark,可以通过具体的数据来对比分析系统的性能表现。
Slice
切片Slice是Go语言中最常用的结构,而在使用切片时可以使用一些技巧来提升代码的性能。
首先是可以尝试对切片进行预分配。我们可以尽可能地为切片提供容量信息,这样可以避免内存发生频繁的拷贝。
第二是面对大内存的数组结构时,考虑使用copy来代替re-slice,这样可以让大对象能够及时地得到释放。
Map
Map和切片一样,也可以考虑在创建时进行容量的预分配。由于不断地向map中添加元素,势必会导致触发map的扩容。当map扩容发生时,必然会产生内存拷贝和rehash的消耗。因此,建议根据实际需求提前预估好需要的空间以提升系统性能。
字符串处理
字符串在Go语言中是不可变类型,占用的内存大小是固定的。而字符串的拼接操作是很常见的一种操作。在Go语言中,字符串的拼接操作有三种方式,不同的方式对应的代码性能也不尽相同:
- 直接使用+进行拼接:直接使用+进行拼接的性能是最低的。每进行一次拼接,都需要开辟一段新的空间,重新地进行一次内存分配。
- 使用bytes.Buffer进行拼接:bytes.Buffer底层使用[]byte数组进行存储,采用内存扩容策略,因此不需要重新分配内存。但最终转化为字符串的过程中,bytes.Buffer重新申请了一块空间。
- 使用strings.Builder进行拼接:strings.Builder底层同样使用[]byte数组进行存储,采用内存扩容策略,因此也不需要重新分配内存。在返回时,strings.Builder直接将底层的[]byte数组转化成字符串,并直接返回。因此,strings.Builder比bytes.Buffer更快一些。
字符串的拼接与切片一样,也是支持预分配的,通过预分配可以进一步地提升拼接性能。
空结构体
除了时间上的性能优化,Go语言还有在空间上进行优化的方法。我们可以通过使用空结构体来节省内存。这是因为空结构体实例是不占内存的,它可以作为各种场景下的占位符,最为常见的场景是set的实现。
atomic包
最后说一下多线程场景下的性能调优。为了保证线程安全,并发编程的过程中可能会用到锁机制来进行保证。但锁的实现是通过操作系统来实现的,属于系统调用,性能稍差一些。而使用一些原子操作就可以在一定程度上提升性能,这是因为原子操作是通过硬件来实现的,比锁机制更加高效。
此外,sync.Mutex的应用应该是去保护一段逻辑,而不仅仅是用于保护一个变量的线程安全。而对于非数值的操作是可以考虑使用能够承载接口的atomic.Value来提升性能的。
以上便是在性能优化过程当中比较常见的几类建议。简单来说就是,在满足正确可靠、简洁清晰的质量要求的前提下提高程序的性能,避免常见的性能陷阱。当然,对代码性能的优化也需要适当,不要一味地追求程序的性能,毕竟越高级的性能优化手段越容易导致系统出现问题。
性能调优实战
到目前为止,我们了解了高质量编程的原则和一些实践规范,了解到了一些常用的性能优化建议。那么下面就来熟悉一下实际工作中是如何进行性能调优的。
性能调优简介
性能调优原则
首先我们来了解一下性能调优的原则:
- 要依靠数据而不是猜测
- 要定位最大瓶颈而不是细枝末节
- 不要过早优化
- 不要过度优化
性能分析工具pprof实战
既然我们在进行性能调优的过程中依靠数据来说话,那么程序性能指标数据就需要有特定的工具来帮忙获取。Go语言当然也为我们提供了方便的性能分析工具——pprof。
pprof功能简介
pprof工具大致可以分成四个主要的部分:
- 分析部分:有网页端展示和可视化终端展示两种方式
- 具体的工具:可以在runtime/pprof中找到源码,同时Go语言的http标准库中也对pprof做了封装,允许用户在http服务中直接使用
- 采样部分:可以采样程序运行时的CPU、堆内存、协程、锁竞争、阻塞调用和系统线程的使用数据
- 展示:用户可以通过列表、调用图、火焰图、源码、反汇编等视图去展示采集到的性能指标,方便分析
pprof排查实战
- 搭建pprof实战项目
提供一个提前埋入“炸弹”代码的开源项目,能够产生可观测的性能问题。“炸弹”主要埋在了CPU、堆内存、协程、锁竞争和阻塞操作上。项目地址:github.com/wolfogre/go…
- 浏览器查看指标
在浏览器中打开http://localhost:6060/debug/pprof 可以在浏览器端查看项目的指标数据。页面上展示了可用的程序运行采样数据,下面也有对应的简单说明:
-
- allocs:内存分配情况
-
- blocks:阻塞操作情况
-
- cmdline:程序启动命令行
-
- goroutine:当前所有协程的堆栈信息
-
- heap:堆上内存使用情况
-
- mutex:锁竞争操作情况
-
- profile:CPU占用情况
-
- threadcreate:当前所有创建的系统线程的堆栈信息
-
- trace:程序运行跟踪信息
通过浏览器查看的指标数据可读性很差,需要借助pprof工具协助阅读这些指标。
- CPU问题排查
我们通过使用go tool pprof + 采样链接的方式来启动采样。采样结束后,采样数据会被记录和下载,并展示出pprof终端。
下面介绍几个常用的命令:
-
- top命令
输入top命令,查看CPU占用最高的函数。返回信息以表格的形式分成5列进行展示:
-
-
- Flat:当前函数的占用
-
-
-
- Flat%:Flat占总量的比例
-
-
-
- Sum%:上面所有行的Flat%总和
-
-
-
- Cum:当前函数加上其调用函数的总占用
-
-
-
- Cum%:Cum占总量的比例
-
表格之前描述采样的总体信息。默认会展示资源占用最高的10个函数,如果只需要查看最高的N个函数,可以输入topN。
通过观察表格数据可以发现这样一个情况:Flat和Cum有时相等,有时不等,有时又等于0。这是因为Cum - Flat的值反应了函数中调用其他函数所消耗的资源。当函数中没有调用其他函数时,Flat和Cum相等,而当函数中只有调用其他函数逻辑时,Flat等于0。
-
- list命令
list命令会根据后面给定的正则表达式查找代码,并按行展示出每一行的占用。
-
- web命令
web命令用于实现调用关系可视化,可以生成一张调用关系图,默认会使用浏览器打开。图中除了展示每个节点的资源占用情况以外,还会将他们的调用关系穿起来。占用资源最大的节点会被用最红最大的方框标注出来。
将CPU问题代码注释掉之后,发现CPU的数值降下来了,然而内存的使用依然很高,下面开始排查内存问题。
- Heap-堆内存问题排查
除了使用pprof终端排查问题外,我们还可以使用-http:=8080参数开启pprof自带的Web UI,性能指标会以网页的形式呈现。
通过输入go tool pprof -http:8080 + 采样链接再次启动pprof工具,等待采样完成后,浏览器会被自动打开,展示出熟悉的web视图。
在页面顶端的view菜单中可以切换不同的视图。在Source视图下,可以看到页面上展示出刚刚看到的四个调用和具体的源码视图。我们也可以在顶部的搜索框中使用正则表达式过滤出需要的代码。
将对应的问题代码进行注释,此时便又排查掉了一个炸弹。
然而内存问题并没有真正地完全排查掉,这是因为此时我们是在inuse_space视图下进行问题排查,而在sample菜单下实际上提供了四种堆内存的指标,而默认情况下展示的是inuse_space指标。堆内存的四个指标分别是:
-
- alloc_objects:程序累计申请的对象数
-
- alloc_space:程序累计申请的内存大小
-
- inuse_objects:程序当前持有的对象数
-
- inuse_space:程序当前占用的内存大小
切换到alloc_space视图下,我们便马上定位到了另一处问题代码,该处代码在做无意义的内存申请,每次申请过后便直接释放,因此无法在inuse_space视图下排查到。将此处代码进行注释,便又排查掉一处“炸弹”。
- 协程问题排查
Go语言是一门自带垃圾回收机制的语言,因此,一般情况下内存泄漏不是很容易发生。但是Go语言的协程是容易泄漏的,进而会导致内存泄漏的产生。下面可以观察一下协程的使用情况。
打开http://localhost:6060/debug/pprof/ 可以发现该“炸弹”程序已经有105条协程在运行,虽然这个数量级并不是很大,但对于一个小程序来说是不正常的。
我们再次输入go tool pprof -http:8080 + 采样链接打开pprof工具,可以看到此时工具生成了一张非常长的调用关系图。尽管问题代码所在节点已被标出,但相对来说依然不便于阅读。因此,这里介绍另一种更为直观的展示方式——火焰图。
此时,打开View菜单,切换到Flame Graph视图。可以看到刚才的节点被折叠了起来。
在火焰图中,自顶向下展示了各个节点的调用关系,也反映了各个函数调用之间的层级关系。每一行中,条形越长代表消耗的资源占比越多,显然“又平又长”的节点是占用资源多的节点。
火焰图是非常常用的性能分析工具,在程序逻辑复杂的情况下十分有用。另外,火焰图是动态的,支持用户点击块并进行分析。
再次到Source视图下去搜索对应的问题代码,会发现问题代码处发起了10条无意义的协程,每条协程等待30秒之后才退出,导致了协程的泄漏。
将问题代码进行注释,发现此时的协程数量降到了正常水平。
- 锁问题排查
利用同样的方式,可以定位到锁问题出现的代码。将该问题代码进行注释,即可解决掉锁相关的问题。
- 阻塞问题排查
最后排查一下阻塞问题。在程序中,除了锁的竞争会导致阻塞问题外,还有很多执行逻辑也会导致阻塞。在http://localhost:6060/debug/pprof/ 页面下可以发现阻塞的问题还有两处。
再次打开pprof工具,可以发现我们能够定位到一个阻塞错误。然而根据刚刚统计页面的显示,一共是有两处阻塞问题,但这里却只展示了一处。这是因为未被展示出来的那处阻塞的总用时小于总时长的千分之五而被忽略过滤掉了,这样的过滤策略能够更加有效地突出问题所在,有利于我们对问题的定位。
我们回到统计页面并点开block指标页面,我们发现第二处阻塞发生在http handler中,而这个阻塞操作是符合预期的。
至此,该项目中所有的“炸弹”就都被排查出来了。
pprof的采样过程和原理
以上是利用pprof对项目中问题代码排查的全过程,下面来分析一下pprof工具是如何进行采样分析的。
- CPU采样过程及原理
CPU采样会记录所有的调用栈和它们的占用时间。在采样时,进程会每秒暂停100次,每次会记录当前的调用栈信息。汇总之后,根据调用栈在采样中出现的次数来推断函数的运行时间。
我们需要手动地启动和暂停采样,每秒100次的暂停频率也不能更改。这个定时暂停机制在unix上是依赖信号机制实现的。每次暂停都会接收到一个信号,通过系统计时器来保证这个信号是固定频率发送的。
一共有三个相关角色:进程本身、操作系统和写缓冲。
启动采样时,进程向操作系统注册一个定时器,操作系统会每隔10ms向进程发送一个SIGPROF信号,进程接收到信号后就会对当前的调用栈进行记录。与此同时,进程会启动一个写缓冲的协程,它会每隔100ms从进程中读取已经记录的堆栈信息,并写入到输出流。
当采样停止时,进程向操作系统取消定时器,不再接收信号,写缓冲读取不到新的堆栈时,结束输出。
- Heap-堆内存采样过程及原理
由于pprof的内存采样具有局限性,因此内存指标仅能够采集到堆内存的相关信息。内存采样在是线上依赖了内存分配器的记录,所以它只能记录在堆上分配且会参与GC的那部分内存。底层的cgo调用分配的内存是不会被内存采样记录下来的。
与CPU和协程都不同的是,内存的采样是一个持续的过程,它会记录从程序运行起的所有分配或释放的内存大小和对象数量,并在采样时遍历这些结果进行汇总。alloc的两项指标是从程序运行开始的累计指标,而inuse的两项指标是通过累计分配减去累计释放得到的程序当前持有的指标。
- 协程与系统线程
这两个采样指标在概念上和实现上都比较相似,因此在这里对比着进行介绍:
-
- 协程采样:会记录所有用户发起以及main函数所在的协程的信息,并创建这些协程的调用栈。
-
- 系统线程采样:记录程序创建的所有系统线程的信息。
他们在实现上非常相似,都会在SWT后遍历所有协程/所有系统线程的列表并输出堆栈,最后Start The World继续运行。这个采样是立刻触发的全量记录,可以通过比较两个时间点的差值来得到某一时间段的指标。
- 阻塞与锁
这两个采样指标在流程和原理上也非常相似。
这两个采样记录的都是对应操作发生的调用栈、次数和耗时,不过这两个指标的采样率含义并不相同。
-
- 阻塞操作的采样率是一个“阈值”,消耗超过阈值时间的阻塞操作才会被记录,1为每次操作都会被记录。
-
- 锁竞争的采样率是一个“比例”,运行时会通过随机数来只记录固定比例的锁操作,1为每次操作都会被记录。
它们在实现上也是基本相同的,都是一个“主动上报”的过程。
在阻塞操作或锁操作发生时,会计算出消耗的时间,连同调用栈一起主动上报给采样器,采样器会根据采样率可能会丢弃一些记录。
在采样时,采样器会遍历已经记录的信息,统计出具体操作的次数、调用栈和总耗时。和堆内存一样,你可以对比两个时间点的差值计算出一段时间内的操作指标。
性能调优案例
最后以一个实际案例带大家了解一下工程中是如何进行性能调优的。
对于一个程序来说,从不同的应用层次上看,可以分为业务服务、基础库和Go语言本身三类,对应优化的适用范围也是越来越广。
- 业务服务:一般指直接提供功能的程序,比如专门处理用户评论的操作的程序。
- 基础库:一般指提供通用功能的程序,主要是针对业务服务提供功能,比如监控组件,负责收集业务服务的运行指标
- Go语言本身:指针对Go语言本身进行的优化项。
业务服务优化
- 基本概念
首先了解一些常见的基本概念:
-
- 服务:能够单独部署并承接一定功能的程序被称为服务。
-
- 依赖:当A服务的功能实现依赖于B服务的响应结果时,我们程A服务依赖B服务。
-
- 调用链路:支持一个接口请求的相关服务集合及其相互之间的依赖关系被称为调用链路。
-
- 基础库:公共的工具包和中间件被称为基础库。
下图中展示了一个简单的系统的部署示意图,客户端请求经过网关转发,由不同的业务服务处理,业务服务可能依赖其他的服务,也可能会依赖存储、消息队列等组件。
- 流程
业务服务优化的主要流程分为四个步骤:“建立服务性能评估手段”、“分析性能数据,定位性能瓶颈”、“重点优化项改造”以及“优化效果验证”
建立服务性能评估手段
- 该案例并没有使用benchmark工具,这是因为单独的benchmark无法满足复杂的逻辑分析,并且不同负载下的性能表现会有巨大的差异,因此该场景下并不适合用benchmark进行分析。
- 由于不同的请求参数有着不同的处理逻辑,对应的性能表现也不相同,需要我们尽可能地模拟线上的真实情况,从而分析真正的性能瓶颈。
- 通过压测可以录制线上的请求流量,通过控制回放速度来对服务进行测试。测试的范围可以是单个实例,也可以是整个集群。
- 同样性能数据的采集也可以区分为单机性能采集和集群性能采集。
评估手段建立后,其产出是一个服务器的性能指标分析报告。实际的压测截图会统计压测期间服务的各项监控指标,包括QPS,延迟等内容,同时在压测过程中,也可以采集服务的pprof数据,使用之前的方式来分析性能问题。
分析性能数据,定位性能瓶颈
有了服务优化前的性能报告和一些性能采样数据,我们就可以进行性能瓶颈的定位分析了。而发现服务性能的瓶颈是优化的核心,因此定位性能瓶颈至关重要。
业务服务常见的性能问题有两类:
- 基础组件的不规范使用:如频繁的json解析占用大量CPU资源、日志的不规范使用
- 高并发场景下的优化不足:监控上报采用同步请求导致系统性能达到瓶颈,改为异步请求便可提升系统性能。
重点优化项改造
性能优化的前提是保证正确性,所以在变动较大的性能优化上线前,还需要进行正确性验证。由于线上的场景和流程太多,所以需要借助自动化手段来保证优化后程序的正确性。
同样是线上请求录制,不过这里不仅包含请求参数录制,还会录制线上的返回内容,重放时对比线上的返回内容和优化后服务的返回内容进行正确性的验证。
优化效果验证
优化改造后就可以进行效果验证了。验证可以分为两个部分:
- 首先是重复压测验证,利用同样的数据对优化后的服务进行压测。
- 之后进行上线评估优化效果,通过关注服务监控、逐步放量、收集性能数据来记录真正的优化效果。
进一步优化,服务整体链路分析
以上内容是针对单个服务的优化过程,在熟悉服务的整体部署情况后,可以针对具体的接口链路进行分析调优。我们可以规范上游服务调用接口,明确场景需求;分析链路,通过业务流程来优化提升服务性能,从而做到对服务整体的进一步优化。
基础库优化
比服务优化适用范围更广的是基础库的优化,为了评估某些功能上线后的效果,经常需要使用AB实验来对SDK进行优化,观察不同策略对核心指标的影响。因此,公司内部多数服务都会使用AB实验的SDK。如果能够优化AB组件库的性能,所有用到的服务都会有性能提升。
与业务服务优化相似,基础库的优化也遵循着一个流程:
- 首先,是分析基础库核心逻辑和性能瓶颈,此时我们需要设计完善的改造方案、按需获取数据,并对数据序列化协议进行优化。
- 之后,是进行内部压测验证。
- 最后,进行推广业务服务的落地验证。
Go语言优化
最后是适用范围最广的针对Go语言本身的优化。对Go语言的优化主要是体现在对编译器和运行时内存分配策略的优化,从而构建更高效的Go语言发行版本。优化过程也有一个简单的流程:
- 优化内存分配策略
- 优化代码编译流程,生成更高效的程序
- 内部压测验证
- 推广业务服务落地验证。
对Go语言本身进行优化使得业务服务接入简单,只需要调整编译配置即可,并且其通用性也很强,几乎对所有的Go程序都会生效。
小试牛刀——课后实践
作业描述
- 了解下其他语言的编码规范,是否和 Go 语言编码规范有相通之处,注重理解哪些共同点?
- 编码规范或者性能优化建议大部分是通用的,有没有方式能够自动化对代码进行检测?
- 从 github.com/golang/go/t… 中选择感兴趣的包,看看官方代码是如何编写的?
- 使用 Go 进行并发编程时有哪些性能陷阱或者优化手段?
- 在真实的线上环境中,每个场景或者服务遇到的性能问题也是各种各样,搜索下知名公司的官方公众号或者博客,里面有哪些性能优化的案例?比如 eng.uber.com/category/os…
- Go 语言本身在持续更新迭代,每个版本在性能上有哪些重要的优化点?
实践成果
- 了解下其他语言的编码规范,是否和 Go 语言编码规范有相通之处,注重理解哪些共同点?
答:其他语言与Go语言编码规范上也有着相当之多的相同之处,例如常见的C++语言,Java语言等,都有着和Go语言相同的命名规范、注释规范、异常处理规范等。
- 在命名规范上,都强调简洁、最大程度利用名称来表述含义。
- 在注释方面,都尽可能地对编写的代码进行必要性的说明。
- 在异常处理方面,都尽可能地对异常进行捕获并处理。
- 编码规范或者性能优化建议大部分是通用的,有没有方式能够自动化对代码进行检测?
答:对于目前的集成开发工具来说,大部分已经拥有一定程度的“智能化”,不但可以标注出明显的静态代码缺陷,还可以给出相应的修改意见。利用这些智能的集成开发工具,可以实现一定程度的自动代码检测。
- 从 github.com/golang/go/t… 中选择感兴趣的包,看看官方代码是如何编写的?
答:在sync包下有多个用于并发编程的并发工具类,如:
- Mutex:该类表示互斥锁,可以保证在同一时间内只有一个协程可以获得该锁,其他协程进入阻塞等待状态。
- WaitGroup:该类用于协程的并发等待,内部提供Add()、Done()、Wait()等方法,使用它可以保证协程间的同步。
- Once:该类一般用于单例的情况,内部只有一个Do()函数,并且只会被执行一次。
- 使用 Go 进行并发编程时有哪些性能陷阱或者优化手段?
答:并发编程条件下可能会存在线程安全的问题。比如对共享变量的自增操作,很有可能会出现线程不安全的问题,对此可以采用原子变量进行自增操作来进行改进。
- 在真实的线上环境中,每个场景或者服务遇到的性能问题也是各种各样,搜索下知名公司的官方公众号或者博客,里面有哪些性能优化的案例?比如 eng.uber.com/category/os…
答:Go的GC调优:eng.uber.com 利用一个自我引用的finalizer来监控GC事件,并用ticker监控堆内存来动态调整GOGC值。
- Go 语言本身在持续更新迭代,每个版本在性能上有哪些重要的优化点? 答:
- Go 1.1Go 致力于增强语言特性
- Go 1.2go test 命令支持代码覆盖率报告
- Go 1.3 改善堆栈的管理,增加了sync的pool组件,改进channel的实现
- Go 1.4 实现工作、改进垃圾收集器,引入了internal包
- Go 1.5 垃圾回收器被重新设计实现 map的语法更改
- Go 1.6 增加对于 HTTP/2 协议的默认支持;再一次降低了垃圾回收器的延迟;runtime改变了打印程序结束恐慌的方式。现在只打印发生panic的 goroutine 的堆栈,而不是所有现有的 goroutine
- Go 1.7:Context 库和 vendor 支持优化
- Go 1.8:垃圾回收器的延迟时间进行优化
- Go 1.9:type alias 支持
- Go 1.10:go build/test 增加缓存优化
- Go 1.11:引入 Go modules
- Go 1.12:go vet 工具
- Go 1.13:sync.Pool、defer、errors包优化
- Go 1.14:defer优化,提升time.Timer性能,允许嵌入具有重叠方法集的接口
- Go 1.15:改进了对高核心数的小对象的分配
- Go 1.17:微调了语言特性,允许从切片到数组指针的转换
- Go 1.18:支持泛型,模糊测试
温故知新——总结与感悟
通过张雷老师的这门课,我学习到了与高质量编程相关的多种规范。了解到了编程过程中需要遵循许多约定俗成的规范,进而提升我们自身的编码质量。这样做既可以使得我们的代码可读性增强,提升我们代码的质量,也方便团队其他成员阅读,理解编码的具体含义。
此外,张雷老师还讲授了如何对编写的代码进行性能调优,介绍了好用的pprof工具,方便我们日后在对代码整体的性能上进行检测与优化。整体上来说,这是十分有趣且有意义的一门课,有助于我们养成良好的编码规范和提升代码性能的意识。