Go 编程指南
linux安装go:
https://www.jianshu.com/p/c43ebab25484
开发环境
下载 Go 工具链并安装 https://golang.org/dl/
设置 Go 相关环境变量
- GOROOT - Go 工具链安装目录(Go 1.0 以后不再需要显式指定)。
- GOPATH - Go 模块引用目录 。
- PATH - 通常需要将 $GOROOT/bin 及 $GOPATH/bin 加入到系统 PATH 中。
GOPATH 中需要建立如下子目录
- bin - 存放 go get 或 go install 编译安装的程序。
- pkg - 存放编译的中间结果。
- src - 按照包的结构存放程序源代码。Go 中一个项目的包的名称的根通常按照 仓库地址/项目所有者/项目名称 来命名。例如:github.com/user/project,这个项目就应该放在目录 $GOPATH/src/github.com/user/project 中。
命令行终端
- Windows - 推荐使用 git For windows 自带的 Git Bash。
- 其它系统 - 任一终端应用程序均可
安装 C/C++ 编译器
- Windows - 下载 MinGW 并安装;安装过程中注意选择64位编译器。
- macOS - 从 App Store 中直接安装 Xcode 即可。
- Linux - 在命令行终端上执行 yum install gcc-c++ 或者 apt-get install g++。
安装 Go 开发支持工具
go get -u github.com/golang/dep # Go 包管理工具
go get -u golang.org/x/tools/cmd/goimports # Go imports 格式化工具
go get -u github.com/fjl/gencodec # JSON 或 TOML 序列化代码生成工具
go get -u github.com/kevinburke/go-bindata # Go 内嵌外部资源代码生成工具
安装 IDE
- GoLand - https://www.jetbrains.com/go/
- Visual Studio Code - https://code.visualstudio.com/
代码风格
格式化
- Go 发行版中包含有一个代码格式化工具 gofmt,因此 Go 社区中不再讨论适用于 Go 的代码风格。Go 项目中一般都要求所有代码提交前都需要使用 gofmt 进行格式化。
- go fmt 命令(注意空格)是对 gofmt 的包装。gofmt 专注于单个文件的格式化,go fmt 则提供对整个包的格式化。
- 另外,还有一个代码格式化工具 goimports 在 gofmt 的基础上追加了一些对 import 的格式化。
- 在我们的项目中,通常会包含代码格式化脚本。在包含有格式化脚本的项目中要求代码提交前使用脚本进行格式化;如不包含,则要求提交前使用 go fmt 进行格式化。
命名
包名通常为小写名词。特别的,若某个包含有名为 internal 的子包,则该子包内容仅能被父包引用。
文件名通常为下划线分隔的小写字母与数字。
变量与函数通常使用驼峰式命名,如 totalAmount、NewBlock()。变量通常为名词短语,函数为动词短语。变量与函数的名称的首字母如果是大写的,则表示编译时符号会被导出,可以被其它包引用;反之,则不能。
Getter 在 Go 中通常直接使用变量名称,而不是在变量名称前加 Get。例如:
type Object struct{ owner int }
// 正确func (o *Object) Owner() int { return o.owner }// 错误func (o *Object) GetOwner() int { return o.owner }
注释
Go 同时支持 C 风格的块注释 /* */ 和 C++ 风格的行注释 //。但在实际使用中多使用行注释。块注释多使用在包注释中;块注释也很适合用在表达式内注释,或者用于快速禁用大段代码时。
Go 发行版中包含有一个文档生成工具 godoc,因此对代码中的注释有一定要求。godoc 主要处理的是包注释和符号注释。对于这类注释有如下要求:
- 注释后应紧跟 package xxx 语句或符号声明语句。
- 注释的内容应由以 . 结尾的完整的句子组成。
- 注释不支持 HTML 标签或其它类似的格式标签。
- 注释经 godoc 渲染出来的页面不保证使用等宽字体展现,所以不要依赖空白字符排版。
- 注释内容中缩进的部分被认为是事例,会按照代码来进行渲染。(可以参考 fmt 的包注释)。
包注释
每个包都应该有一个包注释。
当包内包含多个文件时,选取任意一个文件添加注释即可。
如果注释内容比较多,可以添加一个 doc.go 文件来添加包注释。
包注释内容较多时应使用块注释;反之,则使用行注释。
包注释应以 Package 包名 开始。
例如:
/* Package regexp implements a simple library for regular expressions. The syntax of the regular expressions accepted is: regexp: concatenation { '|' concatenation } concatenation: { closure } closure: term [ '*' | '+' | '?' ] term: '^' '$' '.' character '[' [ '^' ] character-ranges ']' '(' regexp ')' */package regexp
// Package path implements utility routines for// manipulating slash-separated filename paths.package path
符号注释
所有被导出的符号(即首字母大写的符号)都应该有注释。
符号注释一般使用行注释。
符号注释应以符号名称开始。
Go 的语法允许对变量声明进行分组。符号注释可以添加在分组之上。
例如:
// Compile parses a regular expression and returns, if successful,// a Regexp that can be used to match against text.func Compile(str string) (*Regexp, error) { ...} // Error codes returned by failures to parse an expression.var ( ErrInternal = errors.New("regexp: internal error") ErrUnmatchedLpar = errors.New("regexp: unmatched '('") ErrUnmatchedRpar = errors.New("regexp: unmatched ')'") ...)
TODO
开发进行时,经常会有对部分代码留空以待后续补充的场景。在这种场景下,要求使用 TODO 或 FIXME 注释。例如:
func DoSomeThing() error { // TODO: Implement this return errors.New("Not implement")} func SafeAdd(int x, int y) int, error { // FIXME: Check integer overflow return x + y, nil}
语言特性
数组(Array)
- 数组是值,将数组赋值给另一个数组会复制所有的元素。
- 如果你把数组作为参数传递给一个函数,函数会收到一个原数组的复制,而不是指针。
- 数组的大小是类型的一部分,[3]int 和 [6]int 是不同的类型。
分片(Slice)
- 定义空分片应使用 var empty []string 而不是 empty := []string{}。前者的初始值为 nil,后者为一个长度为0的分片。
函数内部变量的指针
与 C/C++ 不同,在 Go 中返回函数内部变量的指针是合法的。例如:
func calculate() *Result { result := Result{1} return &result}
context.Context
- context.Context 作为函数参数时,应为第一个。
- 不要把 context.Context 定义为 struct 的成员。
- 不要定义自己的 Context 类型。
- context.Context 是不可写的。
复制
- 不要复制其它包定义的类型的实例。
- 不要复制一个包含指针 receiver 的类型的实例。
加密安全的随机数
- 在生成密钥或类似场景时,应该使用 crypto/rand 而不是 math/rand。
命名返回值
在多返回值函数中,使用命名返回值可以增加可读性,也可以生成更好的文档。尤其是在多个返回值具有相同类型时。
func (f *Foo) Location() (float64, float64, error)func (f *Foo) Location() (latitude, longitude float64, err error)
Receiver 类型
在 Go 中使用 receiver 时,选择使用“值”类型或“指针”类型有时候会是很困难的,尤其是接触 Go 时间不长的开发人员来说。如果你对某个场景不知怎么选择,请使用“指针”类型。但是在某些场景下,使用“值”类型是更直观,甚至是更有效率的,比如说某个不会被更改的小的 struct 或者如 int 这样的基本类型。以下是一些基本的指引:
- 如果 receiver 是一个 map、func 或者 chan,请使用“值”类型。
- 如果 receiver 是一个 slice,并且不会对其再次切片或重新分配内存的操作,请使用“值”类型。
- 如果方法需要修改 receiver 的内容,则必须使用“指针”类型。
- 如果 receiver 是一个 struct,并且其包含 sync.Mutex 或其它类似的用于同步的属性,则必须使用“指针”类型以避免复制。
- 如果 receiver 是一个大的 struct 或者 array,使用“指针”类型更有效率。
- 如果 receiver 是一个 struct、slice 或者 array,并且其包含的元素是一个指向可能被修改的值的指针,请使用“指针”类型。这样可以更清晰的传达出 receiver 可能被修改的意图。
- 如果 receiver 是一个小的 struct 或者 array,并且可以很自然的作为一个值(比如 time.Time),或者不包含能被修改的属性和指针,甚至是一个基础类型(比如 int 或者 string),使用“值”类型更直观。一个“值”类型的 receiver 可以减少垃圾清理的工作。
- 最后,如果你有疑问,请使用“指针”类型。
错误处理
基本原则
- 一般的错误处理中不要使用 panic。
- 不要使用 _ 忽略函数返回的错误。
函数执行错误应以返回值形式处理
在 C/C++ 或其它语言中,经常会以 -1 或者 null 来表示错误。例如:
// Lookup 返回 key 对应的值,若 key 不存在则返回 “”func Lookup(key string) string
// Lookup 被引用时,key 不存在的情况容易被忽略Parse(Lookup(key))
Go 支持的函数多返回值提供了一个更好的解决方案。函数应该返回额外的一个值来表示错误。这个返回值的类型通常是 error 或者 bool。
// Lookup 返回 key 对应的值,若 key 不存在 ok 则为 falsefunc Lookup(key string) (value string, ok bool)
// 这使得这种调用会引起编译错误Parse(Lookup(key))
// 正确的处理方法value, ok := Lookup(key)if !ok {
return fmt.Errorf(“no value for %q”, key)}return Parse(value)
Guard Clauses
为了保证可读性以及保证使代码执行路径清晰可见,应“只”缩进错误处理代码段。这种写法通常叫做 Guard Clauses。
// 错误写法if err != nil {
// 错误处理} else {
// 正常处理}
// 正确写法if err != nil {
// 错误处理
return // 或者 continue}// 正常处理
如果 if 语句包含变量声明,则应该放弃使用 if 的简短语法。
// 错误写法if value, err := function(); err != nil {
// 错误处理} else {
// 使用 value}
// 正确写法value, err := function()if err != nil {
// 错误处理}// 使用 value
并发
设计哲学
- Do not communicate by sharing memory; instead, share memory by communicating.
- 不要使用共享内存来进行通讯;相反的,使用通讯来共享内存。
goroutine 生命周期
- 确保 goroutine 的代码逻辑简单,以便于明显的发现 goroutine 的声明周期。
- 如果不能,请在文档中明确指明在什么时间以及为什么 goroutine 停止执行。
闭包(closure)
在函数式编程中,闭包是一个重要概念。闭包指的是函数与函数引用的外部变量的集合。
type Adder func (int) int func NewAdder(base int) Adder { return func(value int) int { // 此处的匿名函数即为闭包,所引用的变量为 base return base + value }}
for 与 goroutine
for 与 goroutine 联合使用时由于闭包的特性,有时会有意想不到的问题。例如:
func Serve(queue chan *Request) { for req := range queue { go func() { process(req) // 此处有问题 }() }}
此例的本意是为每一个 req 启动一个 goroutine 去进行处理。但是因为在 Go 的循环中,循环变量是重用的,所以所有的 goroutine 都去处理了同一个 req。正确的方法如下:
func Serve(queue chan *Request) { for req := range queue { go func(req *Request) { process(req) }(req) }} 或 func Serve(queue chan *Request) { for req := range queue { req := req // 创建了一个 req 的新实例 go func() { process(req) }() }}
工程规范
- 所有外部输入参数都应检查其合法性。
其它
推荐阅读
- Effective Go - https://golang.org/doc/effective_go.html
- Go Code Review Comments - https://github.com/golang/go/wiki/CodeReviewComments
- Go Packages - https://golang.org/pkg/
转载请注明来源