基础语法
import用来导入标准库,以使用标准库支持的函数,这个导入会比较严谨,没有使用到的必须从import中删除,否则会导致报错。- 声明变量,可以用
var 变量名 变量类型、var 变量名 = 初值或变量名 := 初值等方式。 if,for循环不需要小括号,需要大括号,可以利用range来进行循环遍历的工作。- 切片,相当于一个可变长度的数组,利用
make来创建,append追加元素。 map,利用make(map[key]value)创建,利用map[key]来访问value。- 指针,与C++类似,用
&取地址,*取地址中的值,但是不存在指针运算。 - 结构体方法定义在结构体外,从一个普通函数改为结构体方法,可以把第一个参数加上括号写到函数名前。
并发
通道
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 Module:go.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(增加需要的依赖,删除不需要的依赖)
编译时会选择最低的兼容版本。
测试
单元测试
规则
- 测试文件以
_test.go结尾 - 测试函数
func TestXxx(t *testing.T) - 初始化逻辑放到
TestMain
assert
import增加github.com/stretchr/testify/assert
覆盖率
go test xxx_test.go xxx.go --cover
编码规范
代码格式
gofmt:Go语言官方提供的格式化代码工具
goimports:Go语言官方提供的依赖包管理工具
注释
- 注释应该解释代码的作用
- 注释应该解释代码如何做的
- 注释应该解释代码实现的原因
- 注释应该解释代码什么情况会出错
- 公共符号始终要注释
命名规范
variable:
- 缩略词全大写,但其位于变量开头且不需要导出时全小写
- 变量距离被使用的地方越远,越需要携带更多的上下文信息
- 变量使用有特定含义的名词
函数名:
- 函数名不携带包名的上下文信息,尽量简短
- 当
foo包某个函数返回类型Foo时可省略类型信息,返回类型不是Foo时可以在函数名中加入类型信息 - 导出的函数大写字母开头,非导出的小写字母开头
package:(必须)只包含小写字母,简短,包含一定的上下文信息,不要与标准库同名,(尽量)不用常用变量名,使用单数,谨慎缩写。
控制流程
- 处理逻辑尽量走直线,避免嵌套
- 尽量保持正常代码路径为最小缩进,也就是说错误处理流程嵌套进控制流程中
错误和异常处理
简单错误:指仅出现一次的错误,优先使用errors.New创建匿名变量表示,如果有格式化需求使用fmt.Errorf,用%w将一个错误关联到错误链中。
错误判定:erros.Is判断错误链上所有错误是否含有特定的错误,errors.As在错误链上获取特定种类的错误。
panic:不可逆转的错误可以使用panic,不建议业务代码使用panic。
recover:recover只能在当前goroutine中被defer的函数中使用,嵌套无法生效,defer的顺序是后进先出。
性能优化
性能优化建议
- Benchmark:
go test -bench=. -benchmem - slice:尽量make初始化时提供容量信息,
copy替代re-slice - map:尽量预分配内存
- 使用
strings.Builder代替bytes.Buffer和+ - 使用空结构体做占位符
- 变量保护使用
atomic代替加锁
性能调优原则
- 依靠数据不要猜测
- 定位最大瓶颈
- 不要过早优化
- 不要过度优化
性能分析工具 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负担