文章目录
- 写一个其他人可以使用的模块
- 从另一个模块中使用你的代码
- 添加测试用例
- 编译并安装应用程序
- 23/7/5 补充说明:
写一个其他人可以使用的模块
一个或多个相关的包被组织成一个模块。这些包都包含一些功能类似的函数。例如,你可以创建一个模块,它包含了一些包,这些包实现了各种财务分析的函数,那么编写财务应用程序的人就可以使用这个模块。
Go 代码被组织成包,包被组织成模块。你使用的包的模块指定了 Go 运行代码所需要的上下文,它包括编写代码的Go版本及其所需的其他模块集合。
在模块中添加或改进功能时,你会发布模块的新版本。使用你模块的开发人员可以导入最新的版本进行测试,以便在生产环境中使用。
步骤如下:
- 在 "gogo"目录中【/root/gogo】,创建一个新目录"greetings ",这个目录包含模块的源码。
mkdir greetings
cd greetings
- 使用
go mod init
命令创建一个go.mod
文件。
root@ubuntu:~/gogo/greetings# go mod init example.com/greetings
go: creating new go.mod: module example.com/greetings
- 新建文件" greetings.go",内容如下
package greetings
import "fmt"
// Hello returns a greeting for the named person.
func Hello(name string) string {
// Return a greeting that embeds the name in a message.
message := fmt.Sprintf("Hi, %v. Welcome!", name)
return message
}
- 在这段代码中:
- 第一行声明了代码所属的包为"greetings"。
- 实现了一个名为"Hello"的函数,根据输入的名字,返回特定格式的问候语。
从另一个模块中使用你的代码
- 在 “gogo"目录中,创建一个新目录"hello”【与"greetings"目录同级】。
mkdir hello
cd hello
- 创建一个新文件"hello.go",内容如下:
package main
import (
"fmt"
"example.com/greetings"
)
func main() {
// Get a greeting message and print it.
message := greetings.Hello("Gladys")
fmt.Println(message)
}
- 在这段代码中:
- 第一行声明了一个名为"main"的包。在 Go 中,作为一个程序执行的代码必须包含在 main 包中。
- 第六行导入了名为 "example.com/greetings "的包。
- 在main函数中使用了 greetings 包中的 Hello函数,返回一个问候语。
- 为这个 hello 包创建一个新的模块。
root@ubuntu:~/gogo/hello# go mod init hello
go: creating new go.mod: module hello
- 修改
go.mod
文件,来使用未发布的 greetings 模块。
- 在文件最后添加一行“replace example.com/greetings => …/greetings”,修改后内容如下:
module hello
go 1.13
replace example.com/greetings => ../greetings
- 执行
go build
命令。命令执行后,go.mod
文件内容被修改了,添加了一行内容:
module hello
go 1.13
replace example.com/greetings => ../greetings
require example.com/greetings v0.0.0-00010101000000-000000000000
require 指令指明了 hello 模块依赖 example.com/greetings 模块,而 replace 指令指明了模块的位置。【因为我们的模块未发布】
在引用一个已经发布的模块时,require 指令在模块后会包含一个版本号, replace 指令会被忽略。如下所示:
require example.com/greetings v1.1.0
- 执行 hello 程序。
root@ubuntu:~/gogo/hello# ./hello
Hi, Gladys. Welcome!
添加测试用例
原文中返回并处理一个错误、返回一个随机的问候语、为多人返回问候语,这几节就不提了。看下原文,将代码粘贴下来跑跑,就可以了,这些代码中提到的返回并处理错误、字典、切片等知识点后续还会提到的。
模块代码写完后,为了确保功能和我们预期的相同,我们需要写一些测试用例。Go 原生就对单元测试支持得很好。使用 "testing"包和 go test
命令,我们能快速写出并执行测试。
下面的步骤是我们如何为"greetings"模块添加测试用例:
- 在"greetings" 目录,新建一个名为"greetings_test.go"的文件。文件的名字以"_test.go"结尾告诉
go test
命令文件中包含测试函数。 - 文件内容如下:
package greetings
import (
"testing"
"regexp"
)
// TestHelloName calls greetings.Hello with a name, checking
// for a valid return value.
func TestHelloName(t *testing.T) {
name := "Gladys"
want := regexp.MustCompile(`\b`+name+`\b`)
msg, err := Hello("Gladys")
if !want.MatchString(msg) || err != nil {
t.Fatalf(`Hello("Gladys") = %q, %v, want match for %#q, nil`, msg, err, want)
}
}
// TestHelloEmpty calls greetings.Hello with an empty string,
// checking for an error.
func TestHelloEmpty(t *testing.T) {
msg, err := Hello("")
if msg != "" || err == nil {
t.Fatalf(`Hello("") = %q, %v, want "", error`, msg, err)
}
}
在这段代码中,实现了两个测试函数来测试 greetings.Hello 函数。测试函数的名字必须是"TestName"的形式,"Name"是测试函数的名称。测试函数的参数是一个指向 testing.T 的指针,可以使用这个参数的方法来报告并输出测试日志。
第一个测试函数,测试给 Hello 函数一个非空字符串参数,是否返回的问候语中包含传入的字符串。如果不包含,或者函数返回错误,那么就调用 Fatalf 方法输出提示信息,并终止测试。
第二个测试函数,测试给 Hello 函数一个空字符串参数,是否也返回空字符串,并返回一个错误。
接下来执行go test
命令运行测试函数。加 -v 参数会显示更详细的测试信息。
$ go test
PASS
ok example.com/greetings 0.364s
$ go test -v
=== RUN TestHelloName
--- PASS: TestHelloName (0.00s)
=== RUN TestHelloEmpty
--- PASS: TestHelloEmpty (0.00s)
PASS
ok example.com/greetings 0.372s
编译并安装应用程序
之前我们执行程序都是使用go run
命令,这个命令并不会产生二进制可执行程序,要产生可执行程序文件就需要使用go install
命令。
在安装之前,我们最好设置下安装目录
go env -w GOBIN=/home/liu/go/bin
在 hello 目录执行go install
命令后,生成的二进制程序就放在这个目录下。
23/7/5 补充说明:
一
如果一个模块【文件夹】中没有子文件夹【这个模块只含有一个包】,那么导入的包的路径就是模块的路径,导入后的包的名称是在各.go文件中声明的 package xxx 中的 xxx。一般情况,xxx 和文件夹名称是一样的,所以经常说导入的模块名称就是路径的最后一个分量!
如果一个模块中有多个子文件夹,那么这个模块包含多个包,那么导入的包的路径是模块路径加上子文件夹【相对模块的】路径。
如果这个模块里也定一个了一个包,那么这个包的导入路径就是模块的路径。
二
hello.go 中的 import “example.com/greetings” 和 greetings 文件夹中的使用 go mod init 初始化的模块名“example.com/greetings ”没有任何关系,关键点在于要和go.mod 中的“replace example.com/greetings => …/greetings”对应。
如果将 import “example.com/greetings” 改成 import “a/b/c”,然后 “replace a/b/c => …/greetings” ,效果是一样的。
三
import 导入的是包的路径,也就是一个文件夹的路径,文件夹下可以有多个go文件,但是这些go文件中的包声明必须相同【都包含 package xxx的声明】。
示例中 greetings 文件夹下的源文件名为 “greetings.go”,实际上它可以是任意的名字。它的文件夹名称也可以任意起,模块的名字【go mod init】也可以任意起,关键是在 import 时要能根据路径找到相应的文件夹。
示例中的模块是演示给可以互联网共享使用的,所以把模块名定为"example.com/greetings",就是一个URL,import 的路径也是一个URL。
看一个例子:
目录结构:
├── a
│ ├── aa.go
│ ├── b
│ │ └── bb.go
│ ├── c
│ │ └── cc.go
│ └── go.mod
└── hello
├── d
│ └── dd.go
├── ee.go
├── go.mod
└── hello.go
目录a:
a/aa.go
package aaa
import "fmt"
func D() {
fmt.Println("i'm D from package aaa")
}
a/go.mod
module fds3
go 1.19
a/b/bb.go
package bbb
import "fmt"
func D() {
fmt.Println("i'm D from package bbb")
}
a/c/cc.go
package ccc
import "fmt"
func D() {
fmt.Println("i'm D from package ccc")
}
a目录下定义了fds3模块,包含3个 package: aaa,bbb,ccc。
目录 hello:
hello/d/dd.go
package ddd
import "fmt"
func D() {
fmt.Println("i'm D from package ddd")
}
hello/ee.go
package main
import "fmt"
func D() {
fmt.Println("i'm D from package main")
}
hello/go.mod
module hello2
go 1.19
replace dddfff => ../a
require dddfff v0.0.0-00010101000000-000000000000 // indirect
hello/hello.go
package main
import "hello2/d" // package ddd 在 hello2 模块的子目录中!
import "dddfff" // package aaa
import "dddfff/b" //package bbb
import "dddfff/c" //package ccc
func main() {
aaa.D()
bbb.D()
ccc.D()
ddd.D() //hello/d/ 目录下的包
D() // hello/ee.go 文件中定义的函数
}
hello目录下定义了main模块,包含 package ddd
输出:
administrator@administrator-PC:~/gogo/hello$ go run .
i'm D from package aaa
i'm D from package bbb
i'm D from package ccc
i'm D from package ddd
i'm D from package main