本文主要介绍实际项目开发中的实用规则

在开发中遵守规范也是保持良好团队合作的必要操作,一起来学习吧!

一. 介绍

每种语言都会有基本的语言规范,本文将会介绍Go语言实战建议 Practical Go: Real world advice for writing maintainable Go programs (https://dave.cheney.net/practical-go/presentations/qcon-china.html#_introduction)

二. 指导原则

Go语言有以下3点基本指导原则

  1. 简单性: 简单性是Go语言的最高目标,无论我们编写什么程序,我们都应该同意这一点它们很简单。很多情况下我们都害怕遇到一个问题就是 我不懂这段代码,不知如何修复,这会导致软件复杂不可靠。
  2. 可读性: 可读性很重要,因为所有软件不仅仅是Go语言程序都是由人类编写的,供他人阅读。执行软件的计算机则是次要的。代码的读取次数比写入次数多。一段代码在其生命周期内会被读取数百次,甚至数千次。可读性是能够理解程序正在做什么的关键,编写可维护代码的第一步是确保代码可读。
  3. 生产力: Go程序员应该觉得他们可以通过Go语言完成很多工作。快速编译是Go语言的一个关键特性,也是吸引新开发人员的关键工具,Go语言编译只需要几秒钟。Go程序员相对于C++来说不会花费整天的时间来调试不可思议的编译错误。

三. 标识符

标识符是用来表示名称的单词,例如变量名称、函数名称、方法名称、类型名称、包名称等等。 可读性是良好代码的定义质量,因此选择好名称对于Go代码的可读性至关重要。

  1. 标识符命名应该清晰而不是简洁

    • 好的命名应该简洁: 好的名字不一定是最短的名字,但好的名字不会浪费在无关的东西上。
    • 好的命名应该具备描述性: 好的命名会描述变量或常量的应用,而不是它们的内容。好的命名应该描述函数的结果或方法的行为,而不是它们的操作。好的命名应该描述包的目的而非它的内容。
    • 好的命名应该是高可读性: 好的命名应该是具有高可读性,能够从名字中推断出使用方式。
  2. 标识符长度有以下要求

  • 短变量名称在声明和上次使用之间的距离很短时效果很好。短变量p,我们只需要看2行代码就可以知道它的用途。
  var p = 0
  p += count
  • 长变量名称需要证明自己的合理性,名称越长需要提供的价值越高。userCount长变量一眼就可以知道是用于表示有多人用户
  var userCount int
  • 请勿在变量名称中包含类型名称。例如不应该这样定义var usersMap map[string]int,这个时候后缀的Map可以省略直接定义成这样var users map[string]int
  • 常量应该描述它们持有的值,而不是该如何使用。
  • 对于循环和分支使用单字母变量,参数和返回值使用单个词,函数声明使用多个单词,包的声明使用单个词。
  # 循环
  for i, v := range items {}
  # 函数、参数和返回值
  func getUserCount(endTime int64) (int64) {}
  # 包
  package http
  • 包的名称是调用者用来引用名称的一部分,因此要好好利用这一点。
  1. 不要用变量类型命名你的变量

    • 变量的名称应描述其内容,而不是内容的类型。例如var usersMap map[string]*User 应该命名成var users map[string]*User
    • 对于函数来说也是适用的func WriteConfig(w io.Writer, config *Config),命名参数*Configconfig是多余的,因为我们知道它的Config类型所以使用confc都可以,如果有多个*Config参数可以考虑使用originupdate
  2. 使用一致的命名方式

    • 例如代码在处理数据库请确保每次出现参数时,它都具有相同的名称,例如统一使用db *sql.DB, 如果你看到db你知道它就是*sql.DB
    • 对于方法接收器, 在该类型的每个方法上使用相同的接收者名称,例如结构体type User struct所有的方法接收器统一命名为u *User会更好理解。
    • 有些变量是所有语言都默认会使用的,例如i,j通常用于for循环的变量,n通常用于计数器或累加器,v通常用于值的简写,k通常也用于map的键,s通常用于字符串的简写。
  3. 使用一致的声明样式

    • 声明变量但没有初始化时,请使用varvar表示此变量已被声明为指定类型的零值。
    var players int        默认为0
    var things []*Thing    默认为nil
  • 在同时声明和初始化变量的时候,使用:=
    players := 5
    things := make([]*Thing, 0)
  1. 成为团队合作者,所有代码都需要使用gofmt格式化

四. 注释

注释对Go语言程序的可读性非常重要,注释目的是如下3个。

  1. 注释应该解释其作用。
  2. 注释应该解释其如何做的。
  3. 注释应该解释其原因。

注释的方式有以下几种

  1. 文件注释应该使用// Package xxxx 开头并且注释是谁在什么时间点创建或更新
// Package main ...
// Created by chenguolin 2018-12-26
  1. 公共变量、常量、可见函数注释应该使用//
    // AppName application name
    const AppName = "HTTP_SERVER"

    // Users user count
    var Users map[string]int

    // GetUserName get user name by id
    func GetUserName(id int64) string {}

注释应该满足以下几点要求

  1. 关于变量和常量的注释应描述其内容而非其目的

    • 向变量或常量添加注释时,该注释应描述变量内容,而不是变量目的。
    // randomNumber determined from an unbiased die
    const randomNumber = 6
  • 对于没有初始值的变量,注释应描述谁负责初始化此变量。
    // sizeCalculationDisabled indicates whether it is safe
    // to calculate Types' widths and alignments. See dowidth. 
    var sizeCalculationDisabled bool
  1. 公共符号始终要注释
  • 应该始终为包中声明的每个公共符号变量、常量、函数以及方法添加注释。
  • 任何既不明显也不简短的公共功能必须予以注释。
  • 无论长度或复杂程度如何,对库中的任何函数都必须进行注释。
  • 在编写函数之前,请编写描述函数的注释。如果你发现很难写出注释,那么这就表明你将要编写的代码很难理解。
  1. 不要注释不好的代码,将它重写
  • 如果遇到不好的代码,我们应提出问题,以提醒您稍后重构。标准库中的惯例是注意到它的人用 TODO(username) 的样式来注释。
  1. 与其注释一段代码,不如重构它
  • 好的代码是最好的文档,如果需要些很多的注释来描述,不如重构这个代码。

五. 包的设计

一个好的Go语言包应该具有低程度的源码级耦合,这样随着项目的增长,对一个包的更改不会跨代码库级联。

  1. 一个好的包从它的名字开始
  • 好的包名称应该是唯一的,如果你发现有两个包需要用相同名称那么它可能是太通用了或者与其它包名重叠了,这个时候我们应该考虑修改包名
  • 避免使用类似base,commonutil的包名称,由于这些包包含各种不相关的功能,因此很难根据包提供的内容来描述它们。像utilshelper这样的包名称通常出现在较大的项目中,这些项目已经开发了深层次包的结构。
  • 使用复数形式命名基础包,例如strings来处理字符串。
  1. 尽早 return而不是深度嵌套
  • 由于Go语言的控制流不使用exception,因此不需要为 try和 catch块提供顶级结构而深度缩进代码。所以Go建议尽快return而不是深度嵌套。
  1. 让零值更有用
  • 假设变量没有初始化,每个变量声明都会自动初始化为与零内存的内容相匹配的值。值的类型决定了其零值,对于数字类型它为0,对于指针类型为nil, slices、 map和 channel同样是nil
  • 始终设置变量为已知默认值的属性对于程序的安全性和正确性非常重要,并且可以使 Go 语言程序更简单、更紧凑。

六. 项目结构

通常一个项目是一个git仓库,每个项目都应该有一个明确的目的。您应该避免在一个包实现多个目的,这将有助于避免成为公共库。

httpserver为例,看通用的HTTP service项目的结构如下。

httpserver
├──cmd
  ├──api
  ├──cron
  ├──processor
├──common
  ├──base62
  ...
├──config
  ...
├──context
  ...
├──docs
  ...
├──pkg
  ...
├──scripts
  ...
├──vendor
  ...
├──.gitlab-ci.yml
├──Gopkg.lock
├──Gopkg.toml
├──README.md
├──VERSION
  1. 考虑更少,更大的包

    • 如果只有一个文件,文件名应该和文件夹名称一样。
    • 如果有多个文件,应该按照不同的职责拆分为不同的文件,不同的文件应该负责包的不同区域。 Go 编译器并行编译每个包,在一个包中编译器并行编译每个函数,更改包中代码的布局不会影响编译时间。
  2. 优先内部测试再到外部测试

    • 假设你的包名为http2,您可以编写http2_test.go文件并使用包http2声明。这样做会编译http2_test.go中的代码,就像它是http2包的一部分一样,这就是内部测试。如果http2_test.go文件并使用包http2_test声明,则成为外部测试
    • 编写单元测试时使用内部测试,这样你就可以直接测试每个函数或方法,避免外部测试干扰。
    • 如果有example测试代码放在外部测试文件中。
  3. 确保main包内容尽可能的少

    • main函数和main包的内容应尽可能少, 这是因为程序中只能有一个main函数。
    • 应该将所有业务逻辑从main函数中移出,最好是从main包中移出。
    • main应该做解析flags,开启数据库连接、开启日志等,然后将执行交给更高一级的对象。

七. 函数设计

函数设计非常的重要,如果函数没有设计好那会导致不可兼容的情况,导致程序维护性变差。

  1. 设计难以被误用的 函数

    • 警惕采用几个相同类型参数的函数,可能的解决方案是引入一个helper类型,它会负责如何正确地调用。
    // 错误举例
    func CopyFile(src, dest string) error {}

    CopyFile("file1", "file2")
    CopyFile("file2", "file1")
    对于上面的函数调用,如果参数填错会导致文件被意外copy

    // 准确举例
    type Source string
    func (s *Source) Copy(dest string) error{}

    file1.Copy(file2)
    file2.Copy(file1)
    上述这种使用方式则比较不容易出错
  1. 不鼓励使用nil作为参数

    • 不要在同一个函数签名中混合使用可为nil和不能为nil的参数
  2. 首选可变参数函数而非[]T参数

    • 例如func ShutdownVMs(ids []string) error 但是很多时候这些类型的函数只用一个参数调用,为了满足函数参数的要求,它必须打包到一个切片内。这会增加额外的测试负载,因为你应该涵盖这些情况在测试中。
  3. 让函数定义它们所需的行为

  4. 空行来分解函数,让代码看起来更有层次感

八. 错误处理

  1. 通过消除错误来消除错误处理

    • 当遇到难以忍受的错误处理时,请尝试将某些操作提取到辅助程序类型中。
  2. 错误只处理一次

    • 错误要么在当前位置处理,要么就返回给上层调用,不可同时处理否则就会导致重复的日志输出。
    • 为错误添加相关内容

九. 并发

Go 语言以channel以及select和go语句来支持并发。如果Go语言程序的main函数返回,无论程序在一段时间内启动的其他goroutine在做什么, Go语言程序会无条件地退出。

  1. 如果你的goroutine在得到另一个结果之前无法取得进展,那么让自己完成此工作而不是委托给其他goroutine会更简单。

  2. 将并发性留给调用者

    • 如果你的函数启动了goroutine,你必须为调用者提供一种明确停止goroutine的方法。把异步执行函数的决定留给该函数的调用者通常会更容易些。
  3. 永远不要启动一个停止不了的goroutine

    • 应用程序应该将监视其状态和检测是否重启的工作留给另外的程序来做。不要让你的应用程序负责重新启动自己,最好从应用程序外部处理该过程。
  4. 只在main.main或 init函数中的使用panic

  5. 一旦我们开启的所有goroutine都停止了, main.main就会返回并且进程会干净地停止

  6. 每个Goroutine都需要有Recover机制,除非你允许程序Crash

十. 工具

使用任何语言开发程序都需要有一个统一的规范,否则很容易造成风格不统一导致代码混乱不可维护。使用Golang开发有几个高效率工具

  1. gofmt Golang的开发团队制定了统一的官方代码风格,并且推出了gofmt工具(gofmt或go fmt)来帮助开发者格式化他们的代码到统一的风格。gofmt是一个cli程序,会优先读取标准输入,如果传入了文件路径的话,会格式化这个文件,如果传入一个目录,会格式化目录中所有.go文件,如果不传参数,会格式化当前目录下的所有.go文件。go fmt命令,go fmt命令是gofmt的简单封装。 常用的命令为 go list ./... | xargs -n 1 gofmt -l -w
  2. go vet go vet就是golang中提供的语法检查工具,可以让我检查出package或者源码文件中一些隐含的错误,规范我们的项目代码。 常用的命令为 go vet ./...
  3. golint golint是用来检测Golang代码规范的,不同于gofmt和go vet。golint只是用于代码规范检查 常用的命令为 go list ./... | xargs -n 1 golint