• 2009 年 11 月 10 日正式开源,这一天被确定为 Go 语言的诞生日;
  • 吉祥物是 Rob Pike 的夫人 Renee French 设计的地鼠;
  • 程序员被称为 Gopher
  • 2018 年 8月 19 日,Go 1.5 发布。这是具有里程碑意义的版本。从这个版本开始,Go 实现了自举
  • 设计哲学是 组合
  • Go 1.16 版本,Go module 成为默认的依赖包管理和构建机制;

最小的标准布局:

1
2
3
4
5
- go.mod
- LICENSE
- xx.go
- yy.go
...

或者

1
2
3
4
5
6
- go.mod
- LICENSE
- package1
- xx.go
- package2
- yy.go

常见的其他目录:

  • cmd: 存放项目要构建的可执行文件对应的 main 包的源文件;
  • pkg: 存放项目自身要使用并且同样也是可执行文件对应 main 包要依赖的库文件;
  • internal: 对于不想暴露给外部引用,仅限内部使用的包;

命名

  • 包:小写单个单词命名
  • 包名可以不唯一,但每个包的导入路径唯一
  • 如果缩略词首字母大写,其他字母也要大写,比如 HTTPURL,而不是使用 HttpUrl
    • 同样,使用 ID 而不要用 Id
  • 常量的命名并不要求大写,比如:
1
2
3
const (
defaultMaxMemory = 32 << 20
)

注释

  • 支持 C 风格的块注释 /* */,也支持 C++ 风格的行注释 //
  • 块注释主要用于包的注释;
  • 应该使用完整的句子,即使看起来有一些冗余;
  • 包注释是不应该有空白行;

环境

  • 无论你是否使用默认的安装位置,最好显式定义 GOPATH

语言基础

  • Go 会对未初始的变量赋零值
  • 当需要初始化一个变量为零值时,使用 var x int 这种形式,可以清晰地表达零值的意图
  • Go 中很少使用数组,更多使用切片
    • 如果可以预估出底层数组所需的数量,强烈建议带上 cap 参数
    • 切片在 Go 中表示为 (*T, len, cap),其中 *T 指向切片对应的底层数组指针,len 是切片当前长度,cap 是切片的容量
  • 使用数组字面量,可以不定义具体长度,而使用 ...,比如 var x = [...]int{10, 20, 30}
  • 为了避免复杂的切片行为,不要对子切片使用 append
  • byteuint8 的别名;runeint32 的别名
  • Go 不允许自动的类型转换,如果类型不一致,需要显式进行转换
  • 做了预初始化的 strings.Builder 连接构建字符串效率最高

函数

  • Go 语言是一种值传递的语言。这意味着提供变量给一个函数作为参数时,Go 总是复制该变量的值;
  • context 一般要作为函数的第一个参数;

类型、方法和接口

  • 区分使用指针接收者(receiver)还是值接收者:
    • 如果方法修改了接收者,那么必须使用指针接收者
    • 如果方法需要处理 nil 实例,那么必须使用指针接收者
    • 如果方法不修改接收者的值,可以使用值接收者
  • 不要把 Context 加入到结构体类型,你应该传入到类型方法。
  • Context 是不可变的,所以最好在多个调用中传递相同的 ctx
  • 接口通常应该属于使用接口类型的包,而不是实现接口类型的包。接口类型的具体实现应该返回具体的类型(通常是指针或者结构)类型。这样可以将新的方法加入到实现中而不需要大量重构
    • 也就是「接收接口,返回具体类型」
  • 接口应该是可以使用真实实现的,而不是为 mocking 服务的
  • 不要在接口使用之前定义它(保持最小接口)
1
2
3
4
5
6
7
package producter
type Thinger interface { Thing() bool }

type defaultThinger struct { /* ... */ }
func (t defaultThinger) Thing() bool { /* ... */ }

func NewThinger() Thinger { return defaultThinger { /* ... */ } }

应该返回具体的类型:

1
2
3
4
5
6
package producter

type Thinger struct { /* ... */ }
func (t Thinger) Thing() bool { /* ... */ }

func NewThinger() Thinger { return Thinger { /* ... */ } }

错误处理

  • 返回一个类型为 error 的值作为函数的最后一个返回值来返回错误
  • 不要使用 panic 作为正常情况下的错误处理;
  • 错误字符串信息应该 小写开头
  • 不要使用 _ 抛弃错误。如果函数返回一个 error,应该检查来确保函数成功;
    • 处理这个 error,或者 return,或者真的是特殊情况下 panic

不要这样写:

1
2
3
4
5
if err != nil {
// error handling
} else {
// normal code
}

而是这样写:

1
2
3
4
5
if err != nil {
// error handling
return // or continue, etc.
}
// normal code

模块、包和导入

  • Go 的库管理依赖于三个仓库:仓库、模块和包
    • 项目的源代码通过版本控制系统存储在仓库之中
    • 模块是 Go 库或者应用程序的根目录
    • 模块中包含一个或者多个包,用来组织模块的结构
  • 尽量避免 init 函数
    • init 函数没有输入和输出,那么只能作为副作用使用
    • 可以在同一个包中定义多个 init 函数,但是顺序难以确定
  • 如果只是需要一个包的副作用,例如:import _ "pkg",那么应该只在 main 或者测试中导入
  • 只在测试出现循环依赖的时候,使用 . 导入包,否则程序会难以阅读

单元测试

  • 所有测试文件名都是以 _test.go 结尾
  • 测试函数都是以 Test 开头,并都接受一个类型为 *testing.T 的参数
  • 通常约定是使用 Test 加上被测试函数的名字

Go 工具链

  • gofmt:应该自动用 gofmt 格式化代码;
    • goimportsgofmt 超集,会自动删除不必要的导入;
  • godoc:提取代码文档;

参考资料

  1. Gopher 必读的 Effective Go
  2. Effective Go 补充,Go Code Review Comments

小结

Go 基础概念还是比较简单的,本文并不面面俱到,而且也不是从 0 开始,Go 还有很多细节值得展开,但是时间有限,就没有写了。

(TODO…)