Merge pull request #220 from foreversmart/master

ch7-13,ch7-14,ch7-15 done

Fixes #36 
Fixes #37 
Fixes #38
pull/1/head
chai2010 2016-01-20 00:05:58 +08:00
commit 277c9d3c02
3 changed files with 208 additions and 6 deletions

View File

@ -1,3 +1,82 @@
## 7.13. 類型分支
TODO
## 7.13. 類型開關
接口被以兩種不同的方式使用。在第一個方式中以io.Readerio.Writerfmt.Stringersort.Interfacehttp.Handler和error爲典型一個接口的方法表達了實現這個接口的具體類型間的相思性但是隱藏了代表的細節和這些具體類型本身的操作。重點在於方法上而不是具體的類型上。
第二個方式利用一個接口值可以持有各種具體類型值的能力併且將這個接口認爲是這些類型的union聯合。類型斷言用來動態地區别這些類型併且對每一種情況都不一樣。在這個方式中重點在於具體的類型滿足這個接口而不是在於接口的方法如果它確實有一些的話併且沒有任何的信息隱藏。我們將以這種方式使用的接口描述爲discriminated unions可辨識聯合
如果你熟悉面向對象編程你可能會將這兩種方式當作是subtype polymorphism子類型多態和 ad hoc polymorphism非參數多態但是你不需要去記住這些術語。對於本章剩下的部分我們將會呈現一些第二種方式的例子。
和其它那些語言一樣Go語言査詢一個SQL數據庫的API會榦淨地將査詢中固定的部分和變化的部分分開。一個調用的例子可能看起來像這樣
```go
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字面量符號。
```go
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
```go
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范圍內的新變量。
```go
switch x := x.(type) { /* ... */ }
```
這里我們已經將新的變量也命名爲x和類型斷言一樣重用變量名是很常見的。和一個switch語句相似地一個類型開關隱式的創建了一個語言塊因此新變量x的定義不會和外面塊中的x變量衝突。每一個case也會隱式的創建一個單獨的語言塊。
使用類型開關的擴展形式來重寫sqlQuote函數會讓這個函數更加的清晰
```go
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{}但是我們把它認爲是一個intuintboolstring和nil值的discriminated union可識别聯合

View File

@ -1,3 +1,121 @@
## 7.14. 示例: 基於標記的XML解碼
第4.5章節展示了如何使用encoding/json包中的Marshal和Unmarshal函數來將JSON文檔轉換成Go語言的數據結構。encoding/xml包提供了一個相似的API。當我們想構造一個文檔樹的表示時使用encoding/xml包會很方便但是對於很多程序併不是必須的。encoding/xml包也提供了一個更低層的基於標記的API用於XML解碼。在基於標記的樣式中解析器消費輸入和産生一個標記流四個主要的標記類型StartElementEndElementCharData和Comment每一個都是encoding/xml包中的具體類型。每一個對(\*xml.Decoder).Token的調用都返迴一個標記。
TODO
這里顯示的是和這個API相關的部分
```go
// 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它可以在輸入上一次完成它的工作而從來不要具體化這個文檔樹。
```go
// 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樣式上屬性進行選擇例如一個像這樣<div id="page" class="wide">的元素可以通過匹配id或者class同時還有它的名稱來進行選擇。
練習7.18使用基於標記的解碼API編寫一個可以讀取任意XML文檔和構造這個文檔所代表的普通節點樹的程序。節點有兩種類型CharData節點表示文本字符串和 Element節點表示被命名的元素和它們的屬性。每一個元素節點有一個字節點的切片。
你可能發現下面的定義會對你有幫助。
```go
import "encoding/xml"
type Node interface{} // CharData or *Element
type CharData string
type Element struct {
Type xml.Name
Attr []xml.Attr
Children []Node
}
```

View File

@ -1,3 +1,8 @@
## 7.15. 補充幾點
## 7.15. 一些建議
當設計一個新的包時新的Go程序員總是通過創建一個接口的集合開始和後面定義滿足它們的具體類型。這種方式的結果就是有很多的接口它們中的每一個僅隻有一個實現。不要再這麽做了。這種接口是不必要的抽象它們也有一個運行時損耗。你可以使用導出機製(§6.6)來限製一個類型的方法或一個結構體的字段是否在包外可見。接口隻有當有兩個或兩個以上的具體類型必須以相同的方式進行處理時才需要。
TODO
當一個接口隻被一個單一的具體類型實現時有一個例外,就是由於它的依賴,這個具體類型不能和這個接口存在在一個相同的包中。這種情況下,一個接口是解耦這兩個包的一個好好方式。
因爲在Go語言中隻有當兩個或更多的類型實現一個接口時才使用接口它們必定會從任意特定的實現細節中抽象出來。結果就是有更少和更簡單方法經常和io.Writer或 fmt.Stringer一樣隻有一個的更小的接口。當新的類型出現時小的接口更容易滿足。對於接口設計的一個好的標準就是 ask only for what you need隻考慮你需要的東西
我們完成了對methods和接口的學習過程。Go語言良好的支持面向對象風格的編程但隻不是説你僅僅隻能使用它。不是任何事物都需要被當做成一個對象獨立的函數有它們自己的用處未封裝的數據類型也是這樣。同時觀察到這兩個在本書的前五章的例子中沒有調用超過兩打方法像input.Scan與之相反的是普遍的函數調用如fmt.Printf。