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

发表评论

邮箱地址不会被公开。 必填项已用*标注