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

74 阅读3分钟

简介

高质量代码指的是正确可靠、简洁清晰的代码,对各种异常情况、边界条件都尽量考虑完备,具有较好的可读性和维护性,同时具有简单性和生产力。简单性指的是简单清晰的逻辑,可读性是非常重要的,而生产力在团队整体工作效率中非常重要。而如何编写高质量代码,可以从代码规范做起。

1、高质量编程与代码规范

(1) 代码格式

Go语言官方提供了工具——gofmt,其能自动格式化Go语言代码为官方统一风格。 使用方式为: go fmt <文件名>.go

而在Goland里,可以这样使用:

image.png c7d51e5e-a047-4c5b-865c-a5686f581487.png

goimports也是Go语言官方提供的工具,等于gofmt加上依赖包管理,自动增删依赖的包引用,将依赖包按字母排序并分类。

(2) 注释

很多时候我们在不该写注释的地方写了注释,又在该写注释的地方没写,那么,注释应该在什么时候写呢?

  • 注释应该解释代码作用(适合注释公共符号) 3921fd89-bff7-4989-88bf-6ebd087236bd.png

  • 注释应该解释代码如何做(适合注释实现过程dd631958-0eef-4e3e-808e-c9bd54f7d770.png

但是不需要在一些特别简单的地方(即无法提供代码未提供出的上下文信息)写注释,反例如下: 29b83153-c4c6-4b00-83ff-c41a4931fd2c.png

  • 注释应该解释代码实现的原因(适合解释外部因素

具体来说,即"why",适合提供额外的上下文信息,帮助读者读取从代码中看不出的上下文环境,从而更加容易理解。

822c82aa-0012-4fa3-9ea9-de77275dbf54.png

  • 注释应该解释代码什么情况下会出错(适合解释限制条件

b5f84bd8-f300-4319-856f-c4e84af48c43.png

  • 公共符号始终要添加注释

公共符号即包中的每个公共符号(变量常量函数结构等),不明显的公共功能也需要注释。除此以外,库中的任何函数都需要注释。

下载.png

(3) 命名规范

变量

  • 简洁优于冗长
//不好.因为作用域限制在for循环内,将i冗长为index对理解代码没有任何帮助
	for index:=1;index<=n;index++ {

	}
	//ok
	for i:=1;i<=n;i++ {
		
	}
  • 缩略词全大写

举例来说,使用ServeHTTP而不是ServeHttp,有一个特例是当其位于开头不需要导出时(导出需要首字母大写),使用全小写,如xmlHTTPRequest

  • 变量离其被使用的地方越远,则需要携带越多的上下文信息。
//ok.deadline具有特定的含义
func send(deadline time.Time) {

}
//not ok.t一般指代粗略的时间,在需要使用的时候丢失了上下文信息.
func send(t time.Time) {

}

函数

  • 函数名不携带包名的上下文信息
package foo

import (
	"fmt"
)
//ok.
func add(a,b int) int {
	return a+b
}
//不好,已经是foo包里的函数了.
func fooadd(a,b int) int {
	return a+b
}
  • 函数名尽量简短
func getUser() string {

}
func getUserinfo() string {

}
  • 当函数返回值为包名相关类型时可省略

  • 包内某函数返回类型为T时可以在函数名中加入类型信息。

  • 只有小写字母组成。不包含大写字母和下划线等字符。
  • 简短并包含一定的上下文信息,如 schematask等。
  • 不要与标准库同名,例如不要使用sync

除此以外,尽量遵循以下规则:

  • 不要用常用变量名作包名,如使用bufio而非buf
  • 使用单数而非复数,如encoding
  • 慎用缩写。fmt包就在不破坏上下文的基础上更为简洁。

(4) 控制流程

  • 避免无意义的嵌套

一个很简单的例子:若两个分支都会return某些值,那么可以去除其中一个else

func process(ok bool) string {
	if ok {
		return "ok"
	} else {
		return "not ok"
	}
}

//应改为如下代码:
func process(ok bool) string {
	if ok {
		return "ok"
	} 
	return "not ok"
}
  • 尽量保持代码流程为最小缩进,避免嵌套
func process() error {
	err:=do1()
	if err==nil {
		err = do2()
		if err==nil {
			return nil
		}
		return err
	}
	return err
}

显然上面的代码把简单的逻辑嵌套得太多了,我们完全可以边判断边返回,不需要嵌套在一起。

func process() error {
	if err:=do1();err!=nil {
		return err
	}
	if err:=do2();err!=nil {
		return err
	}
	return nil
}

(5) 错误和异常处理

简单错误

对于简单错误,即仅出现一次,且不需要在其他地方被捕获,那么我们可以直接用匿名变量: 下载.png

当我们有格式化需求时,也可以使用fmt.errorf()来创建实现 error 接口的错误对象。

func process(a []int) error {
	n := len(a)
	for i := 0; ; i++ {
		if i >= n {
			return fmt.Errorf("Index %d is out of bound!", i)
		}
	}
	return nil
}

错误链

考虑这样一个场景,我们在错误的传递中需要给错误更多的上下文信息,即在某类错误中具体又属于哪一类错误呢?即错误的嵌套。因此我们引入了错误链

Errorf()里使用%w关键字,可以将一个错误关联到已有的错误链中。

func process(file string) error {
	data, err := os.ReadFile(file)
	if err != nil {
		return fmt.Errorf("failed to read file: %w", err)
	}
	fmt.Println(data)
	return nil
}

除此之外,Go语言还提供了Unwrap返回错误链的下一个错误,errors.Is()函数判断错误链上是否包含特定的错误。

func process(filename string) error {
	_, err := os.ReadFile(filename)
	if err != nil {
		return fmt.Errorf("failed to read file: %w", err)
	}
	return nil
}

func main() {
	err := process("hello.txt")
	if err != nil {
		fmt.Println(err)
		fmt.Println(errors.Is(err, os.ErrNotExist)) //文件不存在的错误.错误链中存在.为True.
		//依次解包错误.
		err = errors.Unwrap(err)
		fmt.Println(err)
		err = errors.Unwrap(err)
		fmt.Println(err)
		return
	}
}

panic

不建议在业务代码中使用panic。在有panic而不含recover时会使得程序宕机崩溃。因此,若问题可被屏蔽或解决,建议使用error代替。

recover

recover只能在defer的函数中使用,无法嵌套,且只在当前goroutine生效。 如果当前goroutine进入panic时,recover函数的返回值就是panic的输入值,并使得程序停止向上传播panic,不会造成程序崩溃。

func RecoverRun(f func()) {
	defer func() {
		err := recover()
		fmt.Println("Error", err)
	}()
	f()
}

func main() {
	RecoverRun(func() {
		fmt.Println("宕机前")
		panic("这是一个错误!")
		fmt.Println("宕机后") //recover后不会执行当前goroutine剩下的代码.
	})
	fmt.Println("Hello")
}

如上代码所示,panic并没有使整个程序崩溃,只是退出了发生panicgoroutine而已,剩下的部分仍然执行完成了。