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