前言

这篇文章将总结在编写 go 代码中常遇到的一些代码规范问题,本文只设计那些众多语言都会遇到的规范,不深入 go 语言的具体编写规范,如 go 的错误处理,并发处理等。

标识符命名

标识符的命名没有唯一的准则,在不同的语言中有着不同的规范,不同的团队也有着不同的规范。只要符合两个规则即可:模式化,易读。 最好的规范就是源码,标准库的规范是非常值得我们学习,可以查看我以前写的这篇文章 go 标识符命名指南

注释

良好的注释有利于理清自己的思路,同时也便于合作的伙伴阅读。同样可以看一下这篇文章 go 注释讲解

代码风格

在其他语言中有一些让我们难以选择的问题,如大括号的位置,空格的使用,缩进等等,在 go 中已经有一些硬性规定和现成的工具使用。 go fmt 是 go 官方提供的一个工具,使用这个工具会自动格式化代码,我们规定代码都需要使用这个工具来格式化代码。 如

1.package main
2.
3.import "fmt"
4.
5.func main() {
6.        fmt.Println("hello world")
7.    a := 1+   2
8.}

使用 go fmt main.go

1.package main
2.
3.import "fmt"
4.
5.func main() {
6.    fmt.Println("hello world")
7.    a := 1 + 2
8.}

在 Goland 中,可以使用 Ctrl+Alt+L 快捷键格式化代码,可以养成在编写代码时经常格式化代码的习惯。

行长

代码是怎样的风格,编译器并不知道,对代码风格进行规定是为了便于人阅读,而硬性规定则是保证团队不用在这个上面再花费时间。 对于行长来说,这就是一个比较客观的规定,在以前,python 和 linux 的源码都规定不能超过 80 字符,不过现在随着显示器的发展,这个要求没有那么死了,具体的长度可以看团队中大部分人使用多少尺寸的显示器,最好行长能够在显示器上直接显示,而无需移动屏幕。 对于规定来说,靠人来判断是低效率的行为,可以使用工具来统一,在 Goland 中,可以在 settings->editor->code style 中修改代码行长。

函数长度

类似于行长,一个函数的长度也是我们需要考虑的。在实际编码中,并不需要把函数长度这种东西硬性规定,但是我们应该有这个意识,代码长度最好在一个屏幕前就能看完,而不需要移动。 同时,我们也应该意识到,一个函数就应该只做函数名所规定的功能,如果函数过于冗长,或者你发现函数中开始加入了其他功能,你就应该考虑拆分函数了。

导包

分组 可以把导入的包分为两组,把标准库和其他库分开,标准库放在前面 这是 gin 项目 gin 文件,可以参考一下

1.import (
2.    "fmt"
3.    "html/template"
4.    "net"
5.    "net/http"
6.    "os"
7.    "path"
8.    "sync"
9.
10.    "github.com/gin-gonic/gin/internal/bytesconv"
11.    "github.com/gin-gonic/gin/render"
12.)

或者分为三类,从上到下分别为标准库,此项目,其他库

1.import (
2.    "encoding/json"
3.    "strings"
4.
5.    "myproject/models"
6.    "myproject/controller"
7.    "myproject/utils"
8.
9.    "github.com/astaxie/beego"
10.    "github.com/go-sql-driver/mysql"
11.)  

在 Goland 中,在 settings -> editor -> code style -> Go -> Imports 中可以修改默认导包情况。

路径 导包不要使用相对路径,相对路径只是你的电脑的包路径,不具有迁移性,全使用绝对路径

别名 如果包的名字和导入路径的最后一个元素不匹配,则需要使用别名

1.import (
2.  "net/http"
3.
4.  client "example.com/client-go"
5.  trace "example.com/trace/v2"
6.)

当包名冲突时,才使用别名,没有冲突则别使用别名,同时给包名起名时应避免和标准库重名

1.import (
2.  "fmt"
3.  "os"
4.  "runtime/trace"
5.
6.  nettrace "golang.net/x/trace"
7.)

相似声明

对变量,常量,类型的声明,应该把相似声明的声明放在一组,这样逻辑更加清晰,就像 import 包分开一样。

1.const (
2.  a = 1
3.  b = 2
4.)
5.
6.var (
7.  a = 1
8.  b = 2
9.)
10.
11.type (
12.  Area float64
13.  Volume float64
14.)

函数分组和顺序

在 go 中经常是这样生成函数的:

1.先生成一个类型,如 struct
2.然后生成一个类型的 constructor,即 New 函数
3.然后是这个类型的方法

在 go 中,涉及这种函数的使用时,应该按这个顺序来放代码

1.type something struct{ ... }
2.
3.func newSomething() *something {
4.    return &something{}
5.}
6.
7.func (s *something) Cost() {
8.  return calcCost(s.weights)
9.}
10.
11.func (s *something) Stop() {...}
12.
13.func calcCost(n []int) int {...}

代码顺序

上面讲了常见的函数顺序,现在来讲一下 go 文件的常见排序 先不说注释,一般是这样的

1.包名
2.导包  // 注意分组
3.需要在包中使用的变量,常量,类型,接口等  // 注意分组
4.然后设计类型方法的,按上面那样排序列出函数
5.一些普通的,但是需要放在这个文件里的函数

如 gin 项目的 gin 文件

包名

1.package gin

类型

1.const defaultMultipartMemory = 32 << 20 // 32 MB
2.
3.var (
4.    default404Body   = []byte("404 page not found")
5.    default405Body   = []byte("405 method not allowed")
6.    defaultAppEngine bool
7.)

类型和方法

1.// HandlersChain defines a HandlerFunc array.
2.type HandlersChain []HandlerFunc
3.
4.// Last returns the last handler in the chain. ie. the last handler is the main one.
5.func (c HandlersChain) Last() HandlerFunc {
6.    if length := len(c); length > 0 {
7.        return c[length-1]
8.    }
9.    return nil
10.}

普通的函数

1.func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool {
2.    req := c.Request
3.    rPath := req.URL.Path
4.
5.    if fixedPath, ok := root.findCaseInsensitivePath(cleanPath(rPath), trailingSlash); ok {
6.        req.URL.Path = bytesconv.BytesToString(fixedPath)
7.        redirectRequest(c)
8.        return true
9.    }
10.    return false
11.}
12.
13.func redirectRequest(c *Context) {
14.    req := c.Request
15.    rPath := req.URL.Path
16.    rURL := req.URL.String()
17.
18.    code := http.StatusMovedPermanently // Permanent redirect, request with GET method
19.    if req.Method != http.MethodGet {
20.        code = http.StatusTemporaryRedirect
21.    }
22.    debugPrint("redirecting request %d: %s --> %s", code, rPath, rURL)
23.    http.Redirect(c.Writer, req, rURL, code)
24.    c.writermem.WriteHeaderNow()
25.}

测试

单元测试 我们有时会遇到一个笑话,测试人员对代码进行了多次测试,而用户出乎意料的操作却让程序崩溃了。作为开发人员,对代码进行测试也是需要掌握的基本技能。单元测试即对每个函数可能遇到的各种情况都进行测试,那么无论用户进行怎样的输入,程序都能够处理,这也就是我们常说的程序健壮性好。 在 go 中,官方提供了 go test 工具用来单元测试,一般我们都是使用这个工具来进行单元测试。 单元测试有几点要求:

1.测试文件必须以 _test.go 结尾
2.测试函数必须以 Test 开头
3.测试函数参数必须是 t *testing.T

add.go 文件中

1.func Add(a int, b int) int {
2.    return a + b
3.}

测试文件 add_test.go

1.func TestAdd(t *testing.T) {
2.    fmt.Println(Add(1, 2))
3.}

使用 go test 测试

1.$ go test add.go add_test.go 
2.ok      command-line-arguments  0.001s

可以看到测试通过了。

性能测试 单元测试是为了测试函数能否通过,而性能测试则是测试代码的性能。 比如我们有时候需要把 int 转换为 string,需要知道几种转换方法之间的效率,或者想知道遍历 map 和遍历 array 的效率区别,就可以使用性能测试。

同样的,我们可以利用 go test 工具进行性能测试 性能测试也有几点要求

1.测试文件以 _test.go 结尾
2.测试函数以 Benchmark 开头
3.测试函数以 b *testing.B 为参数

测试文件 itoa_test.go

1.func BenchmarkSprintf(b *testing.B){
2.    num:=10
3.    b.ResetTimer()
4.    for i:=0;i<b.N;i++{
5.        fmt.Sprintf("%d",num)
6.    }
7.}

使用命令

1.$ go test itoa_test.go -bench=. -run=none
2.goos: linux
3.goarch: amd64
4.BenchmarkSprintf-6      15505934                75.6 ns/op
5.PASS
6.ok      command-line-arguments  1.254s

就能看到测试成功了,由于我们这里只是为了说明 go 中测试文件的使用和存在,所以就不展开讲了,需要具体学习可以参考其他书籍。

项目目录

编写一个项目时,怎么管理项目的目录也是非常重要的一点。良好的项目目录表明这个项目架构清晰,在 debug 时,项目结构清晰,很容易就能找到 bug 的所在,同时在阅读代码时,良好的项目目录也能便于我们阅读。 一般我们使用 go 语言都是编写 web 项目,所以只需要参考那些 web 项目是怎么组织项目的即可。一般的 web 项目(后端)使用分层架构,如 MVC,MVP,MVVM 架构等 如果是个人开发小型项目的话,没有在编写代码前规划好项目结构,就只能不断的重构才能有一个好的项目结构。所以如果项目结构有问题,也没有什么,多参考那些优秀代码的项目目录,阅读那些总结好的项目结构,如 project-layout, 再不断的重构。 当然,最好是在编写代码前就已经设计好项目结构,这样是最好的。

总结

本文总结了一些在编写高级语言中都常会遇到的问题,如标识符命名,注释,项目目录等问题,这篇文章并不是指南,不要求完全按照上面的来做,同时有许多点也没有展开来讲。重要的是希望你能注意到这些问题,而具体的操作可以直接查看标准库源码,优秀库源码,或者参考那些大公司的规范。

参考

高效的 Go 编程 Effective Go

Go 编码规范指南

[代码规范] Go 语言编码规范指导

CodeReviewComments

Uber Go 语言编码规范

Go 语言实战:编写可维护 Go 语言代码建议

编码规范

go test 单元测试

Go 项目组织实践