diff --git a/ch8/ch8-01.md b/ch8/ch8-01.md index f7d3924..c238e46 100644 --- a/ch8/ch8-01.md +++ b/ch8/ch8-01.md @@ -13,31 +13,30 @@ go f() // create a new goroutine that calls f(); don't wait 在下面的例子中,main goroutine會計算第45個菲波那契數。由於計算函數使用了效率非常低的遞歸,所以會運行相當可觀的一段時間,在這期間我們想要讓用戶看到一個可見的標識來表明程序依然在正常運行,所以顯示一個動畵的小圖標: +gopl.io/ch8/spinner ```go -gopl.io/ch8/spinner func main() { - go spinner(100 * time.Millisecond) - const n = 45 - fibN := fib(n) // slow - fmt.Printf("\rFibonacci(%d) = %d\n", n, fibN) + go spinner(100 * time.Millisecond) + const n = 45 + fibN := fib(n) // slow + fmt.Printf("\rFibonacci(%d) = %d\n", n, fibN) } func spinner(delay time.Duration) { - for { - for _, r := range `-\|/` { - fmt.Printf("\r%c", r) - time.Sleep(delay) - } - } + for { + for _, r := range `-\|/` { + fmt.Printf("\r%c", r) + time.Sleep(delay) + } + } } func fib(x int) int { - if x < 2 { - return x - } - return fib(x-1) + fib(x-2) + if x < 2 { + return x + } + return fib(x-1) + fib(x-2) } - ``` 動畵顯示了幾秒之後,fib(45)的調用成功地返迴,併且打印結果: diff --git a/ch8/ch8-02.md b/ch8/ch8-02.md index e9aecd8..cc807ea 100644 --- a/ch8/ch8-02.md +++ b/ch8/ch8-02.md @@ -4,43 +4,43 @@ 我們的第一個例子是一個順序執行的時鐘服務器,它會每隔一秒鐘將當前時間寫到客戶端: +gopl.io/ch8/clock1 ```go -gopl.io/ch8/clock1 // Clock1 is a TCP server that periodically writes the time. package main import ( - "io" - "log" - "net" - "time" + "io" + "log" + "net" + "time" ) func main() { - listener, err := net.Listen("tcp", "localhost:8000") - if err != nil { - log.Fatal(err) - } + listener, err := net.Listen("tcp", "localhost:8000") + if err != nil { + log.Fatal(err) + } - for { - conn, err := listener.Accept() - if err != nil { - log.Print(err) // e.g., connection aborted - continue - } - handleConn(conn) // handle one connection at a time - } + for { + conn, err := listener.Accept() + if err != nil { + log.Print(err) // e.g., connection aborted + continue + } + handleConn(conn) // handle one connection at a time + } } func handleConn(c net.Conn) { - defer c.Close() - for { - _, err := io.WriteString(c, time.Now().Format("15:04:05\n")) - if err != nil { - return // e.g., client disconnected - } - time.Sleep(1 * time.Second) - } + defer c.Close() + for { + _, err := io.WriteString(c, time.Now().Format("15:04:05\n")) + if err != nil { + return // e.g., client disconnected + } + time.Sleep(1 * time.Second) + } } ``` @@ -66,31 +66,31 @@ $ nc localhost 8000 客戶端將服務器發來的時間顯示了出來,我們用Control+C來中斷客戶端的執行,在Unix繫統上,你會看到^C這樣的響應。如果你的繫統沒有裝nc這個工具,你可以用telnet來實現同樣的效果,或者也可以用我們下面的這個用go寫的簡單的telnet程序,用net.Dial就可以簡單地創建一個TCP連接: +gopl.io/ch8/netcat1 ```go -gopl.io/ch8/netcat1 // Netcat1 is a read-only TCP client. package main import ( - "io" - "log" - "net" - "os" + "io" + "log" + "net" + "os" ) func main() { - conn, err := net.Dial("tcp", "localhost:8000") - if err != nil { - log.Fatal(err) - } - defer conn.Close() - mustCopy(os.Stdout, conn) + conn, err := net.Dial("tcp", "localhost:8000") + if err != nil { + log.Fatal(err) + } + defer conn.Close() + mustCopy(os.Stdout, conn) } func mustCopy(dst io.Writer, src io.Reader) { - if _, err := io.Copy(dst, src); err != nil { - log.Fatal(err) - } + if _, err := io.Copy(dst, src); err != nil { + log.Fatal(err) + } } ``` @@ -114,15 +114,15 @@ killall命令是一個Unix命令行工具,可以用給定的進程名來殺掉 第二個客戶端必須等待第一個客戶端完成工作,這樣服務端才能繼續向後執行;因爲我們這里的服務器程序同一時間隻能處理一個客戶端連接。我們這里對服務端程序做一點小改動,使其支持併發:在handleConn函數調用的地方增加go關鍵字,讓每一次handleConn的調用都進入一個獨立的goroutine。 +gopl.io/ch8/clock2 ```go -gopl.io/ch8/clock2 for { - conn, err := listener.Accept() - if err != nil { - log.Print(err) // e.g., connection aborted - continue - } - go handleConn(conn) // handle connections concurrently + conn, err := listener.Accept() + if err != nil { + log.Print(err) // e.g., connection aborted + continue + } + go handleConn(conn) // handle connections concurrently } ``` diff --git a/ch8/ch8-03.md b/ch8/ch8-03.md index ffdc1a8..28dcbb6 100644 --- a/ch8/ch8-03.md +++ b/ch8/ch8-03.md @@ -4,45 +4,45 @@ clock服務器每一個連接都會起一個goroutine。在本節中我們會創 ```go func handleConn(c net.Conn) { - io.Copy(c, c) // NOTE: ignoring errors - c.Close() + io.Copy(c, c) // NOTE: ignoring errors + c.Close() } ``` 一個更有意思的echo服務應該模擬一個實際的echo的“迴響”,併且一開始要用大寫HELLO來表示“聲音很大”,之後經過一小段延遲返迴一個有所緩和的Hello,然後一個全小寫字母的hello表示聲音漸漸變小直至消失,像下面這個版本的handleConn(譯註:笑看作者腦洞大開): +gopl.io/ch8/reverb1 ```go -gopl.io/ch8/reverb1 func echo(c net.Conn, shout string, delay time.Duration) { - fmt.Fprintln(c, "\t", strings.ToUpper(shout)) - time.Sleep(delay) - fmt.Fprintln(c, "\t", shout) - time.Sleep(delay) - fmt.Fprintln(c, "\t", strings.ToLower(shout)) + fmt.Fprintln(c, "\t", strings.ToUpper(shout)) + time.Sleep(delay) + fmt.Fprintln(c, "\t", shout) + time.Sleep(delay) + fmt.Fprintln(c, "\t", strings.ToLower(shout)) } func handleConn(c net.Conn) { - input := bufio.NewScanner(c) - for input.Scan() { - echo(c, input.Text(), 1*time.Second) - } - // NOTE: ignoring potential errors from input.Err() - c.Close() + input := bufio.NewScanner(c) + for input.Scan() { + echo(c, input.Text(), 1*time.Second) + } + // NOTE: ignoring potential errors from input.Err() + c.Close() } ``` 我們需要陞級我們的客戶端程序,這樣它就可以發送終端的輸入到服務器,併把服務端的返迴輸出到終端上,這使我們有了使用併發的另一個好機會: +gopl.io/ch8/netcat2 ```go -gopl.io/ch8/netcat2 func main() { - conn, err := net.Dial("tcp", "localhost:8000") - if err != nil { - log.Fatal(err) - } - defer conn.Close() - go mustCopy(os.Stdout, conn) - mustCopy(conn, os.Stdin) + conn, err := net.Dial("tcp", "localhost:8000") + if err != nil { + log.Fatal(err) + } + defer conn.Close() + go mustCopy(os.Stdout, conn) + mustCopy(conn, os.Stdin) } ``` @@ -74,15 +74,15 @@ $ killall reverb1 註意客戶端的第三次shout在前一個shout處理完成之前一直沒有被處理,這貌似看起來不是特别“現實”。眞實世界里的迴響應該是會由三次shout的迴聲組合而成的。爲了模擬眞實世界的迴響,我們需要更多的goroutine來做這件事情。這樣我們就再一次地需要go這個關鍵詞了,這次我們用它來調用echo: +gopl.io/ch8/reverb2 ```go -gopl.io/ch8/reverb2 func handleConn(c net.Conn) { - input := bufio.NewScanner(c) - for input.Scan() { - go echo(c, input.Text(), 1*time.Second) - } - // NOTE: ignoring potential errors from input.Err() - c.Close() + input := bufio.NewScanner(c) + for input.Scan() { + go echo(c, input.Text(), 1*time.Second) + } + // NOTE: ignoring potential errors from input.Err() + c.Close() } ``` diff --git a/ch8/ch8-04-1.md b/ch8/ch8-04-1.md index 9fdb573..2931181 100644 --- a/ch8/ch8-04-1.md +++ b/ch8/ch8-04-1.md @@ -10,8 +10,8 @@ 在8.3節的客戶端程序,它在主goroutine中(譯註:就是執行main函數的goroutine)將標準輸入複製到server,因此當客戶端程序關閉標準輸入時,後台goroutine可能依然在工作。我們需要讓主goroutine等待後台goroutine完成工作後再退出,我們使用了一個channel來同步兩個goroutine: +gopl.io/ch8/netcat3 ```Go -gopl.io/ch8/netcat3 func main() { conn, err := net.Dial("tcp", "localhost:8000") if err != nil { diff --git a/ch8/ch8-04-2.md b/ch8/ch8-04-2.md index 371001d..2530854 100644 --- a/ch8/ch8-04-2.md +++ b/ch8/ch8-04-2.md @@ -6,9 +6,8 @@ Channels也可以用於將多個goroutine鏈接在一起,一個Channels的輸 第一個goroutine是一個計數器,用於生成0、1、2、……形式的整數序列,然後通過channel將該整數序列發送給第二個goroutine;第二個goroutine是一個求平方的程序,對收到的每個整數求平方,然後將平方後的結果通過第二個channel發送給第三個goroutine;第三個goroutine是一個打印程序,打印收到的每個整數。爲了保持例子清晰,我們有意選擇了非常簡單的函數,當然三個goroutine的計算很簡單,在現實中確實沒有必要爲如此簡單的運算構建三個goroutine。 +gopl.io/ch8/pipeline1 ```Go -gopl.io/ch8/pipeline1 - func main() { naturals := make(chan int) squares := make(chan int) @@ -65,9 +64,8 @@ go func() { 在下面的改進中,我們的計數器goroutine隻生成100個含數字的序列,然後關閉naturals對應的channel,這將導致計算平方數的squarer對應的goroutine可以正常終止循環併關閉squares對應的channel。(在一個更複雜的程序中,可以通過defer語句關閉對應的channel。)最後,主goroutine也可以正常終止循環併退出程序。 +gopl.io/ch8/pipeline2 ```Go -gopl.io/ch8/pipeline2 - func main() { naturals := make(chan int) squares := make(chan int) diff --git a/ch8/ch8-04-3.md b/ch8/ch8-04-3.md index f2abc74..07b5eb7 100644 --- a/ch8/ch8-04-3.md +++ b/ch8/ch8-04-3.md @@ -18,8 +18,8 @@ func printer(in chan int) 這是改進的版本,這一次參數使用了單方向channel類型: +gopl.io/ch8/pipeline3 ```Go -gopl.io/ch8/pipeline3 func counter(out chan<- int) { for x := 0; x < 100; x++ { out <- x diff --git a/ch8/ch8-04-4.md b/ch8/ch8-04-4.md index a87bb9a..6adf49e 100644 --- a/ch8/ch8-04-4.md +++ b/ch8/ch8-04-4.md @@ -2,7 +2,6 @@ 帶緩存的Channel內部持有一個元素隊列。隊列的最大容量是在調用make函數創建channel時通過第二個參數指定的。下面的語句創建了一個可以持有三個字符串元素的帶緩存Channel。圖8.2是ch變量對應的channel的圖形表示形式。 - ```Go ch = make(chan string, 3) ``` diff --git a/ch8/ch8-05.md b/ch8/ch8-05.md index 64ad7b2..b932bed 100644 --- a/ch8/ch8-05.md +++ b/ch8/ch8-05.md @@ -2,9 +2,10 @@ 本節中,我們會探索一些用來在併行時循環迭代的常見併發模型。我們會探究從全尺寸圖片生成一些縮略圖的問題。gopl.io/ch8/thumbnail包提供了ImageFile函數來幫我們拉伸圖片。我們不會説明這個函數的實現,隻需要從gopl.io下載它。 +gopl.io/ch8/thumbnail ```go -gopl.io/ch8/thumbnail package thumbnail + // ImageFile reads an image from infile and writes // a thumbnail-size version of it in the same directory. // It returns the generated file name, e.g., "foo.thumb.jpg". @@ -13,15 +14,15 @@ func ImageFile(infile string) (string, error) 下面的程序會循環迭代一些圖片文件名,併爲每一張圖片生成一個縮略圖: +gopl.io/ch8/thumbnail ```go -gopl.io/ch8/thumbnail // makeThumbnails makes thumbnails of the specified files. func makeThumbnails(filenames []string) { - for _, f := range filenames { - if _, err := thumbnail.ImageFile(f); err != nil { - log.Println(err) - } - } + for _, f := range filenames { + if _, err := thumbnail.ImageFile(f); err != nil { + log.Println(err) + } + } } ``` @@ -32,9 +33,9 @@ func makeThumbnails(filenames []string) { ```go // NOTE: incorrect! func makeThumbnails2(filenames []string) { - for _, f := range filenames { - go thumbnail.ImageFile(f) // NOTE: ignoring errors - } + for _, f := range filenames { + go thumbnail.ImageFile(f) // NOTE: ignoring errors + } } ``` @@ -45,29 +46,28 @@ func makeThumbnails2(filenames []string) { ```go // makeThumbnails3 makes thumbnails of the specified files in parallel. func makeThumbnails3(filenames []string) { - ch := make(chan struct{}) - for _, f := range filenames { - go func(f string) { - thumbnail.ImageFile(f) // NOTE: ignoring errors - ch <- struct{}{} - }(f) - } - // Wait for goroutines to complete. - for range filenames { - <-ch - } + ch := make(chan struct{}) + for _, f := range filenames { + go func(f string) { + thumbnail.ImageFile(f) // NOTE: ignoring errors + ch <- struct{}{} + }(f) + } + // Wait for goroutines to complete. + for range filenames { + <-ch + } } ``` 註意我們將f的值作爲一個顯式的變量傳給了函數,而不是在循環的閉包中聲明: - ```go for _, f := range filenames { - go func() { - thumbnail.ImageFile(f) // NOTE: incorrect! - // ... - }() + go func() { + thumbnail.ImageFile(f) // NOTE: incorrect! + // ... + }() } ``` @@ -79,22 +79,22 @@ for _, f := range filenames { // makeThumbnails4 makes thumbnails for the specified files in parallel. // It returns an error if any step failed. func makeThumbnails4(filenames []string) error { - errors := make(chan error) + errors := make(chan error) - for _, f := range filenames { - go func(f string) { - _, err := thumbnail.ImageFile(f) - errors <- err - }(f)} - } + for _, f := range filenames { + go func(f string) { + _, err := thumbnail.ImageFile(f) + errors <- err + }(f)} + } - for range filenames { - if err := <-errors; err != nil { - return err // NOTE: incorrect: goroutine leak! - } - } + for range filenames { + if err := <-errors; err != nil { + return err // NOTE: incorrect: goroutine leak! + } + } - return nil + return nil } ``` @@ -108,31 +108,30 @@ func makeThumbnails4(filenames []string) error { // makeThumbnails5 makes thumbnails for the specified files in parallel. // It returns the generated file names in an arbitrary order, // or an error if any step failed. - func makeThumbnails5(filenames []string) (thumbfiles []string, err error) { - type item struct { - thumbfile string - err error - } + type item struct { + thumbfile string + err error + } - ch := make(chan item, len(filenames)) - for _, f := range filenames { - go func(f string) { - var it item - it.thumbfile, it.err = thumbnail.ImageFile(f) - ch <- it - }(f) - } + ch := make(chan item, len(filenames)) + for _, f := range filenames { + go func(f string) { + var it item + it.thumbfile, it.err = thumbnail.ImageFile(f) + ch <- it + }(f) + } - for range filenames { - it := <-ch - if it.err != nil { - return nil, it.err - } - thumbfiles = append(thumbfiles, it.thumbfile) - } + for range filenames { + it := <-ch + if it.err != nil { + return nil, it.err + } + thumbfiles = append(thumbfiles, it.thumbfile) + } - return thumbfiles, nil + return thumbfiles, nil } ``` @@ -144,35 +143,34 @@ func makeThumbnails5(filenames []string) (thumbfiles []string, err error) { // makeThumbnails6 makes thumbnails for each file received from the channel. // It returns the number of bytes occupied by the files it creates. func makeThumbnails6(filenames <-chan string) int64 { - sizes := make(chan int64) - var wg sync.WaitGroup // number of working goroutines - for f := range filenames { - wg.Add(1) - // worker - go func(f string) { - defer wg.Done() - thumb, err := thumbnail.ImageFile(f) - if err != nil { - log.Println(err) - return - } - info, _ := os.Stat(thumb) // OK to ignore error - sizes <- info.Size() - }(f) - } + sizes := make(chan int64) + var wg sync.WaitGroup // number of working goroutines + for f := range filenames { + wg.Add(1) + // worker + go func(f string) { + defer wg.Done() + thumb, err := thumbnail.ImageFile(f) + if err != nil { + log.Println(err) + return + } + info, _ := os.Stat(thumb) // OK to ignore error + sizes <- info.Size() + }(f) + } - // closer - go func() { - wg.Wait() - close(sizes) - }() + // closer + go func() { + wg.Wait() + close(sizes) + }() - - var total int64 - for size := range sizes { - total += size - } - return total + var total int64 + for size := range sizes { + total += size + } + return total } ``` diff --git a/ch8/ch8-06.md b/ch8/ch8-06.md index 6bcd470..e3b5bee 100644 --- a/ch8/ch8-06.md +++ b/ch8/ch8-06.md @@ -2,15 +2,15 @@ 在5.6節中,我們做了一個簡單的web爬蟲,用bfs(廣度優先)算法來抓取整個網站。在本節中,我們會讓這個這個爬蟲併行化,這樣每一個彼此獨立的抓取命令可以併行進行IO,最大化利用網絡資源。crawl函數和gopl.io/ch5/findlinks3中的是一樣的。 +gopl.io/ch8/crawl1 ```go -gopl.io/ch8/crawl1 func crawl(url string) []string { - fmt.Println(url) - list, err := links.Extract(url) - if err != nil { - log.Print(err) - } - return list + fmt.Println(url) + list, err := links.Extract(url) + if err != nil { + log.Print(err) + } + return list } ``` @@ -18,23 +18,23 @@ func crawl(url string) []string { ```go func main() { - worklist := make(chan []string) + worklist := make(chan []string) - // Start with the command-line arguments. - go func() { worklist <- os.Args[1:] }() + // Start with the command-line arguments. + go func() { worklist <- os.Args[1:] }() - // Crawl the web concurrently. - seen := make(map[string]bool) - for list := range worklist { - for _, link := range list { - if !seen[link] { - seen[link] = true - go func(link string) { - worklist <- crawl(link) - }(link) - } - } - } + // Crawl the web concurrently. + seen := make(map[string]bool) + for list := range worklist { + for _, link := range list { + if !seen[link] { + seen[link] = true + go func(link string) { + worklist <- crawl(link) + }(link) + } + } + } } ``` @@ -52,8 +52,7 @@ https://golang.org/doc/ https://golang.org/blog/ ... 2015/07/15 18:22:12 Get ...: dial tcp: lookup blog.golang.org: no such host -2015/07/15 18:22:12 Get ...: dial tcp 23.21.222.120:443: socket: - too many open files +2015/07/15 18:22:12 Get ...: dial tcp 23.21.222.120:443: socket: too many open files ... ``` @@ -65,54 +64,51 @@ https://golang.org/blog/ 讓我們重寫crawl函數,將對links.Extract的調用操作用獲取、釋放token的操作包裹起來,來確保同一時間對其隻有20個調用。信號量數量和其能操作的IO資源數量應保持接近。 +gopl.io/ch8/crawl2 ```go -gopl.io/ch8/crawl2 // tokens is a counting semaphore used to // enforce a limit of 20 concurrent requests. var tokens = make(chan struct{}, 20) func crawl(url string) []string { - fmt.Println(url) - tokens <- struct{}{} // acquire a token - list, err := links.Extract(url) - <-tokens // release the token - if err != nil { - log.Print(err) - } - return list + fmt.Println(url) + tokens <- struct{}{} // acquire a token + list, err := links.Extract(url) + <-tokens // release the token + if err != nil { + log.Print(err) + } + return list } ``` 第二個問題是這個程序永遠都不會終止,卽使它已經爬到了所有初始鏈接衍生出的鏈接。(當然,除非你慎重地選擇了合適的初始化URL或者已經實現了練習8.6中的深度限製,你應該還沒有意識到這個問題)。爲了使這個程序能夠終止,我們需要在worklist爲空或者沒有crawl的goroutine在運行時退出主循環。 - ```go func main() { - worklist := make(chan []string) - var n int // number of pending sends to worklist + worklist := make(chan []string) + var n int // number of pending sends to worklist - // Start with the command-line arguments. - n++ - go func() { worklist <- os.Args[1:] }() + // Start with the command-line arguments. + n++ + go func() { worklist <- os.Args[1:] }() + // Crawl the web concurrently. + seen := make(map[string]bool) - // Crawl the web concurrently. - seen := make(map[string]bool) - - for ; n > 0; n-- { - list := <-worklist - for _, link := range list { - if !seen[link] { - seen[link] = true - n++ - go func(link string) { - worklist <- crawl(link) - }(link) - } - } - } + for ; n > 0; n-- { + list := <-worklist + for _, link := range list { + if !seen[link] { + seen[link] = true + n++ + go func(link string) { + worklist <- crawl(link) + }(link) + } + } + } } - ``` 這個版本中,計算器n對worklist的發送操作數量進行了限製。每一次我們發現有元素需要被發送到worklist時,我們都會對n進行++操作,在向worklist中發送初始的命令行參數之前,我們也進行過一次++操作。這里的操作++是在每啟動一個crawler的goroutine之前。主循環會在n減爲0時終止,這時候説明沒活可榦了。 diff --git a/ch8/ch8-07.md b/ch8/ch8-07.md index 0536a7e..46d8433 100644 --- a/ch8/ch8-07.md +++ b/ch8/ch8-07.md @@ -2,27 +2,27 @@ 下面的程序會進行火箭發射的倒計時。time.Tick函數返迴一個channel,程序會週期性地像一個節拍器一樣向這個channel發送事件。每一個事件的值是一個時間戳,不過更有意思的是其傳送方式。 +gopl.io/ch8/countdown1 ```go -gopl.io/ch8/countdown1 func main() { - fmt.Println("Commencing countdown.") - tick := time.Tick(1 * time.Second) - for countdown := 10; countdown > 0; countdown-- { - fmt.Println(countdown) - j<-tick - } - launch() + fmt.Println("Commencing countdown.") + tick := time.Tick(1 * time.Second) + for countdown := 10; countdown > 0; countdown-- { + fmt.Println(countdown) + j<-tick + } + launch() } ``` 現在我們讓這個程序支持在倒計時中,用戶按下return鍵時直接中斷發射流程。首先,我們啟動一個goroutine,這個goroutine會嚐試從標準輸入中調入一個單獨的byte併且,如果成功了,會向名爲abort的channel發送一個值。 +gopl.io/ch8/countdown2 ```go -gopl.io/ch8/countdown2 abort := make(chan struct{}) go func() { - os.Stdin.Read(make([]byte, 1)) // read a single byte - abort <- struct{}{} + os.Stdin.Read(make([]byte, 1)) // read a single byte + abort <- struct{}{} }() ``` @@ -31,13 +31,13 @@ go func() { ```go select { case <-ch1: - // ... + // ... case x := <-ch2: - // ...use x... + // ...use x... case ch3 <- y: - // ... + // ... default: - // ... + // ... } ``` @@ -47,20 +47,19 @@ select會等待case中有能夠執行的case時去執行。當條件滿足時, 讓我們迴到我們的火箭發射程序。time.After函數會立卽返迴一個channel,併起一個新的goroutine在經過特定的時間後向該channel發送一個獨立的值。下面的select語句會會一直等待到兩個事件中的一個到達,無論是abort事件或者一個10秒經過的事件。如果10秒經過了還沒有abort事件進入,那麽火箭就會發射。 - ```go func main() { - // ...create abort channel... + // ...create abort channel... - fmt.Println("Commencing countdown. Press return to abort.") - select { - case <-time.After(10 * time.Second): - // Do nothing. - case <-abort: - fmt.Println("Launch aborted!") - return - } - launch() + fmt.Println("Commencing countdown. Press return to abort.") + select { + case <-time.After(10 * time.Second): + // Do nothing. + case <-abort: + fmt.Println("Launch aborted!") + return + } + launch() } ``` @@ -70,11 +69,11 @@ func main() { ```go ch := make(chan int, 1) for i := 0; i < 10; i++ { - select { - case x := <-ch: - fmt.Println(x) // "0" "2" "4" "6" "8" - case ch <- i: - } + select { + case x := <-ch: + fmt.Println(x) // "0" "2" "4" "6" "8" + case ch <- i: + } } ``` @@ -82,24 +81,24 @@ for i := 0; i < 10; i++ { 下面讓我們的發射程序打印倒計時。這里的select語句會使每次循環迭代等待一秒來執行退出操作。 +gopl.io/ch8/countdown3 ```go -gopl.io/ch8/countdown3 func main() { - // ...create abort channel... + // ...create abort channel... - fmt.Println("Commencing countdown. Press return to abort.") - tick := time.Tick(1 * time.Second) - for countdown := 10; countdown > 0; countdown-- { - fmt.Println(countdown) - select { - case <-tick: - // Do nothing. - case <-abort: - fmt.Println("Launch aborted!") - return - } - } - launch() + fmt.Println("Commencing countdown. Press return to abort.") + tick := time.Tick(1 * time.Second) + for countdown := 10; countdown > 0; countdown-- { + fmt.Println(countdown) + select { + case <-tick: + // Do nothing. + case <-abort: + fmt.Println("Launch aborted!") + return + } + } + launch() } ``` @@ -120,10 +119,10 @@ ticker.Stop() // cause the ticker's goroutine to terminate ```go select { case <-abort: - fmt.Printf("Launch aborted!\n") - return + fmt.Printf("Launch aborted!\n") + return default: - // do nothing + // do nothing } ``` diff --git a/ch8/ch8-08.md b/ch8/ch8-08.md index f959a02..e78119a 100644 --- a/ch8/ch8-08.md +++ b/ch8/ch8-08.md @@ -2,8 +2,8 @@ 在本小節中,我們會創建一個程序來生成指定目録的硬盤使用情況報告,這個程序和Unix里的du工具比較相似。大多數工作用下面這個walkDir函數來完成,這個函數使用dirents函數來枚舉一個目録下的所有入口。 +gopl.io/ch8/du1 ```go -gopl.io/ch8/du1 // walkDir recursively walks the file tree rooted at dir // and sends the size of each found file on fileSizes. func walkDir(dir string, fileSizes chan<- int64) { @@ -70,9 +70,8 @@ func main() { } func printDiskUsage(nfiles, nbytes int64) { - fmt.Printf("%d files %.1f GB\n", nfiles, float64(nbytes)/1e9) + fmt.Printf("%d files %.1f GB\n", nfiles, float64(nbytes)/1e9) } - ``` 這個程序會在打印其結果之前卡住很長時間。 @@ -87,8 +86,8 @@ $ ./du1 $HOME /usr /bin /etc 下面這個du的變種會間歇打印內容,不過隻有在調用時提供了-v的flag才會顯示程序進度信息。在roots目録上循環的後台goroutine在這里保持不變。主goroutine現在使用了計時器來每500ms生成事件,然後用select語句來等待文件大小的消息來更新總大小數據,或者一個計時器的事件來打印當前的總大小數據。如果-v的flag在運行時沒有傳入的話,tick這個channel會保持爲nil,這樣在select里的case也就相當於被禁用了。 +gopl.io/ch8/du2 ```go -gopl.io/ch8/du2 var verbose = flag.Bool("v", false, "show verbose progress messages") func main() { @@ -116,6 +115,7 @@ loop: printDiskUsage(nfiles, nbytes) // final totals } ``` + 由於我們的程序不再使用range循環,第一個select的case必須顯式地判斷fileSizes的channel是不是已經被關閉了,這里可以用到channel接收的二值形式。如果channel已經被關閉了的話,程序會直接退出循環。這里的break語句用到了標籤break,這樣可以同時終結select和for兩個循環;如果沒有用標籤就break的話隻會退出內層的select循環,而外層的for循環會使之進入下一輪select循環。 現在程序會悠閒地爲我們打印更新流: @@ -133,50 +133,49 @@ $ ./du2 -v $HOME /usr /bin /etc 然而這個程序還是會花上很長時間才會結束。無法對walkDir做併行化處理沒什麽别的原因,無非是因爲磁盤繫統併行限製。下面這個第三個版本的du,會對每一個walkDir的調用創建一個新的goroutine。它使用sync.WaitGroup (§8.5)來對仍舊活躍的walkDir調用進行計數,另一個goroutine會在計數器減爲零的時候將fileSizes這個channel關閉。 +gopl.io/ch8/du3 ```go -gopl.io/ch8/du3 func main() { - // ...determine roots... - // Traverse each root of the file tree in parallel. - fileSizes := make(chan int64) - var n sync.WaitGroup - for _, root := range roots { - n.Add(1) - go walkDir(root, &n, fileSizes) - } - go func() { - n.Wait() - close(fileSizes) - }() - // ...select loop... + // ...determine roots... + // Traverse each root of the file tree in parallel. + fileSizes := make(chan int64) + var n sync.WaitGroup + for _, root := range roots { + n.Add(1) + go walkDir(root, &n, fileSizes) + } + go func() { + n.Wait() + close(fileSizes) + }() + // ...select loop... } func walkDir(dir string, n *sync.WaitGroup, fileSizes chan<- int64) { - defer n.Done() - for _, entry := range dirents(dir) { - if entry.IsDir() { - n.Add(1) - subdir := filepath.Join(dir, entry.Name()) - go walkDir(subdir, n, fileSizes) - } else { - fileSizes <- entry.Size() - } - } + defer n.Done() + for _, entry := range dirents(dir) { + if entry.IsDir() { + n.Add(1) + subdir := filepath.Join(dir, entry.Name()) + go walkDir(subdir, n, fileSizes) + } else { + fileSizes <- entry.Size() + } + } } ``` 由於這個程序在高峯期會創建成百上韆的goroutine,我們需要脩改dirents函數,用計數信號量來阻止他同時打開太多的文件,就像我們在8.7節中的併發爬蟲一樣: - ```go // sema is a counting semaphore for limiting concurrency in dirents. var sema = make(chan struct{}, 20) // dirents returns the entries of directory dir. func dirents(dir string) []os.FileInfo { - sema <- struct{}{} // acquire token - defer func() { <-sema }() // release token - // ... + sema <- struct{}{} // acquire token + defer func() { <-sema }() // release token + // ... ``` 這個版本比之前那個快了好幾倍,盡管其具體效率還是和你的運行環境,機器配置相關。 diff --git a/ch8/ch8-09.md b/ch8/ch8-09.md index 8578966..87a48f2 100644 --- a/ch8/ch8-09.md +++ b/ch8/ch8-09.md @@ -10,17 +10,17 @@ Go語言併沒有提供在一個goroutine中終止另一個goroutine的方法, 隻要一些小脩改,我們就可以把退出邏輯加入到前一節的du程序。首先,我們創建一個退出的channel,這個channel不會向其中發送任何值,但其所在的閉包內要寫明程序需要退出。我們同時還定義了一個工具函數,cancelled,這個函數在被調用的時候會輪詢退出狀態。 +gopl.io/ch8/du4 ```go -gopl.io/ch8/du4 var done = make(chan struct{}) func cancelled() bool { - select { - case <-done: - return true - default: - return false - } + select { + case <-done: + return true + default: + return false + } } ``` @@ -29,8 +29,8 @@ func cancelled() bool { ```go // Cancel traversal when input is detected. go func() { - os.Stdin.Read(make([]byte, 1)) // read a single byte - close(done) + os.Stdin.Read(make([]byte, 1)) // read a single byte + close(done) }() ``` @@ -38,16 +38,16 @@ go func() { ```go for { - select { - case <-done: - // Drain fileSizes to allow existing goroutines to finish. - for range fileSizes { - // Do nothing. - } - return - case size, ok := <-fileSizes: - // ... - } + select { + case <-done: + // Drain fileSizes to allow existing goroutines to finish. + for range fileSizes { + // Do nothing. + } + return + case size, ok := <-fileSizes: + // ... + } } ``` @@ -55,13 +55,13 @@ walkDir這個goroutine一啟動就會輪詢取消狀態,如果取消狀態被 ```go func walkDir(dir string, n *sync.WaitGroup, fileSizes chan<- int64) { - defer n.Done() - if cancelled() { - return - } - for _, entry := range dirents(dir) { - // ... - } + defer n.Done() + if cancelled() { + return + } + for _, entry := range dirents(dir) { + // ... + } } ``` @@ -71,13 +71,13 @@ func walkDir(dir string, n *sync.WaitGroup, fileSizes chan<- int64) { ```go func dirents(dir string) []os.FileInfo { - select { - case sema <- struct{}{}: // acquire token - case <-done: - return nil // cancelled - } - defer func() { <-sema }() // release token - // ...read directory... + select { + case sema <- struct{}{}: // acquire token + case <-done: + return nil // cancelled + } + defer func() { <-sema }() // release token + // ...read directory... } ``` diff --git a/ch8/ch8-10.md b/ch8/ch8-10.md index 3963dee..2d0ba73 100644 --- a/ch8/ch8-10.md +++ b/ch8/ch8-10.md @@ -1,24 +1,25 @@ ## 8.10. 示例: 聊天服務 我們用一個聊天服務器來終結本章節的內容,這個程序可以讓一些用戶通過服務器向其它所有用戶廣播文本消息。這個程序中有四種goroutine。main和broadcaster各自是一個goroutine實例,每一個客戶端的連接都會有一個handleConn和clientWriter的goroutine。broadcaster是select用法的不錯的樣例,因爲它需要處理三種不同類型的消息。 + 下面演示的main goroutine的工作,是listen和accept(譯註:網絡編程里的概念)從客戶端過來的連接。對每一個連接,程序都會建立一個新的handleConn的goroutine,就像我們在本章開頭的併發的echo服務器里所做的那樣。 +gopl.io/ch8/chat ```go -gopl.io/ch8/chat func main() { - listener, err := net.Listen("tcp", "localhost:8000") - if err != nil { - log.Fatal(err) - } - go broadcaster() - for { - conn, err := listener.Accept() - if err != nil { - log.Print(err) - continue - } - go handleConn(conn) - } + listener, err := net.Listen("tcp", "localhost:8000") + if err != nil { + log.Fatal(err) + } + go broadcaster() + for { + conn, err := listener.Accept() + if err != nil { + log.Print(err) + continue + } + go handleConn(conn) + } } ``` @@ -28,29 +29,29 @@ func main() { type client chan<- string // an outgoing message channel var ( - entering = make(chan client) - leaving = make(chan client) - messages = make(chan string) // all incoming client messages + entering = make(chan client) + leaving = make(chan client) + messages = make(chan string) // all incoming client messages ) func broadcaster() { - clients := make(map[client]bool) // all connected clients - for { - select { - case msg := <-messages: - // Broadcast incoming message to all - // clients' outgoing message channels. - for cli := range clients { - cli <- msg - } - case cli := <-entering: - clients[cli] = true + clients := make(map[client]bool) // all connected clients + for { + select { + case msg := <-messages: + // Broadcast incoming message to all + // clients' outgoing message channels. + for cli := range clients { + cli <- msg + } + case cli := <-entering: + clients[cli] = true - case cli := <-leaving: - delete(clients, cli) - close(cli) - } - } + case cli := <-leaving: + delete(clients, cli) + close(cli) + } + } } ``` @@ -60,29 +61,29 @@ broadcaster監聽來自全局的entering和leaving的channel來獲知客戶端 ```go func handleConn(conn net.Conn) { - ch := make(chan string) // outgoing client messages - go clientWriter(conn, ch) + ch := make(chan string) // outgoing client messages + go clientWriter(conn, ch) - who := conn.RemoteAddr().String() - ch <- "You are " + who - messages <- who + " has arrived" - entering <- ch + who := conn.RemoteAddr().String() + ch <- "You are " + who + messages <- who + " has arrived" + entering <- ch - input := bufio.NewScanner(conn) - for input.Scan() { - messages <- who + ": " + input.Text() - } - // NOTE: ignoring potential errors from input.Err() + input := bufio.NewScanner(conn) + for input.Scan() { + messages <- who + ": " + input.Text() + } + // NOTE: ignoring potential errors from input.Err() - leaving <- ch - messages <- who + " has left" - conn.Close() + leaving <- ch + messages <- who + " has left" + conn.Close() } func clientWriter(conn net.Conn, ch <-chan string) { - for msg := range ch { - fmt.Fprintln(conn, msg) // NOTE: ignoring network errors - } + for msg := range ch { + fmt.Fprintln(conn, msg) // NOTE: ignoring network errors + } } ```