基础语法

  1. import用来导入标准库,以使用标准库支持的函数,这个导入会比较严谨,没有使用到的必须从import中删除,否则会导致报错。
  2. 声明变量,可以用var 变量名 变量类型var 变量名 = 初值变量名 := 初值等方式。
  3. iffor循环不需要小括号,需要大括号,可以利用range来进行循环遍历的工作。
  4. 切片,相当于一个可变长度的数组,利用make来创建,append追加元素。
  5. map,利用make(map[key]value)创建,利用map[key]来访问value。
  6. 指针,与C++类似,用&取地址,*取地址中的值,但是不存在指针运算。
  7. 结构体方法定义在结构体外,从一个普通函数改为结构体方法,可以把第一个参数加上括号写到函数名前。

并发

通道

Channel类似一个管道,方便并发核心单元通讯。

操作符:<-,箭头指向数据传输方向

创建make(chan 元素类型,[缓冲大小])

Lock

声明lock sync.Mutex

使用

lock.Lock()
/* code */
lock.Unlock()

WaitGroup

方法Add(delta int)计数器+delta,Done()计数器-1,Wait()阻塞直到计数器为0。

依赖管理

依赖顺序Go Module->Go Vendor->GOPATH

Go Modulego.mod文件管理依赖包版本,go get/mod指令管理依赖包。

go get example.org/pkg@update(默认)/none(删除依赖)/v1.1.2(tag版本)/23dfdd5(特定commit)/master(最新commit)

go mod init(初始化go.mod)/download(下载到本地缓存)/tidy(增加需要的依赖,删除不需要的依赖)

编译时会选择最低的兼容版本。

测试

单元测试

规则

  1. 测试文件以_test.go结尾
  2. 测试函数func TestXxx(t *testing.T)
  3. 初始化逻辑放到TestMain

assert

import增加github.com/stretchr/testify/assert

覆盖率

go test xxx_test.go xxx.go --cover

编码规范

代码格式

gofmt:Go语言官方提供的格式化代码工具

goimports:Go语言官方提供的依赖包管理工具

注释

  1. 注释应该解释代码的作用
  2. 注释应该解释代码如何做的
  3. 注释应该解释代码实现的原因
  4. 注释应该解释代码什么情况会出错
  5. 公共符号始终要注释

命名规范

variable

  1. 缩略词全大写,但其位于变量开头且不需要导出时全小写
  2. 变量距离被使用的地方越远,越需要携带更多的上下文信息
  3. 变量使用有特定含义的名词

函数名

  1. 函数名不携带包名的上下文信息,尽量简短
  2. foo包某个函数返回类型Foo时可省略类型信息,返回类型不是Foo时可以在函数名中加入类型信息
  3. 导出的函数大写字母开头,非导出的小写字母开头

package:(必须)只包含小写字母,简短,包含一定的上下文信息,不要与标准库同名,(尽量)不用常用变量名,使用单数,谨慎缩写。

控制流程

  1. 处理逻辑尽量走直线,避免嵌套
  2. 尽量保持正常代码路径为最小缩进,也就是说错误处理流程嵌套进控制流程中

错误和异常处理

简单错误:指仅出现一次的错误,优先使用errors.New创建匿名变量表示,如果有格式化需求使用fmt.Errorf,用%w将一个错误关联到错误链中。

错误判定erros.Is判断错误链上所有错误是否含有特定的错误,errors.As在错误链上获取特定种类的错误。

panic:不可逆转的错误可以使用panic,不建议业务代码使用panic。

recover:recover只能在当前goroutine中被defer的函数中使用,嵌套无法生效,defer的顺序是后进先出。

性能优化

性能优化建议

  1. Benchmark:go test -bench=. -benchmem
  2. slice:尽量make初始化时提供容量信息,copy替代re-slice
  3. map:尽量预分配内存
  4. 使用strings.Builder代替bytes.Buffer+
  5. 使用空结构体做占位符
  6. 变量保护使用atomic代替加锁

性能调优原则

  1. 依靠数据不要猜测
  2. 定位最大瓶颈
  3. 不要过早优化
  4. 不要过度优化

性能分析工具 pprof

go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10"
go tool pprof -http localhost:8888 ~/pprof/pprof.main.alloc_objects.alloc_space.inuse_objects.inuse_space.001.pb.gz
go tool pprof -http=:8888 "http://localhost:6060/debug/pprof/goroutine"

性能优化与软件质量

  • 保证接口稳定为前提
  • 测试用例尽可能覆盖更多场景
  • 文档记录,做了什么没做什么达到了怎样的效果
  • 隔离,通过选项控制是否开启优化
  • 可观测,必要的日志输出

内存管理

自动内存管理

Mutator:业务线程,分配新对象,修改对象指向关系;Collector:GC线程,找到存活对象,回收死亡对象的内存空间

  • Serial GC:只有一个 collector
  • Parallel GC:支持多个 collectors 同时回收的GC算法
  • Concurrent GC:mutator(s)和collector(s)可以同时执行

评价GC算法:

  • 安全性:不能回收存活对象
  • 吞吐率:花在GC上的时间
  • 暂停时间
  • 内存开销:GC元数据开销

追踪垃圾回收

被回收的对象:指针指向关系不可达的对象

  • 标记根对象
  • 标记可达对象
  • 清理不可达对象,根据对象的生命周期,使用不同的清理策略:
    • Copying GC:将存活对象复制到另外的内存空间
    • Mark-sweep GC将死亡对象内存标记为可分配
    • Mark-compact GC移动并整理存活对象

分代GC(Generational GC)

每个对象经过GC的次数作为年龄,不同年龄的对象制定不同的GC策略,不同年龄的对象处于 heap 的不同区域。

  • Young generation
    • 常规的对象分配
    • 存活对象很少,可以采用 copying collection
    • GC吞吐率高
  • Old generation
    • 对象趋向一直活着,可以采用 mark-sweep collection

引用计数

每个对象都有一个与之关联的引用数,引用数大于0时存活。

优点:

  • 内存管理的开销平摊到程序执行过程
  • 内存管理不需要了解 runtime 的实现细节

缺点:

  • 维护引用计数开销大,需要通过原子操作保证引用计数操作的原子性和可见性
  • 无法回收环形数据结构,可以利用weak reference解决
  • 引用计数带来额外的内存开销
  • 回收内存时依然可能引发暂停

Go 的内存分配

分块:为对象在 heap 上分配内存

提前将内存分块

  • 调用mmap()向OS申请一大块内存,例如4MB
  • 先划分成大块,例如8KB,称作mspan
  • 再划分成特定大小的小块,用于对象分配
  • noscan mspan指分配不包含指针的对象,GC不需要扫描
  • scan mspan指分配包含指针的对象,GC需要扫描
  • 根据对象选择最合适的块分配

缓存

编译优化

函数内联

被调用的函数副本直接替换到调用位置上。

优点

  • 消除函数调用开销
  • 过程间分析转化为过程内分析

缺点

  • 函数体变大
  • 编译生成的Go镜像变大

Beast Mode:调整函数内联策略,使更多函数被内联,拓展了函数边界,更多对象不逃逸

逃逸分析:未逃逸的对象可以在栈上分配,降低GC负担