摘要
Golang编码规范
本文内容转自网络,个人学习记录使用,请勿传播
语言规范
语法检查
- 所有代码能通过gofmt命令检查
- 通过gofmt命令检查:指输出与输入完全相同,可用md5sum命令比较
true/false求值
- 当明确expr为bool类型时,禁止使用==或!=与true/false比较,应该使用expr或!expr
- 判断某个整数表达式expr是否为零时,禁止使用!expr,应该使用expr == 0
1 | // GOOD |
Receiver
Receiver Type
- 如果receiver是map、函数或者chan类型,类型不可以是指针
- 如果receiver是slice,并且方法不会进行reslice或者重新分配slice,类型不可以是指针
- 如果receiver是struct,且包含sync.Mutex类型字段,则必须使用指针避免拷贝。
- 如果receiver是比较大的struct/array,建议使用指针,这样会更有效率
- 如果receiver是struct、array或slice,其中指针元素所指的内容可能在方法内被修改,建议使用指针类型
- 如果receiver是比较小的struct/array,建议使用value类型
解释
- 关于receiver的定义详见 Receiver定义
- struct或者array中的元素个数超过3个,则认为比较大,反之,则认为比较小
receiver命名
- 尽量简短并有意义。
- 禁止使用“this”、”self“等面向对象语言中特定的叫法。
- receiver的命名要保持一致性
1 | // GOOD |
声明空Slices
- 声明slice时,建议使用var方式声明,不建议使用大括号的方式
- var方式声明在slice不被append的情况下避免了内存分配
1 | // GOOD |
Error Handler
- 对于返回值中的error,一定要进行判断和处理,不可以使用 ”_“ 变量忽略error
{
的使用
- struct、函数、条件判断中的“{”,不可以作为独立的一行
1 | // GOOD |
embedding的使用
- embedding只用于”is a”的语义下,而不用于”has a”的语义下
- 一个定义内,多于一个的embedding尽量少用
解释
- 语义上embedding是一种“继承关系“,而不是”成员关系“
- 一个定义内有多个embedding,则很难判断某个成员变量或函数是从哪里继承得到的
- 一个定义内有多个embedding,危害和在python中使用
from xxx import *
是类似的
embedding只是一种“语法”上的shortcut,并没有规定“语义”上代表什么,所以不能从C++形而上学,认为所有多embedding均为“多继承”;另外,不提倡多embedding可能会降低golang的表达力,因此作为规范应当慎重采纳。
1 | // GOOD |
风格规范
Go文件Layout
建议文件按以下顺序进行布局
- General Documentation: 对整个模块和功能的完整描述注释,写在文件头部。
- package:当前package定义
- imports:包含的头文件
- Constants:常量
- Typedefs: 类型定义
- Globals:全局变量定义
- functions:函数实现
1 | /* Copyright 2015 xxx Inc. All Rights Reserved. */ |
- 对于以上的各个部分,采用单个空行分割,同时:
- 多个类型定义采用单个空行分割
- 多个函数采用单个空行分割
- 函数内不同的业务逻辑处理建议采用单个空行分割
- 常量或者变量如果较多,建议按照业务进行分组,组间用单个空行分割
General Documentation Layout
- 建议每个文件开头部分包括文件copyright说明(copyright)
- 建议每个文件开头部分包括文件标题(Title)
- 建议每个文件开头部分包括修改记录(Modification History)
- 建议每个文件开头部分包括文件描述(Description)
解释
- Title中包括文件的名称和文件的简单说明
- Title应该在一行内完成
- Modification History记录文件的修改过程,并且只记录最主要的修改
- 当书写新的函数模块时,只需要使用形如”Add func1()”这样的说明
- 如果后面有对函数中的算法进行了修改,需要指出修改的具体位置和修改方法
- Modification History的具体格式为:<修改时间>, <修改人>, <修改动作 >
- Description 详细描述文件的功能和作用
1 | /* Copyright 2015 Baidu Inc. All Rights Reserved. */ |
import规范
- 需要按照如下顺序进行头文件import,并且每个import部分内的package需按照字母升序排列
- 系统package
- 第三方的package
- 程序自己的package
- 每部分import间用单个空行进行分隔
1 | import ( |
Go函数Layout
函数注释
函数的注释,建议包括以下内容
- Description:对函数的完整描述,主要包括函数功能和使用方法
- Params:对参数的说明
- Returns:对返回值的说明
1 | /* |
函数参数和返回值
- 对于“逻辑判断型”的函数,返回值的意义代表“真”或“假”,返回值类型定义为bool
- 对于“操作型”的函数,返回值的意义代表“成功”或“失败”,返回值类型定义为error
- 如果成功,则返回nil
- 如果失败,则返回对应的error值
- 对于“获取数据型”的函数,返回值的意义代表“有数据”或“无数据/获取数据失败”,返回值类型定义为(data, error)
- 正常情况下,返回为:(data, nil)
- 异常情况下,返回为:(data, error)
- 函数返回值小于等于3个,大于3个时必须通过struct进行包装
- 函数参数不建议超过3个,大于3个时建议通过struct进行包装
1 | // GOOD |
程序规模
- 每行代码不超过100个字符。
- 每行注释不超过100个字符。
- 函数不超过100行。
- 文件不超过2000行。
解释
- 现在宽屏比较流行,所以从传统的80个字符限制扩展到100个字符
- 函数/文件太长一般说明函数定义不明确/程序结构划分不合理,不利于维护
命名规范
文件名
- 文件名都使用小写字母,如果需要,可以使用下划线分割
- 文件名的后缀使用小写字母
1 | // GOOD |
函数名/变量名
- 采用驼峰方式命名,禁止使用下划线命名。首字母是否大写,根据是否需要外部访问来决定
1 | // GOOD |
常量
- 建议都使用大写字母,如果需要,可以使用下划线分割
- 尽量不要在程序中直接写数字,特殊字符串,全部用常量替代
- go标准库中常量也有驼峰的命名方式,故这里不做强制限制。
1 | // GOOD |
缩写词
- 缩写词要保持命名的一致性。
- 同一变量字母大小写的一致性
- 不同变量间的一致性
1 | // GOOD |
缩进
- 使用tab进行缩进。
- 跨行的缩进使用gofmt的缩进方式。
- 设置tabstop=4
要求设置tabstop=4是考虑到不同编辑器跨行字符串对齐显示的一致性
1 | func main() { |
- 错误处理时缩进错误处理代码,对正常代码保持最少的缩进。
1 | // GOOD |
空格
- 圆括号、方括号、花括号内侧都不加空格
- 逗号、冒号(slice中冒号除外)前不加空格,后边加一个空格
- 所有二元运算符前后各加一个空格(作为函数参数时除外)
1 | // GOOD |
括号
- 除非用于明确算术表达式优先级,否则尽量避免冗余的括号
1 | // GOOD |
注释
- 单行注释,采取
//
或者/*...*/
的注释方式。 - 多行注释,采取每行开头
//
或者用/* ... */
包括起来的注释(/*和*/
作为独立的行) - 紧跟在代码之后的注释,使用
//
大多数情况下,使用”//“更方便
1 | /* This is the correct format for a single-line comment */ |
编程实践
error string
error string
尽量使用小写字母,并且结尾不带标点符号
因为可能error string会用于其它上下文中
1 | // GOOD |
Don’t panic
- 除非出现不可恢复的程序错误,不要使用panic,用多返回值和error。
关于lock的保护
- 如果临界区内的逻辑较复杂、无法完全避免panic的发生,则要求适用defer来调用Unlock,即使在临界区过程中发生了panic,也会在函数退出时调用Unlock释放锁
解释
- go提供了recover,可以对panic进行捕获,但如果panic发生在临界区内,则可能导致对锁的使用没有释放
- 这种情况下,即使panic不会导致整个程序的奔溃,也会由于”锁不释放“的问题而使临界区无法被后续的调用访问
1 | // GOOD |
- 上述操作如果造成临界区扩大后,需要建立单独的一个函数访问临界区
1 | func doDemo() { |
如果改造为defer的方式,变为如下代码,实际上扩大了临界区的范围(step2的操作也被放置在临界区了)
1 | func doDemo() { |
需要使用单独的匿名函数,专门用于访问临界区:
1 | func doDemo() { |
unsafe package
- 除非特殊原因,不建议使用unsafe package
- 比如进行指针和数值uintptr之间转换就是一个特殊原因