这是我参与「第五届青训营 」伴学笔记创作活动的第 6 天
高质量编程
高质量编程:编写代码能够达到正确可靠、简洁清晰的目标可称之为高质量代码
当然,高质量代码也需要达到如下目标
- 各种边界条件考虑完备
- 异常处理良好
- 易于维护
编程原则
简单性
用简单清晰的逻辑编写代码,不过分复杂、炫技
你的代码是给多个人来阅读、维护、更新的,并不是只有你一个人,你需要保证你的代码足够简单,这样才能轻松的修复与改进
可读性
编写可维护的代码的第一步是确保代码可读,这与简单性是一致的,我们不能过分炫技
同时我们也需要注意注释的使用,让代码便于理解
生产力
代码面向的是生产,团队整体的工作效率非常重要,我们需要保证代码具有足够的生产力,不产出劣质代码
编码规范
编写高质量Golang代码,我们需要注重:代码格式、注释、命名规范、控制流程、错误和异常处理这5个部分
代码格式
我们可以使用gofmt和goimports来自动格式化我们的代码
注释
我们写一个注释,究竟应该往上写什么?
- 代码的作用
- 代码是怎么做的
- 代码实现的原因
- 代码在什么情况下会出错
这里引用一下Dave Thomas 和 Andrew Hunt的话
Good code has lots of comments, bad code requires lots of comments
好的代码有很多注释,坏的代码需要很多注释
几个参考的注释:
// Open opens the named file for reading. If successful, methods on
// the returned file can be used for reading; the associated file
// descriptor has mode 0_RDONLY.
// If there is an error, it will be of type *PathError
func Open(name string) (*File, error) {
return OpenFile(name, 0_RDONLY, 0)
}
// Add the Referer header from the most recent
// request URL to the new one, if it's not https->http:
if ref := refererForURL(reqs[len(reqs) - 1].URL, req.URL); ref != ""{
req.Header.Set("Referer", ref)
}
switch resp.StatusCode {
// ...
case 307, 308:
redirectMethod = reqMethod
shouldRedirect = true
includeBody = true
if ireq.GetBody == nil && ireq.outgoinglength() != 0 {
// We had a request body, and 307/308 require
// re-sending it, but GetBody is not defined. So just
// return this response to the user istead of an
// error, like we did in Go 1.7 and earlier
shouldRedirect = false
}
}
实际上,如果代码编写到后面,可以发现上述几个要点实际上就是对一些重点核心代码进行注释,我们不需要对一些很浅显的函数进行注释,那没必要
我们还应该知道,代码是最好的注释,注释应该提供代码没有表达出的上下文信息
命名规范 - 变量
- 变量名应当简洁
- 缩略词全大写,但当它位于变量开头而不需要导出时,使用全小写
- ServeHTTP 而不是 ServeHttp
- 使用 XMLHTTPRequest 或者 xmlHTTPRequest
- 变量距离它被使用的地方越远,那么需要携带的信息越多(可以参考全局变量,全局变量不可过短)
命名规范 - 函数
- 函数不携带包名的上下文信息,因为包名和函数名总是成对出现的
- 函数名应当尽量的简短
- 当名为foo的包的某个函数返回类型Foo时,可以省略类型信息而不导致歧义
- 当名为foo的包的某个函数返回类型T(T!=Foo),可以在函数名中加入类型信息
eg.我们有两个函数(位于HTTP包中)
func Serve(l net.Listener, handler Handler) error
func ServeHTTP(l net.Listener, handler Handler) error
实际上在调用的时候,我们会使用HTTP.func()的格式来,如果我们使用第二个函数,那么就是HTTP.ServeHTTP(),显得多余了
命名规范 - 包名
- 只由小写字母组成。不包含大写字母和下划线等字符
- 简短并包含一定的上下文信息。例如 schema、task 等
- 不要与标准库同名。例如不要使用sync或者strings
这些规则尽量满足
- 不使用常用变量名作为包名。例如使用bufio而不是buf
- 使用单数而不是复数。例如使用encoding 而不是 encodings
- 谨慎的使用缩写。例如使用fmt在不破坏上下文的情况下比format更加简短
控制流程
- 尽量保证正常代码路径为最小缩进:优先处理错误情况/特殊情况,尽早返回或继续循环来减少嵌套
- 线性原则,处理逻辑尽量走直线,避免复杂的嵌套分支
- 正常流程代码沿着屏幕向下移动
- 提升代码可维护性和可读性
- 故障问题大多出现在复杂的条件语句和循环语句中
错误和异常处理
简单处理
简单的错误指的是只出现一次的错误,而且在其他地方不需要捕获该错误
我们优先使用 errors.New 来创建匿名变量来直接表示简单错误
如果有格式化需求,使用 fmt.Errorf
func defaultCheckRedirect(req *Request, via []*Request) error {
if len(via) >= 10 {
return errors.New("Stopped after 10 redirects")
}
return nil
}
错误的Wrap和Unwrap
错误的Wrap实际上是提供了一个error嵌套另一个error的能力,从而生成一个error的跟踪链
在fmt.Errorf中使用%w关键字来将一个错误关联到错误链中
list, _, err := c.GetBytes(cache.Subkey(a.actionID, "srcfiles"))
if err != nil {
return fmt.Errorf("reading srcfile list: %w", err)
}
错误判定
判定一个错误是否为特定错误,使用 errors.Is
不同于使用 ==,使用这个方法可以判定错误链上的所有错误是否含有特定的错误
data, err = lockedfile.Read(targ)
if errors.Is(err, fs.ErrNotExist) {
// Treat non-existent as empty, to bootstrap the "latest" file
// the first time we connect to a given database
return []byte(), nil
}
return data, err
在错误链上获取特定种类的错误,使用errors.As
panic
- 不建议在业务代码中使用panic
- 调用函数不包含recover会造成程序崩溃
- 若问题可以被屏蔽或解决,建议使用error代替panic
- 当程序启动阶段发生不可逆转的错误时,可以在init()或main()中使用panic
其实panic面向的就是不可逆转的时候强制终止程序运行,一般错误就用error就可以了
recover
- recover只能在被defer的函数中使用
- 嵌套无法生效
- 只在当前goroutine生效
- defer语句是后进先出
- 如果需要更多的上下文信息,可以revocer后在log中记录当前的调用栈
小结
- error尽可能提供简明的上下文信息链,方便定位问题
- panic用于真正异常的情况
- recover生效范围,在当前goroutine的被defer函数中生效