diff --git a/ch4/ch4-01.md b/ch4/ch4-01.md index f71056f..f8aebca 100644 --- a/ch4/ch4-01.md +++ b/ch4/ch4-01.md @@ -1,8 +1,8 @@ ## 4.1. 數組 -數組是一個固定長度的特定類型元素組成的序列,有零個或多個元素。因爲數組的長度是固定的,因此在Go語言中很少直接使用數組。Slice(切片),是可以增長和收縮動態序列,slice功能也更靈活,但是要理解slice工作原理的話需要先理解數組。 +數組是一個由固定長度的特定類型元素組成的序列,一個數組可以由零個或多個元素組成。因爲數組的長度是固定的,因此在Go語言中很少直接使用數組。Slice(切片),是可以增長和收縮動態序列,slice功能也更靈活,但是要理解slice工作原理的話需要先理解數組。 -數組的每個元素可以通過下標來訪問,下標的范圍是從0開始到數組長度減1的位置。內置的len函數將返迴數組中元素的個數。 +數組的每個元素可以通過索引下標來訪問,索引下標的范圍是從0開始到數組長度減1的位置。內置的len函數將返迴數組中元素的個數。 ```Go var a [3]int // array of 3 integers @@ -20,7 +20,7 @@ for _, v := range a { } ``` -默認情況下,數組的每個元素都被初始化爲元素類型對應的零值,對於數字類型來説就是0。我們可以使用數組字面值語法用一組值來初始化數組: +默認情況下,數組的每個元素都被初始化爲元素類型對應的零值,對於數字類型來説就是0。我們也可以使用數組字面值語法用一組值來初始化數組: ```Go var q [3]int = [3]int{1, 2, 3} @@ -28,21 +28,21 @@ var r [3]int = [3]int{1, 2} fmt.Println(r[2]) // "0" ``` -在數組字面值中,如果數組的長度位置出現的是“...”省略號,則表示數組的長度是根據初始化值的個數來計算的。上面q數組的定義可以簡化爲 +在數組字面值中,如果在數組的長度位置出現的是“...”省略號,則表示數組的長度是根據初始化值的個數來計算的。因此,上面q數組的定義可以簡化爲 ```Go q := [...]int{1, 2, 3} fmt.Printf("%T\n", q) // "[3]int" ``` -數組的長度是數組類型的一個組成部分,因此[3]int和[4]int是兩種不同的數組類型。數組的長度必鬚是常量表達式,因爲數組的長度需要在程序的編譯階段確定。 +數組的長度是數組類型的一個組成部分,因此[3]int和[4]int是兩種不同的數組類型。數組的長度必鬚是常量表達式,因爲數組的長度需要在編譯階段確定。 ```Go q := [3]int{1, 2, 3} q = [4]int{1, 2, 3, 4} // compile error: cannot assign [4]int to [3]int ``` -我們將會發現,數組、slice、map和結構體字面值的寫法都很相似。上面的形式是直接提順序供初始化值序列,但是也可以指定一個索引和對應值的列表初始化,像下面這樣: +我們將會發現,數組、slice、map和結構體字面值的寫法都很相似。上面的形式是直接提順序供初始化值序列,但是也可以指定一個索引和對應值列表的方式初始化,就像下面這樣: ```Go type Currency int @@ -56,18 +56,18 @@ const ( symbol := [...]string{USD: "$", EUR: "€", GBP: "£", RMB: "¥"} -fmt.Println(RMB, symbol[RMB]) // "3 "" +fmt.Println(RMB, symbol[RMB]) // "3 ¥" ``` -這種形式的數組字面值形式中,初始化索引的順序是無關緊要的,而且一些索引可以省略,和前面提到的規則一樣,未知道初始值的元素將用零值初始化。例如, +在這種形式的數組字面值形式中,初始化索引的順序是無關緊要的,而且沒用到的索引可以省略,和前面提到的規則一樣,未指定初始值的元素將用零值初始化。例如, ```Go r := [...]int{99: -1} ``` -定義了一個含有100個原始的數組r,最後一個元素初始化爲-1,其它元素都是用0初始化。 +定義了一個含有100個元素的數組r,最後一個元素被初始化爲-1,其它元素都是用0初始化。 -如果一個數組的元素類型是可以相互比較的,那麽數組類型也是可以相互比較的,因此我們可以直接通過==比較運算符來比較兩個數組,隻有當兩個數組的所有元素都是相等的時候數組才是相等的。相對的是不相等比較運算符!=。 +如果一個數組的元素類型是可以相互比較的,那麽數組類型也是可以相互比較的,這時候我們可以直接通過==比較運算符來比較兩個數組,隻有當兩個數組的所有元素都是相等的時候數組才是相等的。是不相等比較運算符!=遵循同樣的規則。 ```Go a := [2]int{1, 2} @@ -78,7 +78,7 @@ d := [3]int{1, 2} fmt.Println(a == d) // compile error: cannot compare [2]int == [3]int ``` -作爲一個更可信的例子,crypto/sha256包的Sum256函數用於生成一個針對任意的字節類型的slice消息的摘要。消息摘要有256bit大小,因此對應[32]byte數組類型。如果兩個消息摘要是相同的,那麽可以認爲兩個消息本身也是相同(譯註:理論上有HASH碼碰撞的清空,但是實際應用可以基本忽略);如果消息摘要不同,那麽消息本身比如也是不同的。下面的例子用SHA256算法分别生成“x”和“X”兩個信息的摘要: +作爲一個眞實的例子,crypto/sha256包的Sum256函數對一個任意的字節slice類型的數據生成一個對應的消息摘要。消息摘要有256bit大小,因此對應[32]byte數組類型。如果兩個消息摘要是相同的,那麽可以認爲兩個消息本身也是相同(譯註:理論上有HASH碼碰撞的情況,但是實際應用可以基本忽略);如果消息摘要不同,那麽消息本身必然也是不同的。下面的例子用SHA256算法分别生成“x”和“X”兩個信息的摘要: ```Go gopl.io/ch4/sha256 @@ -97,11 +97,11 @@ func main() { } ``` -兩個消息雖然隻有一個字符的差異,但是生成的消息摘要則幾乎有一半的bit位是不同的。需要註意Printf函數的%x參數,它用於指定以十六進製的格式打印全部的數組或slice的的元素,%t參數是用於打印布爾型數據,%T參數是用於顯示一個值對應的數據類型。 +上面例子中,兩個消息雖然隻有一個字符的差異,但是生成的消息摘要則幾乎有一半的bit位是不相同的。需要註意Printf函數的%x參數,它用於指定以十六進製的格式打印數組或slice全部的元素,%t參數是用於打印布爾型數據,%T參數是用於顯示一個值對應的數據類型。 -當調用一個函數的時候,函數的每個調用參數將會被賦值給函數本身的參數變量,所以函數參數變量接收的是一個複製的副本,併不是原始調用的參數。因爲函數參數傳遞的機製導致傳遞大的數組類型將是低效的,併且對數組參數的任何的脩改都是發生在複製的數組上,併不是直接脩改調用時原始的數組變量。在這個方面,Go語言對待數組的方式和其它很多編程語言不同,其它編程語言可能會隱式地將數組作爲引用或指針對象傳入被調用的函數。 +當調用一個函數的時候,函數的每個調用參數將會被賦值給函數內部的參數變量,所以函數參數變量接收的是一個複製的副本,併不是原始調用的參數。因爲函數參數傳遞的機製導致傳遞大的數組類型將是低效的,併且對數組參數的任何的脩改都是發生在複製的數組上,併不能直接脩改調用時原始的數組變量。在這個方面,Go語言對待數組的方式和其它很多編程語言不同,其它編程語言可能會隱式地將數組作爲引用或指針對象傳入被調用的函數。 -當然,我們可以顯式地傳入一個數組指針,那樣的話函數對數組的任何脩改都可以直接反饋到調用者。下面的函數用於給[32]byte類型的數組清零: +當然,我們可以顯式地傳入一個數組指針,那樣的話函數通過指針對數組的任何脩改都可以直接反饋到調用者。下面的函數用於給[32]byte類型的數組清零: ```Go func zero(ptr *[32]byte) { @@ -111,7 +111,7 @@ func zero(ptr *[32]byte) { } ``` -其實數組字面值[32]byte{}就可以生成一個32字節的數組。而且每個數組的元素都是零值初始化,也就是0。我們可以將上面的zero函數寫的更簡潔一點: +其實數組字面值[32]byte{}就可以生成一個32字節的數組。而且每個數組的元素都是零值初始化,也就是0。因此,我們可以將上面的zero函數寫的更簡潔一點: ```Go func zero(ptr *[32]byte) { @@ -119,10 +119,8 @@ func zero(ptr *[32]byte) { } ``` -雖然通過指針來傳遞數組參數是高效的,而且也允許在函數內部脩改數組的值,但是因爲數組依然是殭化的類型,因爲數組的類型包含長度信息。zero函數併不能接收指向[16]byte類型數組的指針,而且也沒有任何添加或刪除數組元素的方法。由於這些原因,除了像SHA256這類需要處理特定大小數組的函數外,數組依然很少用作函數參數;相反,我們一般使用slice來替代數組。 +雖然通過指針來傳遞數組參數是高效的,而且也允許在函數內部脩改數組的值,但是數組依然是殭化的類型,因爲數組的類型包含了殭化的長度信息。上面的zero函數併不能接收指向[16]byte類型數組的指針,而且也沒有任何添加或刪除數組元素的方法。由於這些原因,除了像SHA256這類需要處理特定大小數組的特例外,數組依然很少用作函數參數;相反,我們一般使用slice來替代數組。 -**練習 4.1:** 編寫一個函數,計算兩個SHA256碼中不同bit的數目。(參考2.6.2節的PopCount函數。) +**練習 4.1:** 編寫一個函數,計算兩個SHA256哈希碼中不同bit的數目。(參考2.6.2節的PopCount函數。) **練習 4.2:** 編寫一個程序,默認打印標準輸入的以SHA256哈希碼,也可以通過命令行標準參數選擇SHA384或SHA512哈希算法。 - - diff --git a/ch7/ch7-03.md b/ch7/ch7-03.md index aacf676..89c4788 100644 --- a/ch7/ch7-03.md +++ b/ch7/ch7-03.md @@ -20,7 +20,7 @@ rwc = w // compile error: io.Writer lacks Close method ``` 因爲ReadWriter和ReadWriteCloser包含所有Writer的方法,所以任何實現了ReadWriter和ReadWriteCloser的類型必定也實現了Writer接口 -在進一步學習前,必鬚先解釋表示一個類型持有一個方法當中的細節。迴想在6.2章中,對於每一個命名過的具體類型T;它一些方法的接收者是類型T本身然而另一些則是一個*T的指針。還記得在T類型的參數上調用一個*T的方法是合法的,隻要這個參數是一個變量;編譯器隱式的穫取了它的地址。但這僅僅是一個語法醣:T類型的值不擁有所有*T指針的方法,那這樣它就可能隻實現更少的接口。 +在進一步學習前,必鬚先解釋表示一個類型持有一個方法當中的細節。迴想在6.2章中,對於每一個命名過的具體類型T;它一些方法的接收者是類型T本身然而另一些則是一個*T的指針。還記得在T類型的參數上調用一個*T的方法是合法的,隻要這個參數是一個變量;編譯器隱式的獲取了它的地址。但這僅僅是一個語法醣:T類型的值不擁有所有*T指針的方法,那這樣它就可能隻實現更少的接口。 舉個例子可能會更清晰一點。在第6.5章中,IntSet類型的String方法的接收者是一個指針類型,所以我們不能在一個不能尋址的IntSet值上調用這個方法: ```go @@ -38,9 +38,9 @@ var _ = s.String() // OK: s is a variable and &s has a String method var _ fmt.Stringer = &s // OK var _ fmt.Stringer = s // compile error: IntSet lacks String method ``` -12.8章包含了一個打印齣任意值的所有方法的程序,然後可以使用godoc -analysis=type tool(§10.7.4)展示每個類型的方法和具體類型和接口之間的關繫 +12.8章包含了一個打印出任意值的所有方法的程序,然後可以使用godoc -analysis=type tool(§10.7.4)展示每個類型的方法和具體類型和接口之間的關繫 -就像信封封裝和隱藏信件起來一樣,接口類型封裝和隱藏具體類型和它的值。卽使具體類型有其它的方法也隻有接口類型暴露齣來的方法會被調用到: +就像信封封裝和隱藏信件起來一樣,接口類型封裝和隱藏具體類型和它的值。卽使具體類型有其它的方法也隻有接口類型暴露出來的方法會被調用到: ```go os.Stdout.Write([]byte("hello")) // OK: *os.File has Write method os.Stdout.Close() // OK: *os.File has Close method @@ -50,7 +50,7 @@ w = os.Stdout w.Write([]byte("hello")) // OK: io.Writer has Write method w.Close() // compile error: io.Writer lacks Close method ``` -一個有更多方法的接口類型,比如io.ReadWriter,和少一些方法的接口類型,例如io.Reader,進行對比;更多方法的接口類型會告訴我們更多關於它的值持有的信息,併且對實現它的類型要求更加嚴格。那麽關於interface{}類型,它沒有任何方法,請講齣哪些具體的類型實現了它? +一個有更多方法的接口類型,比如io.ReadWriter,和少一些方法的接口類型,例如io.Reader,進行對比;更多方法的接口類型會告訴我們更多關於它的值持有的信息,併且對實現它的類型要求更加嚴格。那麽關於interface{}類型,它沒有任何方法,請講出哪些具體的類型實現了它? 這看上去好像沒有用,但實際上interface{}被稱爲空接口類型是不可或缺的。因爲空接口類型對實現它的類型沒有要求,所以我們可以將任意一個值賦給空接口類型。 ```go @@ -63,7 +63,7 @@ any = new(bytes.Buffer) ``` 盡管不是很明顯,從本書最早的的例子中我們就已經在使用空接口類型。它允許像fmt.Println或者5.7章中的errorf函數接受任何類型的參數。 -對於創建的一個interface{}值持有一個boolean,float,string,map,pointer,或者任意其它的類型;我們當然不能直接對它持有的值做操作,因爲interface{}沒有任何方法。我們會在7.10章中學到一種用類型斷言來穫取interface{}中值的方法。 +對於創建的一個interface{}值持有一個boolean,float,string,map,pointer,或者任意其它的類型;我們當然不能直接對它持有的值做操作,因爲interface{}沒有任何方法。我們會在7.10章中學到一種用類型斷言來獲取interface{}中值的方法。 因爲接口實現隻依賴於判斷的兩個類型的方法,所以沒有必要定義一個具體類型和它實現的接口之間的關繫。也就是説,嚐試文檔化和斷言這種關繫幾乎沒有用,所以併沒有通過程序強製定義。下面的定義在編譯期斷言一個*bytes.Buffer的值實現了io.Writer接口類型: ```go @@ -79,7 +79,7 @@ var _ io.Writer = (*bytes.Buffer)(nil) 但是併不意味着隻有指針類型滿足接口類型,甚至連一些有設置方法的接口類型也可能會被Go語言中其它的引用類型實現。我們已經看過slice類型的方法(geometry.Path, §6.1)和map類型的方法(url.Values, §6.2.1),後面還會看到函數類型的方法的例子(http.HandlerFunc, §7.7)。甚至基本的類型也可能會實現一些接口;就如我們在7.4章中看到的time.Duration類型實現了fmt.Stringer接口。 -一個具體的類型可能實現了很多不相關的接口。考慮在一個組織齣售數字文化産品比如音樂,電影和書籍的程序中可能定義了下列的具體類型: +一個具體的類型可能實現了很多不相關的接口。考慮在一個組織出售數字文化産品比如音樂,電影和書籍的程序中可能定義了下列的具體類型: ``` go Album Book @@ -97,7 +97,7 @@ type Artifact interface { Created() time.Time } ``` -其它的一些特性隻對特定類型的文化産品纔有。和文字排版特性相關的隻有books和magazines,還有隻有movies和TV劇集和屏幕分辨率相關。 +其它的一些特性隻對特定類型的文化産品才有。和文字排版特性相關的隻有books和magazines,還有隻有movies和TV劇集和屏幕分辨率相關。 ```go type Text interface { Pages() int @@ -124,4 +124,4 @@ type Streamer interface { Format() string } ``` -每一個具體類型的組基於它們相同的行爲可以表示成一個接口類型。不像基於類的語言,他們一個類實現的接口集合需要進行顯式的定義,在Go語言中我們可以在需要的時候定義一個新的抽象或者特定特點的組,而不需要脩改具體類型的定義。當具體的類型來自不同的作者時這種方式會特别有用。當然也確實沒有必要在具體的類型中指齣這些共性。 +每一個具體類型的組基於它們相同的行爲可以表示成一個接口類型。不像基於類的語言,他們一個類實現的接口集合需要進行顯式的定義,在Go語言中我們可以在需要的時候定義一個新的抽象或者特定特點的組,而不需要脩改具體類型的定義。當具體的類型來自不同的作者時這種方式會特别有用。當然也確實沒有必要在具體的類型中指出這些共性。 diff --git a/ch8/ch8-04-1.md b/ch8/ch8-04-1.md index 3e5d75c..7aeb3b2 100644 --- a/ch8/ch8-04-1.md +++ b/ch8/ch8-04-1.md @@ -1,3 +1,39 @@ ### 8.4.1. 不帶緩存的Channels -TODO +一個基於無緩存Channels的發送操作將導致發送者goroutine阻塞,直到另一個goroutine在相同的Channels上執行接收操作,當發送的值通過Channels成功傳輸之後,兩個goroutine可以繼續執行後面的語句。反之,如果接收操作先發送,那麽接收者goroutine也將阻塞,直到有另一個goroutine在相同的Channels上執行發送操作。 + +基於無緩存Channels的發送和接收操作將導致兩個goroutine做一次同步操作。因爲這個原因,無緩存Channels有時候也被稱爲同步Channels。當通過一個無緩存Channels發送數據時,接收者收到數據發生在喚醒發送者goroutine之前(譯註:*happens before*,這是Go語言併發內存模型的一個關鍵術語!)。 + +在討論併發編程時,當我們説x事件在y事件之前發生(*happens before*),我們併不是説x事件在時間上比y時間更早;我們要表達的意思是要保證在此之前事件都已經完成了,例如在此之前的更新某些變量的操作已經完成,你可以放心依賴這些保證了。 + +當我們説x事件旣不是在y事件之前發生也不是在y事件之後發生,我們就説x事件和y事件是併發的。這併不是意味着x事件和y事件就一定是同時發生的,我們隻是不能確定這兩個事件發生的先後順序。在下一章中我們將看到,當兩個goroutine併發訪問了相同的變量時,我們有必要保證某些事件的執行順序,以避免出現某些併發問題。 + +在8.3節的客戶端程序,它在主goroutine中(譯註:就是執行main函數的goroutine)將標準輸入複製到server,因此當客戶端程序關閉標準輸入時,後台goroutine可能依然在工作。我們需要讓主goroutine等待後台goroutine完成工作後再退出,我們使用了一個channel來同步兩個goroutine: + +```Go +gopl.io/ch8/netcat3 +func main() { + conn, err := net.Dial("tcp", "localhost:8000") + if err != nil { + log.Fatal(err) + } + done := make(chan struct{}) + go func() { + io.Copy(os.Stdout, conn) // NOTE: ignoring errors + log.Println("done") + done <- struct{}{} // signal the main goroutine + }() + mustCopy(conn, os.Stdin) + conn.Close() + <-done // wait for background goroutine to finish +} +``` + +當用戶關閉了標準輸入,主goroutine中的mustCopy函數調用將返迴,然後調用conn.Close()關閉讀和寫方向的網絡連接。關閉網絡鏈接中的寫方向的鏈接將導致server程序收到一個文件(end-of-file)結束的信號。關閉網絡鏈接中讀方向的鏈接將導致後台goroutine的io.Copy函數調用返迴一個“read from closed connection”(“從關閉的鏈接讀”)類似的錯誤,因此我們臨時移除了錯誤日誌語句;在練習8.3將會提供一個更好的解決方案。(需要註意的是go語句調用了一個函數字面量,這Go語言中啟動goroutine常用的形式。) + +在後台goroutine返迴之前,它先打印一個日誌信息,然後向done對應的channel發送一個值。主goroutine在退出前先等待從done對應的channel接收一個值。因此,總是可以在程序退出前正確輸出“done”消息。 + +基於channels發送消息有兩個重要方面。首先每個消息都有一個值,但是有時候通訊的事實和發生的時刻也同樣重要。當我們更希望強調通訊發生的時刻時,我們將它稱爲**消息事件**。有些消息事件併不攜帶額外的信息,它僅僅是用作兩個goroutine之間的同步,這時候我們可以用`struct{}`空結構體作爲channels元素的類型,雖然也可以使用bool或int類型實現同樣的功能,`done <- 1`語句也比`done <- struct{}{}`更短。 + +**練習 8.3:** 在netcat3例子中,conn雖然是一個interface類型的值,但是其底層眞實類型是`*net.TCPConn`,代表一個TCP鏈接。一個TCP鏈接有讀和寫兩個部分,可以使用CloseRead和CloseWrite方法分别關閉它們。脩改netcat3的主goroutine代碼,隻關閉網絡鏈接中寫的部分,這樣的話後台goroutine可以在標準輸入被關閉後繼續打印從reverb1服務器傳迴的數據。(要在reverb2服務器也完成同樣的功能是比較睏難的;參考**練習 8.4:**) + diff --git a/progress.md b/progress.md index 8553f07..6567341 100644 --- a/progress.md +++ b/progress.md @@ -53,7 +53,7 @@ - [x] Chapter 7: Interfaces - [x] 7.1 Interfaces as Contracts - [x] 7.2 Interface Types - - [ ] 7.3 Interface Satisfaction + - [x] 7.3 Interface Satisfaction - [ ] 7.4 Parsing Flags with flag.Value - [ ] 7.5 Interface Values - [ ] 7.6 Sorting with sort.Interface