diff --git a/ch7/ch7-12.md b/ch7/ch7-12.md index f7a0967..622dc42 100644 --- a/ch7/ch7-12.md +++ b/ch7/ch7-12.md @@ -1,5 +1,7 @@ ## 7.12. 通過類型斷言詢問行爲 + 下面這段邏輯和net/http包中web服務器負責寫入HTTP頭字段(例如:"Content-type:text/html)的部分相似。io.Writer接口類型的變量w代表HTTP響應;寫入它的字節最終被發送到某個人的web瀏覽器上。 + ```go func writeHeader(w io.Writer, contentType string) error { if _, err := w.Write([]byte("Content-Type: ")); err != nil { @@ -11,11 +13,13 @@ func writeHeader(w io.Writer, contentType string) error { // ... } ``` + 因爲Write方法需要傳入一個byte切片而我們希望寫入的值是一個字符串,所以我們需要使用[]byte(...)進行轉換。這個轉換分配內存併且做一個拷貝,但是這個拷貝在轉換後幾乎立馬就被丟棄掉。讓我們假裝這是一個web服務器的核心部分併且我們的性能分析表示這個內存分配使服務器的速度變慢。這里我們可以避免掉內存分配麽? 這個io.Writer接口告訴我們關於w持有的具體類型的唯一東西:就是可以向它寫入字節切片。如果我們迴顧net/http包中的內幕,我們知道在這個程序中的w變量持有的動態類型也有一個允許字符串高效寫入的WriteString方法;這個方法會避免去分配一個零時的拷貝。(這可能像在黑夜中射擊一樣,但是許多滿足io.Writer接口的重要類型同時也有WriteString方法,包括\*bytes.Buffer,\*os.File和\*bufio.Writer。) 我們不能對任意io.Writer類型的變量w,假設它也擁有WriteString方法。但是我們可以定義一個隻有這個方法的新接口併且使用類型斷言來檢測是否w的動態類型滿足這個新接口。 + ```go // writeString writes s to w. // If w has a WriteString method, it is invoked instead of w.Write. @@ -39,20 +43,24 @@ func writeHeader(w io.Writer, contentType string) error { // ... } ``` + 爲了避免重複定義,我們將這個檢査移入到一個實用工具函數writeString中,但是它太有用了以致標準庫將它作爲io.WriteString函數提供。這是向一個io.Writer接口寫入字符串的推薦方法。 這個例子的神奇之處在於沒有定義了WriteString方法的標準接口和沒有指定它是一個需要行爲的標準接口。而且一個具體類型隻會通過它的方法決定它是否滿足stringWriter接口,而不是任何它和這個接口類型表明的關繫。它的意思就是上面的技術依賴於一個假設;這個假設就是,如果一個類型滿足下面的這個接口,然後WriteString(s)就方法必須和Write([]byte(s))有相同的效果。 + ```go interface { io.Writer WriteString(s string) (n int, err error) } ``` + 盡管io.WriteString記録了它的假設,但是調用它的函數極少有可能會去記録它們也做了同樣的假設。定義一個特定類型的方法隱式地獲取了對特定行爲的協約。對於Go語言的新手,特别是那些來自有強類型語言使用背景的新手,可能會發現它缺乏顯式的意圖令人感到混亂,但是在實戰的過程中這幾乎不是一個問題。除了空接口interface{},接口類型很少意外巧合地被實現。 上面的writeString函數使用一個類型斷言來知道一個普遍接口類型的值是否滿足一個更加具體的接口類型;併且如果滿足,它會使用這個更具體接口的行爲。這個技術可以被很好的使用不論這個被詢問的接口是一個標準的如io.ReadWriter或者用戶定義的如stringWriter。 這也是fmt.Fprintf函數怎麽從其它所有值中區分滿足error或者fmt.Stringer接口的值。在fmt.Fprintf內部,有一個將單個操作對象轉換成一個字符串的步驟,像下面這樣: + ```go package fmt @@ -66,6 +74,7 @@ func formatOneValue(x interface{}) string { // ...all other types... } ``` + 如果x滿足這個兩個接口類型中的一個,具體滿足的接口決定對值的格式化方式。如果都不滿足,默認的case或多或少會統一地使用反射來處理所有的其它類型;我們可以在第12章知道具體是怎麽實現的。 再一次的,它假設任何有String方法的類型滿足fmt.Stringer中約定的行爲,這個行爲會返迴一個適合打印的字符串。 diff --git a/ch7/ch7-13.md b/ch7/ch7-13.md index 6f8aaac..362d000 100644 --- a/ch7/ch7-13.md +++ b/ch7/ch7-13.md @@ -1,4 +1,5 @@ ## 7.13. 類型開關 + 接口被以兩種不同的方式使用。在第一個方式中,以io.Reader,io.Writer,fmt.Stringer,sort.Interface,http.Handler,和error爲典型,一個接口的方法表達了實現這個接口的具體類型間的相思性,但是隱藏了代表的細節和這些具體類型本身的操作。重點在於方法上,而不是具體的類型上。 第二個方式利用一個接口值可以持有各種具體類型值的能力併且將這個接口認爲是這些類型的union(聯合)。類型斷言用來動態地區别這些類型併且對每一種情況都不一樣。在這個方式中,重點在於具體的類型滿足這個接口,而不是在於接口的方法(如果它確實有一些的話),併且沒有任何的信息隱藏。我們將以這種方式使用的接口描述爲discriminated unions(可辨識聯合)。 @@ -6,6 +7,7 @@ 如果你熟悉面向對象編程,你可能會將這兩種方式當作是subtype polymorphism(子類型多態)和 ad hoc polymorphism(非參數多態),但是你不需要去記住這些術語。對於本章剩下的部分,我們將會呈現一些第二種方式的例子。 和其它那些語言一樣,Go語言査詢一個SQL數據庫的API會榦淨地將査詢中固定的部分和變化的部分分開。一個調用的例子可能看起來像這樣: + ```go import "database/sql" @@ -16,7 +18,9 @@ func listTracks(db sql.DB, artist string, minYear, maxYear int) { // ... } ``` + Exec方法使用SQL字面量替換在査詢字符串中的每個'?';SQL字面量表示相應參數的值,它有可能是一個布爾值,一個數字,一個字符串,或者nil空值。用這種方式構造査詢可以幫助避免SQL註入攻擊;這種攻擊就是對手可以通過利用輸入內容中不正確的引文來控製査詢語句。在Exec函數內部,我們可能會找到像下面這樣的一個函數,它會將每一個參數值轉換成它的SQL字面量符號。 + ```go func sqlQuote(x interface{}) string { if x == nil { @@ -37,9 +41,11 @@ func sqlQuote(x interface{}) string { } } ``` + switch語句可以簡化if-else鏈,如果這個if-else鏈對一連串值做相等測試。一個相似的type switch(類型開關)可以簡化類型斷言的if-else鏈。 在它最簡單的形式中,一個類型開關像普通的switch語句一樣,它的運算對象是x.(type)-它使用了關鍵詞字面量type-併且每個case有一到多個類型。一個類型開關基於這個接口值的動態類型使一個多路分支有效。這個nil的case和if x == nil匹配,併且這個default的case和如果其它case都不匹配的情況匹配。一個對sqlQuote的類型開關可能會有這些case: + ```go switch x.(type) { case nil: // ... @@ -49,15 +55,19 @@ switch x.(type) { default: // ... } ``` + 和(§1.8)中的普通switch語句一樣,每一個case會被順序的進行考慮,併且當一個匹配找到時,這個case中的內容會被執行。當一個或多個case類型是接口時,case的順序就會變得很重要,因爲可能會有兩個case同時匹配的情況。default case相對其它case的位置是無所謂的。它不會允許落空發生。 註意到在原來的函數中,對於bool和string情況的邏輯需要通過類型斷言訪問提取的值。因爲這個做法很典型,類型開關語句有一個擴展的形式,它可以將提取的值綁定到一個在每個case范圍內的新變量。 + ```go switch x := x.(type) { /* ... */ } ``` + 這里我們已經將新的變量也命名爲x;和類型斷言一樣,重用變量名是很常見的。和一個switch語句相似地,一個類型開關隱式的創建了一個語言塊,因此新變量x的定義不會和外面塊中的x變量衝突。每一個case也會隱式的創建一個單獨的語言塊。 使用類型開關的擴展形式來重寫sqlQuote函數會讓這個函數更加的清晰: + ```go func sqlQuote(x interface{}) string { switch x := x.(type) { @@ -77,6 +87,7 @@ func sqlQuote(x interface{}) string { } } ``` + 在這個版本的函數中,在每個單一類型的case內部,變量x和這個case的類型相同。例如,變量x在bool的case中是bool類型和string的case中是string類型。在所有其它的情況中,變量x是switch運算對象的類型(接口);在這個例子中運算對象是一個interface{}。當多個case需要相同的操作時,比如int和uint的情況,類型開關可以很容易的合併這些情況。 盡管sqlQuote接受一個任意類型的參數,但是這個函數隻會在它的參數匹配類型開關中的一個case時運行到結束;其它情況的它會panic出“unexpected type”消息。雖然x的類型是interface{},但是我們把它認爲是一個int,uint,bool,string,和nil值的discriminated union(可識别聯合) diff --git a/ch7/ch7-14.md b/ch7/ch7-14.md index aaddeb8..26fff91 100644 --- a/ch7/ch7-14.md +++ b/ch7/ch7-14.md @@ -1,9 +1,11 @@ ## 7.14. 示例: 基於標記的XML解碼 + 第4.5章節展示了如何使用encoding/json包中的Marshal和Unmarshal函數來將JSON文檔轉換成Go語言的數據結構。encoding/xml包提供了一個相似的API。當我們想構造一個文檔樹的表示時使用encoding/xml包會很方便,但是對於很多程序併不是必須的。encoding/xml包也提供了一個更低層的基於標記的API用於XML解碼。在基於標記的樣式中,解析器消費輸入和産生一個標記流;四個主要的標記類型-StartElement,EndElement,CharData,和Comment-每一個都是encoding/xml包中的具體類型。每一個對(\*xml.Decoder).Token的調用都返迴一個標記。 這里顯示的是和這個API相關的部分: + ```go -// encoding/xml +encoding/xml package xml type Name struct { @@ -30,11 +32,13 @@ type Decoder struct{ /* ... */ } func NewDecoder(io.Reader) *Decoder func (*Decoder) Token() (Token, error) // returns next Token in sequence ``` + 這個沒有方法的Token接口也是一個可識别聯合的例子。傳統的接口如io.Reader的目的是隱藏滿足它的具體類型的細節,這樣就可以創造出新的實現;在這個實現中每個具體類型都被統一地對待。相反,滿足可識别聯合的具體類型的集合被設計確定和暴露,而不是隱藏。可識别的聯合類型幾乎沒有方法;操作它們的函數使用一個類型開關的case集合來進行表述;這個case集合中每一個case中有不同的邏輯。 下面的xmlselect程序獲取和打印在一個XML文檔樹中確定的元素下找到的文本。使用上面的API,它可以在輸入上一次完成它的工作而從來不要具體化這個文檔樹。 + ```go -// gopl.io/ch7/xmlselect +gopl.io/ch7/xmlselect // Xmlselect prints the text of selected elements of an XML document. package main @@ -84,9 +88,11 @@ func containsAll(x, y []string) bool { return false } ``` + 每次main函數中的循環遇到一個StartElement時,它把這個元素的名稱壓到一個棧里;併且每次遇到EndElement時,它將名稱從這個棧中推出。這個API保證了StartElement和EndElement的序列可以被完全的匹配,甚至在一個糟糕的文檔格式中。註釋會被忽略。當xmlselect遇到一個CharData時,隻有當棧中有序地包含所有通過命令行參數傳入的元素名稱時它才會輸出相應的文本。 下面的命令打印出任意出現在兩層div元素下的h2元素的文本。它的輸入是XML的説明文檔,併且它自己就是XML文檔格式的。 + ``` $ go build gopl.io/ch1/fetch $ ./fetch http://www.w3.org/TR/2006/REC-xml11-20060816 | @@ -101,11 +107,13 @@ html body div div h2: A References html body div div h2: B Definitions for Character Normalization ... ``` -練習7.17:擴展xmlselect程序以便讓元素不僅僅可以通過名稱選擇,也可以通過它們CSS樣式上屬性進行選擇;例如一個像這樣
的元素可以通過匹配id或者class同時還有它的名稱來進行選擇。 -練習7.18:使用基於標記的解碼API,編寫一個可以讀取任意XML文檔和構造這個文檔所代表的普通節點樹的程序。節點有兩種類型:CharData節點表示文本字符串,和 Element節點表示被命名的元素和它們的屬性。每一個元素節點有一個字節點的切片。 +**練習 7.17:** 擴展xmlselect程序以便讓元素不僅僅可以通過名稱選擇,也可以通過它們CSS樣式上屬性進行選擇;例如一個像這樣
的元素可以通過匹配id或者class同時還有它的名稱來進行選擇。 + +**練習 7.18:** 使用基於標記的解碼API,編寫一個可以讀取任意XML文檔和構造這個文檔所代表的普通節點樹的程序。節點有兩種類型:CharData節點表示文本字符串,和 Element節點表示被命名的元素和它們的屬性。每一個元素節點有一個字節點的切片。 你可能發現下面的定義會對你有幫助。 + ```go import "encoding/xml" diff --git a/ch7/ch7-15.md b/ch7/ch7-15.md index 93681fc..243e027 100644 --- a/ch7/ch7-15.md +++ b/ch7/ch7-15.md @@ -1,4 +1,5 @@ ## 7.15. 一些建議 + 當設計一個新的包時,新的Go程序員總是通過創建一個接口的集合開始和後面定義滿足它們的具體類型。這種方式的結果就是有很多的接口,它們中的每一個僅隻有一個實現。不要再這麽做了。這種接口是不必要的抽象;它們也有一個運行時損耗。你可以使用導出機製(§6.6)來限製一個類型的方法或一個結構體的字段是否在包外可見。接口隻有當有兩個或兩個以上的具體類型必須以相同的方式進行處理時才需要。 當一個接口隻被一個單一的具體類型實現時有一個例外,就是由於它的依賴,這個具體類型不能和這個接口存在在一個相同的包中。這種情況下,一個接口是解耦這兩個包的一個好好方式。