我们从一个数据源拉取数据,然后和搜索项做对比,然后将匹配的显示在终端窗口,解码成XML或者JSON,上面的全程利用Go的并发机制来保证速度
整体的程序架构如下

上面程序分为不同的步骤,在不同的goroutine中运行,
整体的架构如下

我们有四个文件夹
分辨是data 包含了数据库,也就是数据源,文件夹matchers中包含rss源的搜索器
serach包含了搜索逻辑,最后有一个入口
main入口
我们主入口在main.go中可以找到,代码如下所示
package main
//main函数保存在名为main的包里面,如果main不再main的包里,构建工具就不会生成可执行的文件
//也就是项目的入口
import (
"log"
"os"
//_下划线代表着,可以引入了不使用
_"./matchers"
"./search"
)
// init说明在main函数执行前调用
func init() {
// 改变输出到stdout
log.SetOutput(os.Stdout)
}
func main() {
//进行执行
search.Run("president
上面有着标准的注解
然后是search包,里面有4个不同的代码文件,每个对应的独立的职责
整个程序都围绕着匹配器,这个匹配是针对特定的信息,在示例代码中有两个匹配器,可以获取读入查找RSS数据源,并加入能读入JSON或者CSV的匹配器
代码如下
package search
//包名一致
import (
"log"
"sync"
)
//引入了标准库
//log包提供了打印日志信息到标准输出的方式,sync提供了goroutine的功能
// 包级别的变量,String为key value为Matcher类型
var matchers = make(map[string]Matcher)
//小写字母开头的变量,说明不公开,不能直接访问,可以间接的访问,比如利用函数的返回值
//map这个引用类型,需要先调用make来进行构造,如果不进行构造直接赋值,只会得到一个nil
// Run的代码如下
func Run(searchTerm string) {
// 获取搜索源
feeds, err := RetrieveFeeds()
if err != nil {
log.Fatal(err)
}
// 初始化一个无缓冲的通道,接收匹配后的结果
results := make(chan *Result)
// 创建一个waitGroup,处理所有的数据源
var waitGroup sync.WaitGroup
//设置每个数据源的goroutine的数量
waitGroup.Add(len(feeds))
//遍历所有的数据源,启动不同的goroutine来查找
for _, feed := range feeds {
// for循环获取数据源,从map中取出对应的匹配器
// 这里的_起的是占位符的功能,即为调用的函数返回多个值,不需要其中的某个值,就 //可以利用_来忽略
matcher, exists := matchers[feed.Type]
if !exists {
matcher = matchers["default"]
}
//启动go线程开始搜索,term要搜索的内容,result,要搜索的结果
go func(matcher Matcher, feed *Feed) {
Match(matcher, feed, searchTerm, results)
waitGroup.Done()
}(matcher, feed)
}
//设置一个监视者,查看是否完成了
go func() {
// 等待所有线程完成
waitGroup.Wait()
// 关闭通道
// 退出
close(results)
}()
//显示返回的结果
Display(results)
}
// Register is called to register a matcher for use by the program.
func Register(feedType string, matcher Matcher) {
if _, exists := matchers[feedType]; exists {
log.Fatalln(feedType, "Matcher already registered")
}
log.Println("Register", feedType, "matcher")
matchers[feedType] = matcher
}
我们看一下基本的函数定义
Run函数的定义如下
func Run(searchTerm string)
Go使用关键字func声明函数
后面是参数和返回值
在上面的func Run中,我们声明了一个参数,类型是string,名为searchTerm
另一种方式就是在调用search中的RetrieveFeeds函数,返回了一组Feeds类型的切片和一个err
如果出现了错误,就不要返回这个函数返回的另一个值
func RetrieveFeeds() ([]*Feed, error) {
运算符 := 声明一个变量,同时给这个变量赋予初始值,编译器根据返回值类型来确定每个变量的类型
为了防止main函数返回导致关闭所有启动的goroutine,在并发程序书写的时候,手动的清理所有开启的goroutine
我们利用WaitGroup来跟踪goroutine的工作,WaitGroup变量的值为将要启动的goroutine的数量
这就是一个CountDownLatch,每当有一个完成其工作后,就会来递减这个WatiGroup的值,直到0
然后进行for循环,在循环中,我们利用了一个range字段
range关键字可以迭代数组 字符串 切片 映射 通道
range会让迭代返回两个值,一个是迭代的元素在切片中的索引位置,第二个是元素的副本
在获取查询map的时候,可以赋值给一个变量或者赋值给两个变量,如果指定的是两个值,会额外返回一个布尔标志,标志键是否存在
当然,可以通过返回的值是不是零值来判断是否存在
利用关键字go 加上 func来启动了一个goroutine来执行搜索
这个函数是一个匿名函数,没有声明名字的函数,在for range中,我们启动这个匿名函数
feed是一个指针变量,可以再函数之间共享数据,方便使用以及修改变量状态
匿名函数中,我们指定传入了matcher和feed两个变量
我们在其中调用了Match的函数
一旦执行完成就会减少WaitGroup的技术,一旦每个goroutine执行完成,就会调用
而且WaitGroup没有作为参数传入匿名函数,但是匿名参数还是能访问到这个值
这就是Go的闭包,但是匿名函数的入参不能以闭包的方式获取,不然会获取到同一个matcher来处理同一个feed
Run函数中,我们调用了Display函数,之后程序自动结束了
之前还有Display函数来打印出所有的匹配
我们看一下search.go文件的获取feed函数
其会读取data.json文件,并返回数据源的切片这些数据源会输出内容,然后使用各自的匹配器进行搜索
代码如下
package search
import (
//编解码JSON的功能
"encoding/json"
"os"
)
//小写说明不暴露在包的外面
const dataFile = "data/data.json"
//结构组成的切片中,以便我们在程序中使用这些数据
//我们声明了这个叫做Feed的结构类型,并对外暴露
//声明了三个字段,每个字段后的`引号被称为标记,描述了JSON解码的元数据,创建Feed类型的切片
type Feed struct {
Name string `json:"site"`
URI string `json:"link"`
Type string `json:"type"`
}
// 此代码读取文件并解析为JSON
func RetrieveFeeds() ([]*Feed, error) {
file, err := os.Open(dataFile)
if err != nil {
return nil, err
}
// 等待函数结束的时候,关闭文件
//defer关键字修饰的操作,会在函数调用在函数返回时才执行,使用完文件后,主动关闭文件,使用defer来保证关闭
//类似finally
defer file.Close()
// 文件解码到切片中,切片的每一项都是一个指向Feed类型的指针
var feeds []*Feed
err = json.NewDecoder(file).Decode(&feeds)
return feeds, err
}
对于Matcher这个接口,声明了一个Search的方法,我们输入一个Feed类型的指针和一个String类型的搜索项,返回来两个值,一个指向Result类型项的指针的切片,另外一个是错误值
命名接口的时候,遵循了Go的命名惯例,如果接口中只包含一个方法,这个类型的名字以er结尾
如果让一个用户定义的类型实现一个接口,这个用户定义的类型要实现接口类型声明中的所有方法,切换到default.go代码文件,默认匹配器如何实现Matcher接口的
package search
// defaultMatcher 实现了默认匹配器
type defaultMatcher struct{}
func init() {
var matcher defaultMatcher
//注册到默认匹配器中
Register("default", matcher)
}
// 函数的实现
func (m defaultMatcher) Search(feed *Feed, searchTerm string) ([]*Result, error) {
return nil, ni
我们省了一个空的结构,名为defaultMatcher,创建空结构的实例的时候,不会分配任何的内存
然后默认的函数实现,是返回两个nil值
因为大部分的方法在调用后需要维护接受者的状态,所以,我们可以将方法的接收者声明为指针,对于defaultMatcher来说,使用值是因为值不需要分配内存
直接通过值和指针调用方法不同
使用指针作为接受者,只能在接口类型的值是一个指针时候使用
使用值作为接受者,接口类型是值或者指针,都可以调用

上面的代码说明了,defaultMatcher的值比指针更加好使
然后是match.go
里面实现了Match函数
package search
import (
"log"
)
// Result contains the result of a search.
type Result struct {
Field string
Content string
}
// Matcher defines the behavior required by types that want
// to implement a new search type.
type Matcher interface {
Search(feed *Feed, searchTerm string) ([]*Result, error)
}
// Match is launched as a goroutine for each individual feed to run
// searches concurrently.
func Match(matcher Matcher, feed *Feed, searchTerm string, results chan<- *Result) {
// 进行搜索
searchResults, err := matcher.Search(feed, searchTerm)
if err != nil {
log.Println(err)
return
}
// 结果写入通道
for _, result := range searchResults {
results <- result
}
}
// 输出至终端窗口
func Display(results chan *Result) {
// for循环
for result := range results {
//进行打印
log.Printf("%s:\n%s\n\n", result.Field, result.Content)
上面的Match函数使用实现了Matcher接口的值或者指针,然后调用了Search,并且检测返回值是否是一个错误
通道被关闭的时候,range关键字保证了只有处理完所有才会结束,这一步我们看一下调取者,就是
//设置一个监视者,查看是否完成了
go func() {
// 等待所有线程完成
waitGroup.Wait()
// 关闭通道
// 退出
close(results)
}()
等待所有的Goroutine结束,并关闭通道
接下来是初始化不同的匹配器
这个我们看deafult.go的部分代码
func init() {
var matcher defaultMatcher
//注册到默认匹配器中
Register("default", matcher)
}
在default.go中,我们利用了init函数,编译器一旦发现了init函数,就会给这个函数先于main函数进行执行,代码文件default,go执行汇总创建了这个defaultMatcher类型的值,并将其调用Register,传递给了search.go中的类级别变量
// 包级别的变量,String为key value为Matcher类型 var matchers = make(map[string]Matcher)
最后是RSS匹配器
这是一个外部的匹配器的实现,为了匹配接口,Search方法的实现都不同
RSS匹配器也实现了相关的matcher接口
实际代码如下
package matchers
//导入标准包
import (
"encoding/xml"
"errors"
"fmt"
"log"
"net/http"
"regexp"
"../search"
)
type (
// 和rss文档字段关联起来
item struct {
XMLName xml.Name `xml:"item"`
PubDate string `xml:"pubDate"`
Title string `xml:"title"`
Description string `xml:"description"`
Link string `xml:"link"`
GUID string `xml:"guid"`
GeoRssPoint string `xml:"georss:point"`
}
// image defines the fields associated with the image tag
// in the rss document.
image struct {
XMLName xml.Name `xml:"image"`
URL string `xml:"url"`
Title string `xml:"title"`
Link string `xml:"link"`
}
// channel defines the fields associated with the channel tag
// in the rss document.
channel struct {
XMLName xml.Name `xml:"channel"`
Title string `xml:"title"`
Description string `xml:"description"`
Link string `xml:"link"`
PubDate string `xml:"pubDate"`
LastBuildDate string `xml:"lastBuildDate"`
TTL string `xml:"ttl"`
Language string `xml:"language"`
ManagingEditor string `xml:"managingEditor"`
WebMaster string `xml:"webMaster"`
Image image `xml:"image"`
Item []item `xml:"item"`
}
// 也是定义了和rss文档关联的字段
rssDocument struct {
XMLName xml.Name `xml:"rss"`
Channel channel `xml:"channel"`
}
)
// rssMatcher实现了Matcher接口,利用空结构来维护了Matcher接口
type rssMatcher struct{}
// 然后init将匹配器注册到程序中,search包中的注册函数
// 我们利用别名导入了matchers包,完成了init的调用
func init() {
var matcher rssMatcher
search.Register("rss", matcher)
}
// Search looks at the document for the specified search term.
func (m rssMatcher) Search(feed *search.Feed, searchTerm string) ([]*search.Result, error) {
//声明了一个Result类型,默认为nil的切片,类型的声明在search文件中
var results []*search.Result
log.Printf("Search Feed Type[%s] Site[%s] For URI[%s]\n", feed.Type, feed.Name, feed.URI)
// 调用下方的http方法
document, err := m.retrieve(feed)
if err != nil {
return nil, err
}
for _, channelItem := range document.Channel.Item {
// 检查标题部分进行包含搜索项,利用了ChannelItem的title
matched, err := regexp.MatchString(searchTerm, channelItem.Title)
if err != nil {
return nil, err
}
//找到了,就进行保存
if matched {
//append这个函数根据需要,动态的增加切片长度,
//利用取地址运算符 & 来获取这个新值的地址,并存入了切片
results = append(results, &search.Result{
Field: "Title",
Content: channelItem.Title,
})
}
// 检测描述部分是否包含
matched, err = regexp.MatchString(searchTerm, channelItem.Description)
if err != nil {
return nil, err
}
// 找到了匹配项,就会作为结果保存
if matched {
results = append(results, &search.Result{
Field: "Description",
Content: channelItem.Description,
})
}
}
return results, nil
}
// 发送HTTP get请求获取rss数据源并解码
func (m rssMatcher) retrieve(feed *search.Feed) (*rssDocument, error) {
if feed.URI == "" {
return nil, errors.New("No rss feed uri provided")
}
// 发送网络请求
resp, err := http.Get(feed.URI)
if err != nil {
return nil, err
}
// 函数结束时候关闭连接
defer resp.Body.Close()
// 检查是不是200
if resp.StatusCode != 200 {
//利用fmt.error返回一个自定义的错误
return nil, fmt.Errorf("HTTP Response Error %d\n", resp.StatusCode)
}
// 尝试解析到我们定义的结构类型中
var document rssDocument
err = xml.NewDecoder(resp.Body).Decode(&document)
re
这样整体的一个框架就基本搭建出来了
我们讲解了接口相关,入参和返回值,包的基本问题
以及实现了一个RSS匹配器,并进行返回
我们总结一下
每个代码都需要属于一个包,而包名需要和代码所在文件夹的同名
Go提供了多种初始化变量的方式,变量值都会初始化为零值
使用指针可以在函数间或者goroutine之间共享数据
通过启动goroutine和使用通道来完成并发或者同步
Go语言提供了内置函数来支持很多操作