这是我参与「第五届青训营 」伴学笔记创作活动的第 12 天
今天和大家分享高性能编码规范的相关知识点
高质量编码规范
编写质量高,易于阅读,易于维护的代码在工作中十分重要,这里列出了一些比较公认的编码规范。
注释
什么代码需要注释?
- 公共符号注释:公共符号一定要注释,包中声明的每个公共符号:变量,常量,函数以及结构体都需要添加注释。
- 长代码块注释:任何既不明显也不简短的公共功能必须予以注释
- 库函数注释:无论长度和复杂度如何,对库中的任何函数都必须进行注释
- 例外:不需要注释实现接口的方法。例如
func (r *FileReader) Read(Buf []byte) (int, error)因为该接口的使用注释应该在接口定义处已经写明了。
注释应该说明什么?
-
注释应该解释代码的作用
-
解释代码什么情况下会出错,出什么错
// Mul 实现a/b的出发功能,当b为0时返回错误 func Mul(a, b int) (err error) { ... } -
合适注释复杂代码块的实现过程
// 加入凭证配置和指定加密算法,并根据自定义key生成token字符串 c := &UserClaims{ Name: "aei", Expire: int(604800 + time.Now().Unix()), // 有效期7天 } claim := jwt.NewWithClaims(jwt.SigningMethodHS256, c) str, err := claim.SignedString([]byte(key))当然,如果一个函数名或者代码块已经很清晰的描述了其作用和使用方法,其实就没必要再加注释了:
func isNumber(num interface{}) bool
-
公共符号需要注释说明:
// A JWT Token. Different fields will be used depending on whether you're // creating or parsing/verifying a token. type Token struct { Raw string // The raw token. Populated when you Parse a token Method SigningMethod // The signing method used or to be used Header map[string]interface{} // The first segment of the token Claims Claims // The second segment of the token Signature string // The third segment of the token. Populated when you Parse a token Valid bool // Is the token valid? Populated when you Parse/Verify a token } -
代码时最好的注释,代码清晰,命名清晰最重要,其次才是通过注释提供代码未明确表达出来的信息
命名规范
- 变量应该有意义,比如截止时间变量,我们使用
deadline比使用t更加清晰有效; - 函数名应该具体简洁,不需要带上下文信息。比如 service 层的获取用户信息函数,使用
UserInfo比使用ServiceUserInfo更加简洁; - 包名只由小写字母组成,不包含大写字母和下划线等字符,应该简短并包含一定的上下文信息,并且不使用常用变量和标准库的名字,例如使用
userservice而不是user
流程控制
流程控制中出现多个分支时,我们应该避免嵌套和冗余。例如:
// 不优雅的写法
if foo {
return x
} else {
return err
}
// 优雅写法
if foo {
return x
}
return nil
当分支比较多或者代码比较复杂时,我们应该尽早处理特殊情况或者错误情况,来尽早返回减少嵌套
// 不优雅的写法
func Func() error {
err := doSomething()
if err == nil {
err := doAnotherThing()
if err == nil {
return nil
}
return err
}
return err
}
// 优雅的写法
func Func() error {
if err := doSomething(); err != nil {
return err
}
if err := doAnotherThing(); err != nil {
return err
}
return nil
}
错误处理
简单错误
对于简单的自定义错误,我们可以直接使用errors.New返回
return errors.New("to much fish")
复杂错误(实现错误跟踪链)
error
复杂的错误我们可以使用错误包装和解包。错误的包装实际上提供了一个error嵌套另一个error的能力,从而在解包时形成一个跟踪链。
在 fmt.Errorf 中使用 %w 关键字来进行包装
var ErrDemo = errors.New("123")
func main() {
err1 := ErrDemo
msg := "from main"
err2 := fmt.Errorf("error: %v %w", msg, ErrDemo)
err3 := fmt.Errorf("error: %w %v", err2, "又一个错误")
// errors.Is 时会自动解包,当遇到 %w 包装的 错误时,会自动找出其中的错误并解包
fmt.Println(err1, errors.Is(err1, ErrDemo),
"\n", err2, errors.Is(err2, ErrDemo),
"\n", err3, errors.Is(err3, ErrDemo))
}
// 123 true
// error: "from main" 123 true
// error: error: from main 123 又一个错误 true
上面的例子中,我们使用 fmt.Errorf 即可以添加额外信息,又可以使用 errors.Is来判断当前 error 的具体类型
同时,我们也可以通过 errors.As去指定去重其中某个类型的错误。
panic
我们不建议在业务代码中去使用 panic ,因为如果调用函数不包括 recover 会造成程序崩溃。如果问题可以被屏蔽或解决,我们可以使用 error 实现。
但是有些最基础的功能,例如连接数据库,实现消息队列等,我们需要尽早的暴露这里面出现的错误(因为如果基础功能出现了错误,上面的业务可能大部分都用不了),此时可以使用 panic
deffer
defer 语句可以尽早的写在函数前。
func Func() {
defer fmt.Printf("1")
...
}
另外,如果一个函数中出现了多个 defer 函数,defer 语句会遵循后进先出:
func Func() {
defer fmt.Printf("1")
defer fmt.Printf("2")
}
// 2 1
代码风格
代码格式方面,我们可以使用 gofmt 。它作为官方提供的工具,能自动格式化Go语言代码为官方的统一风格。并且常见的 IDE 都内置或具有相关的插件,可以很方便的配置。
另一方面我们也可以使用 Go 语言官方提供的工具 goimports,实际等于 gofmt 加上依赖包管理,可以自动增删依赖的包引用,将依赖包按字母排序并分类。
Goland 已经内置了改工具,我们可以在 Setting -> Tools -> Actions on Save 中找到 Reformat code 和 Optimize imports 并勾选上来开启相关功能。在 Setting -> Editor -> Code Style -> Go 找到 Imports 下的 排序风格来选择使用 gofmt 还是 goimports
当然 Goland 一般都已经把这些提前配置了,个人更推荐使用 goimports 管理包引用,而代码风格,引入风格等都使用 gofmt