Go-高质量编程与性能调优

高质量编程:

什么是高质量:

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

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

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

易读易维护

编程原则
简单性

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

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

可读性

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

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

生产力

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

编写规范
  • 代码格式

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

Go-高质量编程与性能调优_第1张图片

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

你可能感兴趣的:(go,golang)