1.3 编程规范

Go 编程规范 #

代码组织 #

目录 #

/cmd

# 程序和库的私有代码。这里的代码都是你不希望被别的应用和库所引用的.
# 把你真正的应用代码放在 /internal/app 目录 (比如: /internal/app/myapp) 把你的应用间共享的代码放在 /internal/pkg 目录 (比如: internal/pkg/myprivlib)
/internal

/pkg

不应该存在的目录 #

/src

参考:


文件内容 #

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

  • General Documentation: 对整个模块和功能的完整描述注释,写在文件头部。
  • package: 当前 package 定义
  • imports: 包含的头文件
  • Constants: 常量
  • Typedefs: 类型定义
  • Globals: 全局变量定义
  • functions: 函数实现

语言规范 #

申明 slice #

申明 slice 时,建议使用 var 方式申明,不建议使用大括号的方式

// GOOD:
var t []string

// BAD:
t := []string{}

embedding #

  • embedding 只用于 “is a” 的语义下,而不用于 “has a” 的语义下
    • 语义上 embedding 是一种继承关系,而不是成员关系
  • 一个定义内,多于一个的 embedding 尽量少用
    • 一个定义内有多个 embedding,则很难判断某个成员变量或函数是从哪里继承得到的
    • 一个定义内有多个 embedding,危害和在 python 中使用 from xxx import * 是类似的
type Automobile struct {
    // ...
}

type Engine struct {
    // ....
}

// GOOD:
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
}

函数参数和返回值 #

  • 必须

    • 函数返回值小于等于 3 个,大于 3 个时必须通过 struct 进行包装
  • 建议

    • 函数参数不建议超过 3 个,大于 3 个时建议通过 struct 进行包装
    • 函数返回值
      • 逻辑判断型:返回值的意义代表 “真” 或 “假”,返回值类型定义为 bool
      • 操作型:返回值的意义代表 “成功” 或 “失败”,返回值类型定义为 error
      • 获取数据型:返回值的意义代表 “有数据” 或 “无数据 / 获取数据失败”,返回值类型定义为(data, error)

缩进 #

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

空格 #

  • 圆括号、方括号、花括号内侧都不加空格
  • 逗号、冒号(slice 中冒号除外)前不加空格,后边加一个空格
  • 所有二元运算符前后各加一个空格(作为函数参数时除外
var (
    s = make([]int, 10)
)

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

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

命名 #

文件名 #

文件名都使用小写字母,如果需要,可以使用下划线分割

文件名的后缀使用小写字母

函数名 / 变量名 #

采用驼峰方式命名,禁止使用下划线命名

首字母是否大写,根据是否需要外部访问来决定

常量 #

尽量不要在程序中直接写数字,特殊字符串,全部用常量替代

包名 #

关于包的命名,Go 团队建议以简单,扁平为原则。 例如,strutilsstring utility 函数的名字,http 是 HTTP 请求的名字。

包的名字应避免使用下划线,中划线或掺杂大写字母。


编程实践 #

error string #

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

Don’t panic #

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

关于 lock 的保护 #

  • 如果临界区内的逻辑较复杂、无法完全避免 panic 的发生,则要求适用 defer 来调用 Unlock,即使在临界区过程中发生了 panic,也会在函数退出时调用 Unlock 释放锁
    • go 提供了 recover,可以对 panic 进行捕获,但如果 panic 发生在临界区内,则可能导致对锁的使用没有释放
    • 这种情况下,即使 panic 不会导致整个程序的奔溃,也会由于” 锁不释放 “的问题而使临界区无法被后续的调用访问
  • 上述操作如果造成临界区扩大后,需要建立单独的一个函数访问临界区。
func doDemo() {
    lock.Lock()

    // step1: 临界区内的操作

    lock.Unlock()

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

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

    // step1: 临界区内的操作

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

// 需要使用单独的匿名函数,专门用于访问临界区
func doDemo() {
    func() {
        lock.Lock()
        defer lock.Unlock()

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

    }()

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

unsafe package #

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