<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??碼云GVP開源項目 12k star Uniapp+ElementUI 功能強大 支持多語言、二開方便! 廣告
                # 第七章 接口 接口類型是對其它類型行為的抽象和概括;因為接口類型不會和特定的實現細節綁定在一起,通過這種抽象的方式我們可以讓我們的函數更加靈活和更具有適應能力。 很多面向對象的語言都有相似的接口概念,但Go語言中接口類型的獨特之處在于它是滿足隱式實現的。也就是說,我們沒有必要對于給定的具體類型定義所有滿足的接口類型;簡單地擁有一些必需的方法就足夠了。這種設計可以讓你創建一個新的接口類型滿足已經存在的具體類型卻不會去改變這些類型的定義;當我們使用的類型來自于不受我們控制的包時這種設計尤其有用。 在本章,我們會開始看到接口類型和值的一些基本技巧。順著這種方式我們將學習幾個來自標準庫的重要接口。很多Go程序中都盡可能多的去使用標準庫中的接口。最后,我們會在(§7.10)看到類型斷言的知識,在(§7.13)看到類型開關的使用并且學到他們是怎樣讓不同的類型的概括成為可能。 ### 7.1. 接口約定 目前為止,我們看到的類型都是具體的類型。一個具體的類型可以準確的描述它所代表的值并且展示出對類型本身的一些操作方式就像數字類型的算術操作,切片類型的索引、附加和取范圍操作。具體的類型還可以通過它的方法提供額外的行為操作。總的來說,當你拿到一個具體的類型時你就知道它的本身是什么和你可以用它來做什么。 在Go語言中還存在著另外一種類型:接口類型。接口類型是一種抽象的類型。它不會暴露出它所代表的對象的內部值的結構和這個對象支持的基礎操作的集合;它們只會展示出它們自己的方法。也就是說當你有看到一個接口類型的值時,你不知道它是什么,唯一知道的就是可以通過它的方法來做什么。 在本書中,我們一直使用兩個相似的函數來進行字符串的格式化:fmt.Printf它會把結果寫到標準輸出和fmt.Sprintf它會把結果以字符串的形式返回。得益于使用接口,我們不必可悲的因為返回結果在使用方式上的一些淺顯不同就必需把格式化這個最困難的過程復制一份。實際上,這兩個函數都使用了另一個函數fmt.Fprintf來進行封裝。fmt.Fprintf這個函數對它的計算結果會被怎么使用是完全不知道的。 ~~~ package fmt func Fprintf(w io.Writer, format string, args ...interface{}) (int, error) func Printf(format string, args ...interface{}) (int, error) { return Fprintf(os.Stdout, format, args...) } func Sprintf(format string, args ...interface{}) string { var buf bytes.Buffer Fprintf(&buf, format, args...) return buf.String() } ~~~ Fprintf的前綴F表示文件(File)也表明格式化輸出結果應該被寫入第一個參數提供的文件中。在Printf函數中的第一個參數os.Stdout是*os.File類型;在Sprintf函數中的第一個參數&buf是一個指向可以寫入字節的內存緩沖區,然而它 并不是一個文件類型盡管它在某種意義上和文件類型相似。 即使Fprintf函數中的第一個參數也不是一個文件類型。它是io.Writer類型這是一個接口類型定義如下: ~~~ package io // Writer is the interface that wraps the basic Write method. type Writer interface { // Write writes len(p) bytes from p to the underlying data stream. // It returns the number of bytes written from p (0 <= n <= len(p)) // and any error encountered that caused the write to stop early. // Write must return a non-nil error if it returns n < len(p). // Write must not modify the slice data, even temporarily. // // Implementations must not retain p. Write(p []byte) (n int, err error) } ~~~ io.Writer類型定義了函數Fprintf和這個函數調用者之間的約定。一方面這個約定需要調用者提供具體類型的值就像*os.File和*bytes.Buffer,這些類型都有一個特定簽名和行為的Write的函數。另一方面這個約定保證了Fprintf接受任何滿足io.Writer接口的值都可以工作。Fprintf函數可能沒有假定寫入的是一個文件或是一段內存,而是寫入一個可以調用Write函數的值。 因為fmt.Fprintf函數沒有對具體操作的值做任何假設而是僅僅通過io.Writer接口的約定來保證行為,所以第一個參數可以安全地傳入一個任何具體類型的值只需要滿足io.Writer接口。一個類型可以自由的使用另一個滿足相同接口的類型來進行替換被稱作可替換性(LSP里氏替換)。這是一個面向對象的特征。 讓我們通過一個新的類型來進行校驗,下面*ByteCounter類型里的Write方法,僅僅在丟失寫向它的字節前統計它們的長度。(在這個+=賦值語句中,讓len(p)的類型和*c的類型匹配的轉換是必須的。) *gopl.io/ch7/bytecounter* ~~~ type ByteCounter int func (c *ByteCounter) Write(p []byte) (int, error) { *c += ByteCounter(len(p)) // convert int to ByteCounter return len(p), nil } ~~~ 因為*ByteCounter滿足io.Writer的約定,我們可以把它傳入Fprintf函數中;Fprintf函數執行字符串格式化的過程不會去關注ByteCounter正確的累加結果的長度。 ~~~ var c ByteCounter c.Write([]byte("hello")) fmt.Println(c) // "5", = len("hello") c = 0 // reset the counter var name = "Dolly" fmt.Fprintf(&c, "hello, %s", name) fmt.Println(c) // "12", = len("hello, Dolly") ~~~ 除了io.Writer這個接口類型,還有另一個對fmt包很重要的接口類型。Fprintf和Fprintln函數向類型提供了一種控制它們值輸出的途徑。在2.5節中,我們為Celsius類型提供了一個String方法以便于可以打印成這樣"100°C" ,在6.5節中我們給*IntSet添加一個String方法,這樣集合可以用傳統的符號來進行表示就像"{1 2 3}"。給一個類型定義String方法,可以讓它滿足最廣泛使用之一的接口類型fmt.Stringer: ~~~ package fmt // The String method is used to print values passed // as an operand to any format that accepts a string // or to an unformatted printer such as Print. type Stringer interface { String() string } ~~~ 我們會在7.10節解釋fmt包怎么發現哪些值是滿足這個接口類型的。 **練習 7.1:** 使用來自ByteCounter的思路,實現一個針對對單詞和行數的計數器。你會發現bufio.ScanWords非常的有用。 **練習 7.2:** 寫一個帶有如下函數簽名的函數CountingWriter,傳入一個io.Writer接口類型,返回一個新的Writer類型把原來的Writer封裝在里面和一個表示寫入新的Writer字節數的int64類型指針 ~~~ func CountingWriter(w io.Writer) (io.Writer, *int64) ~~~ **練習 7.3:** 為在gopl.io/ch4/treesort (§4.4)的*tree類型實現一個String方法去展示tree類型的值序列。 ### 7.2. 接口類型 接口類型具體描述了一系列方法的集合,一個實現了這些方法的具體類型是這個接口類型的實例。 io.Writer類型是用的最廣泛的接口之一,因為它提供了所有的類型寫入bytes的抽象,包括文件類型,內存緩沖區,網絡鏈接,HTTP客戶端,壓縮工具,哈希等等。io包中定義了很多其它有用的接口類型。Reader可以代表任意可以讀取bytes的類型,Closer可以是任意可以關閉的值,例如一個文件或是網絡鏈接。(到現在你可能注意到了很多Go語言中單方法接口的命名習慣) ~~~ package io type Reader interface { Read(p []byte) (n int, err error) } type Closer interface { Close() error } ~~~ 在往下看,我們發現有些新的接口類型通過組合已經有的接口來定義。下面是兩個例子: ~~~ type ReadWriter interface { Reader Writer } type ReadWriteCloser interface { Reader Writer Closer } ~~~ 上面用到的語法和結構內嵌相似,我們可以用這種方式以一個簡寫命名另一個接口,而不用聲明它所有的方法。這種方式本稱為接口內嵌。盡管略失簡潔,我們可以像下面這樣,不使用內嵌來聲明io.Writer接口。 ~~~ type ReadWriter interface { Read(p []byte) (n int, err error) Write(p []byte) (n int, err error) } ~~~ 或者甚至使用種混合的風格: ~~~ type ReadWriter interface { Read(p []byte) (n int, err error) Writer } ~~~ 上面3種定義方式都是一樣的效果。方法的順序變化也沒有影響,唯一重要的就是這個集合里面的方法。 **練習 7.4:** strings.NewReader函數通過讀取一個string參數返回一個滿足io.Reader接口類型的值(和其它值)。實現一個簡單版本的NewReader,并用它來構造一個接收字符串輸入的HTML解析器(§5.2) **練習 7.5:** io包里面的LimitReader函數接收一個io.Reader接口類型的r和字節數n,并且返回另一個從r中讀取字節但是當讀完n個字節后就表示讀到文件結束的Reader。實現這個LimitReader函數: ~~~ func LimitReader(r io.Reader, n int64) io.Reader ~~~ ### 7.3. 實現接口的條件 一個類型如果擁有一個接口需要的所有方法,那么這個類型就實現了這個接口。例如,*os.File類型實現了io.Reader,Writer,Closer,和ReadWriter接口。*bytes.Buffer實現了Reader,Writer,和ReadWriter這些接口,但是它沒有實現Closer接口因為它不具有Close方法。Go的程序員經常會簡要的把一個具體的類型描述成一個特定的接口類型。舉個例子,*bytes.Buffer是io.Writer;*os.Files是io.ReadWriter。 接口指定的規則非常簡單:表達一個類型屬于某個接口只要這個類型實現這個接口。所以: ~~~ var w io.Writer w = os.Stdout // OK: *os.File has Write method w = new(bytes.Buffer) // OK: *bytes.Buffer has Write method w = time.Second // compile error: time.Duration lacks Write method var rwc io.ReadWriteCloser rwc = os.Stdout // OK: *os.File has Read, Write, Close methods rwc = new(bytes.Buffer) // compile error: *bytes.Buffer lacks Close method ~~~ 這個規則甚至適用于等式右邊本身也是一個接口類型 ~~~ w = rwc // OK: io.ReadWriteCloser has Write method 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.5章中,IntSet類型的String方法的接收者是一個指針類型,所以我們不能在一個不能尋址的IntSet值上調用這個方法: ~~~ type IntSet struct { /* ... */ } func (*IntSet) String() string var _ = IntSet{}.String() // compile error: String requires *IntSet receiver ~~~ 但是我們可以在一個IntSet值上調用這個方法: ~~~ var s IntSet var _ = s.String() // OK: s is a variable and &s has a String method ~~~ 然而,由于只有*IntSet類型有String方法,所有也只有*IntSet類型實現了fmt.Stringer接口: ~~~ var _ fmt.Stringer = &s // OK var _ fmt.Stringer = s // compile error: IntSet lacks String method ~~~ 12.8章包含了一個打印出任意值的所有方法的程序,然后可以使用godoc -analysis=type tool(§10.7.4)展示每個類型的方法和具體類型和接口之間的關系 就像信封封裝和隱藏信件起來一樣,接口類型封裝和隱藏具體類型和它的值。即使具體類型有其它的方法也只有接口類型暴露出來的方法會被調用到: ~~~ os.Stdout.Write([]byte("hello")) // OK: *os.File has Write method os.Stdout.Close() // OK: *os.File has Close method var w io.Writer 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{}類型,它沒有任何方法,請講出哪些具體的類型實現了它? 這看上去好像沒有用,但實際上interface{}被稱為空接口類型是不可或缺的。因為空接口類型對實現它的類型沒有要求,所以我們可以將任意一個值賦給空接口類型。 ~~~ var any interface{} any = true any = 12.34 any = "hello" any = map[string]int{"one": 1} any = new(bytes.Buffer) ~~~ 盡管不是很明顯,從本書最早的的例子中我們就已經在使用空接口類型。它允許像fmt.Println或者5.7章中的errorf函數接受任何類型的參數。 對于創建的一個interface{}值持有一個boolean,float,string,map,pointer,或者任意其它的類型;我們當然不能直接對它持有的值做操作,因為interface{}沒有任何方法。我們會在7.10章中學到一種用類型斷言來獲取interface{}中值的方法。 因為接口實現只依賴于判斷的兩個類型的方法,所以沒有必要定義一個具體類型和它實現的接口之間的關系。也就是說,嘗試文檔化和斷言這種關系幾乎沒有用,所以并沒有通過程序強制定義。下面的定義在編譯期斷言一個*bytes.Buffer的值實現了io.Writer接口類型: ~~~ // *bytes.Buffer must satisfy io.Writer var w io.Writer = new(bytes.Buffer) ~~~ 因為任意*bytes.Buffer的值,甚至包括nil通過(*bytes.Buffer)(nil)進行顯示的轉換都實現了這個接口,所以我們不必分配一個新的變量。并且因為我們絕不會引用變量w,我們可以使用空標識符來來進行代替。總的看,這些變化可以讓我們得到一個更樸素的版本: ~~~ // *bytes.Buffer must satisfy io.Writer var _ io.Writer = (*bytes.Buffer)(nil) ~~~ 非空的接口類型比如io.Writer經常被指針類型實現,尤其當一個或多個接口方法像Write方法那樣隱式的給接收者帶來變化的時候。一個結構體的指針是非常常見的承載方法的類型。 但是并不意味著只有指針類型滿足接口類型,甚至連一些有設置方法的接口類型也可能會被Go語言中其它的引用類型實現。我們已經看過slice類型的方法(geometry.Path, §6.1)和map類型的方法(url.Values, §6.2.1),后面還會看到函數類型的方法的例子(http.HandlerFunc, §7.7)。甚至基本的類型也可能會實現一些接口;就如我們在7.4章中看到的time.Duration類型實現了fmt.Stringer接口。 一個具體的類型可能實現了很多不相關的接口。考慮在一個組織出售數字文化產品比如音樂,電影和書籍的程序中可能定義了下列的具體類型: ~~~ Album Book Movie Magazine Podcast TVEpisode Track ~~~ 我們可以把每個抽象的特點用接口來表示。一些特性對于所有的這些文化產品都是共通的,例如標題,創作日期和作者列表。 ~~~ type Artifact interface { Title() string Creators() []string Created() time.Time } ~~~ 其它的一些特性只對特定類型的文化產品才有。和文字排版特性相關的只有books和magazines,還有只有movies和TV劇集和屏幕分辨率相關。 ~~~ type Text interface { Pages() int Words() int PageSize() int } type Audio interface { Stream() (io.ReadCloser, error) RunningTime() time.Duration Format() string // e.g., "MP3", "WAV" } type Video interface { Stream() (io.ReadCloser, error) RunningTime() time.Duration Format() string // e.g., "MP4", "WMV" Resolution() (x, y int) } ~~~ 這些接口不止是一種有用的方式來分組相關的具體類型和表示他們之間的共同特定。我們后面可能會發現其它的分組。舉例,如果我們發現我們需要以同樣的方式處理Audio和Video,我們可以定義一個Streamer接口來代表它們之間相同的部分而不必對已經存在的類型做改變。 ~~~ type Streamer interface { Stream() (io.ReadCloser, error) RunningTime() time.Duration Format() string } ~~~ 每一個具體類型的組基于它們相同的行為可以表示成一個接口類型。不像基于類的語言,他們一個類實現的接口集合需要進行顯式的定義,在Go語言中我們可以在需要的時候定義一個新的抽象或者特定特點的組,而不需要修改具體類型的定義。當具體的類型來自不同的作者時這種方式會特別有用。當然也確實沒有必要在具體的類型中指出這些共性。 ### 7.4. flag.Value接口 在本章,我們會學到另一個標準的接口類型flag.Value是怎么幫助命令行標記定義新的符號的。思考下面這個會休眠特定時間的程序: gopl.io/ch7/sleep ~~~ var period = flag.Duration("period", 1*time.Second, "sleep period") func main() { flag.Parse() fmt.Printf("Sleeping for %v...", *period) time.Sleep(*period) fmt.Println() } ~~~ 在它休眠前它會打印出休眠的時間周期。fmt包調用time.Duration的String方法打印這個時間周期是以用戶友好的注解方式,而不是一個納秒數字: ~~~ $ go build gopl.io/ch7/sleep $ ./sleep Sleeping for 1s... ~~~ 默認情況下,休眠周期是一秒,但是可以通過 -period 這個命令行標記來控制。flag.Duration函數創建一個time.Duration類型的標記變量并且允許用戶通過多種用戶友好的方式來設置這個變量的大小,這種方式還包括和String方法相同的符號排版形式。這種對稱設計使得用戶交互良好。 ~~~ $ ./sleep -period 50ms Sleeping for 50ms... $ ./sleep -period 2m30s Sleeping for 2m30s... $ ./sleep -period 1.5h Sleeping for 1h30m0s... $ ./sleep -period "1 day" invalid value "1 day" for flag -period: time: invalid duration 1 day ~~~ 因為時間周期標記值非常的有用,所以這個特性被構建到了flag包中;但是我們為我們自己的數據類型定義新的標記符號是簡單容易的。我們只需要定義一個實現flag.Value接口的類型,如下: ~~~ package flag // Value is the interface to the value stored in a flag. type Value interface { String() string Set(string) error } ~~~ String方法格式化標記的值用在命令行幫組消息中;這樣每一個flag.Value也是一個fmt.Stringer。Set方法解析它的字符串參數并且更新標記變量的值。實際上,Set方法和String是兩個相反的操作,所以最好的辦法就是對他們使用相同的注解方式。 讓我們定義一個允許通過攝氏度或者華氏溫度變換的形式指定溫度的celsiusFlag類型。注意celsiusFlag內嵌了一個Celsius類型(§2.5),因此不用實現本身就已經有String方法了。為了實現flag.Value,我們只需要定義Set方法: *gopl.io/ch7/tempconv* ~~~ // *celsiusFlag satisfies the flag.Value interface. type celsiusFlag struct{ Celsius } func (f *celsiusFlag) Set(s string) error { var unit string var value float64 fmt.Sscanf(s, "%f%s", &value, &unit) // no error check needed switch unit { case "C", "°C": f.Celsius = Celsius(value) return nil case "F", "°F": f.Celsius = FToC(Fahrenheit(value)) return nil } return fmt.Errorf("invalid temperature %q", s) } ~~~ 調用fmt.Sscanf函數從輸入s中解析一個浮點數(value)和一個字符串(unit)。雖然通常必須檢查Sscanf的錯誤返回,但是在這個例子中我們不需要因為如果有錯誤發生,就沒有switch case會匹配到。 下面的CelsiusFlag函數將所有邏輯都封裝在一起。它返回一個內嵌在celsiusFlag變量f中的Celsius指針給調用者。Celsius字段是一個會通過Set方法在標記處理的過程中更新的變量。調用Var方法將標記加入應用的命令行標記集合中,有異常復雜命令行接口的全局變量flag.CommandLine.Programs可能有幾個這個類型的變量。調用Var方法將一個*celsiusFlag參數賦值給一個flag.Value參數,導致編譯器去檢查*celsiusFlag是否有必須的方法。 ~~~ // CelsiusFlag defines a Celsius flag with the specified name, // default value, and usage, and returns the address of the flag variable. // The flag argument must have a quantity and a unit, e.g., "100C". func CelsiusFlag(name string, value Celsius, usage string) *Celsius { f := celsiusFlag{value} flag.CommandLine.Var(&f, name, usage) return &f.Celsius } ~~~ 現在我們可以開始在我們的程序中使用新的標記: *gopl.io/ch7/tempflag* ~~~ var temp = tempconv.CelsiusFlag("temp", 20.0, "the temperature") func main() { flag.Parse() fmt.Println(*temp) } ~~~ 下面是典型的場景: ~~~ $ go build gopl.io/ch7/tempflag $ ./tempflag 20°C $ ./tempflag -temp -18C -18°C $ ./tempflag -temp 212°F 100°C $ ./tempflag -temp 273.15K invalid value "273.15K" for flag -temp: invalid temperature "273.15K" Usage of ./tempflag: -temp value the temperature (default 20°C) $ ./tempflag -help Usage of ./tempflag: -temp value the temperature (default 20°C) ~~~ **練習 7.6:** 對tempFlag加入支持開爾文溫度。 **練習 7.7:** 解釋為什么幫助信息在它的默認值是20.0沒有包含°C的情況下輸出了°C。 ### 7.5. 接口值 概念上講一個接口的值,接口值,由兩個部分組成,一個具體的類型和那個類型的值。它們被稱為接口的動態類型和動態值。對于像Go語言這種靜態類型的語言,類型是編譯期的概念;因此一個類型不是一個值。在我們的概念模型中,一些提供每個類型信息的值被稱為類型描述符,比如類型的名稱和方法。在一個接口值中,類型部分代表與之相關類型的描述符。 下面4個語句中,變量w得到了3個不同的值。(開始和最后的值是相同的) ~~~ var w io.Writer w = os.Stdout w = new(bytes.Buffer) w = nil ~~~ 讓我們進一步觀察在每一個語句后的w變量的值和動態行為。第一個語句定義了變量w: ~~~ var w io.Writer ~~~ 在Go語言中,變量總是被一個定義明確的值初始化,即使接口類型也不例外。對于一個接口的零值就是它的類型和值的部分都是nil(圖7.1)。 ![](https://box.kancloud.cn/2016-01-10_5691fbe3edf31.png) 一個接口值基于它的動態類型被描述為空或非空,所以這是一個空的接口值。你可以通過使用w==nil或者w!=nil來判讀接口值是否為空。調用一個空接口值上的任意方法都會產生panic: ~~~ w.Write([]byte("hello")) // panic: nil pointer dereference ~~~ 第二個語句將一個*os.File類型的值賦給變量w: ~~~ w = os.Stdout ~~~ 這個賦值過程調用了一個具體類型到接口類型的隱式轉換,這和顯式的使用io.Writer(os.Stdout)是等價的。這類轉換不管是顯式的還是隱式的,都會刻畫出操作到的類型和值。這個接口值的動態類型被設為*os.Stdout指針的類型描述符,它的動態值持有os.Stdout的拷貝;這是一個代表處理標準輸出的os.File類型變量的指針(圖7.2)。 ![](https://box.kancloud.cn/2016-01-10_5691fbe40846e.png) 調用一個包含*os.File類型指針的接口值的Write方法,使得(*os.File).Write方法被調用。這個調用輸出“hello”。 ~~~ w.Write([]byte("hello")) // "hello" ~~~ 通常在編譯期,我們不知道接口值的動態類型是什么,所以一個接口上的調用必須使用動態分配。因為不是直接進行調用,所以編譯器必須把代碼生成在類型描述符的方法Write上,然后間接調用那個地址。這個調用的接收者是一個接口動態值的拷貝,os.Stdout。效果和下面這個直接調用一樣: ~~~ os.Stdout.Write([]byte("hello")) // "hello" ~~~ 第三個語句給接口值賦了一個*bytes.Buffer類型的值 ~~~ w = new(bytes.Buffer) ~~~ 現在動態類型是*bytes.Buffer并且動態值是一個指向新分配的緩沖區的指針(圖7.3)。 ![](https://box.kancloud.cn/2016-01-10_5691fbe4167ae.png) Write方法的調用也使用了和之前一樣的機制: ~~~ w.Write([]byte("hello")) // writes "hello" to the bytes.Buffers ~~~ 這次類型描述符是*bytes.Buffer,所以調用了(*bytes.Buffer).Write方法,并且接收者是該緩沖區的地址。這個調用把字符串“hello”添加到緩沖區中。 最后,第四個語句將nil賦給了接口值: ~~~ w = nil ~~~ 這個重置將它所有的部分都設為nil值,把變量w恢復到和它之前定義時相同的狀態圖,在圖7.1中可以看到。 一個接口值可以持有任意大的動態值。例如,表示時間實例的time.Time類型,這個類型有幾個對外不公開的字段。我們從它上面創建一個接口值, ~~~ var x interface{} = time.Now() ~~~ 結果可能和圖7.4相似。從概念上講,不論接口值多大,動態值總是可以容下它。(這只是一個概念上的模型;具體的實現可能會非常不同) ![](https://box.kancloud.cn/2016-01-10_5691fbe425c24.png) 接口值可以使用==和!=來進行比較。兩個接口值相等僅當它們都是nil值或者它們的動態類型相同并且動態值也根據這個動態類型的==操作相等。因為接口值是可比較的,所以它們可以用在map的鍵或者作為switch語句的操作數。 然而,如果兩個接口值的動態類型相同,但是這個動態類型是不可比較的(比如切片),將它們進行比較就會失敗并且panic: ~~~ var x interface{} = []int{1, 2, 3} fmt.Println(x == x) // panic: comparing uncomparable type []int ~~~ 考慮到這點,接口類型是非常與眾不同的。其它類型要么是安全的可比較類型(如基本類型和指針)要么是完全不可比較的類型(如切片,映射類型,和函數),但是在比較接口值或者包含了接口值的聚合類型時,我們必須要意識到潛在的panic。同樣的風險也存在于使用接口作為map的鍵或者switch的操作數。只能比較你非常確定它們的動態值是可比較類型的接口值。 當我們處理錯誤或者調試的過程中,得知接口值的動態類型是非常有幫助的。所以我們使用fmt包的%T動作: ~~~ var w io.Writer fmt.Printf("%T\n", w) // "<nil>" w = os.Stdout fmt.Printf("%T\n", w) // "*os.File" w = new(bytes.Buffer) fmt.Printf("%T\n", w) // "*bytes.Buffer" ~~~ 在fmt包內部,使用反射來獲取接口動態類型的名稱。我們會在第12章中學到反射相關的知識。 ### 7.5.1. 警告:一個包含nil指針的接口不是nil接口 一個不包含任何值的nil接口值和一個剛好包含nil指針的接口值是不同的。這個細微區別產生了一個容易絆倒每個Go程序員的陷阱。 思考下面的程序。當debug變量設置為true時,main函數會將f函數的輸出收集到一個bytes.Buffer類型中。 ~~~ const debug = true func main() { var buf *bytes.Buffer if debug { buf = new(bytes.Buffer) // enable collection of output } f(buf) // NOTE: subtly incorrect! if debug { // ...use buf... } } // If out is non-nil, output will be written to it. func f(out io.Writer) { // ...do something... if out != nil { out.Write([]byte("done!\n")) } } ~~~ 我們可能會預計當把變量debug設置為false時可以禁止對輸出的收集,但是實際上在out.Write方法調用時程序發生了panic: ~~~ if out != nil { out.Write([]byte("done!\n")) // panic: nil pointer dereference } ~~~ 當main函數調用函數f時,它給f函數的out參數賦了一個*bytes.Buffer的空指針,所以out的動態值是nil。然而,它的動態類型是*bytes.Buffer,意思就是out變量是一個包含空指針值的非空接口(如圖7.5),所以防御性檢查out!=nil的結果依然是true。 ![](https://box.kancloud.cn/2016-03-12_56e3b60ad895f.png) 動態分配機制依然決定(*bytes.Buffer).Write的方法會被調用,但是這次的接收者的值是nil。對于一些如*os.File的類型,nil是一個有效的接收者(§6.2.1),但是*bytes.Buffer類型不在這些類型中。這個方法會被調用,但是當它嘗試去獲取緩沖區時會發生panic。 問題在于盡管一個nil的*bytes.Buffer指針有實現這個接口的方法,它也不滿足這個接口具體的行為上的要求。特別是這個調用違反了(*bytes.Buffer).Write方法的接收者非空的隱含先覺條件,所以將nil指針賦給這個接口是錯誤的。解決方案就是將main函數中的變量buf的類型改為io.Writer,因此可以避免一開始就將一個不完全的值賦值給這個接口: ~~~ var buf io.Writer if debug { buf = new(bytes.Buffer) // enable collection of output } f(buf) // OK ~~~ 現在我們已經把接口值的技巧都講完了,讓我們來看更多的一些在Go標準庫中的重要接口類型。在下面的三章中,我們會看到接口類型是怎樣用在排序,web服務,錯誤處理中的。 ### 7.6. sort.Interface接口 排序操作和字符串格式化一樣是很多程序經常使用的操作。盡管一個最短的快排程序只要15行就可以搞定,但是一個健壯的實現需要更多的代碼,并且我們不希望每次我們需要的時候都重寫或者拷貝這些代碼。 幸運的是,sort包內置的提供了根據一些排序函數來對任何序列排序的功能。它的設計非常獨到。在很多語言中,排序算法都是和序列數據類型關聯,同時排序函數和具體類型元素關聯。相比之下,Go語言的sort.Sort函數不會對具體的序列和它的元素做任何假設。相反,它使用了一個接口類型sort.Interface來指定通用的排序算法和可能被排序到的序列類型之間的約定。這個接口的實現由序列的具體表示和它希望排序的元素決定,序列的表示經常是一個切片。 一個內置的排序算法需要知道三個東西:序列的長度,表示兩個元素比較的結果,一種交換兩個元素的方式;這就是sort.Interface的三個方法: ~~~ package sort type Interface interface { Len() int Less(i, j int) bool // i, j are indices of sequence elements Swap(i, j int) } ~~~ 為了對序列進行排序,我們需要定義一個實現了這三個方法的類型,然后對這個類型的一個實例應用sort.Sort函數。思考對一個字符串切片進行排序,這可能是最簡單的例子了。下面是這個新的類型StringSlice和它的Len,Less和Swap方法 ~~~ type StringSlice []string func (p StringSlice) Len() int { return len(p) } func (p StringSlice) Less(i, j int) bool { return p[i] < p[j] } func (p StringSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } ~~~ 現在我們可以通過像下面這樣將一個切片轉換為一個StringSlice類型來進行排序: ~~~ sort.Sort(StringSlice(names)) ~~~ 這個轉換得到一個相同長度,容量,和基于names數組的切片值;并且這個切片值的類型有三個排序需要的方法。 對字符串切片的排序是很常用的需要,所以sort包提供了StringSlice類型,也提供了Strings函數能讓上面這些調用簡化成sort.Strings(names)。 這里用到的技術很容易適用到其它排序序列中,例如我們可以忽略大些或者含有特殊的字符。(本書使用Go程序對索引詞和頁碼進行排序也用到了這個技術,對羅馬數字做了額外邏輯處理。)對于更復雜的排序,我們使用相同的方法,但是會用更復雜的數據結構和更復雜地實現sort.Interface的方法。 我們會運行上面的例子來對一個表格中的音樂播放列表進行排序。每個track都是單獨的一行,每一列都是這個track的屬性像藝術家,標題,和運行時間。想象一個圖形用戶界面來呈現這個表格,并且點擊一個屬性的頂部會使這個列表按照這個屬性進行排序;再一次點擊相同屬性的頂部會進行逆向排序。讓我們看下每個點擊會發生什么響應。 下面的變量tracks包好了一個播放列表。(One of the authors apologizes for the other author’s musical tastes.)每個元素都不是Track本身而是指向它的指針。盡管我們在下面的代碼中直接存儲Tracks也可以工作,sort函數會交換很多對元素,所以如果每個元素都是指針會更快而不是全部Track類型,指針是一個機器字碼長度而Track類型可能是八個或更多。 *gopl.io/ch7/sorting* ~~~ type Track struct { Title string Artist string Album string Year int Length time.Duration } var tracks = []*Track{ {"Go", "Delilah", "From the Roots Up", 2012, length("3m38s")}, {"Go", "Moby", "Moby", 1992, length("3m37s")}, {"Go Ahead", "Alicia Keys", "As I Am", 2007, length("4m36s")}, {"Ready 2 Go", "Martin Solveig", "Smash", 2011, length("4m24s")}, } func length(s string) time.Duration { d, err := time.ParseDuration(s) if err != nil { panic(s) } return d } ~~~ printTracks函數將播放列表打印成一個表格。一個圖形化的展示可能會更好點,但是這個小程序使用text/tabwriter包來生成一個列是整齊對齊和隔開的表格,像下面展示的這樣。注意到*tabwriter.Writer是滿足io.Writer接口的。它會收集每一片寫向它的數據;它的Flush方法會格式化整個表格并且將它寫向os.Stdout(標準輸出)。 ~~~ func printTracks(tracks []*Track) { const format = "%v\t%v\t%v\t%v\t%v\t\n" tw := new(tabwriter.Writer).Init(os.Stdout, 0, 8, 2, ' ', 0) fmt.Fprintf(tw, format, "Title", "Artist", "Album", "Year", "Length") fmt.Fprintf(tw, format, "-----", "------", "-----", "----", "------") for _, t := range tracks { fmt.Fprintf(tw, format, t.Title, t.Artist, t.Album, t.Year, t.Length) } tw.Flush() // calculate column widths and print table } ~~~ 為了能按照Artist字段對播放列表進行排序,我們會像對StringSlice那樣定義一個新的帶有必須Len,Less和Swap方法的切片類型。 ~~~ type byArtist []*Track func (x byArtist) Len() int { return len(x) } func (x byArtist) Less(i, j int) bool { return x[i].Artist < x[j].Artist } func (x byArtist) Swap(i, j int) { x[i], x[j] = x[j], x[i] } ~~~ 為了調用通用的排序程序,我們必須先將tracks轉換為新的byArtist類型,它定義了具體的排序: ~~~ sort.Sort(byArtist(tracks)) ~~~ 在按照artist對這個切片進行排序后,printTrack的輸出如下 ~~~ Title Artist Album Year Length ----- ------ ----- ---- ------ Go Ahead Alicia Keys As I Am 2007 4m36s Go Delilah From the Roots Up 2012 3m38s Ready 2 Go Martin Solveig Smash 2011 4m24s Go Moby Moby 1992 3m37s ~~~ 如果用戶第二次請求“按照artist排序”,我們會對tracks進行逆向排序。然而我們不需要定義一個有顛倒Less方法的新類型byReverseArtist,因為sort包中提供了Reverse函數將排序順序轉換成逆序。 ~~~ sort.Sort(sort.Reverse(byArtist(tracks))) ~~~ 在按照artist對這個切片進行逆向排序后,printTrack的輸出如下 ~~~ Title Artist Album Year Length ----- ------ ----- ---- ------ Go Moby Moby 1992 3m37s Ready 2 Go Martin Solveig Smash 2011 4m24s Go Delilah From the Roots Up 2012 3m38s Go Ahead Alicia Keys As I Am 2007 4m36s ~~~ sort.Reverse函數值得進行更近一步的學習因為它使用了(§6.3)章中的組合,這是一個重要的思路。sort包定義了一個不公開的struct類型reverse,它嵌入了一個sort.Interface。reverse的Less方法調用了內嵌的sort.Interface值的Less方法,但是通過交換索引的方式使排序結果變成逆序。 ~~~ package sort type reverse struct{ Interface } // that is, sort.Interface func (r reverse) Less(i, j int) bool { return r.Interface.Less(j, i) } func Reverse(data Interface) Interface { return reverse{data} } ~~~ reverse的另外兩個方法Len和Swap隱式地由原有內嵌的sort.Interface提供。因為reverse是一個不公開的類型,所以導出函數Reverse函數返回一個包含原有sort.Interface值的reverse類型實例。 為了可以按照不同的列進行排序,我們必須定義一個新的類型例如byYear: ~~~ type byYear []*Track func (x byYear) Len() int { return len(x) } func (x byYear) Less(i, j int) bool { return x[i].Year < x[j].Year } func (x byYear) Swap(i, j int) { x[i], x[j] = x[j], x[i] } ~~~ 在使用sort.Sort(byYear(tracks))按照年對tracks進行排序后,printTrack展示了一個按時間先后順序的列表: ~~~ Title Artist Album Year Length ----- ------ ----- ---- ------ Go Moby Moby 1992 3m37s Go Ahead Alicia Keys As I Am 2007 4m36s Ready 2 Go Martin Solveig Smash 2011 4m24s Go Delilah From the Roots Up 2012 3m38s ~~~ 對于我們需要的每個切片元素類型和每個排序函數,我們需要定義一個新的sort.Interface實現。如你所見,Len和Swap方法對于所有的切片類型都有相同的定義。下個例子,具體的類型customSort會將一個切片和函數結合,使我們只需要寫比較函數就可以定義一個新的排序。順便說下,實現了sort.Interface的具體類型不一定是切片類型;customSort是一個結構體類型。 ~~~ type customSort struct { t []*Track less func(x, y *Track) bool } func (x customSort) Len() int func (x customSort) Less(i, j int) bool { return x.less(x.t[i], x.t[j]) } func (x customSort) Swap(i, j int) { x.t[i], x.t[j] = x.t[j], x.t[i] } ~~~ 讓我們定義一個多層的排序函數,它主要的排序鍵是標題,第二個鍵是年,第三個鍵是運行時間Length。下面是該排序的調用,其中這個排序使用了匿名排序函數: ~~~ sort.Sort(customSort{tracks, func(x, y *Track) bool { if x.Title != y.Title { return x.Title < y.Title } if x.Year != y.Year { return x.Year < y.Year } if x.Length != y.Length { return x.Length < y.Length } return false }}) ~~~ 這下面是排序的結果。注意到兩個標題是“Go”的track按照標題排序是相同的順序,但是在按照year排序上更久的那個track優先。 ~~~ Title Artist Album Year Length ----- ------ ----- ---- ------ Go Moby Moby 1992 3m37s Go Delilah From the Roots Up 2012 3m38s Go Ahead Alicia Keys As I Am 2007 4m36s Ready 2 Go Martin Solveig Smash 2011 4m24s ~~~ 盡管對長度為n的序列排序需要 O(n log n)次比較操作,檢查一個序列是否已經有序至少需要n?1次比較。sort包中的IsSorted函數幫我們做這樣的檢查。像sort.Sort一樣,它也使用sort.Interface對這個序列和它的排序函數進行抽象,但是它從不會調用Swap方法:這段代碼示范了IntsAreSorted和Ints函數和IntSlice類型的使用: ~~~ values := []int{3, 1, 4, 1} fmt.Println(sort.IntsAreSorted(values)) // "false" sort.Ints(values) fmt.Println(values) // "[1 1 3 4]" fmt.Println(sort.IntsAreSorted(values)) // "true" sort.Sort(sort.Reverse(sort.IntSlice(values))) fmt.Println(values) // "[4 3 1 1]" fmt.Println(sort.IntsAreSorted(values)) // "false" ~~~ 為了使用方便,sort包為[]int,[]string和[]float64的正常排序提供了特定版本的函數和類型。對于其他類型,例如[]int64或者[]uint,盡管路徑也很簡單,還是依賴我們自己實現。 **練習 7.8:** 很多圖形界面提供了一個有狀態的多重排序表格插件:主要的排序鍵是最近一次點擊過列頭的列,第二個排序鍵是第二最近點擊過列頭的列,等等。定義一個sort.Interface的實現用在這樣的表格中。比較這個實現方式和重復使用sort.Stable來排序的方式。 **練習 7.9:** 使用html/template包 (§4.6) 替代printTracks將tracks展示成一個HTML表格。將這個解決方案用在前一個練習中,讓每次點擊一個列的頭部產生一個HTTP請求來排序這個表格。 **練習 7.10:** sort.Interface類型也可以適用在其它地方。編寫一個IsPalindrome(s sort.Interface) bool函數表明序列s是否是回文序列,換句話說反向排序不會改變這個序列。假設如果!s.Less(i, j) && !s.Less(j, i)則索引i和j上的元素相等。 ### 7.7. http.Handler接口 在第一章中,我們粗略的了解了怎么用net/http包去實現網絡客戶端(§1.5)和服務器(§1.7)。在這個小節中,我們會對那些基于http.Handler接口的服務器API做更進一步的學習: *net/http* ~~~ package http type Handler interface { ServeHTTP(w ResponseWriter, r *Request) } func ListenAndServe(address string, h Handler) error ~~~ ListenAndServe函數需要一個例如“localhost:8000”的服務器地址,和一個所有請求都可以分派的Handler接口實例。它會一直運行,直到這個服務因為一個錯誤而失敗(或者啟動失敗),它的返回值一定是一個非空的錯誤。 想象一個電子商務網站,為了銷售它的數據庫將它物品的價格映射成美元。下面這個程序可能是能想到的最簡單的實現了。它將庫存清單模型化為一個命名為database的map類型,我們給這個類型一個ServeHttp方法,這樣它可以滿足http.Handler接口。這個handler會遍歷整個map并輸出物品信息。 *gopl.io/ch7/http1* ~~~ func main() { db := database{"shoes": 50, "socks": 5} log.Fatal(http.ListenAndServe("localhost:8000", db)) } type dollars float32 func (d dollars) String() string { return fmt.Sprintf("$%.2f", d) } type database map[string]dollars func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) { for item, price := range db { fmt.Fprintf(w, "%s: %s\n", item, price) } } ~~~ 如果我們啟動這個服務, ~~~ $ go build gopl.io/ch7/http1 $ ./http1 & ~~~ 然后用1.5節中的獲取程序(如果你更喜歡可以使用web瀏覽器)來連接服務器,我們得到下面的輸出: ~~~ $ go build gopl.io/ch1/fetch $ ./fetch http://localhost:8000 shoes: $50.00 socks: $5.00 ~~~ 目前為止,這個服務器不考慮URL只能為每個請求列出它全部的庫存清單。更真實的服務器會定義多個不同的URL,每一個都會觸發一個不同的行為。讓我們使用/list來調用已經存在的這個行為并且增加另一個/price調用表明單個貨品的價格,像這樣/price?item=socks來指定一個請求參數。 *gopl.io/ch7/http2* ~~~ func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) { switch req.URL.Path { case "/list": for item, price := range db { fmt.Fprintf(w, "%s: %s\n", item, price) } case "/price": item := req.URL.Query().Get("item") price, ok := db[item] if !ok { w.WriteHeader(http.StatusNotFound) // 404 fmt.Fprintf(w, "no such item: %q\n", item) return } fmt.Fprintf(w, "%s\n", price) default: w.WriteHeader(http.StatusNotFound) // 404 fmt.Fprintf(w, "no such page: %s\n", req.URL) } } ~~~ 現在handler基于URL的路徑部分(req.URL.Path)來決定執行什么邏輯。如果這個handler不能識別這個路徑,它會通過調用w.WriteHeader(http.StatusNotFound)返回客戶端一個HTTP錯誤;這個檢查應該在向w寫入任何值前完成。(順便提一下,http.ResponseWriter是另一個接口。它在io.Writer上增加了發送HTTP相應頭的方法。)等效地,我們可以使用實用的http.Error函數: ~~~ msg := fmt.Sprintf("no such page: %s\n", req.URL) http.Error(w, msg, http.StatusNotFound) // 404 ~~~ /price的case會調用URL的Query方法來將HTTP請求參數解析為一個map,或者更準確地說一個net/url包中url.Values(§6.2.1)類型的多重映射。然后找到第一個item參數并查找它的價格。如果這個貨品沒有找到會返回一個錯誤。 這里是一個和新服務器會話的例子: ~~~ $ go build gopl.io/ch7/http2 $ go build gopl.io/ch1/fetch $ ./http2 & $ ./fetch http://localhost:8000/list shoes: $50.00 socks: $5.00 $ ./fetch http://localhost:8000/price?item=socks $5.00 $ ./fetch http://localhost:8000/price?item=shoes $50.00 $ ./fetch http://localhost:8000/price?item=hat no such item: "hat" $ ./fetch http://localhost:8000/help no such page: /help ~~~ 顯然我們可以繼續向ServeHTTP方法中添加case,但在一個實際的應用中,將每個case中的邏輯定義到一個分開的方法或函數中會很實用。此外,相近的URL可能需要相似的邏輯;例如幾個圖片文件可能有形如/images/*.png的URL。因為這些原因,net/http包提供了一個請求多路器ServeMux來簡化URL和handlers的聯系。一個ServeMux將一批http.Handler聚集到一個單一的http.Handler中。再一次,我們可以看到滿足同一接口的不同類型是可替換的:web服務器將請求指派給任意的http.Handler 而不需要考慮它后面的具體類型。 對于更復雜的應用,一些ServeMux可以通過組合來處理更加錯綜復雜的路由需求。Go語言目前沒有一個權威的web框架,就像Ruby語言有Rails和python有Django。這并不是說這樣的框架不存在,而是Go語言標準庫中的構建模塊就已經非常靈活以至于這些框架都是不必要的。此外,盡管在一個項目早期使用框架是非常方便的,但是它們帶來額外的復雜度會使長期的維護更加困難。 在下面的程序中,我們創建一個ServeMux并且使用它將URL和相應處理/list和/price操作的handler聯系起來,這些操作邏輯都已經被分到不同的方法中。然后我門在調用ListenAndServe函數中使用ServeMux最為主要的handler。 *gopl.io/ch7/http3* ~~~ func main() { db := database{"shoes": 50, "socks": 5} mux := http.NewServeMux() mux.Handle("/list", http.HandlerFunc(db.list)) mux.Handle("/price", http.HandlerFunc(db.price)) log.Fatal(http.ListenAndServe("localhost:8000", mux)) } type database map[string]dollars func (db database) list(w http.ResponseWriter, req *http.Request) { for item, price := range db { fmt.Fprintf(w, "%s: %s\n", item, price) } } func (db database) price(w http.ResponseWriter, req *http.Request) { item := req.URL.Query().Get("item") price, ok := db[item] if !ok { w.WriteHeader(http.StatusNotFound) // 404 fmt.Fprintf(w, "no such item: %q\n", item) return } fmt.Fprintf(w, "%s\n", price) } ~~~ 讓我們關注這兩個注冊到handlers上的調用。第一個db.list是一個方法值 (§6.4),它是下面這個類型的值 ~~~ func(w http.ResponseWriter, req *http.Request) ~~~ 也就是說db.list的調用會援引一個接收者是db的database.list方法。所以db.list是一個實現了handler類似行為的函數,但是因為它沒有方法,所以它不滿足http.Handler接口并且不能直接傳給mux.Handle。 語句http.HandlerFunc(db.list)是一個轉換而非一個函數調用,因為http.HandlerFunc是一個類型。它有如下的定義: *net/http* ~~~ package http type HandlerFunc func(w ResponseWriter, r *Request) func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) } ~~~ HandlerFunc顯示了在Go語言接口機制中一些不同尋常的特點。這是一個有實現了接口http.Handler方法的函數類型。ServeHTTP方法的行為調用了它本身的函數。因此HandlerFunc是一個讓函數值滿足一個接口的適配器,這里函數和這個接口僅有的方法有相同的函數簽名。實際上,這個技巧讓一個單一的類型例如database以多種方式滿足http.Handler接口:一種通過它的list方法,一種通過它的price方法等等。 因為handler通過這種方式注冊非常普遍,ServeMux有一個方便的HandleFunc方法,它幫我們簡化handler注冊代碼成這樣: *gopl.io/ch7/http3a* ~~~ mux.HandleFunc("/list", db.list) mux.HandleFunc("/price", db.price) ~~~ 從上面的代碼很容易看出應該怎么構建一個程序,它有兩個不同的web服務器監聽不同的端口的,并且定義不同的URL將它們指派到不同的handler。我們只要構建另外一個ServeMux并且在調用一次ListenAndServe(可能并行的)。但是在大多數程序中,一個web服務器就足夠了。此外,在一個應用程序的多個文件中定義HTTP handler也是非常典型的,如果它們必須全部都顯示的注冊到這個應用的ServeMux實例上會比較麻煩。 所以為了方便,net/http包提供了一個全局的ServeMux實例DefaultServerMux和包級別的http.Handle和http.HandleFunc函數。現在,為了使用DefaultServeMux作為服務器的主handler,我們不需要將它傳給ListenAndServe函數;nil值就可以工作。 然后服務器的主函數可以簡化成: *gopl.io/ch7/http4* ~~~ func main() { db := database{"shoes": 50, "socks": 5} http.HandleFunc("/list", db.list) http.HandleFunc("/price", db.price) log.Fatal(http.ListenAndServe("localhost:8000", nil)) } ~~~ 最后,一個重要的提示:就像我們在1.7節中提到的,web服務器在一個新的協程中調用每一個handler,所以當handler獲取其它協程或者這個handler本身的其它請求也可以訪問的變量時一定要使用預防措施比如鎖機制。我們后面的兩章中講到并發相關的知識。 **練習 7.11:** 增加額外的handler讓客服端可以創建,讀取,更新和刪除數據庫記錄。例如,一個形如 `/update?item=socks&price=6` 的請求會更新庫存清單里一個貨品的價格并且當這個貨品不存在或價格無效時返回一個錯誤值。(注意:這個修改會引入變量同時更新的問題) **練習 7.12:** 修改/list的handler讓它把輸出打印成一個HTML的表格而不是文本。html/template包(§4.6)可能會對你有幫助。 ### 7.8. error接口 從本書的開始,我們就已經創建和使用過神秘的預定義error類型,而且沒有解釋它究竟是什么。實際上它就是interface類型,這個類型有一個返回錯誤信息的單一方法: ~~~ type error interface { Error() string } ~~~ 創建一個error最簡單的方法就是調用errors.New函數,它會根據傳入的錯誤信息返回一個新的error。整個errors包僅只有4行: ~~~ package errors func New(text string) error { return &errorString{text} } type errorString struct { text string } func (e *errorString) Error() string { return e.text } ~~~ 承載errorString的類型是一個結構體而非一個字符串,這是為了保護它表示的錯誤避免粗心(或有意)的更新。并且因為是指針類型*errorString滿足error接口而非errorString類型,所以每個New函數的調用都分配了一個獨特的和其他錯誤不相同的實例。我們也不想要重要的error例如io.EOF和一個剛好有相同錯誤消息的error比較后相等。 ~~~ fmt.Println(errors.New("EOF") == errors.New("EOF")) // "false" ~~~ 調用errors.New函數是非常稀少的,因為有一個方便的封裝函數fmt.Errorf,它還會處理字符串格式化。我們曾多次在第5章中用到它。 ~~~ package fmt import "errors" func Errorf(format string, args ...interface{}) error { return errors.New(Sprintf(format, args...)) } ~~~ 雖然*errorString可能是最簡單的錯誤類型,但遠非只有它一個。例如,syscall包提供了Go語言底層系統調用API。在多個平臺上,它定義一個實現error接口的數字類型Errno,并且在Unix平臺上,Errno的Error方法會從一個字符串表中查找錯誤消息,如下面展示的這樣: ~~~ package syscall type Errno uintptr // operating system error code var errors = [...]string{ 1: "operation not permitted", // EPERM 2: "no such file or directory", // ENOENT 3: "no such process", // ESRCH // ... } func (e Errno) Error() string { if 0 <= int(e) && int(e) < len(errors) { return errors[e] } return fmt.Sprintf("errno %d", e) } ~~~ 下面的語句創建了一個持有Errno值為2的接口值,表示POSIX ENOENT狀況: ~~~ var err error = syscall.Errno(2) fmt.Println(err.Error()) // "no such file or directory" fmt.Println(err) // "no such file or directory" ~~~ err的值圖形化的呈現在圖7.6中。 ![](https://box.kancloud.cn/2016-03-12_56e3b60aeb2d8.png) Errno是一個系統調用錯誤的高效表示方式,它通過一個有限的集合進行描述,并且它滿足標準的錯誤接口。我們會在第7.11節了解到其它滿足這個接口的類型。 ### 7.9. 示例: 表達式求值 在本節中,我們會構建一個簡單算術表達式的求值器。我們將使用一個接口Expr來表示Go語言中任意的表達式。現在這個接口不需要有方法,但是我們后面會為它增加一些。 ~~~ // An Expr is an arithmetic expression. type Expr interface{} ~~~ 我們的表達式語言由浮點數符號(小數點);二元操作符+,-,*, 和/;一元操作符-x和+x;調用pow(x,y),sin(x),和sqrt(x)的函數;例如x和pi的變量;當然也有括號和標準的優先級運算符。所有的值都是float64類型。這下面是一些表達式的例子: ~~~ sqrt(A / pi) pow(x, 3) + pow(y, 3) (F - 32) * 5 / 9 ~~~ 下面的五個具體類型表示了具體的表達式類型。Var類型表示對一個變量的引用。(我們很快會知道為什么它可以被輸出。)literal類型表示一個浮點型常量。unary和binary類型表示有一到兩個運算對象的運算符表達式,這些操作數可以是任意的Expr類型。call類型表示對一個函數的調用;我們限制它的fn字段只能是pow,sin或者sqrt。 *gopl.io/ch7/eval* ~~~ // A Var identifies a variable, e.g., x. type Var string // A literal is a numeric constant, e.g., 3.141. type literal float64 // A unary represents a unary operator expression, e.g., -x. type unary struct { op rune // one of '+', '-' x Expr } // A binary represents a binary operator expression, e.g., x+y. type binary struct { op rune // one of '+', '-', '*', '/' x, y Expr } // A call represents a function call expression, e.g., sin(x). type call struct { fn string // one of "pow", "sin", "sqrt" args []Expr } ~~~ 為了計算一個包含變量的表達式,我們需要一個environment變量將變量的名字映射成對應的值: ~~~ type Env map[Var]float64 ~~~ 我們也需要每個表示式去定義一個Eval方法,這個方法會根據給定的environment變量返回表達式的值。因為每個表達式都必須提供這個方法,我們將它加入到Expr接口中。這個包只會對外公開Expr,Env,和Var類型。調用方不需要獲取其它的表達式類型就可以使用這個求值器。 ~~~ type Expr interface { // Eval returns the value of this Expr in the environment env. Eval(env Env) float64 } ~~~ 下面給大家展示一個具體的Eval方法。Var類型的這個方法對一個environment變量進行查找,如果這個變量沒有在environment中定義過這個方法會返回一個零值,literal類型的這個方法簡單的返回它真實的值。 ~~~ func (v Var) Eval(env Env) float64 { return env[v] } func (l literal) Eval(_ Env) float64 { return float64(l) } ~~~ unary和binary的Eval方法會遞歸的計算它的運算對象,然后將運算符op作用到它們上。我們不將被零或無窮數除作為一個錯誤,因為它們都會產生一個固定的結果無限。最后,call的這個方法會計算對于pow,sin,或者sqrt函數的參數值,然后調用對應在math包中的函數。 ~~~ func (u unary) Eval(env Env) float64 { switch u.op { case '+': return +u.x.Eval(env) case '-': return -u.x.Eval(env) } panic(fmt.Sprintf("unsupported unary operator: %q", u.op)) } func (b binary) Eval(env Env) float64 { switch b.op { case '+': return b.x.Eval(env) + b.y.Eval(env) case '-': return b.x.Eval(env) - b.y.Eval(env) case '*': return b.x.Eval(env) * b.y.Eval(env) case '/': return b.x.Eval(env) / b.y.Eval(env) } panic(fmt.Sprintf("unsupported binary operator: %q", b.op)) } func (c call) Eval(env Env) float64 { switch c.fn { case "pow": return math.Pow(c.args[0].Eval(env), c.args[1].Eval(env)) case "sin": return math.Sin(c.args[0].Eval(env)) case "sqrt": return math.Sqrt(c.args[0].Eval(env)) } panic(fmt.Sprintf("unsupported function call: %s", c.fn)) } ~~~ 一些方法會失敗。例如,一個call表達式可能未知的函數或者錯誤的參數個數。用一個無效的運算符如!或者<去構建一個unary或者binary表達式也是可能會發生的(盡管下面提到的Parse函數不會這樣做)。這些錯誤會讓Eval方法panic。其它的錯誤,像計算一個沒有在environment變量中出現過的Var,只會讓Eval方法返回一個錯誤的結果。所有的這些錯誤都可以通過在計算前檢查Expr來發現。這是我們接下來要講的Check方法的工作,但是讓我們先測試Eval方法。 下面的TestEval函數是對evaluator的一個測試。它使用了我們會在第11章講解的testing包,但是現在知道調用t.Errof會報告一個錯誤就足夠了。這個函數循環遍歷一個表格中的輸入,這個表格中定義了三個表達式和針對每個表達式不同的環境變量。第一個表達式根據給定圓的面積A計算它的半徑,第二個表達式通過兩個變量x和y計算兩個立方體的體積之和,第三個表達式將華氏溫度F轉換成攝氏度。 ~~~ func TestEval(t *testing.T) { tests := []struct { expr string env Env want string }{ {"sqrt(A / pi)", Env{"A": 87616, "pi": math.Pi}, "167"}, {"pow(x, 3) + pow(y, 3)", Env{"x": 12, "y": 1}, "1729"}, {"pow(x, 3) + pow(y, 3)", Env{"x": 9, "y": 10}, "1729"}, {"5 / 9 * (F - 32)", Env{"F": -40}, "-40"}, {"5 / 9 * (F - 32)", Env{"F": 32}, "0"}, {"5 / 9 * (F - 32)", Env{"F": 212}, "100"}, } var prevExpr string for _, test := range tests { // Print expr only when it changes. if test.expr != prevExpr { fmt.Printf("\n%s\n", test.expr) prevExpr = test.expr } expr, err := Parse(test.expr) if err != nil { t.Error(err) // parse error continue } got := fmt.Sprintf("%.6g", expr.Eval(test.env)) fmt.Printf("\t%v => %s\n", test.env, got) if got != test.want { t.Errorf("%s.Eval() in %v = %q, want %q\n", test.expr, test.env, got, test.want) } } } ~~~ 對于表格中的每一條記錄,這個測試會解析它的表達式然后在環境變量中計算它,輸出結果。這里我們沒有空間來展示Parse函數,但是如果你使用go get下載這個包你就可以看到這個函數。 go test(§11.1) 命令會運行一個包的測試用例: ~~~ $ go test -v gopl.io/ch7/eval ~~~ 這個-v標識可以讓我們看到測試用例打印的輸出;正常情況下像這個一樣成功的測試用例會阻止打印結果的輸出。這里是測試用例里fmt.Printf語句的輸出: ~~~ sqrt(A / pi) map[A:87616 pi:3.141592653589793] => 167 pow(x, 3) + pow(y, 3) map[x:12 y:1] => 1729 map[x:9 y:10] => 1729 5 / 9 * (F - 32) map[F:-40] => -40 map[F:32] => 0 map[F:212] => 100 ~~~ 幸運的是目前為止所有的輸入都是適合的格式,但是我們的運氣不可能一直都有。甚至在解釋型語言中,為了靜態錯誤檢查語法是非常常見的;靜態錯誤就是不用運行程序就可以檢測出來的錯誤。通過將靜態檢查和動態的部分分開,我們可以快速的檢查錯誤并且對于多次檢查只執行一次而不是每次表達式計算的時候都進行檢查。 讓我們往Expr接口中增加另一個方法。Check方法在一個表達式語義樹檢查出靜態錯誤。我們馬上會說明它的vars參數。 ~~~ type Expr interface { Eval(env Env) float64 // Check reports errors in this Expr and adds its Vars to the set. Check(vars map[Var]bool) error } ~~~ 具體的Check方法展示在下面。literal和Var類型的計算不可能失敗,所以這些類型的Check方法會返回一個nil值。對于unary和binary的Check方法會首先檢查操作符是否有效,然后遞歸的檢查運算單元。相似地對于call的這個方法首先檢查調用的函數是否已知并且有沒有正確個數的參數,然后遞歸的檢查每一個參數。 ~~~ func (v Var) Check(vars map[Var]bool) error { vars[v] = true return nil } func (literal) Check(vars map[Var]bool) error { return nil } func (u unary) Check(vars map[Var]bool) error { if !strings.ContainsRune("+-", u.op) { return fmt.Errorf("unexpected unary op %q", u.op) } return u.x.Check(vars) } func (b binary) Check(vars map[Var]bool) error { if !strings.ContainsRune("+-*/", b.op) { return fmt.Errorf("unexpected binary op %q", b.op) } if err := b.x.Check(vars); err != nil { return err } return b.y.Check(vars) } func (c call) Check(vars map[Var]bool) error { arity, ok := numParams[c.fn] if !ok { return fmt.Errorf("unknown function %q", c.fn) } if len(c.args) != arity { return fmt.Errorf("call to %s has %d args, want %d", c.fn, len(c.args), arity) } for _, arg := range c.args { if err := arg.Check(vars); err != nil { return err } } return nil } var numParams = map[string]int{"pow": 2, "sin": 1, "sqrt": 1} ~~~ 我們在兩個組中有選擇地列出有問題的輸入和它們得出的錯誤。Parse函數(這里沒有出現)會報出一個語法錯誤和Check函數會報出語義錯誤。 ~~~ x % 2 unexpected '%' math.Pi unexpected '.' !true unexpected '!' "hello" unexpected '"' log(10) unknown function "log" sqrt(1, 2) call to sqrt has 2 args, want 1 ~~~ Check方法的參數是一個Var類型的集合,這個集合聚集從表達式中找到的變量名。為了保證成功的計算,這些變量中的每一個都必須出現在環境變量中。從邏輯上講,這個集合就是調用Check方法返回的結果,但是因為這個方法是遞歸調用的,所以對于Check方法填充結果到一個作為參數傳入的集合中會更加的方便。調用方在初始調用時必須提供一個空的集合。 在第3.2節中,我們繪制了一個在編譯器才確定的函數f(x,y)。現在我們可以解析,檢查和計算在字符串中的表達式,我們可以構建一個在運行時從客戶端接收表達式的web應用并且它會繪制這個函數的表示的曲面。我們可以使用集合vars來檢查表達式是否是一個只有兩個變量,x和y的函數——實際上是3個,因為我們為了方便會提供半徑大小r。并且我們會在計算前使用Check方法拒絕有格式問題的表達式,這樣我們就不會在下面函數的40000個計算過程(100x100個柵格,每一個有4個角)重復這些檢查。 這個ParseAndCheck函數混合了解析和檢查步驟的過程: *gopl.io/ch7/surface* ~~~ import "gopl.io/ch7/eval" func parseAndCheck(s string) (eval.Expr, error) { if s == "" { return nil, fmt.Errorf("empty expression") } expr, err := eval.Parse(s) if err != nil { return nil, err } vars := make(map[eval.Var]bool) if err := expr.Check(vars); err != nil { return nil, err } for v := range vars { if v != "x" && v != "y" && v != "r" { return nil, fmt.Errorf("undefined variable: %s", v) } } return expr, nil } ~~~ 為了編寫這個web應用,所有我們需要做的就是下面這個plot函數,這個函數有和http.HandlerFunc相似的簽名: ~~~ func plot(w http.ResponseWriter, r *http.Request) { r.ParseForm() expr, err := parseAndCheck(r.Form.Get("expr")) if err != nil { http.Error(w, "bad expr: "+err.Error(), http.StatusBadRequest) return } w.Header().Set("Content-Type", "image/svg+xml") surface(w, func(x, y float64) float64 { r := math.Hypot(x, y) // distance from (0,0) return expr.Eval(eval.Env{"x": x, "y": y, "r": r}) }) } ~~~ ![](https://box.kancloud.cn/2016-03-12_56e3b60b066ff.png) 這個plot函數解析和檢查在HTTP請求中指定的表達式并且用它來創建一個兩個變量的匿名函數。這個匿名函數和來自原來surface-plotting程序中的固定函數f有相同的簽名,但是它計算一個用戶提供的表達式。環境變量中定義了x,y和半徑r。最后plot調用surface函數,它就是gopl.io/ch3/surface中的主要函數,修改后它可以接受plot中的函數和輸出io.Writer作為參數,而不是使用固定的函數f和os.Stdout。圖7.7中顯示了通過程序產生的3個曲面。 **練習 7.13:** 為Expr增加一個String方法來打印美觀的語法樹。當再一次解析的時候,檢查它的結果是否生成相同的語法樹。 **練習 7.14:** 定義一個新的滿足Expr接口的具體類型并且提供一個新的操作例如對它運算單元中的最小值的計算。因為Parse函數不會創建這個新類型的實例,為了使用它你可能需要直接構造一個語法樹(或者繼承parser接口)。 **練習 7.15:** 編寫一個從標準輸入中讀取一個單一表達式的程序,用戶及時地提供對于任意變量的值,然后在結果環境變量中計算表達式的值。優雅的處理所有遇到的錯誤。 **練習 7.16:** 編寫一個基于web的計算器程序。 ### 7.10. 類型斷言 類型斷言是一個使用在接口值上的操作。語法上它看起來像x.(T)被稱為斷言類型,這里x表示一個接口的類型和T表示一個類型。一個類型斷言檢查它操作對象的動態類型是否和斷言的類型匹配。 這里有兩種可能。第一種,如果斷言的類型T是一個具體類型,然后類型斷言檢查x的動態類型是否和T相同。如果這個檢查成功了,類型斷言的結果是x的動態值,當然它的類型是T。換句話說,具體類型的類型斷言從它的操作對象中獲得具體的值。如果檢查失敗,接下來這個操作會拋出panic。例如: ~~~ var w io.Writer w = os.Stdout f := w.(*os.File) // success: f == os.Stdout c := w.(*bytes.Buffer) // panic: interface holds *os.File, not *bytes.Buffer ~~~ 第二種,如果相反斷言的類型T是一個接口類型,然后類型斷言檢查是否x的動態類型滿足T。如果這個檢查成功了,動態值沒有獲取到;這個結果仍然是一個有相同類型和值部分的接口值,但是結果有類型T。換句話說,對一個接口類型的類型斷言改變了類型的表述方式,改變了可以獲取的方法集合(通常更大),但是它保護了接口值內部的動態類型和值的部分。 在下面的第一個類型斷言后,w和rw都持有os.Stdout因此它們每個有一個動態類型*os.File,但是變量w是一個io.Writer類型只對外公開出文件的Write方法,然而rw變量也只公開它的Read方法。 ~~~ var w io.Writer w = os.Stdout rw := w.(io.ReadWriter) // success: *os.File has both Read and Write w = new(ByteCounter) rw = w.(io.ReadWriter) // panic: *ByteCounter has no Read method ~~~ 如果斷言操作的對象是一個nil接口值,那么不論被斷言的類型是什么這個類型斷言都會失敗。我們幾乎不需要對一個更少限制性的接口類型(更少的方法集合)做斷言,因為它表現的就像賦值操作一樣,除了對于nil接口值的情況。 ~~~ w = rw // io.ReadWriter is assignable to io.Writer w = rw.(io.Writer) // fails only if rw == nil ~~~ 經常地我們對一個接口值的動態類型是不確定的,并且我們更愿意去檢驗它是否是一些特定的類型。如果類型斷言出現在一個預期有兩個結果的賦值操作中,例如如下的定義,這個操作不會在失敗的時候發生panic但是代替地返回一個額外的第二個結果,這個結果是一個標識成功的布爾值: ~~~ var w io.Writer = os.Stdout f, ok := w.(*os.File) // success: ok, f == os.Stdout b, ok := w.(*bytes.Buffer) // failure: !ok, b == nil ~~~ 第二個結果常規地賦值給一個命名為ok的變量。如果這個操作失敗了,那么ok就是false值,第一個結果等于被斷言類型的零值,在這個例子中就是一個nil的*bytes.Buffer類型。 這個ok結果經常立即用于決定程序下面做什么。if語句的擴展格式讓這個變的很簡潔: ~~~ if f, ok := w.(*os.File); ok { // ...use f... } ~~~ 當類型斷言的操作對象是一個變量,你有時會看見原來的變量名重用而不是聲明一個新的本地變量,這個重用的變量會覆蓋原來的值,如下面這樣: ~~~ if w, ok := w.(*os.File); ok { // ...use w... } ~~~ ### 7.11. 基于類型斷言區別錯誤類型 思考在os包中文件操作返回的錯誤集合。I/O可以因為任何數量的原因失敗,但是有三種經常的錯誤必須進行不同的處理:文件已經存在(對于創建操作),找不到文件(對于讀取操作),和權限拒絕。os包中提供了這三個幫助函數來對給定的錯誤值表示的失敗進行分類: ~~~ package os func IsExist(err error) bool func IsNotExist(err error) bool func IsPermission(err error) bool ~~~ 對這些判斷的一個缺乏經驗的實現可能會去檢查錯誤消息是否包含了特定的子字符串, ~~~ func IsNotExist(err error) bool { // NOTE: not robust! return strings.Contains(err.Error(), "file does not exist") } ~~~ 但是處理I/O錯誤的邏輯可能一個和另一個平臺非常的不同,所以這種方案并不健壯并且對相同的失敗可能會報出各種不同的錯誤消息。在測試的過程中,通過檢查錯誤消息的子字符串來保證特定的函數以期望的方式失敗是非常有用的,但對于線上的代碼是不夠的。 一個更可靠的方式是使用一個專門的類型來描述結構化的錯誤。os包中定義了一個PathError類型來描述在文件路徑操作中涉及到的失敗,像Open或者Delete操作,并且定義了一個叫LinkError的變體來描述涉及到兩個文件路徑的操作,像Symlink和Rename。這下面是os.PathError: ~~~ package os // PathError records an error and the operation and file path that caused it. type PathError struct { Op string Path string Err error } func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() } ~~~ 大多數調用方都不知道PathError并且通過調用錯誤本身的Error方法來統一處理所有的錯誤。盡管PathError的Error方法簡單地把這些字段連接起來生成錯誤消息,PathError的結構保護了內部的錯誤組件。調用方需要使用類型斷言來檢測錯誤的具體類型以便將一種失敗和另一種區分開;具體的類型比字符串可以提供更多的細節。 ~~~ _, err := os.Open("/no/such/file") fmt.Println(err) // "open /no/such/file: No such file or directory" fmt.Printf("%#v\n", err) // Output: // &os.PathError{Op:"open", Path:"/no/such/file", Err:0x2} ~~~ 這就是三個幫助函數是怎么工作的。例如下面展示的IsNotExist,它會報出是否一個錯誤和syscall.ENOENT(§7.8)或者和有名的錯誤os.ErrNotExist相等(可以在§5.4.2中找到io.EOF);或者是一個*PathError,它內部的錯誤是syscall.ENOENT和os.ErrNotExist其中之一。 ~~~ import ( "errors" "syscall" ) var ErrNotExist = errors.New("file does not exist") // IsNotExist returns a boolean indicating whether the error is known to // report that a file or directory does not exist. It is satisfied by // ErrNotExist as well as some syscall errors. func IsNotExist(err error) bool { if pe, ok := err.(*PathError); ok { err = pe.Err } return err == syscall.ENOENT || err == ErrNotExist } ~~~ 下面這里是它的實際使用: ~~~ _, err := os.Open("/no/such/file") fmt.Println(os.IsNotExist(err)) // "true" ~~~ 如果錯誤消息結合成一個更大的字符串,當然PathError的結構就不再為人所知,例如通過一個對fmt.Errorf函數的調用。區別錯誤通常必須在失敗操作后,錯誤傳回調用者前進行。 ### 7.12. 通過類型斷言詢問行為 下面這段邏輯和net/http包中web服務器負責寫入HTTP頭字段(例如:"Content-type:text/html)的部分相似。io.Writer接口類型的變量w代表HTTP響應;寫入它的字節最終被發送到某個人的web瀏覽器上。 ~~~ func writeHeader(w io.Writer, contentType string) error { if _, err := w.Write([]byte("Content-Type: ")); err != nil { return err } if _, err := w.Write([]byte(contentType)); err != nil { return err } // ... } ~~~ 因為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的動態類型滿足這個新接口。 ~~~ // writeString writes s to w. // If w has a WriteString method, it is invoked instead of w.Write. func writeString(w io.Writer, s string) (n int, err error) { type stringWriter interface { WriteString(string) (n int, err error) } if sw, ok := w.(stringWriter); ok { return sw.WriteString(s) // avoid a copy } return w.Write([]byte(s)) // allocate temporary copy } func writeHeader(w io.Writer, contentType string) error { if _, err := writeString(w, "Content-Type: "); err != nil { return err } if _, err := writeString(w, contentType); err != nil { return err } // ... } ~~~ 為了避免重復定義,我們將這個檢查移入到一個實用工具函數writeString中,但是它太有用了以致標準庫將它作為io.WriteString函數提供。這是向一個io.Writer接口寫入字符串的推薦方法。 這個例子的神奇之處在于沒有定義了WriteString方法的標準接口和沒有指定它是一個需要行為的標準接口。而且一個具體類型只會通過它的方法決定它是否滿足stringWriter接口,而不是任何它和這個接口類型表明的關系。它的意思就是上面的技術依賴于一個假設;這個假設就是,如果一個類型滿足下面的這個接口,然后WriteString(s)就方法必須和Write([]byte(s))有相同的效果。 ~~~ 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內部,有一個將單個操作對象轉換成一個字符串的步驟,像下面這樣: ~~~ package fmt func formatOneValue(x interface{}) string { if err, ok := x.(error); ok { return err.Error() } if str, ok := x.(Stringer); ok { return str.String() } // ...all other types... } ~~~ 如果x滿足這個兩個接口類型中的一個,具體滿足的接口決定對值的格式化方式。如果都不滿足,默認的case或多或少會統一地使用反射來處理所有的其它類型;我們可以在第12章知道具體是怎么實現的。 再一次的,它假設任何有String方法的類型滿足fmt.Stringer中約定的行為,這個行為會返回一個適合打印的字符串。 ### 7.13. 類型開關 接口被以兩種不同的方式使用。在第一個方式中,以io.Reader,io.Writer,fmt.Stringer,sort.Interface,http.Handler,和error為典型,一個接口的方法表達了實現這個接口的具體類型間的相思性,但是隱藏了代表的細節和這些具體類型本身的操作。重點在于方法上,而不是具體的類型上。 第二個方式利用一個接口值可以持有各種具體類型值的能力并且將這個接口認為是這些類型的union(聯合)。類型斷言用來動態地區別這些類型并且對每一種情況都不一樣。在這個方式中,重點在于具體的類型滿足這個接口,而不是在于接口的方法(如果它確實有一些的話),并且沒有任何的信息隱藏。我們將以這種方式使用的接口描述為discriminated unions(可辨識聯合)。 如果你熟悉面向對象編程,你可能會將這兩種方式當作是subtype polymorphism(子類型多態)和 ad hoc polymorphism(非參數多態),但是你不需要去記住這些術語。對于本章剩下的部分,我們將會呈現一些第二種方式的例子。 和其它那些語言一樣,Go語言查詢一個SQL數據庫的API會干凈地將查詢中固定的部分和變化的部分分開。一個調用的例子可能看起來像這樣: ~~~ import "database/sql" func listTracks(db sql.DB, artist string, minYear, maxYear int) { result, err := db.Exec( "SELECT * FROM tracks WHERE artist = ? AND ? <= year AND year <= ?", artist, minYear, maxYear) // ... } ~~~ Exec方法使用SQL字面量替換在查詢字符串中的每個'?';SQL字面量表示相應參數的值,它有可能是一個布爾值,一個數字,一個字符串,或者nil空值。用這種方式構造查詢可以幫助避免SQL注入攻擊;這種攻擊就是對手可以通過利用輸入內容中不正確的引文來控制查詢語句。在Exec函數內部,我們可能會找到像下面這樣的一個函數,它會將每一個參數值轉換成它的SQL字面量符號。 ~~~ func sqlQuote(x interface{}) string { if x == nil { return "NULL" } else if _, ok := x.(int); ok { return fmt.Sprintf("%d", x) } else if _, ok := x.(uint); ok { return fmt.Sprintf("%d", x) } else if b, ok := x.(bool); ok { if b { return "TRUE" } return "FALSE" } else if s, ok := x.(string); ok { return sqlQuoteString(s) // (not shown) } else { panic(fmt.Sprintf("unexpected type %T: %v", x, x)) } } ~~~ switch語句可以簡化if-else鏈,如果這個if-else鏈對一連串值做相等測試。一個相似的type switch(類型開關)可以簡化類型斷言的if-else鏈。 在它最簡單的形式中,一個類型開關像普通的switch語句一樣,它的運算對象是x.(type)-它使用了關鍵詞字面量type-并且每個case有一到多個類型。一個類型開關基于這個接口值的動態類型使一個多路分支有效。這個nil的case和if x == nil匹配,并且這個default的case和如果其它case都不匹配的情況匹配。一個對sqlQuote的類型開關可能會有這些case: ~~~ switch x.(type) { case nil: // ... case int, uint: // ... case bool: // ... case string: // ... default: // ... } ~~~ 和(§1.8)中的普通switch語句一樣,每一個case會被順序的進行考慮,并且當一個匹配找到時,這個case中的內容會被執行。當一個或多個case類型是接口時,case的順序就會變得很重要,因為可能會有兩個case同時匹配的情況。default case相對其它case的位置是無所謂的。它不會允許落空發生。 注意到在原來的函數中,對于bool和string情況的邏輯需要通過類型斷言訪問提取的值。因為這個做法很典型,類型開關語句有一個擴展的形式,它可以將提取的值綁定到一個在每個case范圍內的新變量。 ~~~ switch x := x.(type) { /* ... */ } ~~~ 這里我們已經將新的變量也命名為x;和類型斷言一樣,重用變量名是很常見的。和一個switch語句相似地,一個類型開關隱式的創建了一個語言塊,因此新變量x的定義不會和外面塊中的x變量沖突。每一個case也會隱式的創建一個單獨的語言塊。 使用類型開關的擴展形式來重寫sqlQuote函數會讓這個函數更加的清晰: ~~~ func sqlQuote(x interface{}) string { switch x := x.(type) { case nil: return "NULL" case int, uint: return fmt.Sprintf("%d", x) // x has type interface{} here. case bool: if x { return "TRUE" } return "FALSE" case string: return sqlQuoteString(x) // (not shown) default: panic(fmt.Sprintf("unexpected type %T: %v", x, x)) } } ~~~ 在這個版本的函數中,在每個單一類型的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(可識別聯合) ### 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相關的部分: *encoding/xml* ~~~ package xml type Name struct { Local string // e.g., "Title" or "id" } type Attr struct { // e.g., name="value" Name Name Value string } // A Token includes StartElement, EndElement, CharData, // and Comment, plus a few esoteric types (not shown). type Token interface{} type StartElement struct { // e.g., <name> Name Name Attr []Attr } type EndElement struct { Name Name } // e.g., </name> type CharData []byte // e.g., <p>CharData</p> type Comment []byte // e.g., <!-- Comment --> 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,它可以在輸入上一次完成它的工作而從來不要具體化這個文檔樹。 *gopl.io/ch7/xmlselect* ~~~ // Xmlselect prints the text of selected elements of an XML document. package main import ( "encoding/xml" "fmt" "io" "os" "strings" ) func main() { dec := xml.NewDecoder(os.Stdin) var stack []string // stack of element names for { tok, err := dec.Token() if err == io.EOF { break } else if err != nil { fmt.Fprintf(os.Stderr, "xmlselect: %v\n", err) os.Exit(1) } switch tok := tok.(type) { case xml.StartElement: stack = append(stack, tok.Name.Local) // push case xml.EndElement: stack = stack[:len(stack)-1] // pop case xml.CharData: if containsAll(stack, os.Args[1:]) { fmt.Printf("%s: %s\n", strings.Join(stack, " "), tok) } } } } // containsAll reports whether x contains the elements of y, in order. func containsAll(x, y []string) bool { for len(y) <= len(x) { if len(y) == 0 { return true } if x[0] == y[0] { y = y[1:] } x = x[1:] } 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 | ./xmlselect div div h2 html body div div h2: 1 Introduction html body div div h2: 2 Documents html body div div h2: 3 Logical Structures html body div div h2: 4 Physical Structures html body div div h2: 5 Conformance html body div div h2: 6 Notation 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節點表示被命名的元素和它們的屬性。每一個元素節點有一個字節點的切片。 你可能發現下面的定義會對你有幫助。 ~~~ import "encoding/xml" type Node interface{} // CharData or *Element type CharData string type Element struct { Type xml.Name Attr []xml.Attr Children []Node } ~~~ ### 7.15. 一些建議 當設計一個新的包時,新的Go程序員總是通過創建一個接口的集合開始和后面定義滿足它們的具體類型。這種方式的結果就是有很多的接口,它們中的每一個僅只有一個實現。不要再這么做了。這種接口是不必要的抽象;它們也有一個運行時損耗。你可以使用導出機制(§6.6)來限制一個類型的方法或一個結構體的字段是否在包外可見。接口只有當有兩個或兩個以上的具體類型必須以相同的方式進行處理時才需要。 當一個接口只被一個單一的具體類型實現時有一個例外,就是由于它的依賴,這個具體類型不能和這個接口存在在一個相同的包中。這種情況下,一個接口是解耦這兩個包的一個好好方式。 因為在Go語言中只有當兩個或更多的類型實現一個接口時才使用接口,它們必定會從任意特定的實現細節中抽象出來。結果就是有更少和更簡單方法(經常和io.Writer或 fmt.Stringer一樣只有一個)的更小的接口。當新的類型出現時,小的接口更容易滿足。對于接口設計的一個好的標準就是 ask only for what you need(只考慮你需要的東西) 我們完成了對methods和接口的學習過程。Go語言良好的支持面向對象風格的編程,但只不是說你僅僅只能使用它。不是任何事物都需要被當做成一個對象;獨立的函數有它們自己的用處,未封裝的數據類型也是這樣。同時觀察到這兩個,在本書的前五章的例子中沒有調用超過兩打方法,像input.Scan,與之相反的是普遍的函數調用如fmt.Printf。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看