Go语言规范1 - 统一规范篇

目录

序言

看过很多方面的编码规范,可能每一家公司都有不同的规范,这份编码规范是写给我自己的,同时希望我们公司内部同事也能遵循这个规范来写Go代码。

如果你的代码没有办法找到下面的规范,那么就遵循标准库的规范,多阅读标准库的源码,标准库的代码可以说是我们写代码参考的标杆。

本文中凡是【】内为规则的都是参考的标准库的源码,和【】内为原则的一样都是必须遵守的,【】内为建议的,是仅做建议遵守的。

本文参考了网上流传的众多开发规范帖子和标准款的规范代码,最后梳理而成,因内容过多分为几大篇,可选看,最后感谢那些原帖作者,文末给出了参考帖子的链接

目录

  • 统一规范篇
  • 命名篇
  • 开发篇
  • 优化篇

统一规范篇

本篇主要描述了公司内部同事都必须遵守的一些开发规矩,如统一开发空间,既使用统一的开发工具来保证代码最后的格式的统一,开发中对文件和代码长度的控制,必须经过go语言自带的检测机制等。

1.1 合理规划目录

【原则1.1】合理规划目录,一个目录中只包含一个包(实现一个模块的功能),如果模块功能复杂考虑拆分子模块,或者拆分目录。

说明:在Go中对于模块的划分是基于package这个概念,可以在一个目录中可以实现多个package,但是并不建议这样的实现方式。主要的缺点是模块之间的关系不清晰,另外不利于模块功能扩展。

错误示例:

project
│  config.go
│  controller.go
│  filter.go
│  flash.go
│  log.go
│  memzipfile.go
│  mime.go
│  namespace.go
│  parser.go
│  router.go
│  staticfile.go
│  template.go
│  templatefunc.go
│  tree.go
│  util.go
|  validation.go
|  validators.go

推荐做法:

project  
├─cache  
│  │  cache.go  
│  │  conv.go  
│  │        
│  └─redis  
│          redis.go  
├─config  
│  │  config.go  
│  │  fake.go  
│  │  ini.go  
│  └─yaml  
│          yaml.go  
├─logs  
│      conn.go  
│      console.go  
│      file.go  
│      log.go  
│      smtp.go  
└─validation  
        util.go  
        validation.go  
        validators.go

1.2 GOPATH设置

【建议1.2】使用单一的 GOPATH

虽说Go语言支持拥有多个 GOPATH,但多个GOPATH的情况并不具有弹性。GOPATH本身就是高度自我完备的(通过导入路径)。有多个 GOPATH 会导致某些副作用,例如可能使用了给定的库的不同的版本。你可能在某个地方升级了它,但是其他地方却没有升级。而且,我还没遇到过任何一个需要使用多个 GOPATH 的情况。所以只使用单一的 GOPATH,这会提升你 Go 的开发进度。

许多人不同意这一观点,接下来我会做一些澄清。像 etcd 或 camlistore 这样的大项目使用了像 godep 这样的工具,将所有依赖保存到某个目录中。也就是说,这些项目自身有一个单一的 GOPATH。它们只能在这个目录里找到对应的版本。除非你的项目很大并且极为重要,否则不要为每个项目使用不同的 GOPAHT。如果你认为项目需要一个自己的 GOPATH 目录,那么就创建它,否则不要尝试使用多个 GOPATH。它只会拖慢你的进度。

 所有项目共用一个workspace,如下图所示:

workspace/  
├── bin  
├── pkg  
│   └── linux_amd64  
│         
└── src  
    ├── project1  
    │     
└── project2  
    │     
    └── project3  
    │     
    └── …

优点: 方便发布到github.com, 让第三方通过go get等工具获取。

内部项目,建议采用第一种工程结构。公开项目、提供给第三方集成的项目采用第二种项目结构。

1.3 import 规范

import路径是一个唯一标示的字符串

import在多行的情况下,goimports会自动帮你格式化,但是我们这里还是规范一下import的一些规范,如果你在一个文件里面引入了一个package,还是建议采用如下格式:

import (
    "fmt"
)

如果你的包引入了三种类型的包,标准库包,程序内部包,第三方包,建议采用如下方式进行组织你的包:

import (
    "encoding/json"
    "strings"

    "myproject/models"
    "myproject/controller"
    "myproject/utils"

    "github.com/astaxie/beego"
    "github.com/go-sql-driver/mysql"
)

有顺序的引入包,不同的类型采用空格分离,第一种实标准库,第二是项目包,第三是第三方包。

【规则1.3.1】在非测试文件(*_test.go)中,禁止使用 . 来简化导入包的对象调用。

错误示例:

     // 这是不好的导入
        import . " pubcode/api/broker"

这种写法不利于阅读,因而不提倡。

【规则1.3.2】禁止使用相对路径导入(./subpackage),所有导入路径必须符合 go get 标准。

错误示例:

     // 这是不好的导入
        import "../net"

正确做法:

     // 这是正确的做法
        import "github.com/repo/proj/src/net"

【建议1.3.3】建议使用goimports工具或者IDE工具来管理多行import

go默认已经有了gofmt工具,但是我们强烈建议使用goimport工具,这个在gofmt的基础上增加了自动删除和引入包.

go get golang.org/x/tools/cmd/goimports

不同的编辑器有不同的配置, sublime的配置教程:http://michaelwhatcott.com/gosublime-goimports/

LiteIDE和GoLand默认已经支持了goimports,如果你的不支持请点击属性配置->golangfmt->勾选goimports

保存之前自动fmt你的代码。

好处:import在多行的情况下,goimports工具会自动帮你格式化,自动删除和引入包。很多IDE工具也可以自动检查并纠正import路径

1.4 代码风格

Go语言对代码风格作了很多强制的要求,并提供了工具gofmt, golint, go tool vet等工具检查。

【规则1.4.1】提交代码时,必须使用gofmt对代码进行格式化。

大部分的格式问题可以通过 gofmt 来解决,gofmt 自动格式化代码,保证所有的 go 代码与官方推荐的格式保持一致,所有格式有关问题,都以gofmt的结果为准。所以,建议在提交代码库之前先运行一下这个命令。

gofmt(也可以用go fmt,其操作于程序包的级别,而不是源文件级别),读入Go的源代码,然后输出按照标准风格缩进和垂直对齐的源码,并且保留了根据需要进行重新格式化的注释。如果你想知道如何处理某种新的布局情况,可以运行gofmt;如果结果看起来不正确,则需要重新组织你的程序,不要把问题绕过去。标准程序包中的所有Go代码,都已经使用gofmt进行了格式化。

不需要花费时间对结构体中每个域的注释进行排列,如下面的代码,

     type T struct {
            name string     // name of the object
            value int     // its value
        }

gofmt将会按列进行排列:

     type T struct {
            name string   // name of the object
            value int     // its value
        }

【规则1.4.2】提交代码时,必须使用golint对代码进行检查。

golint 会检测的方面:

  • 变量名规范
  • 变量的声明,像var str string = "test",会有警告,应该var str = "test"
  • 大小写问题,大写导出包的要有注释
  • x += 1 应该 x++

等等

详细可以看官方库示例,https://github.com/golang/lint/tree/master/testdata

想速成的可以看Golang lint简易使用方法自行学习使用

【建议1.4.3】提交代码前,必须使用go vet对代码进行检查。

如果说golint是检查我们的代码规范的话,那么vet工具则是可以帮我们静态分析我们的源码存在的各种问题,例如多余的代码,提前return的逻辑,struct的tag是否符合标准等。

go get golang.org/x/tools/cmd/vet

使用如下:

go vet .

1.5 大小约定

【建议1.5.1】单个文件长度不超过500行。

对开源引入代码可以降低约束,新增代码必须遵循。

【建议1.5.2】单个函数长度不超过50行。

函数两个要求:单一职责、要短小

【规则1.5.3】单个函数圈复杂度最好不要超过10,禁止超过15。

说明:圈复杂度越高,代码越复杂,就越难以测试和维护,同时也说明函数职责不单一。

【规则1.5.4】单行语句不能过长,如不能拆分需要分行写。一行最多120个字符。

换行时有如下建议:

换行时要增加一级缩进,使代码可读性更好;

低优先级操作符处划分新行;换行时操作符应保留在行尾;

换行时建议一个完整的语句放在一行,不要根据字符数断行

示例:

     if ((tempFlag == TestFlag) &&
        (((counterVar - constTestBegin) % constTestModules) >= constTestThreshold)) { 
            // process code 
        }

【建议1.5.5】函数中缩进嵌套必须小于等于3层。

举例,禁止出现以下这种锯齿形的函数:

     func testUpdateOpts PushUpdateOptions) (err error) {
            isNewRef := opts.OldCommitID == git.EMPTY_SHA
            isDelRef := opts.NewCommitID == git.EMPTY_SHA
            if isNewRef && isDelRef {
                if isDelRef {
                    repo, err := GetRepositoryByName(owner.ID, opts.RepoName)
                    if err != nil {
                        if strings.HasPrefix(opts.RefFullName, git.TAG_PREFIX) {
                            if err := CommitRepoAction(CommitRepoActionOptions{
                                PusherName:  opts.PusherName,
                                RepoOwnerID: owner.ID,
                                RepoName:    repo.Name,
                                RefFullName: opts.RefFullName,
                                OldCommitID: opts.OldCommitID,
                                NewCommitID: opts.NewCommitID,
                                Commits:     &PushCommits{},
                            }); err != nil {
                                return fmt.Errorf("CommitRepoAction (tag): %v", err)
                            }
                            return nil
                        }
                    }
                    else {
                        owner, err := GetUserByName(opts.RepoUserName)
                        if err != nil {
                            return fmt.Errorf("GetUserByName: %v", err)
                        }
        
                        return nil
                    }
                }
            }
        
            // other code
        }

提示:如果发现锯齿状函数,应通过尽早通过return等方法重构。

【原则1.5.6】保持函数内部实现的组织粒度是相近的。

举例,不应该出现如下函数:

     func main() {
            initLog()
        
            //这一段代码的组织粒度,明显与其他的不均衡
            orm.DefaultTimeLoc = time.UTC
            sqlDriver := beego.AppConfig.String("sqldriver")
            dataSource := beego.AppConfig.String("datasource")
            modelregister.InitDataBase(sqlDriver, dataSource)
        
            Run()
        }

应该改为:

     func main() {
            initLog()
        
            initORM()  //修改后,函数的组织粒度保持一致 
        
            Run()
        }

参考链接

https://studygolang.com/articles/2059

https://studygolang.com/articles/12033

https://blog.csdn.net/shuanger_/article/details/48241767

https://blog.csdn.net/tanzhe2017/article/list