Go规范

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

代码风格

格式化

  • 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)
    
    }()
    
    }}
    

工程规范

  • 所有外部输入参数都应检查其合法性。

其它

推荐阅读


转载请注明来源

×

喜欢就点赞,疼爱就打赏