其他分享
首页 > 其他分享> > go 常用工具链

go 常用工具链

作者:互联网

go 常用工具链

golint 代码质量检测

可以简单指定文件名或者目录检查代码目录规范

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

go test 测试

(引用极客兔兔文章)

1 如何写好单元测试

单元测试(Unit Tests, UT) 是一个优秀项目不可或缺的一部分,特别是在一些频繁变动和多人合作开发的项目中尤为重要。你或多或少都会有因为自己的提交,导致应用挂掉或服务宕机的经历。如果这个时候你的修改导致测试用例失败,你再重新审视自己的修改,发现之前的修改还有一些特殊场景没有包含,恭喜你减少了一次上库失误。也会有这样的情况,项目很大,启动环境很复杂,你优化了一个函数的性能,或是添加了某个新的特性,如果部署在正式环境上之后再进行测试,成本太高。对于这种场景,几个小小的测试用例或许就能够覆盖大部分的测试场景。而且在开发过程中,效率最高的莫过于所见即所得了,单元测试也能够帮助你做到这一点,试想一下,假如你一口气写完一千行代码,debug 的过程也不会轻松,如果在这个过程中,对于一些逻辑较为复杂的函数,同时添加一些测试用例,即时确保正确性,最后集成的时候,会是另外一番体验。

如何写好单元测试呢?

首先,学会写测试用例。比如如何测试单个函数/方法;比如如何做基准测试;比如如何写出简洁精炼的测试代码;再比如遇到数据库访问等的方法调用时,如何 mock

然后,写可测试的代码。高内聚,低耦合是软件工程的原则,同样,对测试而言,函数/方法写法不同,测试难度也是不一样的。职责单一,参数类型简单,与其他函数耦合度低的函数往往更容易测试。我们经常会说,“这种代码没法测试”,这种时候,就得思考函数的写法可不可以改得更好一些。为了代码可测试而重构是值得的。

接下来将介绍如何使用 Go 语言的标准库 testing 进行单元测试。

2 一个简单例子

Go 语言推荐测试文件和源代码文件放在一块,测试文件以 _test.go 结尾。比如,当前 package 有 calc.go 一个文件,我们想测试 calc.go 中的 AddMul 函数,那么应该新建 calc_test.go 作为测试文件。

example/
   |--calc.go
   |--calc_test.go

假如 calc.go 的代码如下:

package main

func Add(a int, b int) int {
    return a + b
}

func Mul(a int, b int) int {
    return a * b
}

那么 calc_test.go 中的测试用例可以这么写:

package main

import "testing"

func TestAdd(t *testing.T) {
	if ans := Add(1, 2); ans != 3 {
		t.Errorf("1 + 2 expected be 3, but %d got", ans)
	}

	if ans := Add(-10, -20); ans != -30 {
		t.Errorf("-10 + -20 expected be -30, but %d got", ans)
	}
}

运行 go test,该 package 下所有的测试用例都会被执行。

$ go test
ok      example 0.009s

go test -v-v 参数会显示每个用例的测试结果,另外 -cover 参数可以查看覆盖率。

$ go test -v
=== RUN   TestAdd
--- PASS: TestAdd (0.00s)
=== RUN   TestMul
--- PASS: TestMul (0.00s)
PASS
ok      example 0.007s

如果只想运行其中的一个用例,例如 TestAdd,可以用 -run 参数指定,该参数支持通配符 *,和部分正则表达式,例如 ^$

$ go test -run TestAdd -v
=== RUN   TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok      example 0.007s

3 子测试(Subtests)

子测试是 Go 语言内置支持的,可以在某个测试用例中,根据测试场景使用 t.Run创建不同的子测试用例:

// calc_test.go

func TestMul(t *testing.T) {
	t.Run("pos", func(t *testing.T) {
		if Mul(2, 3) != 6 {
			t.Fatal("fail")
		}

	})
	t.Run("neg", func(t *testing.T) {
		if Mul(2, -3) != -6 {
			t.Fatal("fail")
		}
	})
}

运行某个测试用例的子测试:

$ go test -run TestMul/pos -v
=== RUN   TestMul
=== RUN   TestMul/pos
--- PASS: TestMul (0.00s)
    --- PASS: TestMul/pos (0.00s)
PASS
ok      example 0.008s

对于多个子测试的场景,更推荐如下的写法(table-driven tests):

//  calc_test.go
func TestMul(t *testing.T) {
	cases := []struct {
		Name           string
		A, B, Expected int
	}{
		{"pos", 2, 3, 6},
		{"neg", 2, -3, -6},
		{"zero", 2, 0, 0},
	}

	for _, c := range cases {
		t.Run(c.Name, func(t *testing.T) {
			if ans := Mul(c.A, c.B); ans != c.Expected {
				t.Fatalf("%d * %d expected %d, but %d got",
					c.A, c.B, c.Expected, ans)
			}
		})
	}
}

所有用例的数据组织在切片 cases 中,看起来就像一张表,借助循环创建子测试。这样写的好处有:

如果数据量较大,或是一些二进制数据,推荐使用相对路径从文件中读取。

4 帮助函数(helpers)

对一些重复的逻辑,抽取出来作为公共的帮助函数(helpers),可以增加测试代码的可读性和可维护性。 借助帮助函数,可以让测试用例的主逻辑看起来更清晰。

例如,我们可以将创建子测试的逻辑抽取出来:

// calc_test.go
package main

import "testing"

type calcCase struct{ A, B, Expected int }

func createMulTestCase(t *testing.T, c *calcCase) {
	// t.Helper()
	if ans := Mul(c.A, c.B); ans != c.Expected {
		t.Fatalf("%d * %d expected %d, but %d got",
			c.A, c.B, c.Expected, ans)
	}

}

func TestMul(t *testing.T) {
	createMulTestCase(t, &calcCase{2, 3, 6})
	createMulTestCase(t, &calcCase{2, -3, -6})
	createMulTestCase(t, &calcCase{2, 0, 1}) // wrong case
}

在这里,我们故意创建了一个错误的测试用例,运行 go test,用例失败,会报告错误发生的文件和行号信息:

$ go test
--- FAIL: TestMul (0.00s)
    calc_test.go:11: 2 * 0 expected 1, but 0 got
FAIL
exit status 1
FAIL    example 0.007s

可以看到,错误发生在第11行,也就是帮助函数 createMulTestCase 内部。18, 19, 20行都调用了该方法,我们第一时间并不能够确定是哪一行发生了错误。有些帮助函数还可能在不同的函数中被调用,报错信息都在同一处,不方便问题定位。因此,Go 语言在 1.9 版本中引入了 t.Helper(),用于标注该函数是帮助函数,报错时将输出帮助函数调用者的信息,而不是帮助函数的内部信息。

修改 createMulTestCase,调用 t.Helper()

func createMulTestCase(c *calcCase, t *testing.T) {
    t.Helper()
	t.Run(c.Name, func(t *testing.T) {
		if ans := Mul(c.A, c.B); ans != c.Expected {
			t.Fatalf("%d * %d expected %d, but %d got",
				c.A, c.B, c.Expected, ans)
		}
	})
}

运行 go test,报错信息如下,可以非常清晰地知道,错误发生在第 20 行。

$ go test
--- FAIL: TestMul (0.00s)
    calc_test.go:20: 2 * 0 expected 1, but 0 got
FAIL
exit status 1
FAIL    example 0.006s

关于 helper 函数的 2 个建议:

5 setup 和 teardown

如果在同一个测试文件中,每一个测试用例运行前后的逻辑是相同的,一般会写在 setup 和 teardown 函数中。例如执行前需要实例化待测试的对象,如果这个对象比较复杂,很适合将这一部分逻辑提取出来;执行后,可能会做一些资源回收类的工作,例如关闭网络连接,释放文件等。标准库 testing 提供了这样的机制:

func setup() {
	fmt.Println("Before all tests")
}

func teardown() {
	fmt.Println("After all tests")
}

func Test1(t *testing.T) {
	fmt.Println("I'm test1")
}

func Test2(t *testing.T) {
	fmt.Println("I'm test2")
}

func TestMain(m *testing.M) {
	setup()
	code := m.Run()
	teardown()
	os.Exit(code)
}

执行 go test,将会输出

$ go test
Before all tests
I'm test1
I'm test2
PASS
After all tests
ok      example 0.006s

6 网络测试(Network)

6.1 TCP/HTTP

假设需要测试某个 API 接口的 handler 能够正常工作,例如 helloHandler

func helloHandler(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("hello world"))
}

那我们可以创建真实的网络连接进行测试:

// test code
import (
	"io/ioutil"
	"net"
	"net/http"
	"testing"
)

func handleError(t *testing.T, err error) {
	t.Helper()
	if err != nil {
		t.Fatal("failed", err)
	}
}

func TestConn(t *testing.T) {
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	handleError(t, err)
	defer ln.Close()

	http.HandleFunc("/hello", helloHandler)
	go http.Serve(ln, nil)

	resp, err := http.Get("http://" + ln.Addr().String() + "/hello")
	handleError(t, err)

	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	handleError(t, err)

	if string(body) != "hello world" {
		t.Fatal("expected hello world, but got", string(body))
	}
}

6.2 httptest

针对 http 开发的场景,使用标准库 net/http/httptest 进行测试更为高效。

上述的测试用例改写如下:

// test code
import (
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"testing"
)

func TestConn(t *testing.T) {
	req := httptest.NewRequest("GET", "http://example.com/foo", nil)
	w := httptest.NewRecorder()
	helloHandler(w, req)
	bytes, _ := ioutil.ReadAll(w.Result().Body)

	if string(bytes) != "hello world" {
		t.Fatal("expected hello world, but got", string(bytes))
	}
}

使用 httptest 模拟请求对象(req)和响应对象(w),达到了相同的目的。

7 Benchmark 基准测试

基准测试用例的定义如下:

func BenchmarkName(b *testing.B){
    // ...
}

例如:

func BenchmarkHello(b *testing.B) {
    for i := 0; i < b.N; i++ {
        fmt.Sprintf("hello")
    }
}
$ go test -benchmem -bench .
...
BenchmarkHello-16   15991854   71.6 ns/op   5 B/op   1 allocs/op
...

基准测试报告每一列值对应的含义如下:

type BenchmarkResult struct {
    N         int           // 迭代次数
    T         time.Duration // 基准测试花费的时间
    Bytes     int64         // 一次迭代处理的字节数
    MemAllocs uint64        // 总的分配内存的次数
    MemBytes  uint64        // 总的分配内存的字节数
}

如果在运行前基准测试需要一些耗时的配置,则可以使用 b.ResetTimer() 先重置定时器,例如:

func BenchmarkHello(b *testing.B) {
    ... // 耗时操作
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        fmt.Sprintf("hello")
    }
}

使用 RunParallel 测试并发性能

func BenchmarkParallel(b *testing.B) {
	templ := template.Must(template.New("test").Parse("Hello, {{.}}!"))
	b.RunParallel(func(pb *testing.PB) {
		var buf bytes.Buffer
		for pb.Next() {
			// 所有 goroutine 一起,循环一共执行 b.N 次
			buf.Reset()
			templ.Execute(&buf, "World")
		}
	})
}
$ go test -benchmem -bench .
...
BenchmarkParallel-16   3325430     375 ns/op   272 B/op   8 allocs/op
...

go mod、go get 依赖管理

o module是Go1.11版本之后官方推出的版本管理工具,并且从Go1.13版本开始,go module将是Go语言默认的依赖管理工具。

$ go version 查看版本 如果是1.13以后就不用设置环境变量

GO111MODULE

要启用go module支持首先要设置环境变量GO111MODULE,通过它可以开启或关闭模块支持,它有三个可选值:offonauto,默认值是auto

  Mac: $ export GO111MODULE=on

  Windows: $ SET *GO111MODULE=on*

  1. GO111MODULE=off禁用模块支持,编译时会从GOPATHvendor文件夹中查找包。
  2. GO111MODULE=on启用模块支持,编译时会忽略GOPATHvendor文件夹,只根据 go.mod下载依赖。
  3. GO111MODULE=auto,当项目在$GOPATH/src外且项目根目录有go.mod文件时,开启模块支持。

简单来说,设置GO111MODULE=on之后就可以使用go module了,以后就没有必要在GOPATH中创建项目了,并且还能够很好的管理项目依赖的第三方包信息。

使用 go module 管理依赖后会在项目根目录下生成两个文件go.modgo.sum

GOPROXY

Go1.11之后设置GOPROXY命令为:

$ export GOPROXY=https://goproxy.cn 

Go1.13之后GOPROXY默认值为https://proxy.golang.org,在国内是无法访问的,所以十分建议大家设置GOPROXY,这里我推荐使用goproxy.cn

$ go env -w GOPROXY=https://goproxy.cn,direct

go mod命令

常用的go mod命令如下:

go mod download    下载依赖的module到本地cache(默认为$GOPATH/pkg/mod目录)
go mod edit        编辑go.mod文件
go mod graph       打印模块依赖图
go mod init        初始化当前文件夹, 创建go.mod文件
go mod tidy        增加缺少的module,删除无用的module
go mod vendor      将依赖复制到vendor下
go mod verify      校验依赖
go mod why         解释为什么需要依赖

go.mod

go.mod文件记录了项目所有的依赖信息,其结构大致如下:

module github.com/Q1mi/studygo/blogger

go 1.12

require (
	github.com/DeanThompson/ginpprof v0.0.0-20190408063150-3be636683586
	github.com/gin-gonic/gin v1.4.0
	github.com/go-sql-driver/mysql v1.4.1
	github.com/jmoiron/sqlx v1.2.0
	github.com/satori/go.uuid v1.2.0
	google.golang.org/appengine v1.6.1 // indirect
)
replace (github.com/xxxx/foo => ../foo ) // 本地导入

其中,

依赖的版本

go mod支持语义化版本号,比如go get foo@v1.2.3,也可以跟git的分支或tag,比如go get foo@master,当然也可以跟git提交哈希,比如go get foo@e3702bed2。关于依赖的版本支持以下几种格式:

gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7
gopkg.in/vmihailenco/msgpack.v2 v2.9.1
gopkg.in/yaml.v2 <=v2.2.1
github.com/tatsushid/go-fastping v0.0.0-20160109021039-d7bb493dee3e
latest

replace

在国内访问golang.org/x的各个包都需要FQ,你可以在go.mod中使用replace替换成github上对应的库。

replace (
	golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac => github.com/golang/crypto v0.0.0-20180820150726-614d502a4dac
	golang.org/x/net v0.0.0-20180821023952-922f4815f713 => github.com/golang/net v0.0.0-20180826012351-8a410e7b638d
	golang.org/x/text v0.3.0 => github.com/golang/text v0.3.0
)

go get

在项目中执行go get命令可以下载依赖包,并且还可以指定下载的版本。

  1. 运行go get -u将会升级到最新的次要版本或者修订版本(x.y.z, z是修订版本号, y是次要版本号)
  2. 运行go get -u=patch将会升级到最新的修订版本
  3. 运行go get package@version将会升级到指定的版本号version

如果下载所有依赖可以使用go mod download命令。

整理依赖

我们在代码中删除依赖代码后,相关的依赖库并不会在go.mod文件中自动移除。这种情况下我们可以使用go mod tidy命令更新go.mod中的依赖关系。

go mod edit

格式化

因为我们可以手动修改go.mod文件,所以有些时候需要格式化该文件。Go提供了一下命令:

$ go mod edit -fmt

添加依赖项

$ go mod edit -require=golang.org/x/text

移除依赖项

如果只是想修改go.mod文件中的内容,那么可以运行go mod edit -droprequire=package path,比如要在go.mod中移除golang.org/x/text包,可以使用如下命令:

$ go mod edit -droprequire=golang.org/x/text

关于go mod edit的更多用法可以通过go help mod edit查看。

go doc 文档

转自思否

我们从 Go 自带的 godoc 工具讲起吧。前面我们说到的 godoc.org,是 Go 最为官方的文档网站。其中我们可以查阅 Go 原生 package 的文档说明。而 godoc 命令的作用,则是可以让我们在本地建立一个属于自己的 godoc 网站服务(官方的 godoc 其实也基本上是用同一个工具建立起来的)。

自建的 godoc 有两个作用,一是解决某局域网内无法访问 godoc.org 的尴尬,另一个则是可以本地调试自己的文档。

我们可以用下面的命令在本地启动自己的 godoc 服务:

godoc -http=127.0.0.1:6060 -play

或者简写为:

godoc -http=:6060 -play

在浏览器输入 http://127.0.0.1:6060 之后,就可以看到熟悉的 Go 文档页面了:

img

原理上,godoc 读取的包路径来自于 $GOROOT。因此,如果你要让本地的 godoc 认识并解析你自己的开发包,就应该在 $GOROOT 目录下按照路径结构放好自己的工程代码——软链接也是支持的。比如笔者的 jsonvalue 包,我放在了这个路径下:~/project/github.com/Andrew-M-C/go.jsonvalue,于是我就在 $GOROOT 下建了软链接:

$ go env | grep GOROOT | sed 's/GOROOT=//g' | xargs cd
$ ln -s ~/project/github.com ./

然后在浏览器中输入 http://127.0.0.1:6060/pkg/github.com/Andrew-M-C/go.jsonvalue/,就可以看到和 godoc.org 一样的页面了。

godoc 一览

Go 秉承 “注释即文档” 的理念,符合 godoc 的文档均从 Go 代码中提取并生成。我们还是从 jsonvalue 的 godoc 来看,一个一个说明。在 godoc 中,文档包含三大部分:

组成 作用
Overview 总览 包含包的 import 语句和概要说明
Index 目录 包含包中可见性为 public 的常量、类型、方法、函数的总目录及说明
Examples 示例 包含文档中所有示例的快速跳转
Files 文件 列出了包中所有代码文件的超链接

其中第四部分无关紧要。下面我们按顺序说明前三部分

godoc 的 Overview

Package jsonvalue 的 Overview 部分包含了三部分内容:

其中 import 部分是 godoc 自动按照 URL 生成的,这个不用管。至于文字部分和代码部分,godoc 都是从源码中提取出来的。提取的原则是:

比如 jsonvalue 的 Overview 说明,统一放在 doc.go 中,这个文件中只有 package jsonvalue 语句以及包说明——这也是不少文章中推荐的做法。

Overview 的文字部分

请读者打开 doc.go,然后对比 godoc,就可以对照着看到文字部分是怎么被 godoc 呈现出来的。

Overview 的代码部分

在注释中,如果在 // 后面的注释文本中,如果以 tab 进行了锁进,那么 godoc 会将这一行视为代码块。比如下面这一段:

img

其中 "As a quick start:" 行的左边分别为:两个斜杠 + 一个空格。这一行,godoc 视为普通文字;而其余部分的左边为:两个斜杠 + 一个空格 + 一个tab,被 godoc 视为代码部分。于是我们在 godoc 网页上,就可以看到这样的显示结果了:

img

godoc 的代码文档

godoc 工具会搜寻代码中所有源码文件(自测文件除外),然后展示到页面上。搜索的依据如下:

不过在源码说明中,更多的采用代码示例来说明逻辑,因此在这一环节中,代码块比较少用。

这里我用 jsonvalueAt() 函数为例。在代码中,对于 Set() 函数我是这么写的(请无视我蹩脚的英文):

// At completes the following operation of Set(). It defines posttion of value in Set() and return the new value set.
//
// The usage of At() is perhaps the most important. This function will recursivly search for child value, and set the new value specified by Set() or SetXxx() series functions. Please unfold and read the following examples, they are important.
func (s *Set) At(firstParam interface{}, otherParams ...interface{}) (*V, error) {
    ......
}

godoc 解析并格式化效果如下:

img

godoc 的代码示例

读者可以注意到,在我的 At() 函数下,除了上文提到的文档正文之外,还有五个代码示例。那么,文档中的代码示例又应该如何写呢?

首先,我们应该新建至少一个文件,专门用来存放示例代码。比如我就把示例代码写在了 example_jsonvalue_test.go 文件中。这个文件的 package 名也不得与当前包名相同,而应该命名为 包名_test 的格式。

示例代码的声明

如何声明一个示例代码,这里我举两个例子。首先是在 At() 函数下名为 “Example (1)” 的示例。在代码中,我把这个函数命名为:

func ExampleSet_At_1() {
    ......
}

这个函数命名有几个部分:

函数名组成部分 说明
Example 这是示例代码的固有开头
Set 表示这是类型 Set 的示例
第一个下划线 _ 分隔符,在这个分隔符后面的,是 Set 类型的成员函数名
At 表示这是函数 At() 的示例,搭配前面的内容,则表示这是类型 Set 的成员函数 At() 的示例
第二个下划线 _ 分隔符,在这个分隔符后面的内容,是示例代码的额外说明
1 这是示例代码的额外说明,也就是前面 “Example (1)” 括号里的部分

另外,示例代码中应该包含标准输出内容,这样便于读者了解执行情况。标准输出内容在函数内的最后,采用 // Output: 单独起一行开头,剩下的每一行标准输出写一行注释。

相对应地,如果你想要给(不属于任何一个类型的)函数写示例的话,则去掉上文中关于 “类型” 的字段;如果你不需要示例的额外说明符,则去掉 “额外说明” 字段。比如说,我给类型 Opt 写的示例就只有一个,在代码中,只有一行:

func ExampleOpt() {
    ........
}

甚至连示例说明都没有。

如果一个元素包含多个例子,那么 godoc 会按照字母序对示例及其相应的说明排序。这也就是为什么我干脆在 At() 函数中,示例标为一二三四五的原因,因为这是我希望读者阅读示例的顺序。

在官网上发布 godoc

好了,当你写好了自己的 godoc 之后,总不是自己看自己自娱自乐吧,总归是要发布出来给大家看的。

其实发布也很简单:当你将包含了 godox 的代码 push 之后(比如发布到 github 上),就可以在浏览器中输入 https://godoc/org/${package路径名}。比如 jsonvalue 的 Github 路径(也等同于 import 路径)为 github.com/Andrew-M-C/go.jsonvalue,因此输入 godoc.org/github.com/Andrew-M-C/go.jsonvalue

如果这是该页面第一次进入,那么 godoc.org 会首先获取、解析和更新代码仓库中的文档内容,并且格式化之后展示。在页面的底部,会列出该 godoc 的更新时间。

img

如果你发现官网上的 godoc 内容已经落后了,那么可以点 “Refresh now” 链接刷新它。

接下来更重要的是,把这份官网 godoc 的链接,附到你自己的 README 中。还是点上图的 “Tools” 链接,就可以在新页面中,看到相应的 godoc 徽标的链接了。有 htmlmarkdown 格式任君选择。

img


go vet 语法检查

go vet -atomic=false test.go  

更多tag含义如下表,我从网上寻找,大家也可以去golang 官方去找 vet command的文档,里面更精确地描述。

tag 描述
-all 进行全部检查。如果有其他检查标记被设置,则命令程序会将此值变为false。默认值为true。
-asmdecl 对汇编语言的源码文件进行检查。默认值为false。
-assign 检查赋值语句。默认值为false。
-atomic 检查代码中对代码包sync/atomic的使用是否正确。默认值为false。
-composites 是否使用复合结构检查的白名单。仅供测试使用。默认值为true。
-compositeWhiteList 对汇编语言的源码文件进行检查。默认值为false。
-assign 检查赋值语句。默认值为false。
-methods 检查那些拥有标准命名的方法的签名。默认值为false。
-printf 检查代码中对打印函数的使用是否正确。默认值为false。
-printfuncs 需要检查的代码中使用的打印函数的名称的列表,多个函数名称之间用英文半角逗号分隔。默认值为空字符串。
-rangeloops 检查代码中对在range语句块中迭代赋值的变量的使用是否正确。默认值为false。
-structtags 检查结构体类型的字段的标签的格式是否标准。默认值为false。
-unreachable 查找并报告不可到达的代码。默认值为false。

go tool 编译、性能分析

常用的pprof以及trance

标签:testing,test,godoc,func,go,常用工具,mod
来源: https://www.cnblogs.com/binHome/p/14882720.html