作为一个go的新手,一开始跟着Go指南进行Go的学习,在完成指南上的Web爬虫练习时遇到了一些goroutine与channel相关的问题。
指南上一开始给出了原始代码其中最重要的就是Crawl函数,代码如下:
// Crawl 使用 fetcher 从某个 URL 开始递归的爬取页面,直到达到最大深度。 func Crawl(url string, depth int, fetcher Fetcher) { // TODO: 并行的抓取 URL。 // TODO: 不重复抓取页面。 // 下面并没有实现上面两种情况: if depth <= 0 { return } body, urls, err := fetcher.Fetch(url) if err != nil { fmt.Println(err) return } fmt.Printf("found: %s %q\n", url, body) for _, u := range urls { Crawl(u, depth-1, fetcher) } return }这段代码很简单,就是递归地Crawl所获得的所有url。题目要求修改Crawl函数使用并发并且不重复抓取页面。
完成这个功能的难点在于如果依旧使用递归的方式来进行页面抓取,那么子goroutine如何知晓某一url是否已被抓取,一开始,我是用这样的方式实现的:
// Crawl 使用 fetcher 从某个 URL 开始递归的爬取页面,直到达到最大深度。 func Crawl(url string, depth int, fetcher Fetcher) { // TODO: 并行的抓取 URL。 // TODO: 不重复抓取页面。 ch:=make(chan int) count:=1 urlsFetched:=make(map[string]string) go CrawlWithConcurrency(url,depth,fetcher,ch,urlsFetched) for count>0 { count+=<-ch } return } func CrawlWithConcurrency(url string, depth int, fetcher Fetcher, ch chan int,urlsFetched map[string]string) { if depth<=0 { ch <- -1 return } body, urls, err := fetcher.Fetch(url) urlsFetched[url]="true" if err != nil { fmt.Println(err) ch <- -1 return } fmt.Printf("found: %s %q\n", url, body) for _,u:=range urls { _,exists:=urlsFetched[u] if exists { continue } go CrawlWithConcurrency(u,depth-1,fetcher,ch,urlsFetched) ch <- 1 } ch <- -1 return }简单地说就是:
1、执行Crawl函数的主goroutine通过count来确保在所有的子goroutine都结束运行后再结束运行。
2、所有goroutine通过共享urlsFetched内存来确保抓取的页面不会重复。
go是按照CSP来实现并发的,提倡“通过通信来共享内存,而非通过共享内存来通信”的原则。
上面的代码通过共享内存urlsFetched来进行通信是违背CSP原则的,各goroutine都可以读取并修改urlFetched,这样的设计存在隐患,仍然有可能会重复抓取页面,所以共享内存的方式很危险。
为了遵守CSP原则,我修改了代码,修改后代码如下:
type Result struct { depth int urls []string } //主goroutine用于控制程序的结束 func Crawl(url string, depth int, fetcher Fetcher) { ch:=make(chan *Result) urlsFetched:=make(map[string]string) count:=1 urlsFetched[url]="true" go CrawlWorker(url,depth,fetcher,ch) for count>0 { result:=<-ch //depth<=1则不crawlresult中的urls if result.depth>1 { for _,u:=range result.urls{ _,exists:=urlsFetched[u] if exists { continue } else { count++ urlsFetched[u]="true" go CrawlWorker(u,result.depth-1,fetcher,ch) } } } count-- } return } func CrawlWorker(url string, depth int, fetcher Fetcher,ch chan *Result) { body, urls, err := fetcher.Fetch(url) if err != nil { fmt.Println(err) ch<-&Result{depth,urls} return } fmt.Printf("found: %s %q\n", url, body) ch<-&Result{depth,urls} }
修改后的代码主要的内容就是:
1、不再进行递归
2、只让主goroutine管理urlFetched内存
3、其他子goroutine传回运行结果给主goroutine,主goroutine再启动新的子goroutine
这样便遵守了CSP原则,并且更加安全了。
完整代码如下:
有疑问加站长微信联系(非本文作者)