diff --git a/ch7/ch7-07.md b/ch7/ch7-07.md index 5d38dad..a21d5ff 100644 --- a/ch7/ch7-07.md +++ b/ch7/ch7-07.md @@ -1,5 +1,7 @@ ## 7.7. http.Handler接口 + 在第一章中,我們粗略的了解了怎麽用net/http包去實現網絡客戶端(§1.5)和服務器(§1.7)。在這個小節中,我們會對那些基於http.Handler接口的服務器API做更進一步的學習: + ```go // net/http package http @@ -10,9 +12,11 @@ type Handler interface { func ListenAndServe(address string, h Handler) error ``` + ListenAndServe函數需要一個例如“localhost:8000”的服務器地址,和一個所有請求都可以分派的Handler接口實例。它會一直運行,直到這個服務因爲一個錯誤而失敗(或者啟動失敗),它的返迴值一定是一個非空的錯誤。 想象一個電子商務網站,爲了銷售它的數據庫將它物品的價格映射成美元。下面這個程序可能是能想到的最簡單的實現了。它將庫存清單模型化爲一個命名爲database的map類型,我們給這個類型一個ServeHttp方法,這樣它可以滿足http.Handler接口。這個handler會遍歷整個map併輸出物品信息。 + ```go // gopl.io/ch7/http1 func main() { @@ -32,19 +36,25 @@ func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) { } } ``` + 如果我們啟動這個服務, + ``` $ 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來指定一個請求參數。 + ```go // gopl.io/ch7/http2 func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) { @@ -68,14 +78,18 @@ func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) { } } ``` + 現在handler基於URL的路徑部分(req.URL.Path)來決定執行什麽邏輯。如果這個handler不能識别這個路徑,它會通過調用w.WriteHeader(http.StatusNotFound)返迴客戶端一個HTTP錯誤;這個檢査應該在向w寫入任何值前完成。(順便提一下,http.ResponseWriter是另一個接口。它在io.Writer上增加了發送HTTP相應頭的方法。)等效地,我們可以使用實用的http.Error函數: + ```go 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 @@ -92,12 +106,14 @@ 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。 + ```go // gopl.io/ch7/http3 func main() { @@ -127,13 +143,17 @@ func (db database) price(w http.ResponseWriter, req *http.Request) { fmt.Fprintf(w, "%s\n", price) } ``` + 讓我們關註這兩個註冊到handlers上的調用。第一個db.list是一個方法值 (§6.4),它是下面這個類型的值 + ```go 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是一個類型。它有如下的定義: + ```go // net/http package http @@ -144,19 +164,23 @@ 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註冊代碼成這樣: + ```go // 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值就可以工作。 然後服務器的主函數可以簡化成: + ```go // gopl.io/ch7/http4 func main() { @@ -166,6 +190,7 @@ func main() { log.Fatal(http.ListenAndServe("localhost:8000", nil)) } ``` + 最後,一個重要的提示:就像我們在1.7節中提到的,web服務器在一個新的協程中調用每一個handler,所以當handler獲取其它協程或者這個handler本身的其它請求也可以訪問的變量時一定要使用預防措施比如鎖機製。我們後面的兩章中講到併發相關的知識。 練習 7.11:增加額外的handler讓客服端可以創建,讀取,更新和刪除數據庫記録。例如,一個形如/update?item=socks&price=6的請求會更新庫存清單里一個貨品的價格併且當這個貨品不存在或價格無效時返迴一個錯誤值。(註意:這個脩改會引入變量同時更新的問題) diff --git a/ch7/ch7-08.md b/ch7/ch7-08.md index 5827e79..0971ab2 100644 --- a/ch7/ch7-08.md +++ b/ch7/ch7-08.md @@ -1,11 +1,15 @@ ## 7.8. error接口 + 從本書的開始,我們就已經創建和使用過神祕的預定義error類型,而且沒有解釋它究竟是什麽。實際上它就是interface類型,這個類型有一個返迴錯誤信息的單一方法: + ```go type error interface { Error() string } ``` + 創建一個error最簡單的方法就是調用errors.New函數,它會根據傳入的錯誤信息返迴一個新的error。整個errors包僅隻有4行: + ```go package errors @@ -15,11 +19,15 @@ type errorString struct { text string } func (e *errorString) Error() string { return e.text } ``` + 承載errorString的類型是一個結構體而非一個字符串,這是爲了保護它表示的錯誤避免粗心(或有意)的更新。併且因爲是指針類型*errorString滿足error接口而非errorString類型,所以每個New函數的調用都分配了一個獨特的和其他錯誤不相同的實例。我們也不想要重要的error例如io.EOF和一個剛好有相同錯誤消息的error比較後相等。 + ```go fmt.Println(errors.New("EOF") == errors.New("EOF")) // "false" ``` + 調用errors.New函數是非常稀少的,因爲有一個方便的封裝函數fmt.Errorf,它還會處理字符串格式化。我們曾多次在第5章中用到它。 + ```go package fmt @@ -29,7 +37,9 @@ func Errorf(format string, args ...interface{}) error { return errors.New(Sprintf(format, args...)) } ``` + 雖然*errorString可能是最簡單的錯誤類型,但遠非隻有它一個。例如,syscall包提供了Go語言底層繫統調用API。在多個平台上,它定義一個實現error接口的數字類型Errno,併且在Unix平台上,Errno的Error方法會從一個字符串表中査找錯誤消息,如下面展示的這樣: + ```go package syscall @@ -49,12 +59,15 @@ func (e Errno) Error() string { return fmt.Sprintf("errno %d", e) } ``` + 下面的語句創建了一個持有Errno值爲2的接口值,表示POSIX ENOENT狀況: + ```go 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中。 ![](../images/ch7-06.png) diff --git a/ch7/ch7-09.md b/ch7/ch7-09.md index 73de2df..939b159 100644 --- a/ch7/ch7-09.md +++ b/ch7/ch7-09.md @@ -1,16 +1,22 @@ ## 7.9. 示例: 表達式求值 + 在本節中,我們會構建一個簡單算術表達式的求值器。我們將使用一個接口Expr來表示Go語言中任意的表達式。現在這個接口不需要有方法,但是我們後面會爲它增加一些。 + ```go // An Expr is an arithmetic expression. type Expr interface{} ``` + 我們的表達式語言由浮點數符號(小數點);二元操作符+,-,\*, 和/;一元操作符-x和+x;調用pow(x,y),sin(x),和sqrt(x)的函數;例如x和pi的變量;當然也有括號和標準的優先級運算符。所有的值都是float64類型。這下面是一些表達式的例子: + ```go sqrt(A / pi) pow(x, 3) + pow(y, 3) (F - 32) * 5 / 9 ``` + 下面的五個具體類型表示了具體的表達式類型。Var類型表示對一個變量的引用。(我們很快會知道爲什麽它可以被輸出。)literal類型表示一個浮點型常量。unary和binary類型表示有一到兩個運算對象的運算符表達式,這些操作數可以是任意的Expr類型。call類型表示對一個函數的調用;我們限製它的fn字段隻能是pow,sin或者sqrt。 + ```go // gopl.io/ch7/eval // A Var identifies a variable, e.g., x. @@ -37,18 +43,24 @@ type call struct { args []Expr } ``` + 爲了計算一個包含變量的表達式,我們需要一個environment變量將變量的名字映射成對應的值: + ```go type Env map[Var]float64 ``` + 我們也需要每個表示式去定義一個Eval方法,這個方法會根據給定的environment變量返迴表達式的值。因爲每個表達式都必鬚提供這個方法,我們將它加入到Expr接口中。這個包隻會對外公開Expr,Env,和Var類型。調用方不需要獲取其它的表達式類型就可以使用這個求值器。 + ```go type Expr interface { // Eval returns the value of this Expr in the environment env. Eval(env Env) float64 } ``` + 下面給大家展示一個具體的Eval方法。Var類型的這個方法對一個environment變量進行査找,如果這個變量沒有在environment中定義過這個方法會返迴一個零值,literal類型的這個方法簡單的返迴它眞實的值。 + ```go func (v Var) Eval(env Env) float64 { return env[v] @@ -58,7 +70,9 @@ func (l literal) Eval(_ Env) float64 { return float64(l) } ``` + unary和binary的Eval方法會遞歸的計算它的運算對象,然後將運算符op作用到它們上。我們不將被零或無窮數除作爲一個錯誤,因爲它們都會産生一個固定的結果無限。最後,call的這個方法會計算對於pow,sin,或者sqrt函數的參數值,然後調用對應在math包中的函數。 + ```go func (u unary) Eval(env Env) float64 { switch u.op { @@ -96,9 +110,11 @@ func (c call) Eval(env Env) float64 { 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轉換成攝氏度。 + ```go func TestEval(t *testing.T) { tests := []struct { @@ -134,13 +150,17 @@ func TestEval(t *testing.T) { } } ``` + 對於表格中的每一條記録,這個測試會解析它的表達式然後在環境變量中計算它,輸出結果。這里我們沒有空間來展示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 @@ -154,9 +174,11 @@ pow(x, 3) + pow(y, 3) map[F:32] => 0 map[F:212] => 100 ``` + 幸運的是目前爲止所有的輸入都是適合的格式,但是我們的運氣不可能一直都有。甚至在解釋型語言中,爲了靜態錯誤檢査語法是非常常見的;靜態錯誤就是不用運行程序就可以檢測出來的錯誤。通過將靜態檢査和動態的部分分開,我們可以快速的檢査錯誤併且對於多次檢査隻執行一次而不是每次表達式計算的時候都進行檢査。 讓我們往Expr接口中增加另一個方法。Check方法在一個表達式語義樹檢査出靜態錯誤。我們馬上會説明它的vars參數。 + ```go type Expr interface { Eval(env Env) float64 @@ -164,7 +186,9 @@ type Expr interface { Check(vars map[Var]bool) error } ``` + 具體的Check方法展示在下面。literal和Var類型的計算不可能失敗,所以這些類型的Check方法會返迴一個nil值。對於unary和binary的Check方法會首先檢査操作符是否有效,然後遞歸的檢査運算單元。相似地對於call的這個方法首先檢査調用的函數是否已知併且有沒有正確個數的參數,然後遞歸的檢査每一個參數。 + ```go func (v Var) Check(vars map[Var]bool) error { vars[v] = true @@ -211,7 +235,9 @@ func (c call) Check(vars map[Var]bool) error { var numParams = map[string]int{"pow": 2, "sin": 1, "sqrt": 1} ``` + 我們在兩個組中有選擇地列出有問題的輸入和它們得出的錯誤。Parse函數(這里沒有出現)會報出一個語法錯誤和Check函數會報出語義錯誤。 + ``` x % 2 unexpected '%' math.Pi unexpected '.' @@ -221,11 +247,13 @@ math.Pi 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函數混合了解析和檢査步驟的過程: + ```go // gopl.io/ch7/surface import "gopl.io/ch7/eval" @@ -250,7 +278,9 @@ func parseAndCheck(s string) (eval.Expr, error) { return expr, nil } ``` + 爲了編寫這個web應用,所有我們需要做的就是下面這個plot函數,這個函數有和http.HandlerFunc相似的籤名: + ```go func plot(w http.ResponseWriter, r *http.Request) { r.ParseForm() diff --git a/ch9/ch9-02.md b/ch9/ch9-02.md index 2471040..bbddda1 100644 --- a/ch9/ch9-02.md +++ b/ch9/ch9-02.md @@ -56,7 +56,6 @@ func Balance() int { 由於在存款和査詢餘額函數中的臨界區代碼這麽短--隻有一行,沒有分支調用--在代碼最後去調用Unlock就顯得更爲直截了當。在更複雜的臨界區的應用中,尤其是必鬚要盡早處理錯誤併返迴的情況下,就很難去(靠人)判斷對Lock和Unlock的調用是在所有路徑中都能夠嚴格配對的了。Go語言里的defer簡直就是這種情況下的救星:我們用defer來調用Unlock,臨界區會隱式地延伸到函數作用域的最後,這樣我們就從“總要記得在函數返迴之後或者發生錯誤返迴時要記得調用一次Unlock”這種狀態中獲得了解放。Go會自動幫我們完成這些事情。 - ```go func Balance() int { mu.Lock() @@ -135,9 +134,6 @@ func Balance() int { func deposit(amount int) { balance += amount } ``` - 當然,這里的存款deposit函數很小實際上取款withdraw函數不需要理會對它的調用,盡管如此,這里的表達還是表明了規則。 封裝(§6.6), 用限製一個程序中的意外交互的方式,可以使我們獲得數據結構的不變性。因爲某種原因,封裝還幫我們獲得了併發的不變性。當你使用mutex時,確保mutex和其保護的變量沒有被導出(在go里也就是小寫,且不要被大寫字母開頭的函數訪問啦),無論這些變量是包級的變量還是一個struct的字段。 - -