当前位置: 首页 > Go语言, 代码艺术 > 正文

Go语言开发规范

相信很多开发者在Go有了一定的熟悉之后,就在思考如何写出更佳go style的代码。在网上搜索了之后,想必肯定找出了两份经典的文档说明:

Go Code Review Comments:https://github.com/golang/go/wiki/CodeReviewComments

Uber Go Style Guide:https://github.com/uber-go/guide/blob/master/style.md (译文版

没错,这确实两份相当经典的Go语言开发规范的指导性文档。Go Code Review Comments是golang官方给出的指导性规范,结合官方出品的《Effective Go》入门教程,可以帮助我们编写优雅,高效的 Go 代码。笔者在此也是深究上述资料,并结合自己在Go开发以及Code Review方面的经验,在此总结出了自己在Go编程规范方面的思考和一些工程经验方面的经验性总结,和大家分享。

1. 项目目录规范

注:apiserver模块是通用的rpc模块,此处参考bilibili sniper的轻量级go业务框架:https://github.com/bilibili/sniper

2. 添加注释

注释就相当于电影的旁白,是对关键业务代码进行必要补充说明。我的原则是:能不打就不打注释,整体上需言简意赅。必须要打的注释一般有以下基本原则:

(1)业务性很强的代码块(嵌套循环、涉及复杂算法等)

(2)(导出)外部会调用的接口(Go中每个包里可导出的接口/函数)

(3)引用了外部接口/库函数(注释说明参数意义、返回值等)

(4)对数据格式的补充说明(如处理log数据,需要给出一个log数据示例)

(5)对特殊/特例情况(如特殊判断条件)、背景信息的补充说明

(6)对TODO、版本兼容性、issue或维护者的相关说明

3. 注释代码块

随着项目的不断迭代、上线,总会存在一些被注释掉的代码。这其中有些可能是临时的(或由于需求调整或者外部接口变动),而更多的是随着版本迭代遗留下的垃圾代码。这对于后面接手的人来说简直想骂娘。对于需要注释掉的代码,我的基本原则是:

(1)import包中的注释代码须直接去掉

(2)被注释掉的简单调试代码须直接去掉(如fmt.Print、log.Info之类的)

(3)项目初期(从0到1版本)不合适的注释代码应直接去掉(实际上架构设计层面也不建议考虑版本兼容方面的问题)

(4)项目后期(版本稳定发布后)对于兼容性的注释代码建议保留

(5)对于接口/函数中靠前的条件判断或者return之前对结果的修饰的注释代码块建议保留

(6)在循环中的条件判断的注释代码块建议保留

4. 关于空行

空行是存在价值的,但不能随意挥霍。恰当好处的空行,不仅能让读者易于理解代码逻辑,不易产生视觉疲劳,更是体现作者(模块化)思维和匠心。通常以下情况建议空行:

(1)在方法的 return、break、continue 这种断开性语句后必须是空行

(2)在不同语义块之间空行

(3)循环之前和之后一般有空行

5. 代码书写风格

(1)单行一般不超过IDE通常设置的80字符

(2)部分函数定义处考虑方便看到参数和返回值,可不考虑(1)的规范,但应控制在120之内

(2)函数体的长度最好不超过一屏(控制在60/90行之间)

6. package命名

包的名称应该简洁明了而易于理解。基本遵从以下规范:

(1)包应当以小写的单数来命名,且不应使用下划线或驼峰记法

(2)保持package的名字和目录保持一致,且一个目录下只允许定义一个包

(3)尽量采取有意义的包名,简短,有意义,尽量和标准库不要冲突

(4)如有冲突,使用别名,且遵从以上从简原则

(5)不要用“common”、“util”、“shared”或“lib”等信息量不足的名称

7. import分组

如果你的包引入了三种类型的包:标准库包,程序内部包,第三方包,建议采用如下方式进行组织你的包:

有顺序的引入包,不同的类型采用空格分离,第一种实标准库,第二是项目包,第三是三方包。如果程序包名称与导入路径的最后一个元素不匹配,则必须使用导入别名:

在所有其他情况下,除非导入之间有直接冲突,否则应避免导入别名。

8. 变量命名

避免与关键字冲突或容易引起视觉看错的情况,尽量保持简洁易懂(Go标准库中存在很多的单字母简写范例)。和结构体类似,变量名称一般遵循驼峰法,首字母根据访问控制原则大写或者小写,但遇到特有名词时,需要遵循以下规则:

(1)如果变量为私有,且特有名词为首个单词,则使用小写,如 apiClient

(2)其它情况都应当使用该名词原有的写法,如 APIClient、repoID、UserID

(3)错误示例:UrlArray,应该写成 urlArray 或者 URLArray

(4)若变量类型为 bool 类型,则名称应以 Has, Is, Can 或 Allow 开头

9. 常量命名

(1)对于全局宏定义常量,可使用全部大写字母组成。也可使用驼峰首字母大写形式,并根据业务属性分组const块定义:

(2)对于未导出的顶层常量和变量,使用_作为前缀

在未导出的顶级varsconsts, 前面加上前缀_,以使它们在使用时明确表示它们是全局符号(如果使用通用名称可能很容易在其他文件中意外使用错误的值)。其作用域局限在该包范围内。在业务代码中一般不推荐使用,只有在底层类库中才使用这类命名。

10. 变量声明

变量名采用驼峰标准(驼峰长度最好不超过5个单词,字符长度最好不超过30),多个变量申明按类型分组放在一起。在函数外部申明必须使用var,不要采用(:=)。如果将变量明确设置为某个值,则应使用短变量声明形式 (:=):

在某些情况下,var 使用关键字时默认值会更清晰:

11. 共享变量

在高并发的情况下,很容易查询读写问题,故这里定义一定的使用规范:

(1)变量应该尽量避免共享,尽量通过接口的形式提供读写接口,并考虑加锁保护。

(2)一处写多处读,也需要加读写锁。

(3)依赖通讯来实现共享内存,而不是依赖共享内存来实现通讯。

12. 参数传递

(1)当函数内部需要修改接受者,必须使用指针传递

(2)当接受者是map, chan, func, 不要使用指针传递,因为它们本身就是引用类型

(3)当接受者是slice,而函数内部不会对slice进行切片或者重新分配空间,不要使用指针传递

(4)当接受者是一个结构体,并且包含了sync.Mutex或者类似的用于同步的成员。必须使用指针传递,避免成员拷贝

(5)当接受者类型是一个结构体并且很庞大,或者是一个大数组,建议使用指针传递来提高性能,其他场景使用值传递即可.

13. 结构体中的变量声明

如果你使用结构体指针,mutex 可以非指针形式作为结构体的组成字段,或者更好的方式是直接嵌入到结构体中。 如果是私有结构体类型或是要实现 Mutex 接口的类型,我们可以使用嵌入 mutex 的方法:

14. 业务处理中的不可信原则

实际上这也是网络安全中一条非常非常重要的准则。对于不可信的,遵从优先处理的原则。对于一个业务代码,从上到下,其可信度递增,故对于不符合条件的进行优先排除并return(而尽量不要采用else进行嵌套):

再次强调:前置条件,优先判断;边界检查,优先处理;一旦有错误发生,通常马上返回(尽早return);尽量减少多级嵌套。

15. 不必要的else

如果在 if 的两个分支中都设置了变量,则可以将其替换为单个 if(整体上遵从上面的不可信原则):

16. 减少嵌套

基本原则还是遵从上面的不可信原则,尽可能先处理错误情况/特殊情况,并尽早返回,对于多层嵌套,应采用模块化封装的形式尽量保存代码优雅。

17. 外部接口调用

(1)遵循不可信原则进行边界检查,一旦有错误发生,尽早return

(2)采用多返回值,其中最后一个返回值最好是error类型

(3)考虑可能存在的GC处理(一般是defer close)

(4)考虑可能的默认参数设置(如timeout、context等)

(5)异常情况:必须打对应等级打log并记录堆栈错误信息,对于用户层的接口返回,避免直接返回底层敏感错误信息

18. 打log规范

一个工程化很高的项目对打log的规范也是很高很明确的。而对于Go项目,推荐使用logrus  库。对于线上环境,统一采用json格式且小写的message说明。log的类型/等级及其说明如下:

(1)debug:非常具体的信息,只能用于开发调试使用。部署到生产环境后,这个级别的信息只能保持很短的时间。这些信息只能临时存在,并将最终被关闭。要区分DEBUGTRACE会比较困难,对一个在开发及测试完成后将被删除的LOG输出,可能会比较适合定义为TRACE级别.

(2)info:重要的业务处理已经结束。在实际环境中,系统管理员或者高级用户要能理解INFO输出的信息并能很快的了解应用正在做什么。比如,一个和处理机票预订的系统,对每一张票要有且只有一条INFO信息描述 “[Who] booked ticket from [Where] to [Where]”。另外一种对INFO信息的定义是:记录显著改变应用状态的每一个action,比如:数据库更新、外部系统请求。

(3)warn:发生这个级别问题时,处理过程可以继续,但必须对这个问题给予额外关注。这个问题又可以细分成两种情况:

第一种是存在严重的问题但有应急措施(比如数据库不可用,使用Cache);

第二种是潜在问题及建议(如循环中批处理,其中部分错误应warn,并及时continue)。

比如生产环境的应用运行在Development模式下、管理控制台没有密码保护等。系统可以允许这种错误的存在,但必须及时做跟踪检查用户参数错误。可以使用warn日志级别来记录用户输入参数错误的情况,避免用户投诉时,无所适从。

(4)error:系统中发生了非常严重的问题,必须马上有人进行处理。没有系统可以忍受这个级别的问题的存在。比如:NPEs(空指针异常),数据库不可用,关键业务流程中断等。

log的格式化:%s、%f、%d、%v、%+v、%#v

对于调试代码,不允许使用fmt之类的,而是使用log函数。建议使用format格式化而不是拼凑:

19. 关于error的使用

推荐统一使用fmt.Errorf(),这样便于格式化。有指定信息的(最后的error类型除外)使用英文的括号。error的消息体使用小写格式,保持简单明了,并附带相关会话/场景的数据信息。对于通用的简单error类型可在文件头部封装定义:

对于(在接口层)需要返回给外部调用者的error,建议对error进行封装(可以参考grpc的status.Error和grpc.odes的设计实现)。且返回给外部的error需要重新定义(不要将底层的堆栈错误信息返回给外部调用者)。

至此,关于Go语言的基础编码规范就写的差不多了,后面有时间再总结下关于技巧性的经验分享。最后,就用一个GopherChina 2017中关于Go开发设计风格的演讲Go coding in go way结尾吧:

演稿文章: https://tonybai.com/2017/04/20/go-coding-in-go-way/

演讲视频: https://www.youtube.com/watch?v=N4deJtxeNKM

 



这篇博文由 s0nnet 于2020年03月20日发表在 Go语言, 代码艺术 分类下, 通告目前不可用,你可以至底部留下评论。
如无特别说明,独木の白帆发表的文章均为原创,欢迎大家转载,转载请注明: Go语言开发规范 | 独木の白帆

Go语言开发规范:等您坐沙发呢!

发表评论

快捷键:Ctrl+Enter