我们从一个数据源拉取数据,然后和搜索项做对比,然后将匹配的显示在终端窗口,解码成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语言提供了内置函数来支持很多操作