Go语言-编码规范

摘要

Golang编码规范

语言规范

语法检查

  • 所有代码能通过gofmt命令检查
  • 通过gofmt命令检查:指输出与输入完全相同,可用md5sum命令比较

true/false求值

  • 当明确expr为bool类型时,禁止使用==或!=与true/false比较,应该使用expr或!expr
  • 判断某个整数表达式expr是否为零时,禁止使用!expr,应该使用expr == 0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// GOOD
var isWhiteCat bool
var num int

if isWhiteCat {
// ...
}

if num == 0 {
// ...
}

// BAD
var isWhiteCat bool
var num int

if isWhite == true {
// ...
}

if !num {
// ...
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// GOOD
// call()和done()都使用了在上下文中有意义的"c"进行receiver命名
func (c Client) call() error {
// ...
}

func (c Client) done() error {
// ...
}

// BAD
// 1. "c"和"client"命名不一致:done()用了c,call()用了client
// 2. client命名过于冗余
func (c Client) done() error {
// ...
}

func (client Client) call() error {
// ...
}

// 不允许使用self
func (self Server) rcv() error {
// ...
}

// 不允许使用this
func (this Server) call() error {
// ...
}

声明空Slices

  • 声明slice时,建议使用var方式声明,不建议使用大括号的方式
    • var方式声明在slice不被append的情况下避免了内存分配
1
2
3
4
5
// GOOD
var t []string

// BAD
t := []string{}

Error Handler

  • 对于返回值中的error,一定要进行判断和处理,不可以使用 ”_“ 变量忽略error

{的使用

  • struct、函数、条件判断中的“{”,不可以作为独立的一行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// GOOD
if condition {
// ...
} else {
// ...
}

// BAD
// "{"不可以作为独立行
if condition
{
// ...
} else
{
// ...
}

embedding的使用

  • embedding只用于”is a”的语义下,而不用于”has a”的语义下
  • 一个定义内,多于一个的embedding尽量少用

解释

  • 语义上embedding是一种“继承关系“,而不是”成员关系“
  • 一个定义内有多个embedding,则很难判断某个成员变量或函数是从哪里继承得到的
  • 一个定义内有多个embedding,危害和在python中使用from xxx import *是类似的

embedding只是一种“语法”上的shortcut,并没有规定“语义”上代表什么,所以不能从C++形而上学,认为所有多embedding均为“多继承”;另外,不提倡多embedding可能会降低golang的表达力,因此作为规范应当慎重采纳。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// GOOD
type Automobile struct {
// ...
}

type Engine struct {
// ....
}

// 正确的定义
type Car struct {
Automobile // Car is a Automobile
engine Engine // Car has a Engine
}

// BAD
type Car struct {
Automobile // Car is a Automobile
Engine // Car has a Engine, but Car is NOT a Engine
}

风格规范

Go文件Layout

建议文件按以下顺序进行布局

  • General Documentation: 对整个模块和功能的完整描述注释,写在文件头部。
  • package:当前package定义
  • imports:包含的头文件
  • Constants:常量
  • Typedefs: 类型定义
  • Globals:全局变量定义
  • functions:函数实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/* Copyright 2015 xxx Inc. All Rights Reserved. */
/* xxx - the main xxx */
/*
modification history
--------------------
2014/6/5, by xxx, create
*/
/*
DESCRIPTION
This file contains the most important struct xxx
*/

package bfe_server

// imports
import (
"fmt"
"time"
)

import (
"code.google.com/p/log4go"
)

import (
"xxx/xxx_conf"
"xxx_module"
)

const (
version = "1.0.0.0"
)

// typedefs
type BfeModule interface {
Init(cfg xxx_conf.xxxConfig, cbs *xxxCallbacks) error
}

type xxxModules struct {
modules map[string]xxxModule
}

// vars
var errTooLarge = errors.New("http: request too large")

// functions
func foo() {
//...
}
  • 对于以上的各个部分,采用单个空行分割,同时:
    • 多个类型定义采用单个空行分割
    • 多个函数采用单个空行分割
  • 函数内不同的业务逻辑处理建议采用单个空行分割
  • 常量或者变量如果较多,建议按照业务进行分组,组间用单个空行分割

General Documentation Layout

  • 建议每个文件开头部分包括文件copyright说明(copyright)
  • 建议每个文件开头部分包括文件标题(Title)
  • 建议每个文件开头部分包括修改记录(Modification History)
  • 建议每个文件开头部分包括文件描述(Description)

解释

  • Title中包括文件的名称和文件的简单说明
    • Title应该在一行内完成
  • Modification History记录文件的修改过程,并且只记录最主要的修改
    • 当书写新的函数模块时,只需要使用形如”Add func1()”这样的说明
    • 如果后面有对函数中的算法进行了修改,需要指出修改的具体位置和修改方法
    • Modification History的具体格式为:<修改时间>, <修改人>, <修改动作 >
  • Description 详细描述文件的功能和作用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* Copyright 2015 Baidu Inc. All Rights Reserved. */
/* xxx.go - the main structure of xxx */
/*
modification history
--------------------
2014/6/5, by Zhang San, create
*/
/*
DESCRIPTION
This file contains the most important struct xxx
*/

package xxx_server

func func1() {
// ...
}

import规范

  • 需要按照如下顺序进行头文件import,并且每个import部分内的package需按照字母升序排列
    • 系统package
    • 第三方的package
    • 程序自己的package
  • 每部分import间用单个空行进行分隔
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import (
"fmt"
"time"
)

import (
"code.google.com/p/log4go"
)

import (
"xxx/xxx_conf"
"xxx_module"
)

GOOD:
import (
"fmt"
"time"

"code.google.com/p/log4go"

"xxx/xxx_conf"
)

Go函数Layout

函数注释

函数的注释,建议包括以下内容

  • Description:对函数的完整描述,主要包括函数功能和使用方法
  • Params:对参数的说明
  • Returns:对返回值的说明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
* Init - initialize log lib
*
* PARAMS:
* - levelStr: "DEBUG", "TRACE", "INFO", "WARNING", "ERROR", "CRITICAL"
* - when:
* "M", minute
* "H", hour
* "D", day
* "MIDNIGHT", roll over at midnight
* - backupCount: If backupCount is > 0, when rollover is done, no more than
* backupCount files are kept - the oldest ones are deleted.
*
* RETURNS:
* nil, if succeed
* error, if fail
*/
func Init(levelStr string, when string, backupCount int) error {
// ...
}

函数参数和返回值

  • 对于“逻辑判断型”的函数,返回值的意义代表“真”或“假”,返回值类型定义为bool
  • 对于“操作型”的函数,返回值的意义代表“成功”或“失败”,返回值类型定义为error
    • 如果成功,则返回nil
    • 如果失败,则返回对应的error值
  • 对于“获取数据型”的函数,返回值的意义代表“有数据”或“无数据/获取数据失败”,返回值类型定义为(data, error)
    • 正常情况下,返回为:(data, nil)
    • 异常情况下,返回为:(data, error)
  • 函数返回值小于等于3个,大于3个时必须通过struct进行包装
  • 函数参数不建议超过3个,大于3个时建议通过struct进行包装
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// GOOD
type student struct {
name string
email string
id int
class string
}

// bool作为逻辑判断型函数的返回值
func isWhiteCat() bool {
// ...
}

// error作为操作型函数的返回值
func deleteData() error {
// ...
}

// 利用多返回值的语言特性
func getData() (student, error) {
// ...
}

// BAD
type student struct {
name string
email string
id int
class string
}

// 使用int而非bool作为逻辑判断函数的返回值
func isWhiteCat() int {
// ...
}

// 操作型函数没有返回值
func deleteData() {
// ...
}

// 没有充分利用go多返回值的特点
func getData() student {
// ...
}

// 返回值>3
func getData() (string, string, int, string, error) {
// ...
}

程序规模

  • 每行代码不超过100个字符。
  • 每行注释不超过100个字符。
  • 函数不超过100行。
  • 文件不超过2000行。

解释

  • 现在宽屏比较流行,所以从传统的80个字符限制扩展到100个字符
  • 函数/文件太长一般说明函数定义不明确/程序结构划分不合理,不利于维护

命名规范

文件名

  • 文件名都使用小写字母,如果需要,可以使用下划线分割
  • 文件名的后缀使用小写字母
1
2
3
4
5
6
7
8
9
10
11
12
13
// GOOD
// 可以使用下划线分割文件名
web_server.go

// 文件名全部小写
http.go

// BAD
// 文件名不允许出现大写字符
webServer.go

// 文件名后缀不允许出现大写字符
http.GO

函数名/变量名

  • 采用驼峰方式命名,禁止使用下划线命名。首字母是否大写,根据是否需要外部访问来决定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// GOOD
// 本package可以访问的函数
func innerFunc() bool {
// ...
}

// 其余package可以访问的函数
func OuterFunc() error {
// ...
}

// BAD
// 禁止用下划线分割
func inner_Func() bool {
var srv_name string
// ...
}

// 禁止用下划线分割
// 其余package可以访问的函数
func Outer_Func() error {
// ...
}

常量

  • 建议都使用大写字母,如果需要,可以使用下划线分割
  • 尽量不要在程序中直接写数字,特殊字符串,全部用常量替代
  • go标准库中常量也有驼峰的命名方式,故这里不做强制限制。
1
2
3
4
5
6
7
8
9
10
11
12
// GOOD
// 大写字母
const METHOD = "Get"
// 下划线分割
const HEADER_USER_AGENT = "User-Agent"

// go标准库中的命名方式
const defaultUserAgent = "Go 1.1 package http"

// BAD
// 全部为下划线分割的小写字母
const header_user_agent = "User-Agent"

缩写词

  • 缩写词要保持命名的一致性。
    • 同一变量字母大小写的一致性
    • 不同变量间的一致性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// GOOD
var URL string

var ID int
var appID int

type ServeURL struct {
// ...
}

// BAD
var Url string

// not consistent
var ID int
var appid int

type ServUrl struct {
// ...
}

缩进

  • 使用tab进行缩进。
  • 跨行的缩进使用gofmt的缩进方式。
  • 设置tabstop=4

要求设置tabstop=4是考虑到不同编辑器跨行字符串对齐显示的一致性

1
2
3
4
5
6
7
8
9
10
func main() {
rows, err := e.db.Query(`SELECT id, name, state, create_ts, start_ts
FROM workflow
WHERE state=? AND create_ts=?
ORDER BY start_ts DESC`)
}
func longFunctionName(var_one, var_two,
var_three, var_four) {
// ...
}
  • 错误处理时缩进错误处理代码,对正常代码保持最少的缩进。
1
2
3
4
5
6
7
8
9
10
11
12
13
// GOOD
if err != nil {
// error handling
return // or continue, etc.
}
// normal code

// BAD
if err != nil {
// error handling
} else {
// normal code
}

空格

  • 圆括号、方括号、花括号内侧都不加空格
  • 逗号、冒号(slice中冒号除外)前不加空格,后边加一个空格
  • 所有二元运算符前后各加一个空格(作为函数参数时除外)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// GOOD
var (
s = make([]int, 10)
)

func foo() {
m := map[string]string{"language": "golang"}
r := 1 + 2
func1(1+2)
fmt.Println(m["language"])
}

// BAD
var (
s = make( []int , 10 )
)

func foo() {
m := map[string]string{ "language" : "golang" }
r := 1+2
func1(1 + 2)
fmt.Println(m[ "language" ])
}

括号

  • 除非用于明确算术表达式优先级,否则尽量避免冗余的括号
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// GOOD
if x {
}

func func1() int {
var num int
return num
}

// BAD
if (x) {
}

func func1() int {
var num int
return (num)
}

注释

  • 单行注释,采取//或者/*...*/的注释方式。
  • 多行注释,采取每行开头//或者用/* ... */包括起来的注释(/*和*/作为独立的行)
  • 紧跟在代码之后的注释,使用//

大多数情况下,使用”//“更方便

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* This is the correct format for a single-line comment */

// This is the correct format for a single-line comment

/*
* This is the correct format for a multiline comment
* in a section of code.
*/

// This is the correct format for a multiline comment
// in a section of code.

var a int // this is the correct format for a
// multiline comment in a declaration
BOOL b // standard comment at the end of a line

编程实践

error string

  • error string尽量使用小写字母,并且结尾不带标点符号

因为可能error string会用于其它上下文中

1
2
3
4
5
// GOOD
fmt.Errorf("something bad")

// BAD
fmt.Errorf("Something bad")

Don’t panic

  • 除非出现不可恢复的程序错误,不要使用panic,用多返回值和error。

关于lock的保护

  • 如果临界区内的逻辑较复杂、无法完全避免panic的发生,则要求适用defer来调用Unlock,即使在临界区过程中发生了panic,也会在函数退出时调用Unlock释放锁

解释

  • go提供了recover,可以对panic进行捕获,但如果panic发生在临界区内,则可能导致对锁的使用没有释放
  • 这种情况下,即使panic不会导致整个程序的奔溃,也会由于”锁不释放“的问题而使临界区无法被后续的调用访问
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// GOOD
func doDemo() {
lock.Lock()
defer lock.Unlock()

// 访问临界区
}

// BAD
func doDemo() {
lock.Lock()

// 访问临近区

lock.Unlock()
}
  • 上述操作如果造成临界区扩大后,需要建立单独的一个函数访问临界区
1
2
3
4
5
6
7
8
9
func doDemo() {
lock.Lock()

// step1: 临界区内的操作

lock.Unlock()

// step2: 临界区外的操作
}

如果改造为defer的方式,变为如下代码,实际上扩大了临界区的范围(step2的操作也被放置在临界区了)

1
2
3
4
5
6
7
8
func doDemo() {
lock.Lock()
defer lock.Unlock()

// step1: 临界区内的操作

// step2: 临界区外的操作
}

需要使用单独的匿名函数,专门用于访问临界区:

1
2
3
4
5
6
7
8
9
10
11
func doDemo() {
func() {
lock.Lock()
defer lock.Unlock()

// step1: 临界区内的操作操作

}()

// step2: 临界区外的操作
}

unsafe package

  • 除非特殊原因,不建议使用unsafe package
    • 比如进行指针和数值uintptr之间转换就是一个特殊原因