二、快速开始一个Go程序

本节源代码:https://github.com/goinaction/code/tree/master/chapter2/sample
本节实现一个完整的Go语言程序,这个程序从不同的数据源拉取数据,将数据内容与一组搜索项做对比,然后将匹配的内容显示在终端窗口。这个程序会读取文本文件,进行网络调用,解码XML和JSON成为结构化类型数据,并且利用Go语言的开发机制保证这些操作的速度。
1、程序架构
在这里插入图片描述

一个程序分成多个不同步骤,在多个不同的goroutine里运行,从主goroutine开始,一直到执行搜索的goroutine和跟踪结果的goroutine。最后回到主goroutine。
在这里插入图片描述
上图是应用程序的项目结构。
data.json主要内容是程序要拉取和处理的数据源。
rss.go中包含程序里用于支持搜索不同数据源的代码。
search文件夹中包含使用不同匹配器进行搜索的业务逻辑。
最后父级文件夹中有个main.go文件是整个程序的入口。

2、main包
下面先给出main.go文件的内容

package main 

import (
	//导入标准库里的log和os包
	"log"
	"os"
	//前面加下划线对包做初始化操作,但是并不使用包里的标识符
	//因为Go编译器不允许声明导入某个包却不使用,下划线让编译器接受这类导入
	//并且调用对应包内的所有代码文件里定义的init函数
	//这样做的目的是调用matchers包中的rss.go代码文件里定义的init函数
	_ "github.com/goinaction/code/chapter2/sample/matchers"
	//导入search包就可以引用其中的Run函数
	"github.com/goinaction/code/chapter2/sample/search"
)

//init在main之前调用
func init() {
	//将日志输出到标准输出
	log.SetOutput(os.Stdout)
}

//main是整个程序的入口
func main() {
	//使用特定的项做搜索
	search.Run("president")
}

main函数保存在名为main的包里。如果main函数不在main包里,构建工具就不会生成可执行文件。一个包定义一组编译过的代码,包的名字类似于命名空间,可以用来间接访问包内声明的标识符。
import声明了所有的导入项,这段程序的导入就可以使main.go里的代码就可以引用search包里的Run函数。

3、search包
search.go

package search

import (
	"log"
	"sync"
)
//注册用于搜索的匹配器的映射
//声明为Matcher类型的映射(map),这个映射以string类型值作为键,Matcher类型值作为映射后的值,Matcher类型在代码文件matcher.go中声明。
//map是Go语言里的一个引用类型,需要make来构造
var matchers = make(map[stirng]Matcher)

func Run(searchTerm string) {
	//获取需要搜索的数据源列表
	//RetrieveFeeds函数返回两个值
	//第一个值是一组Feed类型的切片,其是一种实现了动态数组的引用类型
	//第二个值是一个错误值,检查其是不是真的错误,如果是真的,调用Fatal函数将其在终端输出
	feeds, err := RetrieveFeeds()
	if err != nil {
		log.Fatal(err)
	}
	
	//创建一个无缓冲的通道,接受匹配后的结果
	results := make(chan *Result)
	
	//构造一个waitGroup,以便处理所有的数据源
	var waitGroup sync.WaitGroup
	
	//设置需要等待处理每个数据源的goroutine的数量
	waitGroup.Add(len(feeds)
	
	//为每个数据源启动一个goroutine来查找结果
	//下划线标识符的作用是占位符,占据了保存range调用返回的索引值的变量的位置
	//如果调用的函数返回多个值,而又不需要其中的某个值,就可以使用下划线标识符将其忽略
	//在这里,我们不需要使用返回的索引值,所以就使用下划线标识符把它忽略掉
	for _, feed := range feeds {
		//获取一个匹配器用于查找
		matchers, exists := matchers[feed.Type]
		if !exists {
			matcher = matchers["default"]
		}
		
		//启动一个goroutine来执行搜索
		//这里使用关键字go启动一个**匿名函数**作为goroutine
		go func(matcher Matcher, feed *Feed) {
			Match(matcher, feed, searchTerm, results)
			waitGroup.Done()
		}(matcher, feed)
	}

	//启动一个goroutine来监控是否所有的工作都做完了
	go func() {
		//等候所有任务完成
		waitGroup.Wait()
		//用关闭通道的方式通知Display函数可退出程序了
		close(results)
	}()
	
	//启动函数,显示返回的结果,并且在最后一个结果显示完后返回
	Display(results)
}

//Register调用时会注册一个匹配器,提供给后面的程序使用
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
}

注意:①、标识符以大写字母开头是公开的,任何导入这个包的代码都能直接访问,以小写字母开头的标识符是不公开的。
②、在Go语言中,所有变量都被初始化为其零值。对于数值类型,零值是0;对于字符串类型,零值是空字符串;对于布尔类型,零值是false;对于指针零值是nil。对于引用类型来说,所引用的底层数据结构会被初始化为对应的零值。
③、:=是简化变量声明运算符,这个运算符用于声明一个变量,同时给这个变量赋予初始值。
④、在Go语言中,如果main函数返回,整个程序也就终止了。Go程序终止时,还会关闭所有之前启动的goroutine。写并发程序的时候,最佳做法是在main函数返回之前清理并终止所有之前启动的goroutine。
这个程序使用sync包的WaitGroup跟踪所有启动的goroutine。非常推荐Waitgroup来跟踪goroutine的工作是否完成。WaitGroup是一个计数信号量,我们可以利用它来统计所有的goroutine是不是都完成了工作。
⑤、下划线标识符作为占位符使用时,占据了保存range调用返回的索引值的变量的位置。如果调用的函数返回多个值,而又不需要其中的某个值,就可以使用下划线标识符将其忽略。在这里,我们不需要使用返回的索引值,所以就使用下划线标识符把它忽略掉
⑥、一个goroutine是一个独立于其他函数运行的函数。使用关键字go启动一个goroutine
⑦、匿名函数也可以接受声明时指定的参数。这里我们指定匿名函数要接受两个参数,一个类型为Matcher,另一个是指向一个Feed类型值的指针。

feed.go

package search

//json包提供编解码JSON的功能
//os包能提供访问操作系统的功能
import (
	"encoding/json"
	"os"
)

const dataFile = "data/data.json"

//Feed包含我们需要处理的数据源的信息
type Feed struct {
	Name string 'json:"site"'
	URI string 'json:"link"'
	TYPE string 'json:"type"'
}

//读取并反序列化源数据文件
func RetrieveFeeds() ([]*Feed, error) {
	//打开文件
	file, err := os.Open(dataFile)
	if err != nil {
		return nil, err
	}

	//当函数返回时关闭文件
	defer file.Close()

	//将文件解码到一个切片里
	//这个切片的每一项是一个指向一个Feed类型值的指针
	var feeds []*Feed
	err = json.NewDecoder(file).Decode(&feeds)
	
	//这个函数不需要检查错误,调用者会做这件事
	return feeds, err
}

①Go编译器可以根据赋值运算符右边的值来推导类型,声明常量的时候不需要指定类型
②关键字defer会安排随后的函数调用在函数返回时才执行

data.json

[
	{
		"site" : "npr",
		"link" : "http://www.npr.org/rss/rss.php?id=1001",
		"type" : "rss"
	},
	{
		"site" : "npr",
		"link" : "http://www.npr.org/rss/rss.php?id=1008",
		"type" : "rss"
	},
	{
		"site" : "npr",
		"link" : "http://www.npr.org/rss/rss.php?id=1006",
		"type" : "rss"
	},
	{
		"site" : "npr",
		"link" : "http://www.npr.org/rss/rss.php?id=1007",
		"type" : "rss"
	}
	...
]

json数据文件包含一个JSON文档数组。数组的每一项都是一个JSON文档,包含获取数据的网站名,数据的链接以及我们期望获得的数据类型。

match.go

package search

import (
	"log"
)

//Result保存搜索的结果
type Result struct {
	Field string
	Content string
}

//Matcher定义了要实现的新搜索类型的行为
type Matcher interface {
	Search(feed *Feed, SearchTerm string)([]*Result, error)
}

//为每个数据源单独启动goroutine来执行这个函数
func Match(matcher Matcher, feed *Feed, searchTerm string, results chan<- *Result) {
	searchResults, err := matcher.Search(feed, searchTerm)
	if err != nil {
		log.Printlf(err)
		return
	}
	for _, result := range searchResults {
		results <- result
	}
}

func Display(results chan *Result) {
	//通道会一直被阻塞,直到有结果写入
	//一旦通道被关闭,for循环就会终止
	for result := range results {
		fmt.Printf("%s:\n%s\n\n", result.Field, result.Content)
	}
}

①interface关键字声明了一个接口,这个接口声明了结构类型或者具名类型需要实现的行为。一个接口的行为最终由在这个接口类型中声明的方法决定
②对于Matcher这个接口来说,只声明了一个Search方法,这个方法输入一个指向Feed类型值的指针和一个string类型的搜索项。这个方法返回两个值:一个指向Result类型值的指针的切片,另一个是错误值
③Golang惯例,如果接口类型只包含一个方法,那么这个类型的名字以er结尾

default.go

package search
//defaultMatcher实现了默认匹配器
type defaultMatcher struct{}
//init函数将默认匹配器注册到程序里
func init() {
	var matcher defaultMatcher
	Register("default", matcher)
}
//Search实现了默认匹配器的行为
func (m defaultMatcher)Search(feed *Feed, searchTerm string)([]*Result, error){
	return nil, nil
}

4、RSS匹配器
一个期望的RSS数据源文档举例

<rss xmlns:npr="http://www.npr.org/rss/" xmlns:nprml="http://api"
<channel>
<title>News</title>
<link>...</link>
<description>...</description>
<language>en</language>
<copyright>Copyright 2014 NPR - For Personal Use
<image>...</image>
<item>
<title>
 Putin Says He'll Respect Ukraine Vote But U.S.
</title>
<description>
 The White House and State Department have called on the
</description>

rss.go

package matchers

import (
	"encoding/xml"
	"errors"
	"fmt"
	"log"
	"net/http"
	"regexp"

	"github.com/goinaction/code/chapter2/sample/search"
)

type (
	// item根据item字段的标签,将定义的字段与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根据image字段的标签,将定义的字段与rss文档的字段关联起来
	image struct {
		XMLName xml.Name `xml:"image"`
		URL     string   `xml:"url"`
		Title   string   `xml:"title"`
		Link    string   `xml:"link"`
	}

	// channel根据channel字段的标签,将定义的字段与rss文档的字段关联起来
	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文档定义了与rss文档关联的字段
	rssDocument struct {
		XMLName xml.Name `xml:"rss"`
		Channel channel  `xml:"channel"`
	}
)

// rssMatcher实现了Matcher接口
type rssMatcher struct{}

// init将匹配器注册到程序里
func init() {
	var matcher rssMatcher
	search.Register("rss", matcher)
}

// 在文档中查找特定的搜索项
func (m rssMatcher) Search(feed *search.Feed, searchTerm string) ([]*search.Result, error) {
	var results []*search.Result

	log.Printf("Search Feed Type[%s] Site[%s] For URI[%s]\n", feed.Type, feed.Name, feed.URI)

	// 获取要搜索的数据
	document, err := m.retrieve(feed)
	if err != nil {
		return nil, err
	}

	for _, channelItem := range document.Channel.Item {
		// 检查标题部分是否包含搜索项
		matched, err := regexp.MatchString(searchTerm, channelItem.Title)
		if err != nil {
			return nil, err
		}

		// 匹配则将其作为结果保存
		if matched {
			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
}

// retrieve发送HTTP Get请求获取rss数据源并解码
func (m rssMatcher) retrieve(feed *search.Feed) (*rssDocument, error) {
	if feed.URI == "" {
		return nil, errors.New("No rss feed uri provided")
	}

	// 从网络获得rss数据源文档
	resp, err := http.Get(feed.URI)
	if err != nil {
		return nil, err
	}

	// 一旦从函数返回,关闭返回的响应链接
	defer resp.Body.Close()

	//正常状态码是200
	if resp.StatusCode != 200 {
		return nil, fmt.Errorf("HTTP Response Error %d\n", resp.StatusCode)
	}

	//将rss数据源文档解码到我们定义的数据结构里
	var document rssDocument
	err = xml.NewDecoder(resp.Body).Decode(&document)
	return &document, err
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值