高质量编程与性能调优实战 1| 青训营

77 阅读6分钟

什么是高质量:编写的代码能够达到正确可靠,简洁清晰的目标可称之为高质量代码

  • 各种边界条件是否考虑完备
  • 异常情况处理,稳定性保证
  • 易读易维护

编程原则:

  • 简单性

    • 消除”多余的复杂性“,以简单清晰的逻辑编写代码
    • ’不理解的代码无法修复改进
  • 可读性

    • 代码是写给人看的,而不是机器
    • 编写可维护的代码的第一步是确保代码可读
  • 生产力

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

编码规范:

  • 代码格式尽可能地统一

    • 推荐使用gofmt自动格式化代码
  • 注释(好的代码有很多注释,不好的代码需要很多注释,应该提供一些使用该代码的上下文信息

    • 注释应该做的:

      1. 注释应该注释的内容
      2. 注释应该解释的代码,它是如何做的
      3. 注释应该解释代码实现的原因,适当解释代码的外部因素,提供额外的上下文
      4. 注释应该解释代码什么情况下可能会出错,解释代码的限制条件
    • 公共符号始终要进行注释

      1. 包中声明的每个公共的符号:变量,常量,函数,以及结构都需要进行注释
      2. 任何及不明显也不简短的公共功能必须予以注释
      3. 无论长度或者复杂程度如何,对库中的任何函数进行注释
      4. 有一个例外,不需要注释实现接口的方法
  • 命名规范(核心目标是降低阅读代码的成本,重点考虑上下文的信息)

    • 简洁胜于冗长

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

      1. 利用ServeHTTP而不是ServeHttp
      2. 使用XMLHTTPResquest或者xmlHTTPRequest
    • 变量距离其被使用的地方越远,则需要携带更多的上下文信息,参数名是可以代表着一些特点含义,知道它的意思

    • 函数名:

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

      1. 只由小写字母组成。不含有大写字母或者下划线等
      2. 简短并且包含一定的上下文信息
      3. 不要与标准库重名
  • 控制流程

    • 避免嵌套,保持正常流程

    • 尽量保持正常代码路径为最小缩进。优先处理错误情况或者特殊情况,尽早返回

    • 故障问题大多数出现在复杂的条件语句和循环嵌套当中

      // Bad
      func OneFunc() error{
      	err:= doSomething()
      	if err == nil{
      		err := doAnoterThing()
      		if err == nil{
      			return nil
      		}
      		return err
      	}
      	return err	
      }
      // 进行改进
      func OneFunc() error{
          if err := doSomething(); err != nil{
              return err
          }
          if err := doAnotherthing(); err != nil{
              return err
          }
          return
      }
      
  • 错误和异常处理

    • 简单的错误指仅出现一次的错误,其在其它的地方不需要捕获该错误

    • 优先使用errors.New来创建匿名变量来直接表示简单错误

    • 如果由格式化的需求,使用fmt.Errof

      func defaultCheckRedirect(req *Request, via []*Request) error {
      	if len(via) >= 10 {
      		return errors.New("stopped after 10 redirects")
      	}
      	return nil
      }
      
    • 错误的Wrap和Unwrap

      1. 错误的wrap实际上是提供了一个error嵌套另一个error的能力,从而生成error的跟踪链

      2. 在fmt.Errorf中使用:%w关键字来将一个错误关联到另一个错误上去

        list, _, err := c.GEtBytes(cache.Subkey(a.actionID,"srcfiles"))
        if err != nil{
            return fmt.Errorf("reading srcfiles list: w%",err)
        }
        
    • 判断一个错误是否为指定的错误,使用errors.Is,不同于==,该方法可以判断错误链上是否含有该错误

      if errors.Is(err,fs.ErrNotExist){
          return true,nil
      }
      
    • 在错误链上获取特定种类的错误,使用errors.As

      if errors.As(err,fs.pathError)
      
    • panic

      1. 业务中不建议使用你panic,如果出现说明业务基本崩溃了,用于真正的异常
      2. 若有问题可以被屏蔽或者解决,建议使用error来代替panic
      3. 调用时不使用recover会让程序奔溃
    • recover

      1. recover只能在defer的函数中使用
      2. 嵌套无法失效
      3. 只能在gotuntine中被defer的函数生效
      4. defer语句会在函数返回前被调用,多个defer语句是后进先出的

高质量编程与性能调优实战

简介:

  • 性能优化的前提是满足正确可靠,简洁清晰等质量因素
  • 性能优化是综合评估,有时候时间效率和空间效率可能对立

性能优化建议--Benchmark

go语言提供了支持基准性能测试,如何使用得去查一查

slice

  • slice预分配内存,尽可能在使用make()初始化切片时提供容量信息

    data := make([]int,0)
    data := make([]int,0,10)
    
  • 切片的底层实现就是可扩容的链表

  • 陷阱:大内存未释放

    1. 在已有的切片上创建切片,不会创建新的底层数组
    2. 原切片较大,代码在原切片的基础上新建小切片
    3. 原底层数组在内存中引用,得不到释放
  • 可使用copy替代re-slice

    func GetLastBySlice(origin []int) []int{
    	return origin[len(origin) - 2:]
    }
    
    // 下面这个节省的空间更多
    fun getLastByCopy(origin []int) []int{
    	result := make([]int,2)
    	copy(result,origin[len(origin) - 2:])
    	return result
    }
    

map

  • map预分配内存

    • 不断地向map中添加新元素地操作会触发map地扩容
    • 提前分配好空间可以减少内存拷贝和Rehash地消耗
    • 建议提前根据实际需求预估好需要的空间大小

strings.Buider进行字符串处理

  • 使用+拼接性能最差,strings.Buider,bytes.Buffer相近,strings。Buffer更快
  • 字符串在go里面时不可变类型,占用大小固定
  • 使用+每次都会重新分配内存
  • strings.Buider和bytes.Buffer底层都是使用[]byte数组

使用空结构体节省内存

使用atomic包

  • 锁的实现是通过操作系统来实现,属于系统调用, 调用成本很高

  • atomic操作是通过硬件实现,效率比锁高

  • sync.Mutex应该用来保护一段逻辑,。不仅仅用于保护一个变量

  • 对于非值操作,可以使用atomic.Value,能承载一个interface{}

    type atomicCounter struct{
        i int32
    }
    func AtomicAddOne(c *atomicCounter){
        atomic.AddInt32(&c.i,1)
    }
    
    type mutexCounter struct{
        i int32
        m sync.Mutex
    }
    func MutexAddOne(c *mutexCounter){
        c.m.Lock()
        c.i++
        c.m.UnLock()
    }
    

小结:

  • 避免常见的性能陷阱可以保证大部分程序的功能
  • 普通应用代码,不要一味地追求程序的性能
  • 越高级的性能优化手段越容易出现问题
  • 在满足正确可靠,简洁清晰的质量要求的前提下提高程序性能