编程原则与编码规范 | 青训营笔记

67 阅读6分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 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函数中生效