Go 是为 21 世纪的应用开发而构建的现代编程语言。在过去十年中,硬件和技术有了显著的进步,而大多数其他语言都没有利用这些技术进步。正如我们将在本书中看到的,Go 允许我们构建网络应用程序,利用多核系统提供的并发性和并行性。
在本章中,我们将介绍完成本书其余部分所需的一些主题,例如:
- Go 配置-
GOROOT
、GOPATH
等。 - 围棋包管理
- 本书中使用的项目结构
- 集装箱技术与 Docker 的使用
- 用围棋写测试
为了运行或构建 Go 项目,我们需要访问 Go 二进制文件及其库。Go 的典型安装(说明见https://golang.org/dl/ 在基于 Unix 的系统上,将把 Go 二进制文件置于/usr/bin/go
。但是,可以在不同的路径上安装 Go。在这种情况下,我们需要将GOROOT
环境变量设置为指向我们的 Go 安装路径,并将其附加到PATH
环境变量中。
程序员倾向于处理许多项目,最好将源代码与非编程相关文件分开。通常的做法是将源代码放在单独的位置或工作区中。每种编程语言都有自己关于如何设置语言相关项目的约定,Go 也不例外。
GOPATH
是开发人员必须设置的最重要的环境变量。它告诉 Go 编译器在哪里可以找到项目及其依赖项的源代码。GOPATH
中有一些约定需要遵循,它们必须处理文件夹层次结构。
这是一个包含项目源代码及其依赖项的目录。一般来说,我们希望源代码具有版本控制,并托管在云上。如果我们或其他任何人能够轻松地使用我们的项目,那也太好了。这需要我们做一些额外的设置。
让我们假设我们的项目位于http://git-server.com/user-name/my-go-project
。我们希望在本地系统上克隆并构建此项目。为了使其正常工作,我们需要将其克隆到$GOPATH/src/git-server.com/user-name/my-go-project
。当我们第一次构建一个带有依赖项的 Go 项目时,我们将看到src/
文件夹有许多目录和子目录,其中包含我们项目的依赖项。
Go 是一种编译编程语言;我们有源代码和要在项目中使用的依赖项的代码。通常,每次构建二进制文件时,编译器都必须读取项目的源代码和依赖项,然后将其编译为机器代码。每次编译主程序时编译未更改的依赖项将导致非常缓慢的构建过程。这就是对象文件存在的原因;它们允许我们将依赖项编译成可重用的机器代码,这些代码可以很容易地包含在 Go 二进制文件中。
这些对象文件存储在$GOPATH/pkg
中;它们遵循类似于src/
的目录结构,只是它们位于子目录中。这些目录倾向于遵循<OS>_<CPU-Architecture>
的命名模式,因为我们可以为多个系统构建可执行二进制文件:
$ tree $GOPATH/pkg
pkg
└── linux_amd64
├── github.com
│ ├── abbot
│ │ └── go-http-auth.a
│ ├── dimfeld
│ │ └── httppath.a
│ ├── oklog
│ │ └── ulid.a
│ ├── rcrowley
│ │ └── go-metrics.a
│ ├── sirupsen
│ │ └── logrus.a
│ ├── sony
│ │ └── gobreaker.a
└── golang.org
└── x
├── crypto
│ ├── bcrypt.a
│ ├── blowfish.a
│ └── ssh
│ └── terminal.a
├── net
│ └── context.a
└── sys
Go 将我们的项目编译并构建为可执行的二进制文件,并将它们放置在此目录中。根据构建规范,它们可能在当前系统或其他系统上可执行。为了使用bin/
目录中可用的二进制文件,我们需要设置相应的GOBIN=$GOPATH/bin
环境变量。
在过去,所有的程序都是从头开始编写的,每个实用函数和每个运行代码的库都必须手工编写。现在一天,我们不想定期处理低层次的细节;从零开始编写所有必需的库和实用程序是不可想象的。Go 附带了一个丰富的图书馆,足以满足我们的大部分需求。但是,我们可能需要一些标准库没有提供的额外库或特性。这样的库应该可以在互联网上找到,我们可以下载并添加到我们的项目中,开始使用它们。
在上一节GOPATH中,我们讨论了如何将所有项目保存到$GOPATH/src/git-server.com/user-name/my-go-project
表单的合格路径中。这对于我们可能拥有的所有依赖项都是正确的。在 Go 中有多种处理依赖关系的方法。让我们看看其中的一些。
go get
是标准库为包管理提供的实用工具。我们可以通过运行以下命令来安装新的包/库:
$ go get git-server.com/user-name/library-we-need
这将下载并构建源代码,然后将其作为二进制可执行文件安装(如果可以作为独立可执行文件使用)。go get
实用程序还安装为我们的项目检索的依赖项所需的所有依赖项。
go get
实用程序是一个非常简单的工具。它将在 Git 存储库上安装最新的主提交。对于简单的项目,这可能就足够了。然而,随着项目的规模和复杂性开始增长,跟踪所使用的依赖关系的版本可能变得至关重要。不幸的是,go get
对于这样的项目来说不是很好,我们可能想看看其他包管理工具。
glide
是 Go 社区中使用最广泛的包管理工具之一。它解决了go get
的局限性,但需要开发人员手动安装。以下是安装和使用glide
的简单方法:
$ curl https://glide.sh/get | sh
$ mkdir new-project && cd new-project
$ glide create
$ glide get github.com/last-ent/skelgor # A helper project to generate project skeleton.
$ glide install # In case any dependencies or configuration were manually added.
$ glide up # Update dependencies to latest versions of the package.
$ tree
.
├── glide.lock
├── glide.yaml
└── vendor
└── github.com
└── last-ent
└── skelgor
├── LICENSE
├── main.go
└── README.md
如果您不希望通过curl
和sh
安装glide
,可在上的项目页面提供其他选项,并对其进行更详细的说明 https://github.com/masterminds/glide 。
go dep
是 Go 社区正在开发的一种新的依赖关系管理工具。现在,它需要 Go 1.7 或更新版本才能编译,并且可以在生产中使用。然而,它仍在进行更改,尚未合并到 Go 的标准库中。
项目可能不仅仅包含项目的源代码,例如,配置文件和项目文档。根据偏好,项目的结构方式可能会发生巨大的变化。但是,需要记住的最重要的一点是,整个程序的入口点是通过main
函数实现的,该函数作为惯例在main.go
中实现。
我们将在本书中构建的应用程序具有以下初始结构:
$ tree
.
├── common
│ ├── helpers.go
│ └── test_helpers.go
└── main.go
本书中讨论的源代码可以通过两种方式获得:
- 使用
go get -u github.com/last-ent/distributed-go
- 从网站下载代码包并解压缩到
$GOPATH/src/github.com/last-ent/distributed-go
整本书的代码现在应该可以在$GOPATH/src/github.com/last-ent/distributed-go
上找到,并且每个章节的特定代码可以在特定章节号的目录中找到。
例如
第 1 章代码->$GOPATH/src/github.com/last-ent/distributed-go/chapter1
第 2 章代码->$GOPATH/src/github.com/last-ent/distributed-go/chapter2
等等
每当我们在任何特定章节中讨论代码时,都意味着我们在相应章节的文件夹中。
在本书中,我们将编写 Go 程序,这些程序将被编译成二进制文件并直接在我们的系统上运行。然而,在后面的章节中,我们将使用docker-compose
构建和运行多个 Go 应用程序。这些应用程序可以在本地系统上运行,没有任何实际问题;然而,我们的最终目标是能够在服务器上运行这些程序,并能够通过 internet 访问它们。
在 20 世纪 90 年代和 21 世纪初,将应用程序部署到 internet 的标准方法是获取服务器实例,将代码或二进制文件复制到实例上,然后启动程序。这在一段时间内效果很好,但很快就出现了并发症。以下是其中一些:
- 在开发人员机器上工作的代码可能在服务器上不工作。
- 在服务器实例上完美运行的程序可能会在将最新补丁应用到服务器操作系统时失败。
- 对于作为服务的一部分添加的每个新实例,必须运行各种安装脚本,以便我们可以将新实例与所有其他实例相媲美。这可能是一个非常缓慢的过程。
- 必须格外小心,以确保新实例及其安装的所有软件版本与我们的程序使用的 API 兼容。
- 确保将所有配置文件和重要环境变量复制到新实例中也很重要;否则,应用程序可能会在几乎没有线索的情况下失败。
- 通常,在本地系统、测试系统和生产系统上运行的程序版本的配置都不同,这意味着我们的应用程序有可能在三种类型的系统之一上失败。如果出现这种情况,我们将不得不花费额外的时间和精力,试图找出问题是否针对某个特定实例、某个特定系统,等等。
如果我们能够以明智的方式避免出现这种情况,那就太好了。容器尝试使用操作系统级虚拟化解决此问题。这是什么意思?
所有程序和应用程序都在称为用户空间的内存段中运行。这允许操作系统确保程序不会导致重大硬件或软件问题。这允许我们从用户空间应用程序中可能发生的任何程序崩溃中恢复。
容器的真正优势在于,它们允许我们在独立的用户空间中运行应用程序,我们甚至可以自定义用户空间的以下属性:
- 连接的设备,如网络适配器和 TTY
- CPU 和 RAM 资源
- 可从主机操作系统访问的文件和文件夹
然而,这如何帮助我们解决前面提到的问题?为此,让我们深入研究一下 To.T0} Doker-AuthT1。
现代软件开发广泛使用容器进行产品开发,并将产品部署到服务器实例。Docker 是 Docker,Inc(https://www.docker.com ),在撰写本文时,它是最常用的容器技术。另一个主要替代方案是 CoreOS 开发的rkt(https://coreos.com/rkt ),但在本书中,我们将只关注 Docker。
看看目前对 Docker 的描述,我们可能想知道它是否是另一个虚拟机。然而,情况并非如此,因为 VM 要求我们在机器或虚拟机监控程序上运行完整的来宾操作系统,以及所有必需的二进制文件。对于 Docker,我们使用操作系统级虚拟化,这允许我们在隔离的用户空间中运行容器。
虚拟机的最大优点是,我们可以在一个系统上运行不同类型的操作系统,例如 Windows、FreeBSD 和 Linux。但是,对于 Docker,我们可以运行任何风格的 Linux,唯一的限制是它必须是 Linux:
Docker 容器与 VM
Docker 容器的最大优点是,由于它作为一个离散进程在 Linux 上本机运行,因此它是轻量级的,并且不知道主机操作系统的所有功能。
在开始使用 Docker 之前,让我们简单地看看码头工人是如何使用的,它是如何构造的,以及什么是整个系统的主要组成部分。
以下列表和随附图像应有助于理解 Docker pipeline 的架构:
- Dockerfile:它包含关于如何构建运行我们程序的映像的说明。
- Docker 客户端:这是用户用来与 Docker 守护进程交互的命令行程序。
- Docker 守护程序:这是一个守护程序应用程序,它侦听用于管理构建或运行容器并将容器推送到 Docker 注册表的命令。它还负责配置容器网络、卷等。
- Docker 镜像:Docker 镜像包含构建容器二进制文件所需的所有步骤,可在安装了 Docker 的任何 Linux 机器上执行。
- Docker registry:Docker registry 负责存储和检索 Docker 图像。我们可以使用公共 Docker 注册表或私人注册表。Docker Hub 用作默认 Docker 注册表。
- Docker 容器:Docker 容器与我们目前讨论的容器不同。Docker 容器是 Docker 映像的可运行实例。可以创建、启动、停止 Docker 容器,等等。
- Docker API:我们前面讨论的 Docker 客户端是一个与 Docker API 交互的命令行界面。这意味着 Docker 守护程序不需要与 Docker 客户端在同一台计算机上运行。我们将在本书中使用的默认设置使用 UNIX 套接字或网络接口与本地系统上的 Docker 守护程序进行对话:
Docker 架构
让我们确保 Docker 设置工作正常。出于我们的目的,Docker 社区版应该足够了(https://www.docker.com/community-edition )。一旦我们安装了它,我们将通过运行一些基本命令来检查它是否工作。
让我们首先检查已安装的版本:
$ docker --version
Docker version 17.12.0-ce, build c97c6d6
让我们试着深入了解 Docker 安装的细节:
$ docker info
Containers: 38
Running: 0
Paused: 0
Stopped: 38
Images: 24
Server Version: 17.12.0-ce
在 Linux 上,当您尝试运行 docker 命令时,可能会出现权限被拒绝错误。为了与 Docker 交互,您可以在命令前面加上sudo
前缀,也可以创建一个“Docker”用户组并将您的用户添加到此组。更多详情参见链接https://docs.docker.com/install/linux/linux-postinstall/.
让我们尝试运行 Docker 映像。如果您还记得关于 Docker 注册表的讨论,您就知道我们不需要使用 Dockerfile 构建 Docker 映像来运行 Docker 容器。我们可以直接从 Docker Hub(默认 Docker 注册表)中提取图像,并将其作为容器运行:
$ docker run docker/whalesay cowsay Welcome to GopherLand!
Unable to find image 'docker/whalesay:latest' locally
Trying to pull repository docker.io/docker/whalesay ...
sha256:178598e51a26abbc958b8a2e48825c90bc22e641de3d31e18aaf55f3258ba93b: Pulling from docker.io/docker/whalesay
e190868d63f8: Pull complete
909cd34c6fd7: Pull complete
0b9bfabab7c1: Pull complete
a3ed95caeb02: Pull complete
00bf65475aba: Pull complete
c57b6bcc83e3: Pull complete
8978f6879e2f: Pull complete
8eed3712d2cf: Pull complete
Digest: sha256:178598e51a26abbc958b8a2e48825c90bc22e641de3d31e18aaf55f3258ba93b
Status: Downloaded newer image for docker.io/docker/whalesay:latest
________________________
< Welcome to GopherLand! >
------------------------
\
\
\
## .
## ## ## ==
## ## ## ## ===
/""""""""""""""""___/ ===
~~~ {~~ ~~~~ ~~~ ~~~~ ~~ ~ / ===- ~~~
\______ o __/
\ __/
\__________/
前面的命令也可以执行,但如图所示,仅使用docker run ...
,这更方便:
$ docker pull docker/whalesay & docker run docker/whalesay cowsay Welcome to GopherLand!
一旦我们有了一长组构建的图像,我们就可以将它们全部列出,对于 Docker 容器也是如此:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
docker.io/docker/whalesay latest 6b362a9f73eb 2 years ago 247 MB
$ docker container ls --all
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a1b1efb42130 docker/whalesay "cowsay Welcome to..." 5 minutes ago Exited (0) 5 minutes ago frosty_varahamihira
最后,需要注意的是,当我们继续使用 docker 构建和运行图像和容器时,我们将开始创建一个“悬空”图像的积压,我们可能不再真正使用这些图像。然而,它们最终会吃掉储存空间。为了消除这种“悬空”图像,我们可以使用以下命令:
$ docker rmi --force 'docker images -q -f dangling=true'
# list of hashes for all deleted images.
现在,我们已经掌握了 Docker 的基本知识,让我们看看本书中将用作模板的Dockerfile
文件。
接下来,让我们看一个示例:
FROM golang:1.10
# The base image we want to use to build our docker image from.
# Since this image is specialized for golang it will have GOPATH = /go
ADD . /go/src/hello
# We copy files & folders from our system onto the docker image
RUN go install hello
# Next we can create an executable binary for our project with the command,
'go install' ENV NAME Bob
# Environment variable NAME will be picked up by the program 'hello'
and printed to console.ENTRYPOINT /go/bin/hello
# Command to execute when we start the container # EXPOSE 9000 # Generally used for network applications. Allows us to connect to the
application running inside the container from host system's localhost.
让我们创建一个简单的最小 Go 程序,以便在 Docker 映像中使用它。取NAME
环境变量,打印<NAME> is your uncle.
后退出:
package main
import (
"fmt"
"os"
)
func main() {
fmt.Println(os.Getenv("NAME") + " is your uncle.")
}
现在我们已经准备好了所有代码,让我们使用Dockerfile
文件构建 Docker 映像:
$ cd docker
$ tree
.
├── Dockerfile
└── main.go"
0 directories, 2 files $ # -t tag lets us name our docker images so that we can easily refer to them $ docker build . -t hello-uncle Sending build context to Docker daemon 3.072 kB Step 1/5 : FROM golang:1.9.1 ---> 99e596fc807e Step 2/5 : ADD . /go/src/hello ---> Using cache ---> 64d080d7eb39 Step 3/5 : RUN go install hello ---> Using cache ---> 13bd4a1f2a60 Step 4/5 : ENV NAME Bob ---> Using cache ---> cc432fe8ffb4 Step 5/5 : ENTRYPOINT /go/bin/hello ---> Using cache ---> e0bbfb1fe52b Successfully built e0bbfb1fe52b $ # Let's now try to run the docker image. $ docker run hello-uncle Bob is your uncle. $ # We can also change the environment variables on the fly. $ docker run -e NAME=Sam hello-uncle Sam is your uncle.
测试是编程的一个重要部分,无论是在 Go 中还是在任何其他语言中。Go 有一种简单的编写测试的方法,在本节中,我们将介绍一些帮助测试的重要工具。
我们需要遵循某些规则和约定来测试代码。它们可以列出如下:
- 源文件和关联的测试文件放在同一个包/文件夹中
- 任何给定源文件的测试文件名为
<source-file-name>_test.go
- 测试函数需要有“Test”前缀,函数名中的下一个字符应该大写
在本节的其余部分中,我们将查看三个文件及其相关测试:
variadic.go
和variadic_test.go
addInt.go
和addInt_test.go
nil_test.go
(这些测试没有任何源文件)
在此过程中,我们将介绍可能使用的任何其他概念。
为了理解第一组测试,我们需要了解什么是可变函数以及 Go 如何处理它。让我们从定义开始:
可变函数是在函数调用过程中可以接受任意数量参数的函数。
假设 Go 是一种静态类型语言,类型系统对变量函数施加的唯一限制是,传递给它的参数数量不定,应该是相同的数据类型。但是,这并不限制我们传递其他变量类型。如果传递了参数,则函数会将参数作为元素的一部分接收,否则nil
,当不传递任何参数时。
让我们看一下代码,以便更好地了解:
// variadic.go
package main
func simpleVariadicToSlice(numbers ...int) []int {
return numbers
}
func mixedVariadicToSlice(name string, numbers ...int) (string, []int) {
return name, numbers
}
// Does not work.
// func badVariadic(name ...string, numbers ...int) {}
我们使用数据类型前面的...
前缀将函数定义为可变函数。请注意,每个函数只能有一个可变参数,它必须是最后一个参数。如果我们取消注释badVariadic
的行并尝试测试代码,我们可以看到这个错误。
我们想测试两个有效的函数,simpleVariadicToSlice
和mixedVariadicToSlice
,以了解上一节中定义的各种规则。然而,为了简洁起见,我们将测试这些:
simpleVariadicToSlice
:这是针对无参数、三个参数,以及如何将切片传递给变量函数mixedVariadicToSlice
:这是接受一个简单参数和一个可变参数
现在让我们看一下测试这两个函数的代码:
// variadic_test.go
package main
import "testing"
func TestSimpleVariadicToSlice(t *testing.T) {
// Test for no arguments
if val := simpleVariadicToSlice(); val != nil {
t.Error("value should be nil", nil)
} else {
t.Log("simpleVariadicToSlice() -> nil")
}
// Test for random set of values
vals := simpleVariadicToSlice(1, 2, 3)
expected := []int{1, 2, 3}
isErr := false
for i := 0; i < 3; i++ {
if vals[i] != expected[i] {
isErr = true
break
}
}
if isErr {
t.Error("value should be []int{1, 2, 3}", vals)
} else {
t.Log("simpleVariadicToSlice(1, 2, 3) -> []int{1, 2, 3}")
}
// Test for a slice
vals = simpleVariadicToSlice(expected...)
isErr = false
for i := 0; i < 3; i++ {
if vals[i] != expected[i] {
isErr = true
break
}
}
if isErr {
t.Error("value should be []int{1, 2, 3}", vals)
} else {
t.Log("simpleVariadicToSlice([]int{1, 2, 3}...) -> []int{1, 2, 3}")
}
}
func TestMixedVariadicToSlice(t *testing.T) {
// Test for simple argument & no variadic arguments
name, numbers := mixedVariadicToSlice("Bob")
if name == "Bob" && numbers == nil {
t.Log("Recieved as expected: Bob, <nil slice>")
} else {
t.Errorf("Received unexpected values: %s, %s", name, numbers)
}
}
让我们运行这些测试并查看输出。我们将在运行测试时使用-v
标志来查看每个单独测试的输出:
$ go test -v ./{variadic_test.go,variadic.go}
=== RUN TestSimpleVariadicToSlice
--- PASS: TestSimpleVariadicToSlice (0.00s)
variadic_test.go:10: simpleVariadicToSlice() -> nil
variadic_test.go:26: simpleVariadicToSlice(1, 2, 3) -> []int{1, 2, 3}
variadic_test.go:41: simpleVariadicToSlice([]int{1, 2, 3}...) -> []int{1, 2, 3}
=== RUN TestMixedVariadicToSlice
--- PASS: TestMixedVariadicToSlice (0.00s)
variadic_test.go:49: Received as expected: Bob, <nil slice>
PASS
ok command-line-arguments 0.001s
variadic_test.go
中的测试详细说明了可变函数的规则。然而,您可能已经注意到,TestSimpleVariadicToSlice
在其函数体中运行了三个测试,但go test
将其视为一个测试。Go 提供了一种在单个函数中运行多个测试的好方法,我们将在addInt_test.go
中查看它们。
对于本例,我们将使用一个非常简单的函数,如下代码所示:
// addInt.go
package main
func addInt(numbers ...int) int {
sum := 0
for _, num := range numbers {
sum += num
}
return sum
}
您可能还注意到在TestSimpleVariadicToSlice
中,我们复制了很多逻辑,而唯一的变化因素是输入和期望值。一种称为表驱动开发的测试样式定义了一个包含运行测试所需的所有数据的表,迭代表中的“行”,并对它们运行测试。
让我们看看我们将针对无参数和可变参数进行的测试:
// addInt_test.go
package main
import (
"testing"
)
func TestAddInt(t *testing.T) {
testCases := []struct {
Name string
Values []int
Expected int
}{
{"addInt() -> 0", []int{}, 0},
{"addInt([]int{10, 20, 100}) -> 130", []int{10, 20, 100}, 130},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
sum := addInt(tc.Values...)
if sum != tc.Expected {
t.Errorf("%d != %d", sum, tc.Expected)
} else {
t.Logf("%d == %d", sum, tc.Expected)
}
})
}
}
现在让我们运行这个文件中的测试,我们希望我们运行的testCases
表中的每一行都被视为一个单独的测试:
$ go test -v ./{addInt.go,addInt_test.go}
=== RUN TestAddInt
=== RUN TestAddInt/addInt()_->_0
=== RUN TestAddInt/addInt([]int{10,_20,_100})_->_130
--- PASS: TestAddInt (0.00s)
--- PASS: TestAddInt/addInt()_->_0 (0.00s)
addInt_test.go:23: 0 == 0
--- PASS: TestAddInt/addInt([]int{10,_20,_100})_->_130 (0.00s)
addInt_test.go:23: 130 == 130
PASS
ok command-line-arguments 0.001s
我们还可以创建不特定于任何特定源文件的测试;唯一的标准是文件名需要有<text>_test.go
格式。nil_test.go
中的测试阐明了该语言的一些有用特性,开发人员在编写测试时可能会发现这些特性很有用。详情如下:
httptest.NewServer
想象一下,我们必须针对发送回一些数据的服务器测试代码的情况。启动和协调一个完整的服务器来访问一些数据是很困难的。http.NewServer
为我们解决了这个问题。 **t.Helper
:如果我们使用相同的逻辑来通过或失败很多testCases
,那么将该逻辑分离为单独的函数是有意义的。但是,这会使测试运行调用堆栈倾斜。我们可以通过在测试中注释t.Helper()
并重新运行go test
来了解这一点。*
*我们还可以格式化命令行输出以打印漂亮的结果。我们将展示一个简单的示例,为通过的案例添加勾号,为失败的案例添加十字标记。
在测试中,我们将运行测试服务器,对其发出 GET 请求,然后测试预期输出与实际输出:
// nil_test.go
package main
import (
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
)
const passMark = "\u2713"
const failMark = "\u2717"
func assertResponseEqual(t *testing.T, expected string, actual string) {
t.Helper() // comment this line to see tests fail due to 'if expected != actual'
if expected != actual {
t.Errorf("%s != %s %s", expected, actual, failMark)
} else {
t.Logf("%s == %s %s", expected, actual, passMark)
}
}
func TestServer(t *testing.T) {
testServer := httptest.NewServer(
http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
path := r.RequestURI
if path == "/1" {
w.Write([]byte("Got 1."))
} else {
w.Write([]byte("Got None."))
}
}))
defer testServer.Close()
for _, testCase := range []struct {
Name string
Path string
Expected string
}{
{"Request correct URL", "/1", "Got 1."},
{"Request incorrect URL", "/12345", "Got None."},
} {
t.Run(testCase.Name, func(t *testing.T) {
res, err := http.Get(testServer.URL + testCase.Path)
if err != nil {
t.Fatal(err)
}
actual, err := ioutil.ReadAll(res.Body)
res.Body.Close()
if err != nil {
t.Fatal(err)
}
assertResponseEqual(t, testCase.Expected, fmt.Sprintf("%s", actual))
})
}
t.Run("Fail for no reason", func(t *testing.T) {
assertResponseEqual(t, "+", "-")
})
}
我们运行三个测试,其中两个测试用例通过,一个测试用例失败。通过这种方式,我们可以看到勾号和叉号的作用:
$ go test -v ./nil_test.go
=== RUN TestServer
=== RUN TestServer/Request_correct_URL
=== RUN TestServer/Request_incorrect_URL
=== RUN TestServer/Fail_for_no_reason
--- FAIL: TestServer (0.00s)
--- PASS: TestServer/Request_correct_URL (0.00s)
nil_test.go:55: Got 1\. == Got 1\. 
--- PASS: TestServer/Request_incorrect_URL (0.00s)
nil_test.go:55: Got None. == Got None. 
--- FAIL: TestServer/Fail_for_no_reason (0.00s)
nil_test.go:59: + != - 
FAIL exit status 1 FAIL command-line-arguments 0.003s
在本章中,我们首先了解成功运行 Go 项目的基本设置。然后我们研究了如何为我们的 Go 项目安装依赖项,以及如何构建我们的项目。我们还研究了容器背后的重要概念,它们解决了什么问题,以及我们将如何在书中使用它们以及一个示例。接下来,我们研究了如何在 Go 中编写测试,在处理变量函数和其他有用的测试函数时,我们学习了一些有趣的概念。
在下一章中,我们将开始研究 Go 编程 goroutines 的核心基础之一,以及使用 Go 编程 goroutines 时需要记住的重要细节。*