快速开始一个 Go 程序

为了能更高效地使用语言进行编码,Go 语言有自己的哲学和编程习惯。Go 语言的设计者们从编程效率出发设计了这门语言,但又不会丢掉访问底层程序结构的能力。设计者们通过一组最少的关键字、内置的方法和语法,最终平衡了这两方面。Go 语言也提供了完善的标准库。标准库提供了构建实际的基于 Web 和基于网络的程序所需的所有核心库。

让我们通过一个完整的 Go 语言程序,来看看 Go 语言是如何实现这些功能的。这个程序实现的功能很常见,能在很多现在开发的 Go 程序里发现类似的功能。这个程序从不同的数据源拉取数据,将数据内容与一组搜索项做对比,然后将匹配的内容显示在终端窗口。这个程序会读取文本文件,进行网络调用,解码 XML 和 JSON 成为结构化类型数据,并且利用Go语言的并发机制保证这些操作的速度。

读者可以下载本章的代码,用自己喜欢的编辑器阅读。代码存放在这个代码库 >> https://github.com/goinaction/code/tree/master/chapter2/sample

没必要第一次就读懂本章的所有内容,可以多读两遍。在学习时,虽然很多现代语言的概念可以对应到 Go 语言中,Go 语言还是有一些独特的特性和风格。如果放下已经熟悉的编程语言,用一种全新的眼光来审视 Go 语言,你会更容易理解并接受 Go 语言的特性,发现 Go 语言的优雅。

程序架构

在深入代码之前,让我们看一下程序的架构(如图 1-1 所示),看看如何在所有不同的数据源中搜索数据。

640?wx_fmt=png

图1-1 程序架构流程图

这个程序分成多个不同步骤,在多个不同的 goroutine 里运行。我们会根据流程展示代码,从主 goroutine 开始,一直到执行搜索的 goroutine 和跟踪结果的 goroutine,最后回到主 goroutine。首先来看一下整个项目的结构,如代码清单 1-1 所示。

代码清单 1-1 应用程序的项目结构

 
 

cd $GOPATH/src/github.com/goinaction/code/chapter2

- sample
- data
data.json -- 包含一组数据源
- matchers
rss.go -- 搜索rss源的匹配器
- search
default.go -- 搜索数据用的默认匹配器
feed.go -- 用于读取json数据文件
match.go -- 用于支持不同匹配器的接口
search.go -- 执行搜索的主控制逻辑
main.go -- 程序的入口

这个应用的代码使用了 4 个文件夹,按字母顺序列出。文件夹 data 中有一个 JSON 文档,其内容是程序要拉取和处理的数据源。文件夹 matchers 中包含程序里用于支持搜索不同数据源的代码。目前程序只完成了支持处理 RSS 类型的数据源的匹配器。文件夹 search 中包含使用不同匹配器进行搜索的业务逻辑。最后,父级文件夹 sample 中有个 main.go 文件,这是整个程序的入口。

现在了解了如何组织程序的代码,可以继续探索并了解程序是如何工作的。让我们从程序的入口开始。

main

程序的主入口可以在 main.go 文件里找到,如代码清单 1-2 所示。虽然这个文件只有 21 行代码,依然有几点需要注意。

代码清单 1-2 main.go

 
 

01 package main
02
03 import (
04 "log"
05 "os"
06
07 _ "github.com/goinaction/code/chapter2/sample/matchers"
08 "github.com/goinaction/code/chapter2/sample/search"
09 )
10
11 // init在main之前调用
12 func init() {
13 // 将日志输出到标准输出
14 log.SetOutput(os.Stdout)
15 }
16
17 // main 是整个程序的入口
18 func main() {
19 // 使用特定的项做搜索
20 search.Run("president")
21 }

每个可执行的 Go 程序都有两个明显的特征。一个特征是第 18 行声明的名为main的函数。构建程序在构建可执行文件时,需要找到这个已经声明的main函数,把它作为程序的入口。第二个特征是程序的第 01 行的包名main,如代码清单 1-3 所示。

代码清单 1-3 main.go:第 01 行

 
 

01 package main

可以看到,main函数保存在名为main的包里。如果main函数不在main包里,构建工具就不会生成可执行的文件。

Go 语言的每个代码文件都属于一个包,main.go 也不例外。包这个特性对于 Go 语言来说很重要,我们会在下一章中接触到更多细节。现在,只要简单了解以下内容:一个包定义一组编译过的代码,包的名字类似命名空间,可以用来间接访问包内声明的标识符。这个特性可以把不同包中定义的同名标识符区别开。

现在,把注意力转到 main.go 的第 03 行到第 09 行,如代码清单 1-4 所示,这里声明了所有的导入项。

代码清单 1-4 main.go:第 03 行到第 09 行

 
 

03 import (
04 "log"
05 "os"
06
07 _ "github.com/goinaction/code/chapter2/sample/matchers"
08 "github.com/goinaction/code/chapter2/sample/search"
09 )

顾名思义,关键字import就是导入一段代码,让用户可以访问其中的标识符,如类型、函数、常量和接口。在这个例子中,由于第 08 行的导入,main.go 里的代码就可以引用search包里的Run函数。程序的第 04 行和第 05 行导入标准库里的logos包。

所有处于同一个文件夹里的代码文件,必须使用同一个包名。按照惯例,包和文件夹同名。就像之前说的,一个包定义一组编译后的代码,每段代码都描述包的一部分。如果回头去看看代码清单 1-1,可以看看第 08 行的导入是如何指定那个项目里名叫search的文件夹的。

读者可能注意到第 07 行导入matchers包的时候,导入的路径前面有一个下划线,如代码清单 1-5 所示。

代码清单 1-5 main.go:第 07 行

 
 

07 _ "github.com/goinaction/code/chapter2/sample/matchers"

这个技术是为了让 Go 语言对包做初始化操作,但是并不使用包里的标识符。为了让程序的可读性更强,Go 编译器不允许声明导入某个包却不使用。下划线让编译器接受这类导入,并且调用对应包内的所有代码文件里定义的init函数。对这个程序来说,这样做的目的是调用matchers包中的 rss.go 代码文件里的init函数,注册 RSS 匹配器,以便后用。我们后面会展示具体的工作方式。

代码文件 main.go 里也有一个init函数,在第 12 行到第 15 行中声明,如代码清单 1-6 所示。

代码清单 1-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 行那条语句的作用,如代码清单 1-7 所示。

代码清单 1-7 main.go:第 19 行到第 20 行

 
 

19 // 使用特定的项做搜索
20 search.Run("president")

可以看到,这一行调用了search包里的Run函数。这个函数包含程序的核心业务逻辑,需要传入一个字符串作为搜索项。一旦Run函数退出,程序就会终止。

现在,让我们看看search包里的代码。

search

这个程序使用的框架和业务逻辑都在search包里。这个包由4个不同的代码文件组成,每个文件对应一个独立的职责。我们会逐步分析这个程序的逻辑,到时再说明各个代码文件的作用。

由于整个程序都围绕匹配器来运作,我们先简单介绍一下什么是匹配器。这个程序里的匹配器,是指包含特定信息、用于处理某类数据源的实例。在这个示例程序中有两个匹配器。框架本身实现了一个无法获取任何信息的默认匹配器,而在matchers包里实现了 RSS 匹配器。 RSS 匹配器知道如何获取、读入并查找 RSS 数据源。随后我们会扩展这个程序,加入能读取 JSON 文档或 CSV 文件的匹配器。我们后面会再讨论如何实现匹配器。

search.go

代码清单 1-8 中展示的是 search.go 代码文件的前 9 行代码。之前提到的Run函数就在这个文件里。

代码清单 1-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 行代码导入标准库的logsync包。

与第三方包不同,从标准库中导入代码时,只需要给出要导入的包名。编译器查找包的时候,总是会到GOROOTGOPATH环境变量(如代码清单 1-9 所示)引用的位置去查找。

代码清单 1-9 GOROOTGOPATH环境变量

 
 

GOROOT="/Users/me/go"
GOPATH="/Users/me/spaces/go/projects"

log包提供打印日志信息到标准输出(stdout)、标准错误(stderr)或者自定义设备的功能。sync包提供同步 goroutine 的功能。这个示例程序需要用到同步功能。第 09 行是全书第一次声明一个变量,如代码清单 1-10 所示。

代码清单 1-10 search/search.go:第 08 行到第 09 行

 
 

08 // 注册用于搜索的匹配器的映射
09 var matchers = make(map[string]Matcher)

这个变量没有定义在任何函数作用域内,所以会被当成包级变量。这个变量使用关键字var声明,而且声明为Matcher类型的映射(map),这个映射以string类型值作为键,Matcher类型值作为映射后的值。Matcher类型在代码文件 matcher.go 中声明,后面再讲这个类型的用途。这个变量声明还有一个地方要强调一下:变量名matchers是以小写字母开头的。

在 Go 语言里,标识符要么从包里公开,要么不从包里公开。当代码导入了一个包时,程序可以直接访问这个包中任意一个公开的标识符。这些标识符以大写字母开头。以小写字母开头的标识符是不公开的,不能被其他包中的代码直接访问。但是,其他包可以间接访问不公开的标识符。例如,一个函数可以返回一个未公开类型的值,那么这个函数的任何调用者,哪怕调用者不是在这个包里声明的,都可以访问这个值。

文章节选自《Go 语言实战》

作者:李兆海 · Go 语言早期使用者和推广者

点击阅读原文扫码试读

640?wx_fmt=png

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值