A Tour of Go - Exercise: Web Crawler をやってみた
冬休みの間にGoの概要を知るためにA Tour of Goをやってみた。Exercise: Web Crawlerにちょっと苦戦したので記録として残しておく。
V1:とりあえずCache機能作る
Cacheを作成するとこまでを目標に実装。Mutexを扱うことを想定して、結果とMutexを同じ場所で管理できるようにstruct Crawlerを作っておいた。CrawlもCrawlerのインスタンスメソッド化。
package main import ( "fmt" // "sync" ) type Fetcher interface { Fetch(url string) (body string, urls []string, err error) } type Crawler struct { // m sync.Mutex result map[string]string } func (c *Crawler) Crawl(url string, depth int, fetcher Fetcher) { if depth <= 0 { return } if c.result[url] != "" { return } body, urls, err := fetcher.Fetch(url) if err != nil { c.result[url] = err.Error() fmt.Println(err) return } c.result[url] = body fmt.Printf("found: %s %q\n", url, body) for _, u := range urls { c.Crawl(u, depth-1, fetcher) } return } func main() { c := Crawler{ result: make(map[string]string) } c.Crawl("https://golang.org/", 4, fetcher) }
V2: go routine + sync.Mutex で非同期化
非同期化したものMutexだけじゃ処理が終わるのを待ってくれない。いまいちな方法だがとりあえずtime.Sleepで処理が終わるのを待たせてみる。チュートリアルの流れ的にMutex使う方法かなと思っていたけどそう単純にはいかなかった。
package main import ( "fmt" "sync" "time" ) type Fetcher interface { Fetch(url string) (body string, urls []string, err error) } type Crawler struct { m sync.Mutex result map[string]string } func (c *Crawler) Crawl(url string, depth int, fetcher Fetcher) { if depth <= 0 { return } if c.result[url] != "" { return } c.m.Lock() body, urls, err := fetcher.Fetch(url) c.m.Unlock() if err != nil { c.result[url] = err.Error() fmt.Println(err) return } c.result[url] = body fmt.Printf("found: %s %q\n", url, body) for _, u := range urls { go c.Crawl(u, depth-1, fetcher) } return } func main() { c := Crawler{ result: make(map[string]string) } go c.Crawl("https://golang.org/", 4, fetcher) time.Sleep(1000) c.m.Lock() fmt.Printf("Cache: %s\n", c.result) c.m.Unlock() }
V3: time.Sleepを使わないように修正
別の方法としてchannelを使う、JavaScriptでいうPromise .allのようなものを探すなどがありそうか。channel使うとデータの受け渡しをする方向に修正が必要か。そう考えるとPromise .all的なものを探して使ってみた。(V2の後にfetcherをstruct Crawlerに入れる修正してた)
import ( "fmt" "sync" ) type Fetcher interface { Fetch(url string) (body string, urls []string, err error) } type Crawler struct { wg *sync.WaitGroup result map[string]string fetcher Fetcher } func (c *Crawler) Crawl(url string, depth int) { if depth <= 0 { return } if c.result[url] != "" { return } body, urls, err := c.fetcher.Fetch(url) if err != nil { c.result[url] = err.Error() fmt.Println(err) return } c.result[url] = body fmt.Printf("found: %s %q\n", url, body) c.wg.Add(len(urls)) for _, u := range urls { go func(u string) { defer c.wg.Done() c.Crawl(u, depth-1) }(u) } return } func main() { c := Crawler{ result: make(map[string]string), wg: &sync.WaitGroup{}, fetcher: fetcher, } c.Crawl("https://golang.org/", 4) c.wg.Wait() }
V3 おまけ
やっていく中で本題とは違うところで引っ掛かった。クロージャ + range + goroutineを使うと挙動が想定と違った。以下のように単純化してループを作成して挙動を確認してみる。
urls := [2]string{ "https://golang.org/cmd/", "https://golang.org/pkg/", }
1個目のコードでは range urls
の初回の結果がバインドされ、ループ2回目も同じ値が入っている。なのでこのようなケースでは引数として値を渡す必要がある。
for _, u := range urls { go func() { fmt.Printf("url = %v\n", u) }() } time.Sleep(1000) } // url = https://golang.org/pkg/ // url = https://golang.org/pkg/
for _, u := range urls { go func(u string) { fmt.Printf("url = %v\n", u) }(u) } time.Sleep(1000) } // url = https://golang.org/cmd/ // url = https://golang.org/pkg/
goroutineを使わない場合はレキシカルな評価をしてくれる。
for _, u := range urls { func() { fmt.Printf("url = %v\n", u) }() } } // url = https://golang.org/cmd/ // url = https://golang.org/pkg/
for _, u := range urls { func(u string) { fmt.Printf("url = %v\n", u) }(u) } } // url = https://golang.org/cmd/ // url = https://golang.org/pkg/
こちらのサンプルコードが、自分の方針と同じ方針だったのは励みと参考になってよかった。
A Tour of Go の Exercise: Web Crawler を書いてみた - Qiita
syncパッケージは奥深そう。アプリケーションレベルではMap, Onceあたりが利用機会ありそうな予感がする。
sync.Condはうまく使うとコードがシンプルにできるのは良さそうだな。
まとめ
冬休み中にA Tour of Goを完走できた!前半はサクサク進めれたのでGo簡単じゃんと思ってたけど並列処理周辺は比較すると理解に時間が必要だった。あくまで前半よりという話なので、シンプルにかつ使いやすい感じに作られているんだろうな。ちょうどいい難易度のExerciseを適宜挟んで理解を促進してくれたのも助かった。
この先は何をやろうかな。