go-高质量编程与性能调优 | 青训营

93 阅读6分钟

高质量编程:

什么是高质量:

——编写代码能达到正确可靠,简洁清晰的目标

各种边界条件是否考虑完备

异常情况处理,稳定性保证

易读易维护

编程原则
简单性

消除“多余的复杂性”,以简单清晰的逻辑编写代码

不理解的代码无法修复改进

可读性

代码是写给人看的,而不是机器

编写可维护代码的第一步是确保代码可读

生产力

团队整体工作效率非常重要

编写规范
  • 代码格式

    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 // 执行命令

image.png 上面第一行-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 )
     }
 }

可以看出预先分配内存会降低内存再分配时间

image.png 大内存未释放

在已有的切片基础上创建切片,不会创建新的底层数组,如下

 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
     }
 }

image.png

 func BenchmarkPreAlloc(b *testing.B) {
 ​
     data := make(map[int]int , n)
     for k := 0; k < n; k++ {
         data[k] = 1
     }
 }

可以看出,结果和切片差不多

image.png 分析:

不断向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数组,内存扩容策略,不需要每次拼接重新分配内存