代码文件 main.go 里也有一个init函数,在第 12 行到第 15 行中声明,如代码清单 2-6 所示。
代码清单 2-6 main.go:第 11 行到第 15 行
11 // init在main之前调用
12 func init() {
13 // 将日志输出到标准输出
14 log.SetOutput(os.Stdout)
15 }
程序中每个代码文件里的init函数都会在main函数执行前调用。这个init函数将标准库里日志类的输出,从默认的标准错误(stderr),设置为标准输出(stdout)设备。在第 7 章,我们会进一步讨论log包和标准库里其他重要的包。
最后,让我们看看main函数第 20 行那条语句的作用,如代码清单 2-7 所示。
代码清单 2-7 main.go:第 19 行到第 20 行
19 // 使用特定的项做搜索
20 search.Run("president")
可以看到,这一行调用了search包里的Run函数。这个函数包含程序的核心业务逻辑,需要传入一个字符串作为搜索项。一旦Run函数退出,程序就会终止。
现在,让我们看看search包里的代码。
2.3 search包
这个程序使用的框架和业务逻辑都在search包里。这个包由 4 个不同的代码文件组成,每个文件对应一个独立的职责。我们会逐步分析这个程序的逻辑,到时再说明各个代码文件的作用。
由于整个程序都围绕匹配器来运作,我们先简单介绍一下什么是匹配器。这个程序里的匹配器,是指包含特定信息、用于处理某类数据源的实例。在这个示例程序中有两个匹配器。框架本身实现了一个无法获取任何信息的默认匹配器,而在matchers包里实现了 RSS 匹配器。RSS 匹配器知道如何获取、读入并查找 RSS 数据源。随后我们会扩展这个程序,加入能读取 JSON 文档或 CSV 文件的匹配器。我们后面会再讨论如何实现匹配器。
2.3.1 search.go
代码清单 2-8 中展示的是 search.go 代码文件的前 9 行代码。之前提到的Run函数就在这个文件里。
代码清单 2-8 search/search.go:第 01 行到第 09 行
01 package search
02
03 import (
04 "log"
05 "sync"
06 )
07
08 // 注册用于搜索的匹配器的映射
09 var matchers = make(map[string]Matcher)
可以看到,每个代码文件都以package关键字开头,随后跟着包的名字。文件夹search下的每个代码文件都使用search作为包名。第 03 行到第 06 行代码导入标准库的log和sync包。与第三方包不同,从标准库中导入代码时,只需要给出要导入的包名。编译器查找包的时候,总是会到GOROOT和GOPATH环境变量(如代码清单 2-9 所示)引用的位置去查找。
代码清单 2-9 GOROOT和GOPATH环境变量
GOROOT="/Users/me/go"
GOPATH="/Users/me/spaces/go/projects" log 包提供打印日志信息到标准输出(stdout)、标准错误(stderr)或者自定义设备的
功能。sync包提供同步 goroutine 的功能。这个示例程序需要用到同步功能。第 09 行是全书第一次声明一个变量,如代码清单 2-10 所示。
代码清单 2-10 search/search.go:第 08 行到第 09 行
08 // 注册用于搜索的匹配器的映射
09 var matchers = make(map[string]Matcher)
这个变量没有定义在任何函数作用域内,所以会被当成包级变量。这个变量使用关键字var 声明,而且声明为Matcher类型的映射(map),这个映射以string类型值作为键,Matcher 类型值作为映射后的值。Matcher类型在代码文件 matcher.go 中声明,后面再讲这个类型的用途。这个变量声明还有一个地方要强调一下:变量名matchers是以小写字母开头的。
在 Go 语言里,标识符要么从包里公开,要么不从包里公开。当代码导入了一个包时,程序可以直接访问这个包中任意一个公开的标识符。这些标识符以大写字母开头。以小写字母开头的标识符是不公开的,不能被其他包中的代码直接访问。但是,其他包可以间接访问不公开的标识符。例如,一个函数可以返回一个未公开类型的值,那么这个函数的任何调用者,哪怕调用者不是在这个包里声明的,都可以访问这个值。
这行变量声明还使用赋值运算符和特殊的内置函数make初始化了变量,如代码清单 2-11 所示。
代码清单 2-11 构建一个映射
make(map[string]Matcher)
map是 Go 语言里的一个引用类型,需要使用make来构造。如果不先构造map并将构造后的值赋值给变量,会在试图使用这个map变量时收到出错信息。这是因为map变量默认的零值是nil。在第 4 章我们会进一步了解关于映射的细节。
在 Go 语言中,所有变量都被初始化为其零值。对于数值类型,零值是0;对于字符串类型,零值是空字符串;对于布尔类型,零值是false;对于指针,零值是nil。对于引用类型来说,所引用的底层数据结构会被初始化为对应的零值。但是被声明为其零值的引用类型的变量,会返回nil作为其值。
现在,让我们看看之前在main函数中调用的Run函数的内容,如代码清单 2-12 所示。
代码清单 2-12 search/search.go:第 11 行到第 57 行
11 // Run执行搜索逻辑
12 func Run(searchTerm string) {
13 // 获取需要搜索的数据源列表
14 feeds, err := RetrieveFeeds()
15 if err != nil {
16 log.Fatal(err)
17 }
18
19 // 创建一个无缓冲的通道,接收匹配后的结果
20 results := make(chan *Result)
21
22 // 构造一个waitGroup,以便处理所有的数据源
23 var waitGroup sync.WaitGroup 24
25 // 设置需要等待处理
26 // 每个数据源的goroutine的数量
27 waitGroup.Add(len(feeds))
28
29 // 为每个数据源启动一个goroutine来查找结果
30 for _, feed := range feeds {
31 // 获取一个匹配器用于查找
32 matcher, exists := matchers[feed.Type] 33 if !exists {
34 matcher = matchers["default"]
35 }
36
37 // 启动一个goroutine来执行搜索
38 go func(matcher Matcher, feed *Feed) {
39 Match(matcher, feed, searchTerm, results)
40 waitGroup.Done()
41 }(matcher, feed)
42 }
43
44 // 启动一个goroutine来监控是否所有的工作都做完了 45 go func() {
46 // 等候所有任务完成
47 waitGroup.Wait()
48
49 // 用关闭通道的方式,通知Display函数
50 // 可以退出程序了
51 close(results)
52 }()
53
54 // 启动函数,显示返回的结果,并且
55 // 在最后一个结果显示完后返回
56 Display(results)
57 }
Run函数包括了这个程序最主要的控制逻辑。这段代码很好地展示了如何组织 Go 程序的代码,以便正确地并发启动和同步 goroutine。先来一步一步考察整个逻辑,再考察每步实现代码的细节。
先来看看Run函数是怎么定义的,如代码清单 2-13 所示。
代码清单 2-13 search/search.go:第 11 行到第 12 行
11 // Run 执行搜索逻辑
12 func Run(searchTerm string) {
Go 语言使用关键字func声明函数,关键字后面紧跟着函数名、参数以及返回值。对于Run 这个函数来说,只有一个参数,是string类型的,名叫searchTerm。这个参数是Run函数要搜索的搜索项,如果回头看看main函数(如代码清单 2-14 所示),可以看到如何传递这个搜索项。