1. 编程原则

实际场景千变万化,各种语言各不相同,但是高质量编程遵循的原则是相通的。

  • 简单性

    • 消除“多余的复杂性”,以简单清晰的逻辑写代码
    • 不理解的代码无法修复改进
  • 可读性
    • 代码是写给人看的而不是机器看的
    • 编写可维护的代码的第一步是确保代码可读
  • 生产力
    • 团队整体工作效率非常重要

2. 命名规范

2.1 变量的命名

  • 缩略词全部大写,但当其位于变量开头而且不需要导出时,需要全小写

    • 例如ServeHTTP而不是用ServeHttp
    • 使用XMLHTTPRequest或者xmlHTTPRequest
  • 变量距离其被使用的地方越远,则需要携带越多的上下文信息
    • 全局变量在其名字中需要更多的上下文信息,使得在不同地方可以轻易辨认出其含义

2.2 函数/方法的命名

  • 函数名尽量不携带包名的上下文信息,因为包名和函数名总是成对出现的
  • 函数名要尽量简短
  • 当名为foo的包的某个函数返回类型foo时,可以省略类型信息而不导致歧义
  • 当名为foo的包的某个函数返回类型T时(T并不是Foo),可以在函数名中加入类型信息。

2.3 包的命名

  • 只由小写字母组成。不包含大写字母和下划线等字符
  • 简短并包含一定的上下文信息。例如 schema、task 等
  • 不要与标准库同名。例如不要使用 sync 或者 strings

3. 编码规范-控制流程

  • 线性原理,处理逻辑尽量走直线,避免复杂的分支嵌套
  • 正常流程代码沿着屏幕向下移动
  • 提升代码可维护性和可读性
  • 故障问题大多出现在复杂的条件语句和循环嵌套中

4. 编码规范-错误捕获与处理

4.1 简单错误处理

  • 简单的错误指的是仅出现一次的错误,而且在别的地方可以不用捕获该错误
  • 优先使用errors.new(“error info”)这样的匿名消息来处理简单错误
  • 如果有格式化的要求则使用fmt.Errorf
    如:
fmt.Errorf("正在处理错误%d", 20036)

4.2 复杂错误处理

  • 错误的warp实际上是提供了一个error嵌套另一个error的能力,从而生成一个error的跟踪链。如何理解跟踪链呢,也就是类似于JAVA里面异常处理的机制,当发生异常时,异常会一层层地向上抛,直到它被捕获并且处理。
  • 在fmt.Errorf中使用%w关键字来将一个错误关联至错误链中。
  • 错误判定,判定一个错误是否为特定错误,使用Error.Is,不同于使用== ,使用该方法可以判定错误链上的所有错误是否含有特定的错误
  • 特定错误获取,在错误链上获取特定种类的错误,使用errors.As
  • panic,不建议在业务代码中使用panic,调用函数不包含recover会造成程序崩溃,若问题可以被屏蔽解决,建议使用error代替panic
  • 当程序启动阶段发生不可逆转的错误时,可以在init或main函数中使用panic
  • recover机制:只能在被defer的函数中使用,嵌套无法生效,只在当前的goroutine生效.
  • defer语句:defer是后进先出的,我们来看一个例子
func test() {if true {defer fmt.Println("1")} else {defer fmt.Println("2")}defer fmt.Println("3")
}
//输出的结果是:
//3
//1
//代码解析:首先首先是先进到了1所对应的defer中,然后最后进到了3所对应的defer中,根据后进先出原则,先执行3,再执行1

5. 性能优化

5.1 性能优化帮助工具-Benchmark

go语言提供了支持基准性能测试的benchmark工具
在终端中执行go test -bench=. -benchmen即可启动

5.2 性能优化建议-slice使用

  • 尽量在make slice时提供合适的容量信息以适应底层。
  • 关于slice:其本质是数组片段的描述
    • 包括数组指针
    • 片段的长度
    • 片段的容量(不改变内存分配情况下的最大片段长度),一旦长度超过该容量就会需要重新为切片分配内存
  • 切片操作并不复制切片所指向的元素,而是在创建新切片时复用原来的切片。
    • 陷阱:大内存未释放,在已有切片的基础上创建切片,不会创建新的底层数组
    • 实例:创建了一个100MB的大切片,此时引用一个它的3MB的小切片,这时候内存中依然有对大切片的内存引用,大切片得不到释放。
    • 策略:使用copy来代替re-slice

5.3 性能优化建议-map的使用

  • 对于map的定义,我们有两种方式,一种是提前预设好map的容量,另一种是不提前预设它的容量
data := make(map[int]int)
data := make(map[int]int,size)
  • 在向map中填充数据的时候,map会不断扩容,如果我们提前对map中的内存进行预分配,可以减少内存拷贝和rehash的消耗。

5.4 性能优化建议-stringbuilder的使用

程序中对于处理拼接字符串有以下三种策略:

  • 直接使用+进行拼接
func addStr_fir(n int, str string) string {s := ""for i := 0; i < n; i++ {s += str}return s
}
  • 使用stringbuilder
func addStr_sec(n int, str string) string {builder := strings.Builder{}for i := 0; i < n; i++ {builder.WriteString(str)}return builder.String()
}
  • 使用byte[]数组
func addStr_thr(n int, str string) string {buf := new(bytes.Buffer)for i := 0; i < n; i++ {buf.WriteString(str)}return buf.String()
}

经过测试,性能最佳的使用stringBuilder的方法,最差的为直接使用加号拼接的,原因分析:

  • 字符串在GO中是不可变类型占用内存的大小是固定,这一点和JAVA中的字符串管理是相同的,因此在使用+后,每次内存都会重新分配
  • StringBuilder和StringBuffer其底层都是[]byte数组
  • 内存扩容策略:不需要每次拼接都重新分配内存
  • 为什么使用StringBuffer比StringBuilder要慢?
    在拼接字符串时,其原理是一致的,差别在于最后,将底层的[]byte数组转化为string时存在差异,

    • stringBuilder是直接将[]byte元素转化为字符串后返回的
    • byte.Buffer是新开辟了一块内存空间,把这个字符存到这里面去,再返回的
  • 支持内存预分配:如果已知未来拼接后的字符串的长度,可以通过Grow()方法来为builder或者buffer预分配内存空间,从而进一步提高性能

5.5 性能优化建议-空结构体的使用

可以使用空结构体来节省内存

  • 空结构体struct实例不占据任何的内存空间
  • 可作为任何场景下的占位符使用
    • 节省资源
    • 不占据任何的内存空间,同时也具有很强的语义,仅作为占位符

5.6 性能优化建议-atomic的使用

对于传统的加锁互斥来保证并发安全,也可以通过调用atomic包内方法来保证并发安全,而且效率更高

type atomicCounter struct {i int32
}func atomicFunc(c *atomicCounter) {atomic.AddInt32(&c.i, 1)
}
  • 锁的实现是通过操作系统来实现的,属于系统调用
  • atomic是通过硬件来实现的,效率比锁要搞
  • sync.Mutex应该用来保护一段逻辑,不仅仅用来保护一个变量
  • 对于非数值操作,可以用atomic.Value来操作,可以承载一个interface{}

6. 性能分析工具-pprof介绍与使用

6.1 分析指标

  • 当flat == cum时,该函数没有调用其他函数
  • 当flat = 0,该函数只调用了其他函数
  • 使用list 关键字可以来定位对应的代码行
  • 使用web可以来使用可视化对刚才的过程进行图解分析

6.2 火焰图的使用

  • 火焰图:从上到下表示调用顺序
  • 每一块代表一个函数,越长代表占用CPU的时间更长
  • 火焰图是动态的,支持点击块进行分析

6.3 通过完成实验来了解pprof的使用流程、

  • 首先我们需要了解到pprof是将当前采集到的数据输出到profile文件中的,所以我们就需要先拿到这个profile文件,我们可以通过该文件来获取对应的信息,输入指令
    go tool pprof "http://127.0.0.1:6060/debug/pprof/profile?seconds=10"
    同时可以通过topN来查看运行时间(占用)最高的几个服务
  • 接着我们使用list eat这个指令来查看代码的所在位置来排除低性能。

    可以看到图中,运行时间最多的是这个for循环,于是我们将有关的代码注释掉,然后对数据进行观测。

    如果想要使用web指令,则需要先安装插件Graphviz,安装后配置bin环境变量即可

    同样可以定位到炸弹所在位置
  • 接下来分析发现内存的利用率依然很高,我们继续来排除原因
    对于内存问题,可以通过查看堆内存的方法来查看到底是哪里存在问题
    go tool pprof -http=:8080 "http://127.0.0.1:6060/debug/pprof/heap",如图:

    从而定位到原因代码2

    还可以通过alloc_space来查看目前是存在无意义的内存分配,比如说这里的话就是一直在申请16MB的内存,但是由于申请了没有用,会马上被GC回收掉,所以就不会显示在insue里面

    alloc_objects:程序累计申请的对象数
    alloc_spoace:程序累计申请的内存大小
    insue_objects:程序当前持有的对象数
    insue_space:程序当前占用的内存大小

    然后我们继续分析,发现开启的协程到达了100的量级,这对于一个简单的小程序来说是很不正常的,我们使用火焰图对其进行排查


    我们发现此循环不断启动新协程,而且每启动还强制睡眠阻塞,我们将其注释掉

    完成后继续分析锁的问题




    至此就已经将程序中的所有炸弹排除了

7. 业务服务优化基本概念

  • 服务:能够单独部署,承载一定功能的程序
  • 依赖:Service A的功能实现依赖Service B的响应结果,称为Service A依赖Service B
  • 调用链路:能支持一个接口请求的相关服务集合及其相互之间的依赖关系
  • 基础库:公共的工具包、中间件

8. 内存管理基础

8.1 自动内存管理

  • 动态内存:程序在运行时根据需求动态分配的内存:malloc

  • 自动内存管理(垃圾回收):由程序语言的运行时系统管理动态内存

    • 避免手动管理内存,专注于实现业务逻辑
    • 保证内存的使用性和安全性
  • Mutator:业务线程,分配新对象,修改对象的指向关系

  • Collector:GC线程,找到存活对象, 回收死亡对象的内存空间

  • Serial GC:只有一个Collector

  • Parallel GC:支持多个Collector同时回收的GC算法

  • Concurrent GC:Mutator(s)和collector(s)可以同时执行,Collector(s)必须感知到对象指向关系的改变

    在GC前一个对象所指向的对象均未被标记,当GC启动后,会将存活的对象都标记上,但是当此时业务进程正在执行,将对象又指向了一个新对象的时候,这时候GC可能会疏忽漏标记了b对象,导致b对象被回收。

  • 评价GC算法的指标

    • 基本要求:不能回收存活的对象
    • 吞吐率:1-(GC时间/程序运行时间)
    • 暂停时间:stop the world(STW)业务是否感知到了
    • 内存开销:GC元数据开销
  • 追踪垃圾回收

    • 对象被回收的条件:指针指向关系不可达的对象
    • 标记根对象
      • 静态变量、全局变量、常量、线程栈等
    • 标记:找到可达对象
      • 求指针指向关系的闭包:从根对象出发,找到所有可达对象
    • 清理:清理所有不可达对象
      • 将存活对象复制到另外的内存空间(copying GC)
      • 将死亡对象的内存区域标记为可分配(mark-sweep GC),使用free-list管理内存
      • 移动并整理存活对象(mark-compact GC),原地整理
    • 根据对象的生命周期,使用不同的标记和清理策略
  • 引用计数

    • 每个对象存有一个引用次数的字段,对象存活的条件就是当其引用次数>0的时候
    • 优点
      • 内存管理的操作被平摊到程序执行的过程中
      • 内存管理不需要了解runtime的实现细节,只需要维护引用次数即可
    • 缺点
      • 当在多线程并发执行的时候,有可能会对同一个对象进行操作,因此需要对对象的操作原子化,通过原子操作保证对引用计数操作的原子性和可见性
      • 当存在环形的数据结构的时候,是无法回收的
      • 内存开销,每个对象都引用额外内存空间存储引用数目
      • 回收内存时可能引发暂停

8.2 分代GC

  • 分代假说:most objects die young,很多对象在分配出来之后就不再使用了
  • 每个对象都有年龄:所谓年龄就是该对象经历GC的次数
  • 目的:针对不同年龄的对象制定不同的GC策略,降低整体内存的开销
  • 不同年龄的对象处于heap的不同区域
  • 对于年轻代而言,由于分代假说的存在,年轻代总是趋向于年龄较低面临被GC的威胁,因此存活的对象较少,此时可以采用copying GC的策略,这时候的代价将会比较低
  • 对于老年代而言,由于对象趋向于一致活着,反复复制开销很大,采用copying GC的策略是不合适的,这时候采用Mark-sweep collection的策略较好

9. GO内存管理

9.1 分块

  • 目标:为对象在heap上分配内存
  • 提前将内存进行分配
    • 调用系统调用mmap(),向OS申请一大块内存,例如4MB
    • 先将内存分成大块,例如8KB,称为mspan
    • 再将大块继续划分成特定大小的小块,用于对象分配(有点类似伙伴系统?)
    • noscan mspan:分配不包含指针的对象,GC不需要扫描
    • scan mspan:分配包含指针的对象,GC需要扫描
  • 对象分配:根据对象大小,选择最合适的块返回

9.2 缓存

  • TCMalloc:thread caching
  • 每个p包含一个mcache用于快速分配,用于绑定于p上的g分配对象
  • mcache管理一组mspan
  • 当mcache中的mspan分配完毕,向mcentral申请带有未分配块的mspan
  • 当mspan中没有未分配对象的时候,mspan会被缓存在mcentral中,而不是立即返回给OS

9.3 GO内存管理优化

  • 对象分配是非常高频的操作:每秒分配GB级的内存
  • 小对象的占比比较高
  • Go内存分配比较耗时
    • 分配路径长 :g->m->p->mcache->mspan->memory block->return pointer
    • pprof:对象分配函数是最频繁调用的函数之一

9.4 Balanced GC

  • 将 noscan 对象在 per-g allocation buffer (GAB) 上分配,并使用移动对象 GC 管理这部分内存,提高对象分配和回收效率
  • 每个 g 会附加一个较大的 allocation buffer (例如 1 KB) 用来分配小于 128 B 的 noscan 小对象
  • 分配对象时,根据对象大小移动 top 指针并返回,快速完成一次对象分配
  • 同原先调用 mallocgc() 进行对象分配的方式相比,balanced GC 缩短了对象分配的路径,减少了对象分配执行的指令数目,降低 CPU 使用
  • 从 Go runtime 内存管理模块的角度看,一个 allocation buffer 其实是一个大对象。本质上 balanced GC 是将多次小对象的分配合并成一次大对象的分配。因此,当 GAB 中哪怕只有一个小对象存活时,Go runtime 也会认为整个大对象(即 GAB)存活。为此,balanced GC 会根据 GC 策略,将 GAB 中存活的对象移动到另外的 GAB 中,从而压缩并清理 GAB 的内存空间,原先的 GAB 空间由于不再有存活对象,可以全部释放.
  • 指针碰撞风格:无需与其他分配请求互斥,分配动作简单高效。

10. Go编译器优化

10.1 函数内联

  • 函数内联:将被调用函数的函数体(callee)的副本替换调用位置上,同时重写代码以反映参数的绑定
  • 优点
    • 消除函数调用的开销,例如传递参数、保存现场等
    • 将过程间分析转化为过程内分析,帮助其他优化,例如逃逸优化等
  • 缺点
    • 函数体变大,对于CPU的instruction cache(可能导致大量的iCache的miss)不友好
    • 编译生成的Go镜像变大了

10.2 Beast mode

  • Go函数内联受到的限制较多

    • 语言特性,例如interface、defer等,限制了函数内联
    • 内联策略非常保守
  • Beast mode:调整函数内联的策略,使更多函数被内联
    • 降低函数调用的开销
    • 增加了其他优化的机会:逃逸分析
  • 开销
    • Go镜像增大~10%
    • 编译时间增加

10.3 逃逸分析

  • 逃逸分析:分析代码中指针的动态作用域,指针在何处可以被访问
  • 大致思路:从对象分配处出发,沿着控制流,观察对象的数据流
    • 若发现指针p在当前作用域s:

      • 作为参数传递给其他函数
      • 传递给全局变量
      • 传递给其他的goroutine
      • 传递给已逃逸的指针指向的对象
    • 则认为指针p逃出了作用域s,否则没有
  • Beast mode:函数内联扩展了函数边界,更多对象不逃逸
  • 优化:未逃逸的对象可以在栈上分配
    • 对象在栈上分配和回收很快,移动sp
    • 减少在heap上的分配,减小GC的负担

golang(3)高质量编程与性能分析相关推荐

  1. 字节青训营第三课之高质量编程与性能调优实战的笔记和总结

    这是字节青训营第三课:高质量编程与性能调优实战的笔记和总结 概要 准备 尝试使用 test 命令,编写并运行简单测试 尝试使用 -bench参数,对函数进行性能测试 推荐阅读Go代码Review建议. ...

  2. 立足自主创新,编程猫助推国内高质量编程教育普及

    来源:金融界百家 2017年,国务院印发<新一代人工智能发展规划>,要求在中小学阶段设置人工智能相关课程,逐步推广编程教育.编程教育逐渐受到广泛重视,作为国内少儿编程赛道先行者,编程猫始终 ...

  3. 【高质量编程指南笔记】

    高质量编程指南-笔记 一.文件结构 1.1 版权和版本的声明 1.2 头文件的结构 1.3 定义(.cpp) 文件的结构 1.4 头文件的作用 1.5 目录结构 二.程序的版式 2.1 长行拆分 2. ...

  4. Go 语言高质量编程

    编写高质量的 Go 代码~ 前言: 本次课程简要介绍了高质量编程的定义和原则,分享了代码格式.注释.命名规范.控制流程.错误和异常处理五方面的常见编码规范,帮助我们在今后的开发过程中写出更加优秀的代码 ...

  5. Golang适合高并发场景的原因分析

    典型的两个现实案例: 我们先看两个用Go做消息推送的案例实际处理能力. 360消息推送的数据: 16台机器,标配:24个硬件线程,64GB内存  Linux Kernel 2.6.32 x86_64  ...

  6. Go 语言编程 — Profiling 性能分析

    目录 文章目录 目录 Profiling runtime MemStat GC pprof trace Profiling Golang 提供了友好的工程化支持,其中之一就是 Profiling(分析 ...

  7. Java高并发编程:性能与线程竞争

    线程的最主要目的是提高程序的运行性能,线程可以使程序更加充分地发挥系统地可处理能力,从而提高系统的资源利用率.此外,线程还可以使程序在运行现有的任务的情况下即开始处理新的任务.但性能的提升会导致复杂性 ...

  8. 【C/C++高质量编程 笔记】

    1.C语言标准的本质:标准C语言没有提供实现,只是定义了标准的函数接口,所有工作都是通过库函数完成的. 2.什么是语言实现: 具体实现一种语言的各种特征并支持特定编程模式的技术和工具,具体说就是编译器 ...

  9. C++高质量编程点滴

    一.你需要一个函数将一个数组赋值为等差数列,并将会在函数的外部使用它. 不合理: int *GetArray( int n ) { int *p = new int[n]; for ( int i = ...

最新文章

  1. Variant Analysis(变种分析)——使用已知漏洞发掘未知漏洞
  2. K Co-prime Permutation 构造,gcd,互质(2020.12.南京)
  3. 在linux中安装软件会出的问题,linux中安装程序出现的问题
  4. 在Asp.NET Core中如何优雅的管理用户机密数据
  5. msp430中如何连续对位进行取反_四元数数控:如何保养视觉对位平台?
  6. 外卖餐饮点餐系统,连锁餐饮,公众号小程序源码2.1.5
  7. 【转】SSH中 整合spring和proxool 连接池
  8. 测视力距离5米还是3米_多功能视力表灯箱的用法
  9. 某大型银行深化系统之十七:性能设计之二
  10. maya2022|autodesk maya 2022新功能介绍
  11. 安徽 gis epsg
  12. 阿里云服务器如何使用?阿里云服务器入门使用教程
  13. 【知识笔记】Debugging
  14. js正则表达式限定输入框内容为非负数
  15. 【回眸】牛客网刷刷刷!嵌入式软件中也会遇到的嵌入式硬件,通讯,通讯协议专题(一)
  16. 中国矿业大学考研计算机应用技术,2020中国矿业大学计算机专业课考试科目变动...
  17. IIS:System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089”类型的权限已失败。
  18. arp命令(windows ),nmap查看局域网内所有主机IP和MAC
  19. cenos 解决Kernel panic – not syncing: Attempted to kill init
  20. 树莓派的系统安装(简单易入手教程!!!)

热门文章

  1. 快手财报,虚惊一场?
  2. arduino tft 方向_Arduino库教程-TFT Library
  3. 爬去微信小程序服务器代码,Python爬取微信小程序通用方法代码实例详解
  4. 【次小生成树】4.秘密的牛奶运输
  5. 初学Pybugthon头秃笔记(三)
  6. Lamport 逻辑时钟
  7. #1408 : The Lastest Time
  8. vscode找不到头文件的解决办法
  9. 苹果在线签名服务器搭建,苹果iOS企业签名 、apple 超级签名网站服务器选用
  10. 《算法》(第四版)------------图