快速开始一个GO程序-《Go In Action》-Ch2

快速开始一个GO程序-《Go In Action》-Ch2

一上来接来个大程序,新手能接得住么
这个程序从不同的数据源拉取数据,将数据内容与一组搜索项做对比,然后将匹配的内容显示在终端窗口。这个程序会读取文本文件,
进行网络调用,解码XML 和JSON 成为结构化类型数据,并且利用Go 语言的并发
机制保证这些操作的速度source code

2.1 程序架构

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

2.2 main 包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"log"
"os"

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

// init is called prior to main.
func init() {
// Change the device for logging to stdout.
log.SetOutput(os.Stdout)
}

// main is the entry point for the program.
func main() {
// Perform the search for the specified term.
search.Run("president")
}

有以下几点需要注意:

  • main()是程序的入口,没有main函数,构建程序不会生成可执行文件
  • 一个包定义一组编译通过的代码,包的名字类似命名空间,可以用来直接访问包内生命的标识符, 可以报不同包中定义的同名标识符区别开
  • 下划线开头的包,是为了进行包的初始化操作,GO不允许声明导入包却不使用,下划线让编译器接受这种到日,并且调用对应包内所有文件代码里定义的init函数,init函数的执行在main函数之前

2.3 search 包

serach 包包含了程序使用的框架和业务逻辑

2.3.1 serach.go

serach文件先获取数据源,然后对每个数据源获取的数据进行匹配,每一个匹配启用一个goroutine。使用sync.WaitGroup控制任务是否完成。
sync.WaitGroup是一个计数信号量,主要有三个方法Add、Done和Wait,每增加一个任务就Add一次,每完成一个任务就Done一次,调用Wait的时候程序会阻塞,直到所有任务完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package search

//从标准库导入代码时,只需要给出要导入包的包名,
//编译器查找包时,总是会到GOROOT和GOPATH环境变量引用的位置去查找
import (
"log"
"sync"
)

// A map of registered matchers for searching.
// 小写字母标识,标识包内变量,不导出 or 不公开
var matchers = make(map[string]Matcher)

// Run performs the search logic.
func Run(searchTerm string) {
// Retrieve the list of feeds to search through.
feeds, err := RetrieveFeeds()
if err != nil {
log.Fatal(err)
}

// Create an unbuffered channel to receive match results to display.
results := make(chan *Result)

// Setup a wait group so we can process all the feeds.
var waitGroup sync.WaitGroup

// Set the number of goroutines we need to wait for while
// they process the individual feeds.
waitGroup.Add(len(feeds))

// Launch a goroutine for each feed to find the results.
for _, feed := range feeds {
// Retrieve a matcher for the search.
matcher, exists := matchers[feed.Type]
if !exists {
matcher = matchers["default"]
}

// Launch the goroutine to perform the search.
go func(matcher Matcher, feed *Feed) {
Match(matcher, feed, searchTerm, results)
waitGroup.Done()
}(matcher, feed)
}

// Launch a goroutine to monitor when all the work is done.
go func() {
// Wait for everything to be processed.
waitGroup.Wait()

// Close the channel to signal to the Display
// function that we can exit the program.
close(results)
}()

// Start displaying results as they are available and
// return after the final result is displayed.
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
}

对于上面代码,有以下问题需要明确下:

  • feeds, err := RetrieveFeeds() 这种一个函数返回两个值,第一个参数返回值,第二个返回错误信息,是GO中常用的模式
  • 声明运算符(:=),这个运算符在声明变量的同时,给变量赋值
  • feeds 是一个切片,可以理解为Go里面的动态数组,是一种引用类型
  • results是一个无缓冲的channel,和map、slice一样,都是引用类型,channel内置同步机制,从而保证通信安全
  • Go中,如果main函数返回,整个程序也就终止了,终止时,会关闭所有之前启动而且还在运行的goroutine
  • for range对feeds切片做迭代,和python里面的 for in一样的道理,每次迭代会返回两个值(index,value),value是一个副本,下划线_是一个占位符
  • 使用go关键字启动一个goroutine,并对这个goroutine做并发调度。上面程序中go启动了一个匿名函数作为goroutine
  • 在Go语言中,所有的变量都是以值的方式传递。所以想要修改真正的值,可以传递指针
  • Go语言支持闭包,匿名函数中访问searchTerm、results就是通过闭包的形势访问的。注意matcher、feed这两个变量并没有使用闭包的形式访问

2.3.2 feed.go

feed会从本地的data/data.json中读取Json数据,并将数据反序列化为feed切片,defer会安排随后的函数调用在函数返回时才执行, 使用defer可以缩短打开文件和关闭文件的代码间隔

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package search

import (
"encoding/json"
"os"
)

const dataFile = "data/data.json"

// Feed contains information we need to process a feed.
type Feed struct {
Name string `json:"site"`
URI string `json:"link"`
Type string `json:"type"`
}

// RetrieveFeeds reads and unmarshals the feed data file.
func RetrieveFeeds() ([]*Feed, error) {
// Open the file.
file, err := os.Open(dataFile)
if err != nil {
return nil, err
}

// Schedule the file to be closed once
// the function returns.
defer file.Close()

// Decode the file into a slice of pointers
// to Feed values.
var feeds []*Feed
err = json.NewDecoder(file).Decode(&feeds)

// We don't need to check for errors, the caller can do this.
return feeds, err
}

2.3.3 match.go/default.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package search

// defaultMatcher implements the default matcher.
type defaultMatcher struct{}

// init registers the default matcher with the program.
func init() {
var matcher defaultMatcher
Register("default", matcher)
}

// Search implements the behavior for the default matcher.
func (m defaultMatcher) Search(feed *Feed, searchTerm string) ([]*Result, error) {
return nil, nil
}

func (m defaultMatcher) Search 意味着search和defaultMatcher的值绑定在了一起,我们可以使用defaultMatcher 类型的值或者指向这个类型值的指针来调用Search 方
法。无论我们是使用接收者类型的值来调用这个方,还是使用接收者类型值的指针来调用这个
方法,编译器都会正确地引用或者解引用对应的值,作为接收者传递给Search 方法

1
2
3
4
5
6
7
8
9
10
11
12
// 方法声明为使用defaultMatcher 类型的值作为接收者
func (m defaultMatcher) Search(feed *Feed, searchTerm string)
// 声明一个指向defaultMatcher 类型值的指针
dm := new(defaultMatch)
// 编译器会解开dm 指针的引用,使用对应的值调用方法
dm.Search(feed, "test")
// 方法声明为使用指向defaultMatcher 类型值的指针作为接收者
func (m *defaultMatcher) Search(feed *Feed, searchTerm string)
// 声明一个defaultMatcher 类型的值
var dm defaultMatch
// 编译器会自动生成指针引用dm 值,使用指针调用方法
dm.Search(feed, "test")

与直接通过值或者指针调用方法不同,如果通过接口类型的值调用方法,规则有很大不同,
如代码清单2-38 所示。使用指针作为接收者声明的方法,只能在接口类型的值是一个指针的时
候被调用。使用值作为接收者声明的方法,在接口类型的值为值或者指针时,都可以被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 方法声明为使用指向defaultMatcher 类型值的指针作为接收者
func (m *defaultMatcher) Search(feed *Feed, searchTerm string)
// 通过interface 类型的值来调用方法
var dm defaultMatcher
var matcher Matcher = dm // 将值赋值给接口类型
matcher.Search(feed, "test") // 使用值来调用接口方法
> go build
cannot use dm (type defaultMatcher) as type Matcher in assignment
// 方法声明为使用defaultMatcher 类型的值作为接收者
func (m defaultMatcher) Search(feed *Feed, searchTerm string)
// 通过interface 类型的值来调用方法
var dm defaultMatcher
var matcher Matcher = &dm // 将指针赋值给接口类型
matcher.Search(feed, "test") // 使用指针来调用接口方法
> go build
Build Successful

match创建不同类型的匹配器,Matcher其实是一个接口,对于每种匹配器又有不同的具体实现。
下面的代码中,Matcher接口定义了一个Search方法,每个实现了Search方法的类型都实现了Matcher接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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) {
// Perform the search against the specified matcher.
searchResults, err := matcher.Search(feed, searchTerm)
if err != nil {
log.Println(err)
return
}

// Write the results to the channel.
for _, result := range searchResults {
results <- result
}
}

// Display writes results to the console window as they
// are received by the individual goroutines.
func Display(results chan *Result) {
// The channel blocks until a result is written to the channel.
// Once the channel is closed the for loop terminates.
for result := range results {
log.Printf("%s:\n%s\n\n", result.Field, result.Content)
}
}

Display方法会迭代results这个channel,有数据时会打印,没数据时会阻塞,当main.go中的close(result)后,for range循环结束

注意到default.go有init函数,这个函数会在main中通过下划线导入包的时候执行,init的功能是初始化匹配器

2.4 RSS 匹配器

rss.go篇幅过长,这里不贴代码了,其中有几个关注的点说下:
在init中注册了一个rssMatcher,这个match和之前的defaultMatcher一样,绑定了Search方法,即实现了Matcher接口

1
2
3
4
func init() {
var matcher rssMatcher
search.Register("rss", matcher)
}

rss.go主要有两个方法retrieve和Search,retrieve负责抓取网略资源,search负责匹配,具体匹配方法这里不表了

2.5 小结

  • 每个代码文件都属于一个包,而包名应该与代码文件所在的文件夹同名。
  • Go 语言提供了多种声明和初始化变量的方式。如果变量的值没有显式初始化,编译器会将变量初始化为零值。
  • 使用指针可以在函数间或者goroutine 间共享数据。
  • 通过启动goroutine 和使用通道完成并发和同步。
  • Go 语言提供了内置函数来支持Go 语言内部的数据结构。
  • 标准库包含很多包,能做很多很有用的事情。
  • 使用Go 接口可以编写通用的代码和框架。
# Golang

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×