高质量编程:
什么是高质量:
——编写代码能达到正确可靠,简洁清晰的目标
各种边界条件是否考虑完备
异常情况处理,稳定性保证
易读易维护
编程原则
简单性
消除“多余的复杂性”,以简单清晰的逻辑编写代码
不理解的代码无法修复改进
可读性
代码是写给人看的,而不是机器
编写可维护代码的第一步是确保代码可读
生产力
团队整体工作效率非常重要
编写规范
-
代码格式
gofmt: go语言官方提供工具,自动格式化为官方统一风格
goimports:go语言官方提供工具,实际等于gofmt加上依赖包管理,自动增删依赖包引用,对其排序分类等
-
注释
注释应该做的:
应该解释代码作用
应该解释代码如何做的
应该解释代码实现的原因
应该解释代码什么情况会出错
应该解释公共符号(公共的变量,常量,函数,不应该取决与代码的长度来选择注释)
代码是最好的注释,并且注释应该要提供代码未表达出的上下文信息
-
命名规范
简介胜于冗长
for index := 0; index < len(s); index++{//bad //do something } for i := 0; i < len(s); i++{ //good //do something }
缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写
例如:使用ServeHTTP而不是ServeHttp 使用XMLHTTPRequest或者xmlHTTPRequest
变量距离其被使用的地方越远,越需要携带更多的上下文信息
函数名不携带包名的上下文信息,因为包名和函数总是成对出现的
函数名尽量简短
对于package来说,只由小写字母组成,不包含大写字母和下划线
简短并包含一定上下文信息,例如schema,task
不要与标准库同名
不使用常用变量名作为包名,例如使用bufo而不是buf
使用单数不使用复数,例如使用encoding而不是encodings
谨慎使用缩写
-
控制流程
避免嵌套
if foo{ // bad return x }else{ return nil } if foo{ // good return x } return nil
简单来说差不多少用else -f else的这样以后添加修改方便
处理逻辑尽量走直线,避免复杂嵌套分支
-
错误和异常处理
简单错误:仅出现一次的错误,有限使用errors.New来创建匿名变量
func ErrorsNew(size int) error { if size > 10 { return errors.New("size bigger , should be less then 10") } return nil }
错误的Wrap和Unwrap
在fmt.Errorf中使用%w关键字将一个错误关联到错误链中
list, _ , err := c.GetBytes(cache.Subkey(a.actionID , "srcfiles")) if err != nil{ return fmt.Errorf("reading srcfiles list : %w" , err) }
错误判定
判定一个错误是否为特定错误,使用errors.ls
不同于使用==,使用该方法可判定错误链上的所有错误是否含有特定错误
data, err = lockedfile.Read( targ ) if errors.Is(err , fs.ErrNotExist){ // Treat non-existent as empty, to bootstrap //the "latest" filethe first time we connect to a given database. return []byteP{} , nil } return data, err }
在错误链上过去特定错误使用errors.AS
if _ ,err := os.Open("non-exit"); err != nil{ var pathError *fs.PathError if errors.As(err , &pathError){ fmt.Println("Failed at path :" , pathError.Path) }else{ fmt.Println(err) } }
panic
不建议在业务代码中使用panic
调用函数不包含recover会造成程序崩溃
若问题可以被屏蔽或解决,建议使用error代替panic
recover
recover只能被defer的函数中使用
嵌套无法生效
只在当前goroutine生效
defer语句是后进先出
优化与性能测试:
性能优化的前提是满足正确可靠,简洁清晰等质量因素
性能优化是综合评估,有时候时间效率和空间效率可能对立
Benchmark
go语言提供支持基准性能测试的工具
func BenchmarkFib10(b *testing.B) {
for i := 0; i < 10; i++{
Fib(10)
}
}
func Fib(n int) int {
if n < 2{
return n
}
return Fib(n - 1) + Fib(n - 2)
}
go test -bench=. -benchmem // 执行命令
上面第一行-16差不多是cpu核数了
10000… 是执行次数
0.0005517 是每次花费时间
0B 是每次申请内存
0allocs是每次申请几次内存
性能优化建议Slice
预分配
尽可能在使用make初始化切片时提供容量信息
func BenchmarkNoPreAlloc(){
data := make([]int , 0)
for k := 0; k < n; k++{
data = append(data, k)
}
}
func BenchmarkPreAlloc(){
data := make([]int , n)
for k := 0; k < n; k++{
data = append(data, k )
}
}
大内存未释放
在已有的切片基础上创建切片,不会创建新的底层数组,如下
func Copy(origin []int) []int{
return origin[3:len(origin)]
}
上述就是想创建个新切片,但是其实底层数组用的是同一个,如果说传入参数在之后不用的,但是因为有个这个返回值只用一段数据却占用这之前的一大块数据,内存就浪费了
另一个问题就是由于工用一个数组,你改变一个切片的值,另一个也就改变了
func main() {
a := []int{1, 2, 3, 4, 5}
b := a
fmt.Println(a) //[1 2 3 4 5]
fmt.Println(b) //[1 2 3 4 5]
b[0] = 1000
fmt.Println(a) //[1000 2 3 4 5]
fmt.Println(b) //[1000 2 3 4 5]
}
由于切片是引用类型,所以a和b其实都指向了同一块内存地址。修改b的同时a的值也会发生变化。
Go语言内建的copy()
函数可以迅速地将一个切片的数据复制到另外一个切片空间中,copy()
函数的使用格式如下:
copy(destSlice, srcSlice []T)
其中:
- srcSlice: 数据来源切片
- destSlice: 目标切片
举个例子:
func main() {
// copy()复制切片
a := []int{1, 2, 3, 4, 5}
c := make([]int, 5, 5)
copy(c, a) //使用copy()函数将切片a中的元素复制到切片c
fmt.Println(a) //[1 2 3 4 5]
fmt.Println(c) //[1 2 3 4 5]
c[0] = 1000
fmt.Println(a) //[1 2 3 4 5]
fmt.Println(c) //[1000 2 3 4 5]
}
性能优化建议-Map
const n = 10000000
func BenchmarkNoPreAlloc(b *testing.B) {
data := make(map[int]int , 0)
for k := 0; k < n; k++ {
data[k] = 1
}
}
func BenchmarkPreAlloc(b *testing.B) {
data := make(map[int]int , n)
for k := 0; k < n; k++ {
data[k] = 1
}
}
可以看出,结果和切片差不多
分析:
不断向map中添加元素操作会触发map扩容
提前分配好空间可以减少内存拷贝和ReHash的消耗
性能优化建议-字符串处理
常见的字符串拼接方式
func plus(n int , str string) string{
s := ""
for i := 0; i < n; i++{
s += str
}
return s
}
func StrBulider(n int , str string)string{
var builder strings.Builder
for i := 0; i < n; i++{
builder.WriteString(str)
}
return builder.String()
}
func ByteBuffer(n int , str string)string{
buf := new(bytes.Buffer)
for i := 0; i < n; i++{
buf.WriteString(str)
}
return buf.String()
}
使用 + 拼接性能最差,后面俩个差不多,strings.Buffer更快
分析:
字符串在Go语言中是不可变类型,占用内存大小固定
使用 + 每次都会重新分配内存
后面的底层都是 []byte数组,内存扩容策略,不需要每次拼接重新分配内存