既然是Go语言实战,那看了就必须动手敲出来,先把书本中的知识点,案例看一遍,有不懂或疑惑,感觉前后无法关联起来的地方,需要翻到前面看相关的知识点,保证能理解书中主要内容,然后在自己凭着自己的理解,重新实现一遍。
案例功能
开启多个 goroutine 去请求 xml 资源, 然后解析xml 数据并存放到 channel 中,最后将 channel 中的数据读取出来,输出到控制台中。
实现步骤
- 读取本地的 data.json 文件(该文件存放了需要请求的 url 等相关信息) ,读取文件需要用到 os 包,并且读取完文件后需要及时关闭文件(defer file.Close()), 然后解析 json 文件,解析 json 文件需要使用到 encoding/json 内置包,然后需要准备一个接受解析结果的结构体 Feed。
- 得到 Feed 数据后,循环所有的 Feed 数据进行 http 请求获取 xml 数据,每个请求开启 goroutine,为了防止主进程提前结束,此时需要使用 sync.WaitGroup 来阻塞主进程,等待所有 goroutine 结束后,再结束主进程,而 http 请求可以使用 go 提供的内置包 net/http, 需要注意的是,使用 http 请求时,记得及时关闭请求 resp.Body.Close()
- http 请求到 xml 数据后,需要解析 xml 数据,解析 xml 需要用到内置包 encoding/xml, 以及准备好相应的结构体(案例解析xml使用到的结构体有:image, item, Channel, RssDocument)来接收解析结果,结构体的字段需要跟返回的xml数据一一对应,写错可能会导致解析到空的数据。
- 得到解析后的xml数据之后,就可以根据传入的关键字进行查找了,匹配字符串的需要使用的内置的正则包 regexp, 如果匹配成功,则把相关的内容组装好,然后 append 到通道 results 中
- 最后遍历通道(channel) results,输出结果到控制台
- 完毕
案例全部逻辑代码
package mainimport ( "encoding/json" "encoding/xml" "fmt" "log" "net/http" "os" "regexp" "sync")type ( Feed struct { Name string `json:"site"` URI string `json:"link"` Type string `json:"type"` } item struct { XMLName xml.Name `xml:"item"` Title string `xml:"title"` Description string `xml:"description"` PubDate string `xml:"pubDate"` Link string `xml:"link"` Guid string `xml:"guid"` ContentEncoded string `xml:"content:encoded"` DcCreator string `xml:"dc:creator"` } image struct { XMLName xml.Name `xml:"image"` Url string `xml:"url"` Title string `xml:"title"` Link string `xml:"link"` } Channel struct { XMLName xml.Name `xml:"channel"` Title string `xml:"title"` Link string `xml:"link"` Description string `xml:"description"` Language string `xml:"language"` Copyright string `xml:"copyright"` Generator string `xml:"generator"` LastBuildDate string `xml:"lastBuildDate"` Item []item `xml:"item"` Image image `xml:"image"` } RssDocument struct { XMLName xml.Name `xml:"rss"` Channel Channel `xml:"channel"` } Result struct { Field string Content string })// 用于阻塞进程,等待所有 goroutine 处理完毕,再结束主进程var wg sync.WaitGroup// 存放最终的匹配的结果var results = make(chan *Result)// 读取本地文件,解构json数据func readFile(path string) ([]*Feed, error) { file, err := os.Open(path) if err != nil { return nil, err } // 注意:打开文件之后,记得要关闭文件 defer file.Close() // 注意:文件读取后,需要结构体来解析json数据 var files []*Feed json.NewDecoder(file).Decode(&files) return files, nil}// 请求需要的数据源func retrieve(feed *Feed) (*RssDocument, error) { // http 请求数据源 resp, err := http.Get(feed.URI) // 请求出错 if err != nil { return nil, err } // 请求结束记得 close 请求 defer resp.Body.Close() // 状态码非200 if resp.StatusCode != 200 { return nil, fmt.Errorf("状态码是 %d\n\n", resp.StatusCode) } // 解析 xml 数据 var document RssDocument xml.NewDecoder(resp.Body).Decode(&document) return &document, nil}// 查找需要查询的字符串func Match(searchTerm string, document *RssDocument) { var result Result for _, item := range document.Channel.Item { // 匹配标题 matched, err := regexp.MatchString(searchTerm, item.Title) if err != nil { } if matched { result = Result{ Field: "Title ", Content: item.Title, } results } // 匹配内容 matched, err = regexp.MatchString(searchTerm, item.Description) if err != nil { } if matched { result = Result{ Field: "Description", Content: item.Description, } results } }}// 查询func run(searchTerm string, feed *Feed) { document, err := retrieve(feed) if err != nil { fmt.Println("请求出错啦!", err) return } // 匹配字符串 Match(searchTerm, document)}func search(searchTerm string) { // os内置库读取本地文件,解析 json 文件成 struct // [https://github.com/goinaction/code/blob/master/chapter2/sample/data/data.json] path := "./data.json" feeds, err := readFile(path) if err != nil { fmt.Println("读取文件出错啦!") } // 需要等待的 goroutine 数量 wg.Add(len(feeds)) // 开启多个goroutine 并发请求数据源, 并 Decode 数据, 然后匹配字符串后,存放到 channel results 中 for _, feed := range feeds { go func(feed *Feed) { run(searchTerm, feed) // 记得处理完进程后,执行 wg.Done 让等待的进程计数减一 wg.Done() }(feed) } // 等待所有 goroutine 结束后,关闭 channel go func() { wg.Wait() close(results) }() // 2. display: display()}// 显示搜索结果func display() { i := 0 for result := range results { i++ log.Printf("[%d] %s:\n%s\n\n", i, result.Field, result.Content) }}func main() { fmt.Println("--------Start-------\n") search("president") fmt.Println("--------Done--------\n")}
相关知识点
1、如果需要声明引用类型(映射map, 切片slice, 通道chan)后赋值,需要使用 make 的方式声明,因为如果使用 var 声明引用类型,会返回默认值 nil, 后面使用赋值会导致出错。
// map// 错误var m map[string]stringm["a"] = "xxx"fmt.Println(m)// 正确var m = make(map[string]string)m["a"] = "xxx"fmt.Println(m)// slice// 错误var s []strings[0] = "xxx"fmt.Println(s)// 正确, 但是 slice 声明的使用需要写第二个参数来表示长度var s = make([]string, 1)s[0] = "xxx"fmt.Println(s)// 正确var s []strings = append(s, "xxx")fmt.Println(s)// chan // 无缓冲 channel , 一般配合 goroutine 使用,// 而且需要等待接收方准备好之后才能发送数据 // 正确var wg sync.WaitGroupwg.Add(1)var ch = make(chan string)go func() { fmt.Println( wg.Done()}()ch "x"wg.Wait() // 有缓冲 channel// 第一个参数表示要存放的数据类型,第二个参数表示缓存长度var ch = make(chan string, 1)ch "xx"fmt.Println(
2、Golang 中所有的函数参数都是以传值方式传递,引用类型也是传值方式,只是传的值有点特殊,传的是指针变量所指向的内存地址,所以在函数内改变引用类型的变量,函数外部的变量也会被改变。
3、go 中并发只需要在函数前面加 go 关键字即可,但是使用 goroutine 时,为了避免主进程提前终止,需要使用 waitGrout 来阻塞,而 waitGrout 是在内置的 sync 包中,使用时,需要 waitGroup.Add(int), waitGroup.Done(), waitGroup.Wait() 配合使用,而waitGroup.Add(int) 中的 int值,一般要和waitGroup.Done() 的次数相等,而waitGroup.Done() 一般是放在 goroutine 中使用,如果 int 小于 waitGroup.Done() 的次数,则会导致主进程提前结束;而 int 大于 waitGroup.Done() 的次数,会导致主进程一直被阻塞,无法继续后续的代码。
goroutine 执行并发,以及waitGroup 配合使用如下:
package mainimport ( "fmt" "sync")func main() { var wg sync.WaitGroup books := []string{"a", "b", "c"} // len(books) wg.Add(len(books)) for i, book := range books { go func(i int, book string) { fmt.Printf("执行%d %s\n", i, book) wg.Done() }(i, book) } // 阻塞主进程,等待所有 goroutine 执行完成后才继续往下执行 wg.Wait() }
4、go中闭包的使用,for 循环和闭包一起使用时,如果不将需要的参数传进函数,而是直接使用外部函数中的变量时,可能会导致使用的都是最后一个值。
var wg sync.WaitGroup books := []string{"a", "b", "c"} // len(books) wg.Add(len(books)) for i, book := range books { go func() { // 执行结果 // 执行2 c // 执行2 c // 执行2 c fmt.Printf("执行%d %s\n", i, book) wg.Done() }() } // 阻塞主进程,等待所有 goroutine 执行完成后才继续往下执行 wg.Wait()
5、接口声明最佳实践,如果是单个方法则以 er 结尾;如果需要维护状态,方法的接受者最好声明为指针;
// 无状态的结构体,即结构体中无任何字段type Dog struct{}// 有状态的结构体type Dog struct{ Name string}
类型的接受者为类型值或类型值指针,那么类型值还是类型值指针都可以调用方法;
而接口类型值只能只能调用接收者为类型值的方法;接口类型值指针可以调用接收者为类型值或类型指针的方法。
package mainimport "log"type People struct { Name string}// 值接收者func (p People) Hi() { log.Print("Hi...", p.Name)}type Animal struct { Species string}// 指针接收者func (a *Animal) yell() { log.Print("Yelling...", a.Species)}type Skill interface { Eat()}func (p People) Eat() { log.Println("People is eating...")}func (a *Animal) Eat() { log.Println("Animal is eating...")}func main() { // 类型调用方法 // 值接收者 //s := People{"Golang"} //s.Hi() //s := &People{"Golang"} //s.Hi() // 指针接收者 //d := Animal{"Dog"} //d.yell() //d := &Animal{"Dog"} //d.yell() // 接口实例调用方法 // 值接收者 //p := People{"Golang"} // People 实现 Skill 接口 // 值类型赋值该接口类型 //var s Skill = p //s.Eat() // 成功 // 指针类型赋值该接口类型 //var s Skill = &p //s.Eat() // 成功 // 指针接收者 // c := Animal{"Cat"} // Animal 实现 Skill 接口 // 值类型赋值该接口类型 //var s Skill = c //s.Eat() // 直接提示有错误 // 指针类型赋值该接口类型 // var s Skill = &c // s.Eat() // 成功}// 只要记住:// 结构体类型的方法, 不管是值类型接收者,还是指针j接收者// 结构体实例都能够调用其结构体的方法// 接口类型的方法,// 值类型接收者的实例(不管是值还是指针)赋值给接口实例,接口实例都可以调用方法// 指针接收者的实例,只有实例指针赋值给接口实例,接口实例才能调用方法
6、包中的init方法会在 main 方法之前执行,可以在这做一些初始化的工作。
7、可以使用下划线 _ 来忽略或者当做变量的占位,来处理不需要的变量或者接收未使用的导入的包(此时会执行该包中 init 方法)
8、通道 channel 的使用,channel 分为无缓冲和有缓冲,有只读和只写。
package mainimport "log"func main() { // 无缓冲 //var ch = make(chan string) //ch //log.Println( // 有缓冲,第一个参数 存放的数据类型,第二个参数 缓冲长度 var ch = make(chan string, 2) ch"How are u ?" ch"I'm fine, and u?" // 读, 第一个值:消息值,第二个值:通道是否已经关闭 //msg, ok := //log.Println(msg, ok) 只写缓冲 //var ch = make(chan //ch //log.Println( // 只读缓冲 //var ch = make( //ch //log.Println( // 关闭 channel close(ch)}
《Go语言实战》的完整代码传送门:
https://github.com/goinaction/code/blob/master/chapter2/sample