递归 Goroutines,告诉 Go 停止从通道读取的最简洁方法是什么?

huangapple go评论122阅读模式
英文:

Recursive Goroutines, what is the neatest way to tell Go to stop reading from channel?

问题

我想知道解决这个问题的惯用方法(目前会引发死锁错误),递归会分支出未知次数,所以我不能简单地关闭通道。

我已经通过传递一个指向数字的指针并递增它来使其工作,并且我已经研究了使用Sync waitgroups。我觉得(可能我错了),我没有想出一个优雅的解决方案。我看过的Go示例往往简单、巧妙而简洁。

这是来自Go之旅的最后一个练习,https://tour.golang.org/#73

你知道“一个Go程序员”会如何处理这个问题吗?任何帮助将不胜感激。我想从一开始就学好。

英文:

I want to know the idiomatic way to solve this (which currently throws a deadlock error), the recursion branches an unknown number of times, so I cannot simply close the channel.

http://play.golang.org/p/avLf_sQJj_

I have made it work, by passing a pointer to a number, and incrementing it, and I've looked into using Sync waitgroups. I didn't feel (and I may be wrong), that I'd came up with an elegant solution. The Go examples I have seen tend to be simple, clever and concise.

This is the last exercise from a Tour of Go, https://tour.golang.org/#73

Do you know 'how a Go programmer' would manage this? Any help would be appreciated. I'm trying to learn well from the start.

答案1

得分: 4

这是我对这个练习的解释。类似的练习有很多,但这是我的解决方案。我使用了sync.WaitGroup和一个自定义的、受互斥锁保护的映射来存储已访问的URL。主要是因为Go的标准map类型不是线程安全的。我还将数据和错误通道合并为一个结构体,该结构体具有一个方法来读取这些通道的内容。主要是为了关注点分离和代码更加清晰。

以下是示例代码(在playground上):

  1. package main
  2. import (
  3. "fmt"
  4. "sync"
  5. )
  6. type Fetcher interface {
  7. // Fetch返回URL的内容和在该页面上找到的URL列表。
  8. Fetch(url string) (body string, urls []string, err error)
  9. }
  10. // Crawl使用fetcher递归地爬取从url开始的页面,最大深度为depth。
  11. func Crawl(wg *sync.WaitGroup, url string, depth int, fetcher Fetcher, cache *UrlCache, results *Results) {
  12. defer wg.Done()
  13. if depth <= 0 || !cache.AtomicSet(url) {
  14. return
  15. }
  16. body, urls, err := fetcher.Fetch(url)
  17. if err != nil {
  18. results.Error <- err
  19. return
  20. }
  21. results.Data <- [2]string{url, body}
  22. for _, url := range urls {
  23. wg.Add(1)
  24. go Crawl(wg, url, depth-1, fetcher, cache, results)
  25. }
  26. }
  27. func main() {
  28. var wg sync.WaitGroup
  29. cache := NewUrlCache()
  30. results := NewResults()
  31. defer results.Close()
  32. wg.Add(1)
  33. go Crawl(&wg, "http://golang.org/", 4, fetcher, cache, results)
  34. go results.Read()
  35. wg.Wait()
  36. }
  37. // Results定义了用于单个爬取的URL的结果通道。
  38. type Results struct {
  39. Data chan [2]string // url + body.
  40. Error chan error // 可能的fetcher错误。
  41. }
  42. func NewResults() *Results {
  43. return &Results{
  44. Data: make(chan [2]string, 1),
  45. Error: make(chan error, 1),
  46. }
  47. }
  48. func (r *Results) Close() error {
  49. close(r.Data)
  50. close(r.Error)
  51. return nil
  52. }
  53. // Read读取爬取的结果或错误,只要通道是打开的。
  54. func (r *Results) Read() {
  55. for {
  56. select {
  57. case data := <-r.Data:
  58. fmt.Println(">", data)
  59. case err := <-r.Error:
  60. fmt.Println("e", err)
  61. }
  62. }
  63. }
  64. // UrlCache定义了我们已经访问过的URL的缓存。
  65. type UrlCache struct {
  66. sync.Mutex
  67. data map[string]struct{} // 空结构体占用0字节,而bool占用1字节。
  68. }
  69. func NewUrlCache() *UrlCache { return &UrlCache{data: make(map[string]struct{})} }
  70. // AtomicSet将给定的URL设置到缓存中,并返回false(如果URL已经存在)。
  71. //
  72. // 所有操作都在同一个锁定的上下文中进行。在多个goroutine中进行修改而没有同步是不安全的。
  73. // 进行Exists()检查和Set()操作的分离将创建竞态条件,因此我们必须将两者合并为一个操作。
  74. func (c *UrlCache) AtomicSet(url string) bool {
  75. c.Lock()
  76. _, ok := c.data[url]
  77. c.data[url] = struct{}{}
  78. c.Unlock()
  79. return !ok
  80. }
  81. // fakeFetcher是一个返回固定结果的Fetcher。
  82. type fakeFetcher map[string]*fakeResult
  83. type fakeResult struct {
  84. body string
  85. urls []string
  86. }
  87. func (f fakeFetcher) Fetch(url string) (string, []string, error) {
  88. if res, ok := f[url]; ok {
  89. return res.body, res.urls, nil
  90. }
  91. return "", nil, fmt.Errorf("not found: %s", url)
  92. }
  93. // fetcher是一个填充了数据的fakeFetcher。
  94. var fetcher = fakeFetcher{
  95. "http://golang.org/": &fakeResult{
  96. "The Go Programming Language",
  97. []string{
  98. "http://golang.org/pkg/",
  99. "http://golang.org/cmd/",
  100. },
  101. },
  102. "http://golang.org/pkg/": &fakeResult{
  103. "Packages",
  104. []string{
  105. "http://golang.org/",
  106. "http://golang.org/cmd/",
  107. "http://golang.org/pkg/fmt/",
  108. "http://golang.org/pkg/os/",
  109. },
  110. },
  111. "http://golang.org/pkg/fmt/": &fakeResult{
  112. "Package fmt",
  113. []string{
  114. "http://golang.org/",
  115. "http://golang.org/pkg/",
  116. },
  117. },
  118. "http://golang.org/pkg/os/": &fakeResult{
  119. "Package os",
  120. []string{
  121. "http://golang.org/",
  122. "http://golang.org/pkg/",
  123. },
  124. },
  125. }
  126. 这个代码没有经过广泛测试所以可能还有一些可以优化和修复的地方但至少可以给你一些思路
  127. <details>
  128. <summary>英文:</summary>
  129. Here is my interpretation of the exercise. There are many like it, but this is mine. I use `sync.WaitGroup` and a custom, mutex-protected map to store visited URLs. Mostly because Go&#39;s standard `map` type is not thread safe. I also combine the data and error channels into a single structure, which has a method doing the reading of said channels. Mostly for separation of concerns and (arguably) keeping things a little cleaner.
  130. Example [on playground](http://play.golang.org/p/DmS-Crw2AD):
  131. package main
  132. import (
  133. &quot;fmt&quot;
  134. &quot;sync&quot;
  135. )
  136. type Fetcher interface {
  137. // Fetch returns the body of URL and
  138. // a slice of URLs found on that page.
  139. Fetch(url string) (body string, urls []string, err error)
  140. }
  141. // Crawl uses fetcher to recursively crawl
  142. // pages starting with url, to a maximum of depth.
  143. func Crawl(wg *sync.WaitGroup, url string, depth int, fetcher Fetcher, cache *UrlCache, results *Results) {
  144. defer wg.Done()
  145. if depth &lt;= 0 || !cache.AtomicSet(url) {
  146. return
  147. }
  148. body, urls, err := fetcher.Fetch(url)
  149. if err != nil {
  150. results.Error &lt;- err
  151. return
  152. }
  153. results.Data &lt;- [2]string{url, body}
  154. for _, url := range urls {
  155. wg.Add(1)
  156. go Crawl(wg, url, depth-1, fetcher, cache, results)
  157. }
  158. }
  159. func main() {
  160. var wg sync.WaitGroup
  161. cache := NewUrlCache()
  162. results := NewResults()
  163. defer results.Close()
  164. wg.Add(1)
  165. go Crawl(&amp;wg, &quot;http://golang.org/&quot;, 4, fetcher, cache, results)
  166. go results.Read()
  167. wg.Wait()
  168. }
  169. // Results defines channels which yield results for a single crawled URL.
  170. type Results struct {
  171. Data chan [2]string // url + body.
  172. Error chan error // Possible fetcher error.
  173. }
  174. func NewResults() *Results {
  175. return &amp;Results{
  176. Data: make(chan [2]string, 1),
  177. Error: make(chan error, 1),
  178. }
  179. }
  180. func (r *Results) Close() error {
  181. close(r.Data)
  182. close(r.Error)
  183. return nil
  184. }
  185. // Read reads crawled results or errors, for as long as the channels are open.
  186. func (r *Results) Read() {
  187. for {
  188. select {
  189. case data := &lt;-r.Data:
  190. fmt.Println(&quot;&gt;&quot;, data)
  191. case err := &lt;-r.Error:
  192. fmt.Println(&quot;e&quot;, err)
  193. }
  194. }
  195. }
  196. // UrlCache defines a cache of URL&#39;s we&#39;ve already visited.
  197. type UrlCache struct {
  198. sync.Mutex
  199. data map[string]struct{} // Empty struct occupies 0 bytes, whereas bool takes 1 bytes.
  200. }
  201. func NewUrlCache() *UrlCache { return &amp;UrlCache{data: make(map[string]struct{})} }
  202. // AtomicSet sets the given url in the cache and returns false if it already existed.
  203. //
  204. // All within the same locked context. Modifying a map without synchronisation is not safe
  205. // when done from multiple goroutines. Doing a Exists() check and Set() separately will
  206. // create a race condition, so we must combine both in a single operation.
  207. func (c *UrlCache) AtomicSet(url string) bool {
  208. c.Lock()
  209. _, ok := c.data[url]
  210. c.data[url] = struct{}{}
  211. c.Unlock()
  212. return !ok
  213. }
  214. // fakeFetcher is Fetcher that returns canned results.
  215. type fakeFetcher map[string]*fakeResult
  216. type fakeResult struct {
  217. body string
  218. urls []string
  219. }
  220. func (f fakeFetcher) Fetch(url string) (string, []string, error) {
  221. if res, ok := f[url]; ok {
  222. return res.body, res.urls, nil
  223. }
  224. return &quot;&quot;, nil, fmt.Errorf(&quot;not found: %s&quot;, url)
  225. }
  226. // fetcher is a populated fakeFetcher.
  227. var fetcher = fakeFetcher{
  228. &quot;http://golang.org/&quot;: &amp;fakeResult{
  229. &quot;The Go Programming Language&quot;,
  230. []string{
  231. &quot;http://golang.org/pkg/&quot;,
  232. &quot;http://golang.org/cmd/&quot;,
  233. },
  234. },
  235. &quot;http://golang.org/pkg/&quot;: &amp;fakeResult{
  236. &quot;Packages&quot;,
  237. []string{
  238. &quot;http://golang.org/&quot;,
  239. &quot;http://golang.org/cmd/&quot;,
  240. &quot;http://golang.org/pkg/fmt/&quot;,
  241. &quot;http://golang.org/pkg/os/&quot;,
  242. },
  243. },
  244. &quot;http://golang.org/pkg/fmt/&quot;: &amp;fakeResult{
  245. &quot;Package fmt&quot;,
  246. []string{
  247. &quot;http://golang.org/&quot;,
  248. &quot;http://golang.org/pkg/&quot;,
  249. },
  250. },
  251. &quot;http://golang.org/pkg/os/&quot;: &amp;fakeResult{
  252. &quot;Package os&quot;,
  253. []string{
  254. &quot;http://golang.org/&quot;,
  255. &quot;http://golang.org/pkg/&quot;,
  256. },
  257. },
  258. }
  259. This has not been tested extensively, so perhaps there are optimisations and fixes that can be applied, but it should at least give you some ideas.
  260. </details>
  261. # 答案2
  262. **得分**: 2
  263. 不要使用`sync.WaitGroup`而是可以扩展发送到解析的URL上的结果并包括找到的新URL数量在主循环中只要有要收集的内容就可以继续读取结果
  264. 在你的情况下找到的URL数量将是生成的goroutine数量但不一定需要是这样我个人会生成固定数量的获取例程这样你就不会打开太多的HTTP请求或者至少可以对其进行控制)。然后你的主循环不会改变因为它不关心获取是如何执行的这里的重要事实是你需要为每个URL发送一个结果或错误 - 我已经修改了代码所以当深度已经为1它不会生成新的例程
  265. 这种解决方案的一个副作用是你可以在主循环中轻松打印进度
  266. 以下是示例代码
  267. ```go
  268. package main
  269. import (
  270. "fmt"
  271. )
  272. type Fetcher interface {
  273. // Fetch返回URL的内容和在该页面上找到的URL切片。
  274. Fetch(url string) (body string, urls []string, err error)
  275. }
  276. type Res struct {
  277. url string
  278. body string
  279. found int // 找到的新URL数量
  280. }
  281. // Crawl使用fetcher递归地爬取从url开始的页面,最大深度为depth。
  282. func Crawl(url string, depth int, fetcher Fetcher, ch chan Res, errs chan error, visited map[string]bool) {
  283. body, urls, err := fetcher.Fetch(url)
  284. visited
    = true
  285. if err != nil {
  286. errs <- err
  287. return
  288. }
  289. newUrls := 0
  290. if depth > 1 {
  291. for _, u := range urls {
  292. if !visited[u] {
  293. newUrls++
  294. go Crawl(u, depth-1, fetcher, ch, errs, visited)
  295. }
  296. }
  297. }
  298. // 将结果与要获取的URL数量一起发送
  299. ch <- Res{url, body, newUrls}
  300. return
  301. }
  302. func main() {
  303. ch := make(chan Res)
  304. errs := make(chan error)
  305. visited := map[string]bool{}
  306. go Crawl("http://golang.org/", 4, fetcher, ch, errs, visited)
  307. tocollect := 1
  308. for n := 0; n < tocollect; n++ {
  309. select {
  310. case s := <-ch:
  311. fmt.Printf("found: %s %q\n", s.url, s.body)
  312. tocollect += s.found
  313. case e := <-errs:
  314. fmt.Println(e)
  315. }
  316. }
  317. }
  318. // fakeFetcher是返回预定义结果的Fetcher。
  319. type fakeFetcher map[string]*fakeResult
  320. type fakeResult struct {
  321. body string
  322. urls []string
  323. }
  324. func (f fakeFetcher) Fetch(url string) (string, []string, error) {
  325. if res, ok := f
    ; ok {
  326. return res.body, res.urls, nil
  327. }
  328. return "", nil, fmt.Errorf("not found: %s", url)
  329. }
  330. // fetcher是一个填充了fakeFetcher的实例。
  331. var fetcher = fakeFetcher{
  332. "http://golang.org/": &fakeResult{
  333. "The Go Programming Language",
  334. []string{
  335. "http://golang.org/pkg/",
  336. "http://golang.org/cmd/",
  337. },
  338. },
  339. "http://golang.org/pkg/": &fakeResult{
  340. "Packages",
  341. []string{
  342. "http://golang.org/",
  343. "http://golang.org/cmd/",
  344. "http://golang.org/pkg/fmt/",
  345. "http://golang.org/pkg/os/",
  346. },
  347. },
  348. "http://golang.org/pkg/fmt/": &fakeResult{
  349. "Package fmt",
  350. []string{
  351. "http://golang.org/",
  352. "http://golang.org/pkg/",
  353. },
  354. },
  355. "http://golang.org/pkg/os/": &fakeResult{
  356. "Package os",
  357. []string{
  358. "http://golang.org/",
  359. "http://golang.org/pkg/",
  360. },
  361. },
  362. }
  363. 是的,遵循@jimt的建议,使对映射的访问线程安全。
  364. <details>
  365. <summary>英文:</summary>
  366. Instead of involving `sync.WaitGroup`, you could extend the result being send on a parsed url and include number of new URLs found. In your main loop you would then keep reading the results as long as there&#39;s something to collect.
  367. In your case number of urls found would be number of go routines spawned, but it doesn&#39;t necessarily need to be. I would personally spawn more or less fixed number of fetching routines, so you don&#39;t open too many HTTP requests (or at least you have control over it). Your main loop wouldn&#39;t change then, as it doesn&#39;t care how the fetching is being executed. The important fact here is that you need to send either a result or error for each url – I&#39;ve modified the code here, so it doesn&#39;t spawn new routines when the depth is already 1.
  368. A side effect of this solution is that you can easily print the progress in your main loop.
  369. Here is the example on playground:
  370. http://play.golang.org/p/BRlUc6bojf
  371. package main
  372. import (
  373. &quot;fmt&quot;
  374. )
  375. type Fetcher interface {
  376. // Fetch returns the body of URL and
  377. // a slice of URLs found on that page.
  378. Fetch(url string) (body string, urls []string, err error)
  379. }
  380. type Res struct {
  381. url string
  382. body string
  383. found int // Number of new urls found
  384. }
  385. // Crawl uses fetcher to recursively crawl
  386. // pages starting with url, to a maximum of depth.
  387. func Crawl(url string, depth int, fetcher Fetcher, ch chan Res, errs chan error, visited map[string]bool) {
  388. body, urls, err := fetcher.Fetch(url)
  389. visited
    = true
  390. if err != nil {
  391. errs &lt;- err
  392. return
  393. }
  394. newUrls := 0
  395. if depth &gt; 1 {
  396. for _, u := range urls {
  397. if !visited[u] {
  398. newUrls++
  399. go Crawl(u, depth-1, fetcher, ch, errs, visited)
  400. }
  401. }
  402. }
  403. // Send the result along with number of urls to be fetched
  404. ch &lt;- Res{url, body, newUrls}
  405. return
  406. }
  407. func main() {
  408. ch := make(chan Res)
  409. errs := make(chan error)
  410. visited := map[string]bool{}
  411. go Crawl(&quot;http://golang.org/&quot;, 4, fetcher, ch, errs, visited)
  412. tocollect := 1
  413. for n := 0; n &lt; tocollect; n++ {
  414. select {
  415. case s := &lt;-ch:
  416. fmt.Printf(&quot;found: %s %q\n&quot;, s.url, s.body)
  417. tocollect += s.found
  418. case e := &lt;-errs:
  419. fmt.Println(e)
  420. }
  421. }
  422. }
  423. // fakeFetcher is Fetcher that returns canned results.
  424. type fakeFetcher map[string]*fakeResult
  425. type fakeResult struct {
  426. body string
  427. urls []string
  428. }
  429. func (f fakeFetcher) Fetch(url string) (string, []string, error) {
  430. if res, ok := f
    ; ok {
  431. return res.body, res.urls, nil
  432. }
  433. return &quot;&quot;, nil, fmt.Errorf(&quot;not found: %s&quot;, url)
  434. }
  435. // fetcher is a populated fakeFetcher.
  436. var fetcher = fakeFetcher{
  437. &quot;http://golang.org/&quot;: &amp;fakeResult{
  438. &quot;The Go Programming Language&quot;,
  439. []string{
  440. &quot;http://golang.org/pkg/&quot;,
  441. &quot;http://golang.org/cmd/&quot;,
  442. },
  443. },
  444. &quot;http://golang.org/pkg/&quot;: &amp;fakeResult{
  445. &quot;Packages&quot;,
  446. []string{
  447. &quot;http://golang.org/&quot;,
  448. &quot;http://golang.org/cmd/&quot;,
  449. &quot;http://golang.org/pkg/fmt/&quot;,
  450. &quot;http://golang.org/pkg/os/&quot;,
  451. },
  452. },
  453. &quot;http://golang.org/pkg/fmt/&quot;: &amp;fakeResult{
  454. &quot;Package fmt&quot;,
  455. []string{
  456. &quot;http://golang.org/&quot;,
  457. &quot;http://golang.org/pkg/&quot;,
  458. },
  459. },
  460. &quot;http://golang.org/pkg/os/&quot;: &amp;fakeResult{
  461. &quot;Package os&quot;,
  462. []string{
  463. &quot;http://golang.org/&quot;,
  464. &quot;http://golang.org/pkg/&quot;,
  465. },
  466. },
  467. }
  468. And yes, follow @jimt advice and make access to the map thread safe.
  469. </details>
  470. # 答案3
  471. **得分**: 0
  472. 以下是我解决Go Tour中Web爬虫练习的方法:
  473. **为了在并行执行中跟踪递归完成**,我使用了原子整数计数器来跟踪并行递归中正在爬取的URL数量。在主函数中,我在循环中等待,直到原子计数器递减回零。
  474. **为了避免再次爬取相同的URL**,我使用了带有互斥锁的映射来跟踪已爬取的URL。
  475. 以下是相应的代码片段。
  476. 你可以在[Github上找到完整的工作代码][1]
  477. ```go
  478. // Safe HashSet Version
  479. type SafeHashSet struct {
  480. sync.Mutex
  481. urls map[string]bool //我们主要希望将其用作哈希集,因此映射的值对我们来说不重要
  482. }
  483. var (
  484. urlSet SafeHashSet
  485. urlCounter int64
  486. )
  487. // 将URL添加到集合中,如果新的URL被添加(如果不存在)
  488. func (m *SafeHashSet) add(newUrl string) bool {
  489. m.Lock()
  490. defer m.Unlock()
  491. _, ok := m.urls[newUrl]
  492. if !ok {
  493. m.urls[newUrl] = true
  494. return true
  495. }
  496. return false
  497. }
  498. // Crawl使用fetcher递归爬取
  499. // 从url开始的页面,最大深度为depth。
  500. func Crawl(url string, depth int, fetcher Fetcher) {
  501. // 当此爬取函数退出时,递减原子URL计数器
  502. defer atomic.AddInt64(&urlCounter, -1)
  503. if depth <= 0 {
  504. return
  505. }
  506. // 如果URL已经处理过,则不处理
  507. isNewUrl := urlSet.add(url)
  508. if !isNewUrl {
  509. fmt.Printf("skip: \t%s\n", url)
  510. return
  511. }
  512. body, urls, err := fetcher.Fetch(url)
  513. if err != nil {
  514. fmt.Println(err)
  515. return
  516. }
  517. fmt.Printf("found: \t%s %q\n", url, body)
  518. for _, u := range urls {
  519. atomic.AddInt64(&urlCounter, 1)
  520. // 并行爬取
  521. go Crawl(u, depth-1, fetcher)
  522. }
  523. return
  524. }
  525. func main() {
  526. urlSet = SafeHashSet{urls: make(map[string]bool)}
  527. atomic.AddInt64(&urlCounter, 1)
  528. go Crawl("https://golang.org/", 4, fetcher)
  529. for atomic.LoadInt64(&urlCounter) > 0 {
  530. time.Sleep(100 * time.Microsecond)
  531. }
  532. fmt.Println("Exiting")
  533. }
英文:

Here is how I solved the Web Crawler exercise of the Go Tour

For tracking recursion completion in parallel execution, I have used Atomic Integer counter to keep track of how many urls are getting crawled in parallel recursions. In the main function, I wait in loop till the atomic counter is decremented back to ZERO.

For avoiding crawling the same URL again, I have used a map with Mutex to keep track of crawled urls.

Below are the code snippets for the same.

You can find the entire working code here on Github

  1. // Safe HashSet Version
  2. type SafeHashSet struct {
  3. sync.Mutex
  4. urls map[string]bool //Primarily we wanted use this as an hashset, so the value of map is not significant to us
  5. }
  6. var (
  7. urlSet SafeHashSet
  8. urlCounter int64
  9. )
  10. // Adds an URL to the Set, returns true if new url was added (if not present already)
  11. func (m *SafeHashSet) add(newUrl string) bool {
  12. m.Lock()
  13. defer m.Unlock()
  14. _, ok := m.urls[newUrl]
  15. if !ok {
  16. m.urls[newUrl] = true
  17. return true
  18. }
  19. return false
  20. }
  21. // Crawl uses fetcher to recursively crawl
  22. // pages starting with url, to a maximum of depth.
  23. func Crawl(url string, depth int, fetcher Fetcher) {
  24. // Decrement the atomic url counter, when this crawl function exits
  25. defer atomic.AddInt64(&amp;urlCounter, -1)
  26. if depth &lt;= 0 {
  27. return
  28. }
  29. // Don&#39;t Process a url if it is already processed
  30. isNewUrl := urlSet.add(url)
  31. if !isNewUrl {
  32. fmt.Printf(&quot;skip: \t%s\n&quot;, url)
  33. return
  34. }
  35. body, urls, err := fetcher.Fetch(url)
  36. if err != nil {
  37. fmt.Println(err)
  38. return
  39. }
  40. fmt.Printf(&quot;found: \t%s %q\n&quot;, url, body)
  41. for _, u := range urls {
  42. atomic.AddInt64(&amp;urlCounter, 1)
  43. // Crawl parallely
  44. go Crawl(u, depth-1, fetcher)
  45. }
  46. return
  47. }
  48. func main() {
  49. urlSet = SafeHashSet{urls: make(map[string]bool)}
  50. atomic.AddInt64(&amp;urlCounter, 1)
  51. go Crawl(&quot;https://golang.org/&quot;, 4, fetcher)
  52. for atomic.LoadInt64(&amp;urlCounter) &gt; 0 {
  53. time.Sleep(100 * time.Microsecond)
  54. }
  55. fmt.Println(&quot;Exiting&quot;)
  56. }

huangapple
  • 本文由 发表于 2014年11月24日 18:55:04
  • 转载请务必保留本文链接:https://go.coder-hub.com/27103161.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定