Go语言包的使用与管理
1. Go语言包概述
Go标准库拥有大量的包,这些包开箱即用,提供了广泛的功能。此外,还可以从godashboard.appspot.com/project获取许多第三方包。同时,Go也允许我们创建自己的自定义包,这些包可以安装到Go标准库副本中,也可以放在自己的Go树(即GOPATH路径)中。
2. 自定义包
2.1 包的基本概念
在Go中,对于任何给定的包,我们可以将包的代码分散到任意数量的文件中,只要这些文件都在同一个目录下。例如,一个使用单个包(main)的程序,即使由六个单独的文件(invoicedata.go、gob.go、inv.go、jsn.go、txt.go和xml.go)组成,只要每个文件(不包括注释)的第一行语句是
package main
即可。
对于大型应用程序,我们可能需要创建特定于应用程序的包,以将应用程序的功能划分为逻辑单元。我们也可以创建包含多个应用程序都能使用的功能的包。Go并不区分供单个应用程序使用的包和供多个应用程序共享的包,但我们可以通过将特定于应用程序的包放在应用程序的子目录中,将共享包放在GOPATH源目录的子目录中来进行区分。GOPATH源目录是一个名为src的目录,GOPATH中的每个目录都应该包含一个src目录,因为这是Go工具(命令)所期望的。我们的程序和包的源代码应该放在GOPATH src目录(如果有多个GOPATH,则是其中一个)的子目录中。虽然也可以将自己的包直接安装到Go树(即GOROOT下),但这样做没有优势,而且在使用包管理系统、安装程序或手动构建Go的系统上可能会带来不便。
2.2 创建自定义包
创建自定义包的最佳位置是GOPATH src目录(或其中一个GOPATH src目录)。特定于应用程序的包可以在应用程序目录内创建,但我们希望共享的包应该直接在GOPATH src目录下创建,理想情况下是在一个唯一的目录下,以避免名称冲突。
按照惯例,包的源代码放在与包同名的目录中。源代码可以分散在任意数量的文件中,文件名可以任意(只要以.go结尾)。例如,下面是一个
stacker
程序和其特定于应用程序的包
stack
的目录布局:
aGoPath/src/stacker/stacker.go
aGoPath/src/stacker/stack/stack.go
这里,
aGoPath
是GOPATH目录(如果只有一个),或者是GOPATH环境变量中以冒号(在Windows上是分号)分隔的路径之一。
如果我们在
stacker
目录中执行
go build
命令,将得到一个名为
stacker
的可执行文件(在Windows上是
stacker.exe
)。但是,如果我们希望可执行文件位于GOPATH bin目录中,或者希望其他程序能够使用
stacker/stack
包,则必须使用
go install
。
当
go install
构建
stacker
程序时,会创建两个目录(如果它们不存在):
aGoPath/bin
包含
stacker
可执行文件,
aGoPath/pkg/linux_amd64/stacker
包含静态
stack
包的二进制库文件。(当然,操作系统/架构目录将与使用的机器相匹配,例如32位Windows的
windows_386
)。
stack
包可以由
stacker
程序使用
import "stacker/stack"
语句导入,即给出其完整的(Unix风格)路径,但不包括
aGoPath/src
部分。实际上,GOPATH中的任何程序或包都可以使用此导入方式,因为Go不区分特定于应用程序的包和共享包。
再看一个例子,有序映射(ordered map)的包
omap
,它打算供多个应用程序使用。为了避免名称冲突,我们在GOPATH目录(或其中一个GOPATH目录)下为要共享的包创建了一个目录,并给它一个唯一的名称(这里是一个域名)。其目录结构如下:
aGoPath/src/qtrac.eu/omap/omap.go
我们的任何程序(只要它们在GOPATH路径中)都可以使用
import "qtrac.eu/omap"
访问有序映射包。如果我们有其他共享包,可以将它们放在
aGoPath/src/qtrac.eu
下,与有序映射包并列。
当
go install
构建
omap
包时,会创建
aGoPath/pkg/linux_amd64/qtrac.eu
目录(如果它不存在),其中包含
omap
包的二进制库文件,操作系统/架构子目录会根据机器而有所不同。
如果我们想在其他包中创建包,可以直接进行。首先创建一个包目录,例如
aGoPath/src/my_package
,然后在其下为每个包创建一个子目录,例如
aGoPath/src/my_package/pkg1
和
aGoPath/src/my_package/pkg2
,以及它们对应的文件
aGoPath/src/my_package/pkg1/pkg1.go
和
aGoPath/src/my_package/pkg2/pkg2.go
。然后,要导入
pkg2
,我们可以写
import "my_package/pkg2"
。Go源代码的归档包就是这种方法的一个例子。也可以在包本身中包含代码(例如,通过创建文件
aGoPath/src/my_package/my_package.go
)。
Go包可以从GOROOT下(具体是
$GOROOT/pkg/${GOOS}_${GOARCH}
,例如
/opt/go/pkg/linux_amd64
)和GOPATH环境变量中的目录下导入。这意味着可能会出现名称冲突。避免名称冲突的最简单方法是确保GOPATH路径有一个唯一的目录,例如我们为
omap
包使用的域名。
下面是创建自定义包的流程:
graph LR
A[确定包类型] --> B{特定于应用程序的包}
B -- 是 --> C[在应用程序目录内创建]
B -- 否 --> D[在GOPATH src目录下创建]
D --> E[选择唯一目录]
E --> F[创建与包同名的目录]
F --> G[将源代码放入目录]
G --> H{是否使用go install}
H -- 是 --> I[生成可执行文件和二进制库文件]
H -- 否 --> J[使用go build生成可执行文件]
2.3 平台特定代码
在某些情况下,我们需要有不同平台之间不同的代码。例如,在类Unix系统上,shell会进行通配符扩展(称为globbing),因此命令行上的
*.txt
可能会作为
["README.txt", "INSTALL.txt"]
传递给程序的
os.Args[1:]
切片。但在Windows上,程序只会得到
["*.txt"]
。我们可以使用
filepath.Glob()
函数让程序进行这种通配符扩展,但当然,我们只需要在Windows上这样做。
一种解决方案是在运行时通过测试
if runtime.GOOS == "windows" { … }
来决定是否使用
filepath.Glob()
,本书的大多数示例都是这样做的(例如
cgrep1/cgrep.go
)。另一种解决方案是将特定于平台的代码放在自己的
.go
文件中。例如,
cgrep3
程序由三个文件组成:
cgrep.go
、
util_linux.go
和
util_windows.go
。
util_linux.go
文件中定义了一个函数:
func commandLineFiles(files []string) []string { return files }
显然,这个函数不会进行任何文件通配符扩展,因为在Linux上不需要这样做。
util_windows.go
文件定义了一个同名的不同函数:
func commandLineFiles(files []string) []string {
args := make([]string, 0, len(files))
for _, name := range files {
if matches, err := filepath.Glob(name); err != nil {
args = append(args, name) // Invalid pattern
} else if matches != nil { // At least one match
args = append(args, matches...)
}
}
return args
}
当我们使用
go build
构建
cgrep3
时,在Linux机器上会编译
util_linux.go
文件,而忽略
util_windows.go
文件;在Windows机器上则相反。这确保了在任何特定的构建中,只有一个
commandLineFiles()
函数被编译。
在Mac OS X和FreeBSD系统上,
util_linux.go
和
util_windows.go
都不会被编译,因此构建会失败。由于这两个平台的shell都会进行通配符扩展,我们可以将
util_linux.go
软链接或复制到
util_darwin.go
和
util_freebsd.go
(对于Go支持的其他所需平台也是如此)。有了这些链接或副本,程序就可以在Mac OS X和FreeBSD平台上构建。
2.4 包的文档
包,特别是那些打算共享的包,需要良好的文档。Go提供了一个文档工具
godoc
,可以在控制台显示包和函数的文档,也可以作为Web服务器将文档作为网页提供。如果包在GOPATH中,
godoc
会自动找到它,并在“Packages”链接的左侧提供一个链接。如果包不在GOPATH中,使用
godoc
时需要加上
-path
选项(除了
-http
选项),并提供包的路径,
godoc
会在“Packages”链接旁边提供一个链接。
默认情况下,
godoc
只显示导出的类型、类、常量和变量,因此所有这些都应该进行文档记录。文档直接写在源代码文件中。
例如,对于包的文档:
// Package omap implements an efficient key-ordered map.
//
// Keys and values may be of any type, but all keys must be comparable
// using the less than function that is passed in to the omap.New()
// function, or the less than function provided by the omap.New*()
// construction functions.
package omap
对于包,紧接在
package
语句之前的注释用作包的描述,第一行(到第一个句点,如果有的话,或者到换行符)用作单行摘要。
对于导出类型的文档:
// Map is a key-ordered map.
// The zero value is an invalid map! Use one of the construction functions
// (e.g., New()), to create a map for a specific key type.
type Map struct {
导出类型的文档必须紧接在
type
语句之前编写,并应始终指出类型的零值是否有效。
对于函数和方法的文档:
// New returns an empty Map that uses the given less than function to
// compare keys. For example:
// type Point { X, Y int }
// pointMap := omap.New(func(a, b interface{}) bool {
// α, β := a.(Point), b.(Point)
// if α.X != β.X {
// return α.X < β.X
// }
// return α.Y < β.Y
// })
func New(less func(interface{}, interface{}) bool) *Map {
函数和方法的文档必须紧接在它们的第一行之前。
下面是文档编写的要点总结:
| 文档对象 | 编写位置 | 注意事项 |
| ---- | ---- | ---- |
| 包 | 紧接在
package
语句之前 | 第一行作为单行摘要 |
| 导出类型 | 紧接在
type
语句之前 | 指出零值是否有效 |
| 函数和方法 | 紧接在第一行之前 | |
2.5 包的单元测试和基准测试
Go标准库通过
testing
包为单元测试提供了很好的支持。为包设置单元测试很简单,只需在要测试的包所在的目录中创建一个测试文件。这个文件的名称应该以包名开头,以
_test.go
结尾。例如,
omap
包的测试文件名为
omap_test.go
。
在一些示例中,测试文件放在自己的唯一包中(例如
omap_test
),它们导入要测试的包和
testing
包,以及其他需要的包。这限制我们使用黑盒测试。然而,一些Go程序员更喜欢白盒测试。这可以通过将测试文件放在与要测试的包相同的包中来轻松实现(例如
omap
),在这种情况下,不需要导入要测试的包。后一种方法意味着可以测试未导出的类型,并且可以为未导出的类型添加特定的方法以支持测试。
测试文件没有
main()
函数。相反,它有一个或多个导出的函数,这些函数的名称以
Test
开头,并且接受一个类型为
*testing.T
的单个参数,不返回任何值。我们当然可以添加任何需要的辅助函数,只要它们的名称不以
Test
开头。
下面是
omap_test.go
文件中的一个测试示例:
func TestStringKeyOMapInsertion(t *testing.T) {
wordForWord := omap.NewCaseFoldedKeyed()
for _, word := range []string{"one", "Two", "THREE", "four", "Five"} {
wordForWord.Insert(word, word)
}
var words []string
wordForWord.Do(func(_, value interface{}) {
words = append(words, value.(string))
})
actual, expected := strings.Join(words, ""), "FivefouroneTHREETwo"
if actual != expected {
t.Errorf("%q != %q", actual, expected)
}
}
这个测试首先创建一个空的
omap.Map
,然后插入一些字符串键(不区分大小写)和字符串值。然后,我们使用
Map.Do()
方法遍历映射中的所有键值对,并将每个值追加到一个字符串切片中。最后,我们将字符串连接成一个字符串,看看是否与我们期望的匹配。如果匹配失败,我们调用
testing.T.Errorf()
方法报告失败并提供一些解释。如果没有调用任何错误或失败函数,则认为测试通过。
除了示例中使用的
Errorf()
方法,
testing
包的
*testing.T
类型还有各种其他方法,如
testing.T.Fail()
、
testing.T.Fatal()
等。所有这些方法为我们提供了对如何响应测试失败的良好控制。
此外,
testing
包还支持基准测试。基准测试函数可以像测试函数一样添加到
package_test.go
文件中,只是在这种情况下,函数的名称必须以
Benchmark
开头,并且接受一个
*testing.B
类型的单个参数,不返回任何值。
下面是一个基准测试示例:
func BenchmarkOMapFindSuccess(b *testing.B) {
b.StopTimer() // Don't time creation and population
intMap := omap.NewIntKeyed()
for i := 0; i < 1e6; i++ {
intMap.Insert(i, i)
}
b.StartTimer() // Time the Find() method succeeding
for i := 0; i < b.N; i++ {
intMap.Find(i % 1e6)
}
}
这个函数首先停止计时器,因为我们不想对
omap.Map
的创建和填充进行计时。然后我们创建一个空的
omap.Map
,并使用一百万个键值对填充它。
默认情况下,
go test
不会运行任何基准测试,所以如果我们想运行它们,必须使用
-test.bench
选项并提供一个正则表达式来匹配我们想要运行的基准测试的名称。正则表达式
.*
匹配任何内容,即测试文件中的所有基准测试函数,但简单的
.
也可以。
例如:
$ go test -test.bench=.
PASS qtrac.eu/omap
PASS
BenchmarkOMapFindSuccess-4 1000000 1380 ns/op
BenchmarkOMapFindFailure-4 1000000 1350 ns/op
这个输出显示运行了两个基准测试,每个测试有1000000次循环迭代,每次操作的纳秒数如所示。迭代次数(即
b.N
的值)由
go test
选择,但如果我们愿意,可以使用
-test.benchtime
选项设置每个基准测试运行的大致秒数。
单元测试和基准测试的流程如下:
graph LR
A[创建测试文件] --> B{选择测试类型}
B -- 单元测试 --> C[编写以Test开头的函数]
C --> D[运行go test]
B -- 基准测试 --> E[编写以Benchmark开头的函数]
E --> F[使用-test.bench选项运行go test]
3. 导入包
Go允许我们为包名设置别名。这个功能很方便和有用,例如,它使我们可以轻松地在包的两个实现之间切换。例如,我们可以这样导入一个包:
import bio "bio_v1"
,这样在我们的代码中,
bio_v1
包就可以作为
bio
而不是
bio_v1
来访问。后来,当有更成熟的实现可用时,我们可以通过将导入改为
import bio "bio_v2"
来切换到它。如果
bio_v1
和
bio_v2
提供相同的API(或者
bio_v2
的API是
bio_v1
的超集),这将起作用,并且意味着代码的其余部分可以保持不变。另一方面,最好避免为标准库包名设置别名,因为这可能会给后续的维护者带来困惑或困扰。
总之,掌握Go语言包的创建、使用和管理对于开发高效、可维护的Go应用程序至关重要。通过合理地创建自定义包、处理平台特定代码、编写文档以及进行单元测试和基准测试,我们可以更好地组织代码,提高代码的质量和性能。同时,灵活运用包的导入和别名功能,可以让我们的代码更加简洁和易于维护。
Go语言包的使用与管理
4. 第三方包
除了自定义包,Go 还可以使用大量的第三方包。这些第三方包可以从 godashboard.appspot.com/project 获取。使用第三方包可以极大地提高开发效率,因为很多常见的功能已经由其他开发者实现并开源。
使用第三方包的步骤如下:
1.
查找合适的第三方包
:通过 Go 包管理工具或相关的开源平台,找到满足需求的第三方包。
2.
安装第三方包
:使用
go get
命令来安装第三方包。例如,要安装
github.com/someuser/somepackage
,可以执行以下命令:
go get github.com/someuser/somepackage
-
导入并使用第三方包
:在代码中使用
import语句导入第三方包,然后就可以使用该包提供的功能。示例代码如下:
package main
import (
"fmt"
"github.com/someuser/somepackage"
)
func main() {
// 使用第三方包的功能
result := somepackage.SomeFunction()
fmt.Println(result)
}
下面是使用第三方包的流程:
graph LR
A[查找第三方包] --> B[使用go get安装]
B --> C[在代码中导入]
C --> D[使用包的功能]
5. Go 命令概述
Go 编译器提供了一些命令(程序),下面简要介绍一些常用的命令:
| 命令 | 功能 |
| ---- | ---- |
|
go build
| 编译包和依赖项,生成可执行文件。如果是单个包,会在当前目录生成可执行文件;如果是多个包,会生成目标文件但不会生成可执行文件。 |
|
go install
| 编译并安装包和依赖项,将可执行文件安装到
GOPATH/bin
目录,将包的二进制库文件安装到
GOPATH/pkg
目录。 |
|
go test
| 运行包的测试文件,支持单元测试和基准测试。可以使用
-test.bench
选项运行基准测试。 |
|
go get
| 下载并安装远程包,通常用于获取第三方包。 |
|
godoc
| 提供包和函数的文档,可以在控制台显示文档,也可以作为 Web 服务器将文档作为网页提供。 |
这些命令在开发过程中非常有用,合理使用它们可以提高开发效率。例如,在开发过程中,我们可以使用
go build
快速编译代码进行测试,使用
go install
将程序安装到系统中,使用
go test
进行单元测试和基准测试。
6. Go 标准库概述
Go 标准库包含大量的包,提供了丰富的功能。下面对 Go 标准库的一些主要包进行简要介绍:
6.1 归档和压缩包
这些包用于处理文件的归档和压缩,例如
archive/tar
用于处理 tar 归档文件,
compress/gzip
用于处理 gzip 压缩文件。
示例代码:
package main
import (
"archive/tar"
"compress/gzip"
"fmt"
"io"
"os"
)
func main() {
// 打开 gzip 文件
gzipFile, err := os.Open("example.tar.gz")
if err != nil {
fmt.Println(err)
return
}
defer gzipFile.Close()
// 创建 gzip 读取器
gzipReader, err := gzip.NewReader(gzipFile)
if err != nil {
fmt.Println(err)
return
}
defer gzipReader.Close()
// 创建 tar 读取器
tarReader := tar.NewReader(gzipReader)
// 遍历 tar 文件中的所有文件
for {
header, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
fmt.Println(err)
return
}
fmt.Println(header.Name)
}
}
6.2 字节和字符串相关包
这些包用于处理字节和字符串,例如
bytes
包提供了字节切片的操作,
strings
包提供了字符串的操作。
示例代码:
package main
import (
"bytes"
"fmt"
"strings"
)
func main() {
// bytes 包示例
byteSlice := []byte("hello")
newByteSlice := bytes.ToUpper(byteSlice)
fmt.Println(string(newByteSlice))
// strings 包示例
str := "hello world"
newStr := strings.ReplaceAll(str, "world", "go")
fmt.Println(newStr)
}
6.3 集合包
这些包用于处理各种集合类型,例如
container/list
提供了双向链表的实现,
container/heap
提供了堆的实现。
示例代码:
package main
import (
"container/list"
"fmt"
)
func main() {
// 创建一个双向链表
l := list.New()
// 向链表中添加元素
e1 := l.PushBack(1)
e2 := l.PushBack(2)
l.InsertBefore(3, e2)
// 遍历链表
for e := l.Front(); e != nil; e = e.Next() {
fmt.Println(e.Value)
}
}
6.4 文件、操作系统和相关包
这些包用于处理文件和操作系统相关的操作,例如
os
包提供了操作系统相关的功能,
io/ioutil
包提供了文件读写的便捷函数。
示例代码:
package main
import (
"fmt"
"io/ioutil"
"os"
)
func main() {
// 读取文件内容
content, err := ioutil.ReadFile("example.txt")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(content))
// 创建文件并写入内容
file, err := os.Create("newfile.txt")
if err != nil {
fmt.Println(err)
return
}
defer file.Close()
_, err = file.WriteString("hello go")
if err != nil {
fmt.Println(err)
return
}
}
6.5 图形相关包
这些包用于处理图形相关的操作,例如
image
包提供了图像的基本操作,
image/draw
包提供了图像绘制的功能。
6.6 数学包
这些包用于处理数学相关的操作,例如
math
包提供了基本的数学函数,
math/rand
包提供了随机数生成的功能。
示例代码:
package main
import (
"fmt"
"math"
"math/rand"
"time"
)
func main() {
// 数学函数示例
result := math.Sqrt(16)
fmt.Println(result)
// 随机数生成示例
rand.Seed(time.Now().UnixNano())
randomNum := rand.Intn(100)
fmt.Println(randomNum)
}
6.7 杂项包
这些包包含一些杂项功能,例如
sync
包提供了并发编程的同步机制,
time
包提供了时间处理的功能。
示例代码:
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
// 启动 3 个工作线程
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(i, &wg)
}
// 等待所有工作线程完成
wg.Wait()
fmt.Println("All workers done")
}
6.8 网络包
这些包用于处理网络相关的操作,例如
net
包提供了网络连接的基本功能,
net/http
包提供了 HTTP 服务器和客户端的实现。
示例代码:
package main
import (
"fmt"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, you've requested: %s\n", r.URL.Path)
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
6.9 反射包
reflect
包提供了反射机制,允许在运行时检查类型、调用方法等。
示例代码:
package main
import (
"fmt"
"reflect"
)
type Person struct {
Name string
Age int
}
func main() {
p := Person{Name: "John", Age: 30}
t := reflect.TypeOf(p)
v := reflect.ValueOf(p)
fmt.Println("Type:", t.Name())
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
value := v.Field(i)
fmt.Printf("%s: %v\n", field.Name, value.Interface())
}
}
通过了解和使用 Go 标准库的这些包,我们可以避免重复造轮子,提高开发效率。在实际开发中,根据具体的需求选择合适的包来使用。
综上所述,Go 语言的包管理功能非常强大,无论是自定义包、第三方包还是标准库包,都为开发者提供了丰富的工具和功能。合理运用这些包,可以开发出高效、可维护的 Go 应用程序。在开发过程中,要注意包的创建、使用、文档编写、测试等方面的细节,以确保代码的质量和性能。
超级会员免费看

被折叠的 条评论
为什么被折叠?



