设计模式已经成为数十万个软件的基础。自从四个 To.T1(Erich Gamma,Richard Helm,Ralph Johnson 和 John Vlissides)的团队写了这本书,Tyt2 设计模式:1994 中的可重用面向对象软件 Ty3Ty.的元素,以 C++和 SimalTalk 为例,23 个经典模式已经在当今的大多数主要语言中重新实现,并且它们已经在您所知道的几乎每个项目中使用。
四人帮发现他们的许多项目中存在许多小型架构,他们开始以更抽象的方式重写这些架构,并发布了这本名著。
这本书全面地解释和实现了四人帮中最常见的设计模式和今天的模式,以及 Go 中一些最常用的并发模式。
但是什么是去。。。?
在过去的 20 年里,我们在计算机科学领域取得了惊人的发展。存储空间大幅增加,RAM 大幅增长,CPU 的。。。好只是更快。它们的增长是否与存储和 RAM 内存一样快?事实并非如此,CPU 行业的 CPU 速度已经达到了极限,这主要是因为它们的速度太快,以至于在散发足够热量的情况下无法获得足够的功率来工作。CPU 制造商现在正在为每台计算机运送更多的内核。这种情况发生在许多系统编程语言的背景下,这些语言不是为多处理器 CPU 或作为唯一机器的大型分布式系统设计的。在谷歌中,他们意识到,当他们在开发 java 或 C++语言中的分布式应用时,这并不是一个问题。
与此同时,我们的程序更大、更复杂、更难维护,并且存在大量不良做法的空间。虽然我们的计算机有更多的内核,速度更快,但我们在开发代码和分布式应用程序时并没有更快。这是 Go 的目标。
Go design 于 2007 年由三名谷歌员工发起,他们致力于一种编程语言的研究,这种语言可以解决大规模分布式系统中的常见问题,就像你在谷歌可以找到的那样。创造者是:
2008 年,编译完成,团队得到了 Russ Cox 和 Ian Lance Taylor 的帮助。该团队在 2009 年开始了他们的开源项目之旅,2012 年 3 月,他们在发布了 50 多个版本后达到了 1.0 版本。
任何 Go 安装都需要两件基本的东西:磁盘上某个地方的语言二进制文件和系统中的GOPATH路径,您的项目和从其他人下载的项目将存储在该路径中。
在以下几行中,我们将探讨如何在 Linux、Windows 和 OS X 中安装 Go 二进制文件。有关如何安装最新版本的 Go 的详细说明,请参阅位于的官方文档 https://golang.org/doc/install 。
要在 Linux 中安装 Go,您有两个选项:
sudo yum install -y golang
sudo apt-get install -y golang
我建议使用第二个并下载发行版。Go 的更新保持向后兼容性,您通常不应该担心频繁更新 Go 二进制文件。
Go 在 Linux 中的高级安装要求您从golang网页下载二进制文件。进入后 https://golang.org 点击下载去按钮(通常在右边),每个发行版都有一些特色下载选项。选择Linux发行版下载最新稳定版本。
在https://golang.org 您还可以下载该语言的测试版。
假设我们已将tar.gz
文件保存在下载文件夹中,那么让我们提取它并将其移动到其他路径。按照惯例,Go 二进制文件通常放在/usr/local/go
目录中:
tar -zxvf go*.*.*.linux-amd64.tar.gz
sudo mv go /usr/local/go
提取时,请记住用您下载的版本替换星号(*
。
现在我们在/usr/local/go
路径中安装了 Go,因此现在我们必须将bin
子文件夹添加到我们的PATH
和我们的 GOPATH 中的bin
文件夹中。
mkdir -p $HOME/go/bin
对于-p,我们告诉 bash 创建所有必要的目录。现在我们需要将 bin 文件夹路径附加到我们的路径,在您的~/.bashrc
末尾附加以下行:
export PATH=$PATH:/usr/local/go/bin
检查我们的go/bin
目录是否可用:
$ go version
Go version go1.6.2 linux/amd64
要在 Windows 中安装 Go,您需要管理员权限。打开您喜爱的浏览器并导航至 https://golang.org
。点击下载按钮,选择微软视窗发行版。一个*.msi
文件将开始下载。
双击 MSI 安装程序以执行它。将出现一个安装程序,要求您接受最终用户许可协议(EULA),并为您的安装选择一个目标文件夹。我们将继续使用默认路径,在我的例子中是C:\Go
。
安装完成后,您必须将位于C:\Go\bin
的二进制 Go文件夹添加到路径中。为此,您必须进入控制面板并选择系统选项。进入系统后,选择高级选项卡并点击环境变量按钮。在这里,您将看到一个包含当前用户变量和系统变量的窗口。在系统变量中,可以找到路径变量。点击编辑**按钮,打开文本框。您可以通过在当前行末尾添加;C:\Go/bin
来添加路径(注意路径开头的分号)。在最新的 Windows 版本(Windows 10)中,您将有一个管理器来轻松添加变量。**
**## Mac OS X
在 Mac OS X 中,安装过程与 Linux 非常相似。打开您喜爱的浏览器并导航至https://golang.org 点击下载 Go。从出现的可能发行版列表中,选择Apple OS X。这将下载一个*.pkg
文件到您的下载文件夹。
一个窗口将引导您完成安装过程,您必须键入管理员密码,以便它可以将 Go 二进制文件放入具有适当权限的/usr/local/go/bin
文件夹中。现在,打开终端进行安装测试,在终端上键入:
$ go version
Go version go1.6.2 darwin/amd64
如果您看到安装的版本,一切正常。如果不起作用,请检查您是否正确遵循了每个步骤,或参考中的文档 https://golang.org 。
Go 将始终在同一工作区下工作。这有助于编译器找到可以使用的包和库。此工作区通常称为GOPATH。
在开发 Go 软件时,GOPATH 在您的工作环境中扮演着非常重要的角色。当您在代码中导入库时,它将在您的$GOPATH/src
中搜索该库。同样,当您安装一些 Go 应用程序时,二进制文件将存储在$GOPATH/bin
中。
同时,所有源代码必须存储在$GOPATH/src
文件夹中的有效路由中。例如,我将我的项目存储在 GitHub 中,我的用户名是Sayden,因此,对于名为minimal mesos go framework的项目,我将具有类似$GOPATH/src/github.com/sayden/minimal-mesos-go-framework
的文件夹结构,它反映了此回购在 GitHub 中存储的 URI:
mkdir -p $HOME/go
$HOME/go
路径将成为我们$GOPATH
的目的地。我们必须设置一个环境变量,$GOPATH
指向此文件夹。要设置环境变量,请使用您喜爱的文本编辑器再次打开文件$HOME/.bashrc
,并在其末尾添加以下行:
export GOPATH=${HOME}/go
保存文件并打开新的终端。要检查是否一切正常,只需向$GOPATH
变量写入一个回音,如下所示:
echo $GOPATH
/home/mcastro/go
如果前面命令的输出指向您选择的 Go 路径,则一切正常,您可以继续编写第一个程序。
如果没有 Hello World 的例子,这本书就不是一本好书。我们的 Hello World 示例再简单不过了,打开您最喜欢的文本编辑器,在我们的$GOPATH/src/[your_name]/hello_world
中创建一个名为main.go
的文件,内容如下:
package main
func main(){
println("Hello World!")
}
保存文件。要运行我们的程序,请打开操作系统的终端窗口:
cmd
,然后点击输入。terminal
。终端应用程序必须高亮显示,以便按 Enter 键。进入终端后,导航到创建main.go
文件的文件夹。这应该在您的$GOPATH/src/[your_name]/hello_world
下执行:
go run main.go
Hello World!
这就是全部。go run [file]
命令将编译并执行我们的应用程序,但不会生成可执行文件。如果您只想构建应用程序并获取可执行文件,则必须使用以下命令构建应用程序:
go build -o hello_world
什么也没发生。但是如果您在当前目录中搜索(ls
命令在 Linux 和 Mac OS X 中;以及dir
在 Windows 中),您将找到一个名为hello_world
的可执行文件。当我们在构建时编写-o hello_world
命令时,我们将此名称命名为可执行文件。您现在可以执行此文件:
/hello_world
Hello World!
我们的信息出现了!在 Windows 中,您只需键入.exe
文件的名称即可获得相同的结果。
go run [my_main_file.go]
命令将在没有中间文件的情况下构建和执行应用程序。go build -o [filename]
命令将创建一个可执行文件,我可以将其带到任何地方,并且没有依赖项。
IDE(集成开发环境)基本上是一个用户界面,通过提供一组工具来加速开发过程中的常见任务(如编译、构建或管理依赖项),帮助开发人员编写程序。IDE 是强大的工具,需要一些时间才能掌握,本书的目的不是解释它们(像 Eclipse 这样的 IDE 有自己的书)。
在围棋中,你有很多选择,但只有两个完全面向围棋开发LiteIDE和Intellij Gogland。虽然 LiteIDE 不是最强大的,但 Intellij 已经付出了很多努力,使 Gogland 成为一个非常好的编辑器,包括完成、调试、重构、测试、视觉覆盖、检查等。具有 Go 插件/集成的常见 IDE 或文本编辑器如下所示:
但您也可以找到 Go 插件:
在撰写本文时,IntelliJ Idea 和 Atom IDE 支持使用名为Delve的插件进行调试。IntelliJ 理念与官方 Go 插件捆绑在一起。在 Atom 中,您必须下载一个名为Go plus的插件和一个调试器,您可以在搜索单词Delve
时找到它。
类型使用户能够以助记名称存储值。所有编程语言都有与数字(例如存储整数、负数或浮点)与字符(存储单个字符)与字符串(存储完整的单词)等相关的类型。Go 语言具有大多数编程语言中常见的类型:
bool
关键字用于表示True
或False
状态的布尔类型。int
类型表示 32 位机器中从 0 到 4294967295 的数字,以及 64 位机器中从 0 到 18446744073709551615 的数字。byte
类型表示 0 到 255 之间的数字。float32
和float64
类型分别是所有 IEEE-754 64/位浮点数的集合。signed int
类型,比如rune
,它是int32
类型的别名,一个从-2147483648 到 2147483647 的数字,complex64
和complex128
是所有复数的集合,有float32
/float64
实部和虚部,比如2.0i。string
关键字表示包含在引号中的字符数组,如"golang"
或"computer"
。array
是一个单一类型和固定大小的元素编号序列(本章后面将详细介绍数组)。具有固定大小的数字列表或单词列表被视为数组。slice
类型是底层数组的一段(本章后面将对此进行详细介绍)。这种类型在开始时有点混乱,因为它看起来像一个数组,但我们将看到,实际上,它们更强大。interface
对于语言来说非常重要,因为它们提供了我们经常需要的许多封装和抽象功能。在本书中,我们将广泛使用接口,稍后将详细介绍这些接口。map
类型是无序的键值结构。因此,对于给定的键,您有一个关联的值。变量是计算机内存中用来存储在程序执行过程中可以修改的值的空间。变量和常量的类型与前文中描述的类型类似。尽管如此,您不需要显式地编写它们的类型(尽管您可以这样做)。这个避免显式类型声明的属性称为推断类型。例如:
//Explicitly declaring a "string" variable
var explicit string = "Hello, I'm a explicitly declared variable"
这里我们声明一个名为字符串类型的变量(关键字为var
)。同时,我们正在定义Hello World!
的值。
//Implicitly declaring a "string". Type inferred
inferred := ", I'm an inferred variable "
但在这里,我们正在做完全相同的事情。我们避免了var
关键字和string
类型声明。在内部,Go 的编译器将推断(猜测)变量的类型为字符串类型。这样,您必须为每个变量定义编写更少的代码。
以下几行使用reflect
包收集有关变量的信息。我们使用它来打印两个变量的类型(代码中的TypeOf
变量):
fmt.Println("Variable 'explicit' is of type:",
reflect.TypeOf(explicit))
fmt.Println("Variable 'inferred' is of type:",
reflect.TypeOf(inferred))
当我们运行程序时,结果如下:
$ go run main.go
Hello, I'm a explicitly declared variable
Hello, I'm an inferred variable
Variable 'explicit' is of type: string
Variable 'inferred' is of type: string
正如我们所料,编译器也将隐式变量的类型推断为字符串。两者都已将预期输出写入控制台。
运算符用于执行算术运算并在许多事物之间进行比较。以下运算符由 Go 语言保留。
最常用的运算符是算术运算符和比较器。算术运算符如下所示:
+
运算符-
运算符*
运算符/
操作员%
运算符++
运算符将 1 添加到当前变量--
运算符将当前变量减去 1另一方面,比较器用于检查两个语句之间的差异:
==
操作员检查两个值是否相等!=
操作员检查两个值是否不同>
操作员检查左值是否高于右值<
操作员检查左值是否低于右值>=
操作员检查左值是否大于或等于右值<=
操作员检查左值是否小于或等于右值&&
操作员检查两个值是否为true
您还可以使用移位器对某个值执行向左或向右的二进制移位,并使用求反运算符反转某个值。在接下来的章节中,我们将大量使用这些运算符,所以现在不要太担心它们,请记住,您不能像这些运算符那样在代码中设置任何变量、字段或函数的名称。
10 的倒数是多少?10 的负值是多少-10? 不准确的二进制代码中的 10 是1010
,所以如果我们对每个数字求反,我们将得到0101
或101
,这是数字 5。
流控制是指能够决定代码的哪一部分或在某个条件下执行某个代码的次数。在 Go 中,它是使用熟悉的命令式子句实现的,如 if、else、switch 和 for。语法很容易掌握。让我们回顾一下 Go 中的主要流量控制语句。
Go 语言和大多数编程语言一样,具有用于流控制的if…else
条件语句。语法与其他语言类似,但不需要在括号之间封装条件:
ten := 10
if ten == 20 {
println("This shouldn't be printed as 10 isn't equal to 20")
} else {
println("Ten is not equals to 20");
}
else...if
条件以类似的方式工作,您也不需要括号,它们被声明为程序员所期望的:
if "a" == "b" || 10 == 10 || true == false {
println("10 is equal to 10")
} else if 11 == 11 &&"go" == "go" {
println("This isn't print because previous condition was satisfied");
} else {
println("In case no condition is satisfied, print this")
}
}
Go 没有像condition ? true : false
这样的三元条件。
switch
语句也类似于大多数命令式语言。取一个变量并检查其可能的值:
number := 3
switch(number){
case 1:
println("Number is 1")
case 2:
println("Number is 2")
case 3:
println("Number is 3")
}
_for_
循环也类似于普通编程语言,但也不使用括号
for i := 0; i<=10; i++ {
println(i)
}
正如您可能想象的那样,如果您有计算机科学背景,我们推断出一个定义为0
的int
变量,并在满足条件(i<=10
时执行括号之间的代码。最后,对于每次执行,我们在i
的值上加上1
。此代码将打印 0 到 10 之间的数字。您还可以使用一种特殊的语法来迭代数组或切片,即range
:
for index, value := range my_array {
fmt.Printf("Index is %d and value is %d", index, value)
}
首先,fmt
(格式)是一个非常常见的 Go 包,我们将广泛使用它来形成我们将在控制台中打印的消息。
关于 for,您可以使用range
关键字检索集合中的每一项,如my_array
,并将它们分配给 value-temporal 变量。它还将为您提供一个index
变量,以了解正在检索的值的位置。这相当于写以下内容:
for index := 0, index < len(my_array); index++ {
value := my_array[index]
fmt.Printf("Index is %d and value is %d", index, value)
}
len
方法用于了解集合的长度。
如果执行此代码,您将看到结果是相同的。
函数是代码的一小部分,它围绕着要执行的某些操作,并返回一个或多个值(或不返回任何值)。它们是开发人员维护结构、封装和代码可读性的主要工具,但也允许有经验的程序员针对其功能开发适当的单元测试。
函数可以非常简单,也可以非常复杂。通常,您会发现更简单的函数也更易于维护、测试和调试。在计算机科学界也有一个很好的建议:一个函数只能做一件事,但它必须做得非常好。
函数是一段代码,它有自己的变量和流,除了全局包或程序变量外,不会影响开括号和闭括号之外的任何内容。Go 中的函数具有以下组成:
func [function_name] (param1 type, param2 type...) (returned type1, returned type2...) {
//Function body
}
根据前面的定义,我们可以有以下示例:
func hello(message string) error {
fmt.Printf("Hello %s\n", message)
return nil
}
函数可以调用其他函数。例如,在我们前面的hello
函数中,我们接收到一个 string 类型的消息参数,并且我们正在调用另一个以参数为参数的函数fmt.Printf("Hello %s\n", message)
。函数也可以在调用其他函数或返回时用作参数。
为你的函数选择一个好的名字是非常重要的,这样你就可以清楚地知道它是关于什么的,而不用在上面写太多的评论。这看起来有点琐碎,但选择一个好名字并不容易。一个简短的名称必须显示函数的作用,并让读者想象它正在处理什么错误,或者它是否正在进行任何类型的日志记录。在函数中,您希望执行特定行为所需的所有操作,但也希望控制预期错误并正确包装它们。
因此,编写一个函数不仅仅是简单地抛出几行代码来完成您需要的工作,这就是为什么编写一个单元测试非常重要的原因,让它们变得小巧简洁。
匿名函数是没有名称的函数。当您希望从不需要上下文的另一个函数返回函数,或者希望将函数传递给其他函数时,这非常有用。例如,我们将创建一个接受一个数字的函数,并返回一个接受第二个数字并将其添加到第一个数字的函数。第二个函数没有声明性名称(我们已将其分配给变量),这就是为什么称其为匿名函数的原因:
func main(){
add := func(m int){
return m+1
}
result := add(6)
//1 + 6 must print 7
println(result)
}
add
变量指向一个匿名函数,该函数将一个变量添加到指定的参数中。如您所见,它只能用于其父函数main
的作用域,不能从其他任何地方调用。
匿名函数是非常强大的工具,我们将在设计模式中广泛使用。
闭包与匿名函数非常相似,但功能更强大。它们之间的关键区别在于,匿名函数本身没有上下文,而闭包具有上下文。让我们重写上一个示例,添加一个任意数,而不是一个:
func main(){
addN := func(m int){
return func(n int){
return m+n
}
}
addFive := addN(5)
result := addN(6)
//5 + 6 must print 7
println(result)
}
addN
变量指向返回另一个函数的函数。但是返回的函数中有m
参数的上下文。每次对addN
的调用都会创建一个具有固定m
值的新函数,因此我们可以使用主addN
函数,每个函数都会添加不同的值。
闭包的这种能力对于创建库或处理不支持类型的函数非常有用。
错误在 Go 中被广泛使用,这可能是由于它的简单性。要创建一个错误,只需调用errors.New(string)
,调用您要在错误上创建的文本。例如:
err := errors.New("Error example")
如前所述,我们可以将错误返回给函数。要处理错误,您将在 Go 代码中看到以下模式:
func main(){
err := doesReturnError()
if err != nil {
panic(err)
}
}
func doesReturnError() error {
err := errors.New("this function simply returns an error")
return err
}
函数可以声明为可变。这意味着它的参数数量可能会有所不同。这样做的目的是为函数的作用域提供一个数组,其中包含调用函数时使用的参数。如果您不想在使用此函数时强制用户提供数组,则此操作非常方便。例如:
func main() {
fmt.Printf("%d\n", sum(1,2,3))
fmt.Printf("%d\n", sum(4,5,6,7,8))
}
func sum(args ...int) (result int) {
for _, v := range args {
result += v
}
return
}
在本例中,我们有一个sum
函数,它将返回其所有参数的总和,但请仔细查看我们调用sum
的main
函数。正如你现在看到的,首先我们用三个参数调用sum
,然后用五个参数调用。对于sum
函数,传递多少个参数无关紧要,因为它将其参数视为一个数组。因此,在我们的sum
定义中,我们只需迭代数组,将每个数字添加到result
整数中。
您是否意识到我们为返回的类型指定了一个名称?通常,我们的声明会写为func sum(args int) int
,但您也可以命名将在函数中用作返回值的变量。在返回类型中命名变量也将使其为零值(在这种情况下,int
将初始化为零)。最后,您只需要返回函数(不带值),它将从作用域中获取相应的变量作为返回值。这也使得跟踪返回变量所遭受的变异变得更容易,同时也确保您没有返回变异的参数。
数组是应用最广泛的计算机编程类型之一。它们是其他类型的列表,您可以使用它们在列表中的位置来访问它们。数组的唯一缺点是不能修改其大小。切片允许使用大小可变的数组。maps
类型将使我们在 Go 中拥有类似字典的结构。让我们看看它们是如何工作的。
数组是单个类型元素的编号序列。您可以在一个唯一变量、三个字符串或 400 个bool
值中存储 100 个不同的无符号整数。它们的大小不能改变。
必须在创建数组时声明数组的长度以及类型。您还可以在创建时指定一些值。例如,这里有 100 个int
值,所有值均以0
为值:
var arr [100]int
或者已经分配了strings
的大小为 3 的数组:
arr := [3]string{"go", "is", "awesome"}
这里有一个包含 2 个bool
值的数组,我们稍后将对其进行初始化:
var arr [2]bool
arr[0] = true
arr[1] = false
在前面的示例中,我们已经初始化了大小为2
的bool
值的array
。我们不需要将arr[1]
赋值给false
,因为语言中的零初始化性质。Go 将bool
数组中的每个值初始化为false
。我们将在本章后面更深入地研究零初始化。
切片类似于数组,但它们的大小可以在运行时更改。这要归功于作为数组的切片的底层结构。因此,与数组一样,您必须指定切片的类型及其大小。因此,使用以下行创建切片:
mySlice := make([]int, 10)
此命令已创建一个包含十个元素的基础数组。如果我们需要更改切片的大小,例如,添加一个新的数字,我们会将该数字附加到切片:
mySlice := append(mySlice, 5)
append 的语法为([array to append an item to],[item to append]),并返回新切片,它不修改实际切片。删除项目时也是如此。例如,让我们删除数组的第一项,如下所示:
mySlice := mySlice[1:]
是的,就像在数组中一样。但是删除第二项呢?我们使用相同的语法:
mySlice = append(mySlice[:1], mySlice[2:]...)
我们将从零索引(包括)到第一个索引(不包括)的所有元素,以及从第二个索引(包括)到数组末尾的每个元素,有效地删除切片中第二个位置的值(当我们开始用 0 计数时,索引 1)。如您所见,我们使用待定参数语法作为第二个参数。
地图就像字典——对于每个单词,我们都有一个定义,但我们可以使用任何类型作为单词或定义,它们永远不会按字母顺序排列。我们可以创建指向数字的字符串映射,一个指向interfaces
和structs
的字符串,分别指向int
和int
到function
。不能用作关键点:切片、函数和贴图。最后,通过使用关键字 make 并指定键类型和值类型来创建映射:
myMap := make(map[string]int)
myMap["one"] = 1
myMap["two"] = 2
fmt.Println(myMap["one"])
解析 JSON 内容时,您还可以使用它们来获取string[interface]
映射:
myJsonMap := make(map[string]interface{})
jsonData := []byte(`{"hello":"world"}`)
err := json.Unmarshal(jsonData, &myJsonMap)
if err != nil {
panic(err)
}
fmt.Printf("%s\n", myJsonMap["hello"])
myJsonMap
变量是一个将存储 JSON 内容的映射,我们需要将其指针传递给Unmarshal
函数。jsonData
变量声明一个字节数组,其中包含 JSON 对象的典型内容;我们使用它作为模拟对象。然后,我们解组存储myJsonMap
变量内存位置结果的 JSON 内容。在检查转换是否正常并且 JSON 字节数组没有语法错误之后,我们可以使用类似 JSON 的语法访问映射的内容。
可见性是程序不同部分可见的函数或变量的属性。因此,变量只能在声明的函数、整个包或整个程序中使用。
如何设置变量或函数的可见性?好吧,一开始可能会让人困惑,但不能简单得多:
在这里您可以看到一个public
函数的示例:
package hello
func Hello_world(){
println("Hello World!")
}
这里,Hello_world
是一个全局函数(在整个源代码中以及代码的第三方用户可以看到该函数)。因此,如果我们的包被称为hello
,我们可以使用hello.Hello_world()
方法从这个包的外部调用这个函数。
package different_package
import "github.com/sayden/go-design-patters/first_chapter/hello"
func myLibraryFunc() {
hello.Hello_world()
}
如你所见,我们在different_package
包中。我们必须导入要与关键字 import 一起使用的包。然后,路径就是您的$GOPATH/src
中包含我们正在寻找的包的路径。此路径方便地匹配 GitHub 帐户或任何其他并发版本系统(CVS存储库的 URL。
零初始化有时会引起混淆。它们是许多已指定类型的默认值,即使您没有为定义提供值。以下是各种类型的零初始化:
bool
类型的false
初始化。int
类型的0
值。0.0
用于float
类型。""
(空字符串)用于string
类型。nil
关键字。struct
。struct
。结构的零值定义为其字段也初始化为零值的结构。在 Go 中编程时,零初始化非常重要,因为如果必须返回int
类型或struct
,则无法返回nil
值。例如,在必须返回bool
值的函数中,请记住这一点。想象一下,您想知道一个数字是否可以被另一个数字整除,但您将0
(零)作为除数。
func main() {
res := divisibleBy(10,0)
fmt.Printf("%v\n", res)
}
func divisibleBy(n, divisor int) bool {
if divisor == 0 {
//You cannot divide by zero
return false
}
return (n % divisor == 0)
}
该程序的输出为false
,但这是不正确的。一个数字被零除是一个错误,不是 10 不能被零除,而是一个数字不能被定义为零除。在这种情况下,零初始化会使事情变得尴尬。那么,我们如何解决这个错误呢?考虑下面的代码:
func main() {
res, err := divisibleBy(10,0)
if err != nil {
log.Fatal(err)
}
log.Printf("%v\n", res)
}
func divisibleBy(n, divisor int) (bool, error) {
if divisor == 0 {
//You cannot divide by zero
return false, errors.New("A number cannot be divided by zero")
}
return (n % divisor == 0), nil
}
我们再次将10
除以0
,但是现在这个函数的输出是A number cannot be divided by zero
。捕捉到错误后,程序顺利完成。
指针是每个 C 或 C++程序员头痛的头号来源。但它们是在非垃圾收集语言中实现高性能代码的主要工具之一。幸运的是,Go 的指针通过提供具有垃圾收集器功能和易用性的高性能指针,实现了这两个方面的最佳效果。
另一方面,对于它的批评者来说,围棋缺乏对构图的继承。不要谈论在围棋中是的对象,你的对象有其他。因此,与继承类vehicle
(汽车是车辆)的car
结构不同,您可以使用包含car
结构的vehicle
结构。
指针被憎恨、喜爱,同时也非常有用。要理解指针是什么可能很困难,所以让我们尝试一个真实世界的解释。正如我们在本章前面提到的,指针就像邮箱一样。想象一下,一幢大楼里有一堆邮箱;它们都有相同的大小和形状,但每一个都是指建筑物内的不同房屋。仅仅因为所有邮箱的大小都相同,并不意味着每个房子的大小都相同。我们甚至可以把几栋房子连接起来,一栋曾经在那里但现在有商业许可证的房子,或者一栋完全空置的房子。所以指针是邮箱,所有邮箱大小相同,都指向一个房子。建筑是我们的记忆,房子是我们的指针所指的类型和分配的记忆。如果你想在家里收到一些东西,简单地发送你家的地址(发送指针)要容易得多,而不是发送整个房子,这样你的包裹就可以放在里面了。但它们也有一些缺点,比如你发送了地址,你的房子(它所指的变量)在发送后消失了,或者它的类型所有者发生了变化——你会遇到麻烦。
这有什么用?假设一个变量中有 4GB 的数据,需要将其传递给另一个函数。如果没有指针,整个变量将被克隆到将要使用它的函数的作用域。因此,使用这个变量将占用 8GB 的内存两倍,希望第二个函数不会在另一个函数中再次使用,从而进一步提高这个数字。
您可以使用一个指针将这个块的一个非常小的引用传递给第一个函数,这样就只克隆了一个小的引用,并且可以保持较低的内存使用率。
虽然这并不是最具学术性和精确性的解释,但它很好地说明了指针是什么,而没有解释堆栈或堆是什么,或者它们在 x86 体系结构中是如何工作的。
GO 中的指针与 C 或 C++指针相比非常有限。不能使用指针算法,也不能创建引用堆栈中确切位置的指针。
Go 中的指针可以这样声明:
number := 5
这里,number := 5
代码表示我们的 4GB 变量,pointer_to_number
包含对该变量的引用(用一个符号表示)。这是变量的方向(你放在这个house/type/variable
邮箱中的那个)。我们打印变量pointer_to_number
,它是一个简单的变量:
println(pointer_to_number)
0x005651FA
那个号码是多少?好的,指向内存中变量的方向。我如何打印房子的实际价值?好的,我们用星号(*)
告诉编译器获取指针引用的值,这是我们的 4GB 变量。
println(*pointer_to_number)
5
结构是 Go 中的一个对象。它与 OOP 中的类有一些相似之处,因为它们有字段。结构可以实现接口和声明方法。但是,例如,在 Go 中,没有继承。缺乏继承看起来很有限,但事实上,组合而不是继承是语言的要求。
要声明结构,必须在其名称前面加上关键字type
和后缀struct
,然后声明括号之间的任何字段或方法,例如:
type Person struct {
Name string
Surname string
Hobbies []string
id string
}
在这段代码中,我们声明了一个包含三个公共字段(Name
、Age
、和Hobbies
)和一个私有字段(id
)的Person
结构,如果您回想一下本章中的可见性部分,Go 中的小写字段指的是同一个包中的私有字段)。有了这个struct
,我们现在可以创建任意数量的Person
实例。现在我们将编写一个名为GetFullName
的函数,该函数将给出它所属的struct
的名称和姓氏的组成:
func (person *Person) GetFullName() string {
return fmt.Sprintf("%s %s", person.Name, person.Surname)
}
func main() {
p := Person{
Name: "Mario",
Surname: "Castro",
Hobbies: []string{"cycling", "electronics", "planes"},
id: "sa3-223-asd",
}
fmt.Printf("%s likes %s, %s and %s\n", p.GetFullName(), p.Hobbies[0], p.Hobbies[1], p.Hobbies[2])
}
方法的定义与函数类似,但方式略有不同。有一个指向创建的struct
实例的指针(p *Person)
(回想本章中的指针部分)。这就像在 Java 中使用关键字this
或在 Python 中使用关键字self
来引用定点对象一样。
也许你在想为什么(p *Person
有指针操作符来反映p
实际上是指针而不是值?这是因为您还可以通过删除指针签名按值传递 Person,在这种情况下,Person 值的副本将传递给函数。这有一些含义,例如,如果按值传递 p,则在 p 中所做的任何更改都不会反映在源代码p
中。但是我们的GetFullName()
方法呢?
func (person Person) GetFullName() string {
return fmt.Sprintf("%s %s", person.Name, person.Surname)
}
其控制台输出在外观上没有影响,但在评估函数之前传递了完整副本。但是如果我们在这里修改person
,源p
不会受到影响,新的person
值只在这个函数的范围内可用。
在main
函数中,我们创建了一个名为p
的结构实例。如您所见,我们使用隐式表示法来创建变量(:=
符号)。要设置字段,必须参考字段名称、冒号、值和逗号(不要忘记结尾的逗号!)。要访问实例化结构的字段,我们只需按名称引用它们,如p.Name
或p.Surname
。您使用相同的语法访问结构的方法,如p.GetFullName()
。
该程序的输出为:
$ go run main.go
Mario Castro likes cycling, electronics and planes
结构也可以包含另一个结构(组合)并实现除自身之外的接口方法,但是,什么是接口方法?
接口在面向对象编程、函数式编程(traits
中,尤其是在设计模式中,都是必不可少的。Go 的源代码到处都是接口,因为它们提供了在函数的帮助下交付非耦合代码所需的抽象。作为一名程序员,在编写库时,以及在编写将来将使用新功能维护的代码时,也需要这种类型的抽象。
接口在一开始很难掌握,但一旦您了解了它们的行为并为常见问题提供了非常优雅的解决方案,就非常容易了。我们将在本书中广泛使用它们,因此请特别关注本节。
界面是非常简单但功能强大的东西。它通常被定义为实现它的对象之间的契约,但在我看来,对于界面世界的新手来说,这个解释还不够清楚。
水管也是合同;无论你通过什么,它一定是液体。任何人都可以使用管道,管道将输送你放入其中的任何液体(不知道其中的内容)。水管是强制用户必须通过液体(而不是其他东西)的接口。
让我们想想另一个例子:火车。火车的铁路就像一个接口。火车必须用规定的宽度来建造(实现)它的宽度,这样它才能进入铁路,但铁路永远不知道它到底在运载什么(乘客或货物)。例如,铁路的接口将具有以下方面:
type RailroadWideChecker interface {
CheckRailsWidth() int
}
RailroadWideChecker
是我们的列车必须执行的类型,以提供有关其宽度的信息。列车将验证列车是否太宽或太窄,无法使用其铁路:
type Railroad struct {
Width int
}
func (r *Railroad) IsCorrectSizeTrain(r RailRoadWideChecker) bool {
return r.CheckRailsWidth() != r.Width
}
Railroad
由一个虚构的 station 对象实现,该对象包含关于该车站内铁路宽度的信息,并且有一种方法可以使用IsCorrectSizeTrain
方法检查列车是否符合铁路的需要。IsCorrectSizeTrain
方法接收一个接口对象,该对象是一个指向实现该接口的列车的指针,并返回列车和铁路宽度之间的验证:
Type Train struct {
TrainWidth int
}
func (p *Train) CheckRailsWidth() int {
return p.TrainWidth
}
现在我们创造了一辆客运列车。它有一个包含宽度的字段,并实现了我们的CheckRailsWidth
接口方法。这种结构被认为满足了RailRoadWideChecker
接口的需求(因为它实现了接口要求的方法)。
现在,我们将创建一条10
单元宽的铁路和两列火车——一列10
单元宽,适合铁路尺寸,另一列15
单元不能使用铁路。
func main(){
railroad := Railroad{Width:10}
passengerTrain := Train{TrainWidth: 10}
cargoTrain := Train {TrainWidth: 15}
canPassengerTrainPass := railroad.IsCorrectSizeTrain(passengerTrain)
canCargoTrainPass := railroad.IsCorrectSizeTrain(cargoTrain)
fmt.Printf("Can passenger train pass? %b\n", canPassengerTrainPass)
fmt.Printf("Can cargo train pass? %b\n", canCargoTrainPass)
}
让我们分析一下这个main
函数。首先,我们创建了一个名为railroad
的10
单元铁路对象。然后是两列列车,乘客和货物分别为10
和15
单元宽度。然后,我们将这两个对象传递给 Railway 方法,该方法接受RailroadWideChecker
接口的接口。铁路本身并不单独知道每列火车的宽度(我们将有一个庞大的火车列表),但它有一个火车必须实现的接口,以便它可以请求每种宽度,并返回一个值,告诉您火车是否可以使用铁路。最后,调用printf
函数的输出如下:
Can passenger train pass? true
Can cargo train pass? false
正如我前面提到的,在本书中,接口的使用非常广泛,如果读者仍然感到困惑,那也没关系,因为在本书中,它们将是大量的示例。
在编写某些库的第一行时,很难引入许多 bug。但是,一旦源代码变得越来越大,就更容易破坏。团队不断壮大,现在许多人都在编写相同的源代码,新的功能添加到您在开始编写的代码之上。由于对某些函数进行了一些修改,代码停止了工作,现在没有人能够跟踪到这些函数。
这在企业中是一个常见的场景,测试试图减少(它不能完全解决它,它不是圣杯)。当您在开发过程中编写单元测试时,您可以检查某个新特性是否破坏了旧特性,或者您当前的新特性是否实现了需求中预期的一切。
Go 有一个功能强大的测试包,允许您也可以非常轻松地在 TDD 环境中工作。检查代码部分也非常方便,无需编写使用它的整个主应用程序。
测试在每种编程语言中都非常重要。Go 创建者知道这一点,并决定在核心包中提供测试所需的所有库和包。测试或代码覆盖不需要任何第三方库。
允许测试 Go 应用程序的软件包称为 testing(测试)。我们将创建一个小应用程序,将通过命令行提供的两个数字相加:
func main() {
//Atoi converts a string to an int
a, _ := strconv.Atoi(os.Args[1])
b, _ := strconv.Atoi(os.Args[2])
result := sum(a,b)
fmt.Printf("The sum of %d and %d is %d\n", a, b, result)
}
func sum(a, b int) int {
return a + b
}
让我们在终端中执行我们的程序以获得总和:
$ go run main.go 3 4
The sum of 3 and 4 is 7
顺便说一下,我们正在使用strconv
包将字符串转换为其他类型,在本例中,转换为int
。方法Atoi
接收一个字符串并返回一个int
和一个error
,为了简单起见,我们在这里忽略了它(使用下划线)。
如果需要,可以使用下划线忽略变量返回,但通常不希望忽略错误。
好的,让我们编写一个测试来检查和的正确结果。我们正在创建一个名为main_test.go
的新文件。按照惯例,测试文件的命名与他们正在测试的文件一样,加上_test
后缀:
func TestSum(t *testing.T) {
a := 5
b := 6
expected := 11
res := sum(a, b)
if res != expected {
t.Errorf("Our sum function doens't work, %d+%d isn't %d\n", a, b, res)
}
}
Go 中的测试由以前缀Test
、测试名称和注入名为t
的testing.T
指针开始的写入方法使用。与其他语言不同,Go 中没有用于测试的断言或特殊语法。您可以使用 Go 语法检查错误,并调用t
以获取错误信息,以防失败。如果代码到达Test
函数末尾时没有出现错误,则该函数已通过测试。
要在 Go 中运行测试,必须使用go test -v
命令(-v
用于接收测试的详细输出)关键字,如下所示:
$ go test -v
=== RUN TestSum
--- PASS: TestSum (0.00s)
PASS
ok github.com/go-design-patterns/introduction/ex_xx_testing 0.001s
我们的测试是正确的。让我们看看如果我们故意破坏东西,并将测试的预期值从11
更改为10
会发生什么:
$ go test
--- FAIL: TestSum (0.00s)
main_test.go:12: Our sum function doens't work, 5+6 isn't 10
FAIL
exit status 1
FAIL github.com/sayden/go-design-patterns/introduction/ex_xx_testing 0.002s
测试失败了(正如我们预料的那样)。测试包提供您在测试中设置的信息。让我们再次让它工作并检查测试覆盖率。再次将变量expected
的值从10
改为11
,运行命令go test -cover
查看代码覆盖率:
$ go test -cover
PASS
coverage: 20.0% of statements
ok github.com/sayden/go-design-patterns/introduction/ex_xx_testing 0.001s
-cover
选项为我们提供了关于给定包的代码覆盖率的信息。不幸的是,它没有提供有关整个应用程序覆盖范围的信息。
TDD 是测试驱动开发的首字母缩写。它包括在编写函数之前先编写测试(而不是之前先编写sum
函数,然后再编写test
函数)。
TDD 改变了编写代码和结构代码的方式,因此可以对其进行测试(在 GitHub 中可以找到很多代码,即使是您可能在过去编写的代码,也很难进行测试)。
那么,它是如何工作的呢?让我们用一个真实的例子来解释这一点——假设你在夏天,你想以某种方式得到提神。你可以建一个水池,用冷水灌满,然后跳进里面。但就 TDD 而言,步骤将是:
让我们重复上一个例子,但使用乘法。首先,我们将编写要测试的函数声明:
func multiply(a, b int) int {
return 0
}
现在,让我们编写一个测试来检查前面函数的正确性:
import "testing"
func TestMultiply(t *testing.T) {
a := 5
b := 6
expected := 30
res := multiply(a, b)
if res != expected {
t.Errorf("Our multiply function doens't work, %d*%d isn't %d\n", a, b, res)
}
}
我们通过命令行进行测试:
$ go test
--- FAIL: TestMultiply (0.00s)
main_test.go:12: Our multiply function doens't work, 5+6 isn't 0
FAIL
exit status 1
FAIL github.com/sayden/go-designpatterns/introduction/ex_xx_testing/multiply
0.002s
美好的就像我们的水池示例中还没有水一样,我们的函数也返回了一个不正确的值。现在我们有了一个函数声明(但尚未定义)和失败的测试。现在,我们必须通过编写函数并执行测试来检查,从而使测试通过:
func multiply(a, b int) int {
return a*b
}
我们再次执行我们的测试套件。正确编写代码后,测试应通过,以便继续执行折射过程:
$ go test
PASS
ok github.com/sayden/go-design-patterns/introduction/ex_xx_testing/multiply
0.001s
伟大的我们在 TDD 之后开发了multiply
功能。现在我们必须重构我们的代码,但我们不能使它更简单或更可读,这样循环就可以被认为是闭合的。
在本书中,我们将编写许多测试来定义我们希望在模式中实现的功能。TDD 促进了封装和抽象(就像设计模式一样)。
到目前为止,我们的大多数示例都是应用程序。应用程序由其main
函数和包定义。但是使用 Go,您还可以创建纯库。在库中,包不需要调用 main,也不需要main
函数。
由于库不是应用程序,您无法使用它们构建二进制文件,您需要使用它们的main
包。
例如,让我们创建一个算术库来对整数执行常见的运算:和、减、乘和除。我们将不深入讨论有关实现的许多细节,而将重点放在 Go 库的特殊性上:
package arithmetic
func Sum(args ...int) (res int) {
for _, v := range args {
res += v
}
return
}
首先,我们需要为我们的图书馆命名;我们通过给整个包命名来设置这个名称。这意味着此文件夹中的每个文件也必须具有此包名,并且在本例中,整个文件组也构成了名为算术的库(因为它只包含一个包)。这样,我们就不需要引用这个库的文件名,提供库名和路径就足以导入和使用它了。我们已经定义了一个Sum
函数,它接受您需要的任意多个参数,并将返回一个整数,在函数的作用域中,该整数将被调用res
。这允许我们初始化为返回的值0
。我们定义了一个包(不是main
包,而是一个库包),并将其命名为arithmetic
。由于这是一个库包,我们不能直接从命令行运行它,因此我们必须为它创建main
函数或单元测试文件。为简单起见,我们将创建一个main
函数,该函数现在运行一些操作,但让我们先完成库:
func Subtract(args ...int) int {
if len(args) < 2 {
return 0
}
res := args[0]
for i := 1; i < len(args); i++ {
res -= args[i]
}
return res
}
如果参数数量小于零,Subtraction
代码将返回0
,如果有两个或更多参数,Subtraction
代码将返回所有参数的减法:
func Multiply(args ...int) int {
if len(args) < 2 {
return 0
}
res := 1
for i := 0; i < len(args); i++ {
res *= args[i]
}
return res
}
Multiply
函数以类似的方式工作。当参数少于两个时返回0
,当参数有两个或两个以上时返回所有参数的乘积。最后,Division
代码会有一点变化,因为如果您要求它除以零,它将返回一个错误:
func Divide(a, b int) (float64, error) {
if b == 0 {
return 0, errors.New("You cannot divide by zero")
}
return float64(a) / float64(b), nil
}
现在我们已经完成了我们的库,但是我们需要一个main
函数来使用它,因为库不能直接转换为可执行文件。我们的主要功能如下所示:
package main
import (
"fmt"
"bitbucket.org/mariocastro/go-design-patterns/introduction/libraries/arithmetic"
)
func main() {
sumRes := arithmetic.Sum(5, 6)
subRes := arithmetic.Subtract(10, 5)
multiplyRes := arithmetic.Multiply(8, 7)
divideRes, _ := arithmetic.Divide(10, 2)
fmt.Printf("5+6 is %d. 10-5 is %d, 8*7 is %d and 10/2 is %f\n", sumRes, subRes, multiplyRes, divideRes)
}
我们正在对定义的每个函数执行操作。仔细看看import
条款。它从$GOPATH
中的文件夹中获取我们编写的库,该文件夹与中的 URL 相匹配 https://bitbucket.org/ 。然后,要使用库中定义的每一个函数,必须在每个方法之前命名库的包名。
您是否意识到我们使用大写名称调用函数?由于我们以前看到的可见性规则,包中导出的函数必须具有大写名称,否则在包的作用域之外它们将不可见。因此,记住这条规则,您不能在包内调用小写函数或变量,包调用后总是跟在大写名称后面。
让我们回顾一下关于库的一些命名约定:
Go get 是从 CVS 存储库获取第三方项目的工具。您可以使用 Go-get 来获得一系列额外的好处,而不是使用git clone
命令。让我们使用 CoreOS 的ETCD项目编写一个示例,这是一个著名的分布式键值存储。
CoreOS 的 ETCD 托管在 GitHub 上的https://github.com/coreos/etcd.git 。要使用 Go get 工具下载此项目源代码,我们必须在终端中键入其结果导入路径,该路径将包含在我们的 GOPATH 中:
$ go get github.com/coreos/etcd
请注意,我们刚刚输入了最相关的信息,以便 Go 了解其余信息。您将获得一些输出,这取决于项目的状态,但之后,它将消失。但是发生了什么?
$GOPATH/src/github.com/coreos
中创建了一个文件夹。$GOPATH/src/github.com/coreos/etcd
上找到。$GOPATH/bin
文件夹中。只需输入go get [project]
命令,您就可以从系统中的项目中获得所有材料。然后,在 Go 应用程序中,您可以通过导入源中的路径来使用任何库。因此,对于 ETCD 项目,它将是:
import "github.com/coreos/etcd"
当您想要从 Git 存储库中获取项目时,熟悉 Go-get 工具的使用并停止使用git clone
是非常重要的。这将为您在尝试导入不包含在 GOPATH 中的项目时节省一些麻烦。
JSON 是JavaScript 对象表示法的首字母缩略词,顾名思义,它是本机 JavaScript。它已经变得非常流行,是当今最常用的交流方式。Go 非常支持 JSON 序列化/反序列化,它使用JSON
包为您完成了大部分脏活。首先,使用 JSON 时需要学习两个概念:
map[string]interface{}
来解释数据,我们现在将看到。让我们看一个封送字符串的示例:
import (
"encoding/json"
"fmt"
)
func main(){
packt := "packt"
jsonPackt, ok := json.Marshal(packt)
if !ok {
panic("Could not marshal object")
}
fmt.Println(string(jsonPackt))
}
$ "pack"
首先,我们定义了一个名为packt
的变量来保存packt
字符串的内容。然后,我们使用了json
库将Marshal
命令用于我们的新变量。这将返回一个新的带有 JSON 和标志的bytearray
,以提供和boolOK
操作结果。当我们打印字节数组的内容(之前转换为字符串)时,会出现预期值。注意,packt
实际上出现在引号之间,就像 JSON 表示一样。
您是否意识到我们已经进口了encoding/json
包装?为什么它的前缀是encoding
?如果您查看 Go 在src/encoding
文件夹中的源代码,您会发现许多有趣的编码/解码包,如 XML、十六进制、二进制甚至 CSV。
现在有更复杂的事情:
type MyObject struct {
Number int
`json:"number"`
Word string
}
func main(){
object := MyObject{5, "Packt"}
oJson, _ := json.Marshal(object)
fmt.Printf("%s\n", oJson)
}
$ {"Number":5,"Word":"Packt"}
方便的是,它也可以很好地处理结构,但是如果我不想在 JSON 数据中使用大写怎么办?您可以在结构声明中定义 JSON 的输出/输入名称:
type MyObject struct {
Number int
Word string
}
func main(){
object := MyObject{5, "Packt"}
oJson, _ := json.Marshal(object)
fmt.Printf("%s\n", oJson)
}
$ {"number":5,"string":"Packt"}
我们不仅将键的名称小写,甚至还将Word
键的名称更改为字符串。
编组已经足够了,我们将以字节数组的形式接收 JSON 数据,但过程非常类似,有一些变化:
type MyObject struct {
Number int`json:"number"`
Word string`json:"string"`
}
func main(){
jsonBytes := []byte(`{"number":5, "string":"Packt"}`)
var object MyObject
err := json.Unmarshal(jsonBytes, &object)
if err != nil {
panic(err)
}
fmt.Printf("Number is %d, Word is %s\n", object.Number, object.Word)
}
这里最大的区别是,您必须首先为结构分配空间(使用零值),然后将引用传递给方法Unmarshal
,以便它尝试填充它。当您使用Unmarshal
时,第一个参数是包含 JSON 信息的字节数组,而第二个参数是要填充的结构的引用(这就是为什么我们使用了符号)。最后,让我们使用一个通用的map[string]interface{}
方法来保存 JSON 的内容:
type MyObject struct {
Number int `json:"number"`
Word string `json:"string"`
}
func main(){
jsonBytes := []byte(`{"number":5, "string":"Packt"}`)
var dangerousObject map[string]interface{}
err := json.Unmarshal(jsonBytes, &dangerousObject)
if err != nil {
panic(err)
}
fmt.Printf("Number is %d, ", dangerousObject["number"])
fmt.Printf("Word is %s\n", dangerousObject["string"])
fmt.Printf("Error reference is %v\n",
dangerousObject["nothing"])
}
$ Number is %!d(float64=5), Word is Packt
Error reference is <nil>
结果怎么样?这就是为什么我们把这个物体描述为危险的。如果调用 JSON 中不存在的键,则使用此模式时可以指向nil
位置。不仅如此,就像在本例中一样,它还可以将一个值解释为一个float64
,而它只是一个byte
,浪费了大量内存。
因此,当您需要快速访问非常简单的 JSON 数据时,请记住只使用map[string]interface{}
,并且您可以控制前面描述的场景类型。
Go 提供了一系列有用的工具来简化每天的开发过程。同样在 GitHub 的 golang 页面中,有一些工具是 Go 团队支持的,但它们不是编译器的一部分。
大多数项目都使用诸如gofmt
之类的工具,因此所有代码库看起来都很相似。Godoc 帮助我们在 Go 的文档中找到有用的信息,并使用goimport
命令自动导入我们正在使用的软件包。让我们看看。
linter 分析源代码以检测错误或改进。golint
短绒可在上使用 https://github.com/golang/lint 用于安装(不附带编译器)。它非常易于使用,并且集成了一些 IDE,以便在保存源代码文件(例如 Atom 或 Sublime 文本)时运行。您还记得我们在讨论变量时运行的隐式/显式代码吗?让我们用皮棉把它粘起来:
//Explicitly declaring a "string" variable
var explicit string = "Hello, I'm a explicitly declared variable"
//Implicitly declaring a "string".
Type inferred inferred := ", I'm an inferred variable "
$ golint main.go
main.go:10:21:
命令应该在explicitString
变量的声明中省略类型字符串;它将从右侧推断。
它告诉我们 Go 编译器实际上会从代码中推断出这种类型的变量,而您不需要声明它的类型。接口部分的Train
类型如何?
Type Train struct {
TrainWidth int
}
$ golint main.go
导出的main.go:5:6:
类型Train
类型应具有注释或保持不导出状态。
在本例中,它向我们指出必须对Train
类型之类的公共类型进行注释,以便用户可以阅读生成的文档以了解其行为。
gofmt
工具与已经可以访问它的编译器捆绑在一起。它的目的是提供一组缩进、格式、间距和其他一些规则,以实现美观的 Go 代码。例如,让我们以 Hello World 的代码为例,通过在所有地方插入空格使其更为奇怪:
package main
func main(){
println("Hello World!")
}
$ gofmt main.go
package main
func main() {
println("Hello World!")
}
gofmt
命令再次正确打印。此外,我们还可以使用-w
标志覆盖原始文件:
$ gofmt -w main.go
现在我们将正确地更正我们的文件。
Go 文档非常冗长。你可以找到关于你想要实现的任何主题的详细信息。godoc
工具还可以帮助您直接从命令行访问此文档。例如,我们可以查询包encoding/json
:
$godoc cmd/encoding/json
[...]
FUNCTIONS
func Compact(dst *bytes.Buffer, src []byte) error
Compact appends to dst the JSON-encoded src with insignificant space
characters elided.
func HTMLEscape(dst *bytes.Buffer, src []byte)
[...]
您还可以使用grep(一个用于 Linux 和 Mac 的 bash 实用程序)来查找有关某些功能的特定信息。例如,我们将使用 grep 查找涉及解析 JSON 文件的任何内容的文本:
$ godoc cmd/encoding/json | grep parse
Unmarshal
命令解析 JSON 编码的数据,并将结果存储在正在解析的对象中。
golint
命令警告的一件事是使用注释开头与其描述的函数同名。这样,如果您不记得解析 JSON 的函数名,您可以使用godoc
和grep
并搜索parse
,因此行首将始终是函数名,如Unmarshal
命令前面的示例所示。
goimport
工具是围棋中的必备工具。有时,您可以很好地记住您的包,不需要搜索太多就可以记住它们的 API,但在执行导入时更难记住它们所属的项目。goimport
命令帮助您搜索$GOPATH
中出现的包,您可以使用该包自动向您提供项目import
行。如果您将 IDE 配置为在保存时运行goimport
,以便在使用源文件中所有使用过的包时自动导入,则这非常有用。它的工作方式也是相反的——如果你从一个包中删除了你正在使用的函数,而这个包已经不再使用了,它将删除import
行。
关于 Go 打包系统,需要提到的一件重要事情是,它需要在 GOPATH 中具有适当的文件夹结构。这在使用 GitHub 项目时会带来一个小问题。我们习惯于分叉一个项目,克隆我们的分叉,并在将请求提交到原始项目之前开始工作。错误的
当您分叉一个项目时,您将在您的用户名内的 GitHub 上创建一个新的存储库。如果克隆此存储库并开始使用它,则项目中的所有新导入引用都将指向您的存储库,而不是原始存储库!想象一下原始存储库中的以下情况:
package main
import "github.com/original/a_library"
[some code]
然后,制作一个 fork 并添加一个子文件夹,其中包含一个名为a_library/my_library
的库,您希望从主包中使用该库。结果如下:
package main
import (
"github.com/original/a_library"
"github.com/myaccount/a_library/my_library"
)
现在,如果您提交这一行,包含您推送的代码的原始存储库将再次从您的帐户下载此代码,并且它将使用下载的引用!不是项目中包含的那些!
因此,解决这个问题的方法就是用一个指向原始库的go get
命令替换git clone
命令:
$ go get github.com/original/a_library
$ cd $GOPATH/src/github.com/original/a_library
$ git remote add my_origin https://github.com/myaccount/a_libbrary
通过这种修改,您可以在原始代码中正常工作,而不用担心引用会保持正确。完成后,您只需提交并推送到远程服务器。
$ git push my_origin my_brach
通过这种方式,您现在可以访问 GitHub web 用户界面并打开 pull 请求,而不会因引用您的帐户而污染实际的原始代码。
在第一章之后,您必须熟悉 Go 的语法以及编译器附带的一些命令行工具。我们将并发功能留到后面的章节中,因为它们在一开始就非常复杂,因此读者可以先学习该语言的语法,熟悉它并对它充满信心,然后他们可以开始理解通信顺序过程(CSP)并发模式和分布式应用程序。接下来的步骤是从创造性设计模式开始。**