page contents

Go语言实战:开启多个 goroutine 去请求 xml 资源......

既然是Go语言实战,那看了就必须动手敲出来,先把书本中的知识点,案例看一遍,有不懂或疑惑,感觉前后无法关联起来的地方,需要翻到前面看相关的知识点,保证能理解书中主要内容,然后在自己...

attachments-2021-05-ZTPUNcWo6099fbf2f0a35.png

既然是Go语言实战,那看了就必须动手敲出来,先把书本中的知识点,案例看一遍,有不懂或疑惑,感觉前后无法关联起来的地方,需要翻到前面看相关的知识点,保证能理解书中主要内容,然后在自己凭着自己的理解,重新实现一遍。

案例功能

开启多个 goroutine 去请求 xml 资源, 然后解析xml 数据并存放到 channel 中,最后将 channel 中的数据读取出来,输出到控制台中。

实现步骤

  1. 读取本地的 data.json 文件(该文件存放了需要请求的 url 等相关信息) ,读取文件需要用到 os 包,并且读取完文件后需要及时关闭文件(defer file.Close()), 然后解析 json 文件,解析 json 文件需要使用到 encoding/json 内置包,然后需要准备一个接受解析结果的结构体 Feed。

  2. 得到 Feed 数据后,循环所有的 Feed 数据进行 http 请求获取 xml 数据,每个请求开启 goroutine,为了防止主进程提前结束,此时需要使用 sync.WaitGroup 来阻塞主进程,等待所有 goroutine 结束后,再结束主进程,而 http 请求可以使用 go 提供的内置包 net/http, 需要注意的是,使用 http 请求时,记得及时关闭请求 resp.Body.Close()

  3. http 请求到 xml 数据后,需要解析 xml 数据,解析 xml 需要用到内置包 encoding/xml, 以及准备好相应的结构体(案例解析xml使用到的结构体有:image, item, Channel, RssDocument)来接收解析结果,结构体的字段需要跟返回的xml数据一一对应,写错可能会导致解析到空的数据。

  4. 得到解析后的xml数据之后,就可以根据传入的关键字进行查找了,匹配字符串的需要使用的内置的正则包 regexp, 如果匹配成功,则把相关的内容组装好,然后 append 到通道 results 中

  5. 最后遍历通道(channel) results,输出结果到控制台

  6. 完毕

案例全部逻辑代码

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 string1)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)}

更多相关技术内容咨询欢迎前往并持续关注六星社区了解详情。

如果你想用Python开辟副业赚钱,但不熟悉爬虫与反爬虫技术,没有接单途径,也缺乏兼职经验
关注下方微信公众号:Python编程学习圈,获取价值999元全套Python入门到进阶的学习资料以及教程,还有Python技术交流群一起交流学习哦。
attachments-2022-06-4GaVqKdv62abf2f1d595e.jpeg

  • 发表于 2021-05-11 11:40
  • 阅读 ( 745 )
  • 分类:Golang

你可能感兴趣的文章

相关问题

0 条评论

请先 登录 后评论
轩辕小不懂
轩辕小不懂

2403 篇文章

作家榜 »

  1. 轩辕小不懂 2403 文章
  2. 小柒 1474 文章
  3. Pack 1135 文章
  4. Nen 576 文章
  5. 王昭君 209 文章
  6. 文双 71 文章
  7. 小威 64 文章
  8. Cara 36 文章