第13章使用包实现代码重用
13.1 导入包
Go程序以package语句打头。main包是一种特殊的包,其特殊之处在于不能导入。对main包的唯一要求是,必须声明一个main函数,这个函数不接受任何参数且不返回任何值。简而言之,main包是程序的入口。
在main包中,可使用import声明来导入其他包。导入包后,就可使用其中被导出的(即公有的)标识符。在Go语言中,标识符可以是变量、常量、类型、函数或方法。这让包能够通过接口提供各种功能。
举一个函数导出的例子,strings包导出了函数ToLower,可用于将字符串转换为小写,如下所示:
代码语言:javascript复制package main
import (
"fmt"
"strings"
)
func main() {
var str string = strings.ToLower("THIS IS A DEMO")
fmt.Println(str);
}
导入包并使用其中导出的标识符,是重用标准库和其他第三方代码的基本方式。
13.3 使用第三方包
考虑使用第三方库时,您应自问如下几个问题。
- 我明白了这些代码是做什么的吗?
- 这些代码值得信任吗?
- 这些代码的维护情况如何?
- 我真的需要这个库吗?
不要选择几年都没有更新的包,而应选择开发方积极维护的第三方包。
导入第三方包会增加程序的复杂性。很多时候导入一个包只为了使用其中的一个函数,在这种情况下,可复制这个函数,而不导入整个包。
13.4 安装第三方包
要使用第三方库,必须像使用标准库一样使用import语句导入它。
在下面的示例中,将使用Go小组开发的stringutil包。这是一个简单的第三方包,只有一个函数被导出——Reverse。这个函数将一个字符串作为参数,将该字符串反转并返回结果。
要使用第三方包,必须先使用命令go get安装它。这个命令默认随Go一起安装了,它将指向远程服务器中包的路径作为参数,并在本地安装指定的包。
代码语言:javascript复制go get github.com/golang/example/stringutil
这个包被安装到环境变量GOPATH指定的路径中,因此可在程序中使用它。要查看这个包的源代码,可打开目录src中的文件。包的安装目录如下。
代码语言:javascript复制$GOPATH/src/github.com/golang/example/stringutil/
安装这个包后,就可导入它了,代码下所示。
代码语言:javascript复制package main
import (
"fmt"
"github.com/golang/example/stringutil"
)
func main() {
var str string = "THIS IS A DEMO"
fmt.Println(stringutil.Reverse(str));
}
运行结果
代码语言:javascript复制OMED A SI SIHT
通常,第三方包依赖于其他第三方包。命令go get会下载依赖的第三方包,让您无须手工安装每个包依赖的第三方包。
13.5 管理第三方依赖
只更新特定的包
代码语言:javascript复制go get -u github.com/golang/example/stringutil
更新文件系统中所有的包
代码语言:javascript复制go get -u
13.6 创建包
除使用第三方包外,有时还可能需要创建包。本节将创建一个示例包,并将其发布到Github以便与人分享。这是一个处理温度的包,提供了在不同温度格式之间进行转换的函数。请创建一个名为temperature.go的文件,并在其中添加如下程序。
代码语言:javascript复制package temperature
func CtoF(c float64) float64{
return (c * (9/5)) 32
}
别忘了,导入这个包后,就可使用其中所有以大写字母打头的标识符了。要创建私有标识符(变量、函数等),可让它们以小写字母打头。
如何使用此包呢?
- 建立$GOPATH/src/temperature目录
- 将temperature.go放入上述目录
- 通过import导入temperature,示例代码如下
package main
import (
"temperature"
"fmt"
)
func main() {
var i float64 = temperature.CtoF(4.19)
fmt.Println(i)
}
第14章Go语言命名约定
14.1 Go代码格式设置
在代码格式设置方面,Go语言采取了实用而严格的态度。Go语言指定了格式设置约定,这种约定虽然并非强制性的,但命令gofmt可以实现它。虽然编译器不要求按命令gofmt指定的那样设置代码格式,但几乎整个Go社区都使用gofmt,并希望按这个命令指定的方式设置代码格式。
14.2 使用gofmt
为确保按要求的约定设置Go代码的格式,Go提供了gofmt。这个工具的优点在于,让您甚至都无须了解代码格式设置约定。通过不断地学习如何设置代码格式,您自然而然地就会遵循代码格式设置约定。
假设有文件string.go
代码语言:javascript复制package main
import (
"fmt"
)
func main() {
s := "hello";
fmt.Printf("%q", s[1]);
fmt.Printf("%b", s[1]);
}
代码语言:javascript复制gofmt string.go 输出format之后的结果
gofmt -d string.go 输出format后与原文件的差异
diff -u string.go.orig string.go
--- string.go.orig 2020-06-29 17:20:58.889727880 0800
string.go 2020-06-29 17:20:58.889727880 0800
@@ -1,12 1,12 @@
package main
import (
- "fmt"
"fmt"
)
func main() {
- s := "hello";
s := "hello"
- fmt.Printf("%q", s[1]);
- fmt.Printf("%b", s[1]);
fmt.Printf("%q", s[1])
fmt.Printf("%b", s[1])
}
gofmt -w string.go 这将使format后的结果覆盖当前文件
14.3 配置文本编辑器
通过在文本编辑器中使用插件,可自动使用诸如gofmt等工具。
- Vim(vim-go)
- Emacs(go.mode.el)
- Sublime(GoSublime)
- Atom(go-plus)
- Eclipse(goclipse)
- Visual Studio(vscode-go)
14.4 命名约定
- 以大写字母打头的标识符将被导出,而以小写字母打头的不会。
var Foo := "bar" //exported
var foo := "bar" //not exported
- 在Go语言中,对于包含多个单词的变量名,约定是使用骆驼拼写法或帕斯卡拼写法,具体使用哪种拼写法,取决于变量是否需要导出。
var fileName //Camel Case
var FileName //Pascal Case
- 在Go程序中,经常使用指出了数据类型的简短变量名,这让程序员能够专注于逻辑而不是变量。在这种情况下,i表示整型(Integer)数据类型,s表示字符串数据类型,等等。
var i int = 3
var s string = "hello"
var b bool = true
- 在Go源代码中,接口名通常是这样得到的:在动词后面加上后缀er,形成一个名词。后缀er通常表示操作,因此这种命名方式表示操作,如Reader、Writer和ByteWriter。
14.5 使用golint
golint是Go语言提供的一个官方工具。gofmt根据指定的约定设置代码的格式,而命令golint根据Go项目本身的约定查找风格方面的错误。默认不会安装golint,但可像下面这样安装它。
代码语言:javascript复制go get -u github.com/golang/lint/golint
golint会被安装到$GOPATH/bin/目录下
工具golint提供了有关风格方面的提示,还可帮助学习Go生态系统广泛接受的约定。
假设有代码lint.go
代码语言:javascript复制package main
import (
"fmt"
)
func main() {
a_string := "hello"
fmt.Println("Hello, World!");
}
对其执行golint会给出下面的提示
代码语言:javascript复制golint lint.go
lint.go:8:5: don't use underscores in Go names; var a_string should be aString
此外,golint还会检查代码中存在的语法错误。
第15章测试和性能
15.1 测试:软件开发最重要的方面
常用的测试有多种。
- 单元测试。
- 功能测试。
- 集成测试。
15.1.1 单元测试
单元测试针对一小部分代码,并独立地对它们进行测试。通常,这一小部分代码可能是单个函数,而要测试的是其输入和输出。
15.1.2 集成测试
集成测试通常测试的是应用程序各部分协同工作的情况。如果说单元测试检查的是程序的最小组成部分,那么集成测试检查的就是应用程序各个组件协同工作的情况。集成测试还检查诸如网络调用和数据库连接等方面,以确保整个系统按期望的那样工作。
15.1.3 功能测试
功能测试通常被称为端到端测试或由外向内的测试。这些测试从最终用户的角度核实软件按期望的那样工作,它们评估从外部看到的程序的运行情况,而不关心软件内部的工作原理。对用户来说,功能测试可能是最重要的测试。下面是一些功能测试的例子。
- 测试命令行工具,确定在用户提供特定的输入时,它将显示特定的输出。
- 对网页运行自动化测试。
- 对API运行从外到内的测试,并检查响应代码和报头。
15.1.4 测试驱动开发
很多开发人员都提倡采用测试驱动开发(TDD)。这种做法从测试的角度考虑新功能,先编写测试来描述代码片段的功能,再着手编写代码。
15.2 testing包
为支持测试,Go语言在标准库中提供了testing包,它还支持命令go。 与Go语言的其他众多方面一样,您也许理解一些与testing包相关的设计良好的约定。
第一个约定是,Go测试与其测试的代码在一起。测试不是放在独立的测试目录中,而是与它们要测试的代码放在同一个目录中。测试文件是这样命名的:在要测试的文件的名称后面加上后缀_test,因此如果要测试的文件名为strings.go,则测试它的文件将名为strings_test.go,并位于文件strings.go所在的目录中。
代码语言:javascript复制Project/
├── strings.go
└── strings_test.go
第二个约定是,测试为名称以单词Test打头的函数。
第三个约定是,在测试包中创建两个变量:got和want,它们分别表示要测试的值以及期望的值。
例子:
代码语言:javascript复制Project/
├── greet.go
└── greet_test.go
greet.go
代码语言:javascript复制package demo
func Greeting(s string) string{
return ("Hello2 " s)
}
greet_test.go
代码语言:javascript复制package demo
import "testing"
func TestGreeting(t *testing.T){
got := Greeting("George")
want := "Hello George"
if got != want {
t.Fatalf("Expected %q, got %q", want, got)
}
}
在Project目录下执行
代码语言:javascript复制go test
得到
代码语言:javascript复制--- FAIL: TestGreeting (0.00s)
greet_test.go:9: Expected "Hello George", got "Hello2 George"
FAIL
exit status 1
FAIL _/home/ballqiu/go/Project 0.002s
注意: 对于高版本的go, 要设置环境变量GO111MODULE为off或auto。不然执行go test时会报错
代码语言:javascript复制go: cannot find main module; see 'go help modules'
15.4 基准测试
Go提供了功能强大的基准测试框架,能够让您使用基准测试程序来确定完成特定任务时性能最佳的方式是哪一种。
下例显示了3种拼接字符串的方式,请不要过度关注其中的函数。
join.go
代码语言:javascript复制package demo
import (
"bytes"
"strings"
)
func StringFromAssign(j int) string{
var s string
for i:=0; i<j; i {
s = "a"
}
return s
}
func StringFromAppend(j int) string{
s := []string{}
for i:=0; i<j; i {
s = append(s, "a")
}
return strings.Join(s, "")
}
func StringFromBuffer(j int) string{
var buffer bytes.Buffer
for i:=0; i<j; i {
buffer.WriteString("a")
}
return buffer.String()
}
这些函数根据传入的整数值生成相应长度的字符串。事实上,这些函数的功能完全相同。那么,如何确定哪种字符串拼接方式的性能是最佳的呢?
testing包包含一个功能强大的基准测试框架,它能够让您反复地运行函数,从而建立基准。您无须指定运行函数的次数,因为基准测试框架将通过调整它来获得可靠的数据集。基准测试结束后,将生成一个报告,指出每次操作耗用了多少ns。
基准测试名以关键字Benchmark打头,它们接受一个类型为B的参数,并对函数进行基准测试。下例显示了分别对应于3种不同拼接方法的基准测试。 join_test.go
代码语言:javascript复制package demo
import "testing"
func BenchmarkStringFromAssign(b *testing.B){
for n:=0; n<b.N; n {
StringFromAssign(100)
}
}
func BenchmarkStringFromAppend(b *testing.B){
for n:=0; n<b.N; n {
StringFromAppend(100)
}
}
func BenchmarkStringFromBuffer(b *testing.B){
for n:=0; n<b.N; n {
StringFromBuffer(100)
}
}
上述代码目录结构
代码语言:javascript复制Bench/
├── join.go
└── join_test.go
在Bench目录执行
代码语言:javascript复制go test -bench=.
得到结果
代码语言:javascript复制goos: linux
goarch: amd64
BenchmarkStringFromAssign-4 131346 8735 ns/op
BenchmarkStringFromAppend-4 280996 4331 ns/op
BenchmarkStringFromBuffer-4 824763 1498 ns/op
PASS
ok _/home/ballqiu/go/Bench 3.769s
这里运行了基准测试并显示基准值。从这些测试可知,赋值的性能最糟,使用join的性能居中,而使用缓冲区的性能最佳。这个基准测试表明,使用缓冲区来拼接字符串的速度最快!
15.5 提供测试覆盖率
测试覆盖率是度量代码测试详尽程度的指标,它指出了被测试执行了的代码所在的百分比值。 修改15.2的greet.go, 增加Bye函数
代码语言:javascript复制package demo
func Greeting(s string) string{
return ("Hello " s)
}
func Bye(s string) string{
return ("Bye" s)
}
执行
代码语言:javascript复制 go test -cover greet.go greet_test.go
得到结果
代码语言:javascript复制ok command-line-arguments 0.002s coverage: 50.0% of statements
上述输出表明,测试只覆盖了50% 的代码。
15.7 问与答
问:应达到多高的测试覆盖率?
答:实现100%的测试覆盖率是一个值得为之努力的目标,但对大型项目而言,这几乎是不可能的。达到80% 左右的测试覆盖率就可以了,具体多少取决于项目的复杂度。