在之前我寫過一篇關於通過使用http.HandlerFunc
來實現一個定制handler類型用來避免一些平常的錯誤的文章。func MyHandler(w http.ResponseWriter, r *http.Request)
的簽名經常可以看到。這是一個有用的通用的包含一些基本功能的handler類型,但是和其他事情一樣,也有一些不足:
- 當你想要在一個handler中停止處理的時候,必須記得顯示的調用一個return。這個在當你想要跑出一個從定向(301、302),未找到(404)或者服務器端錯誤(500)的狀態的時候是很平常的。如果不這么做可能會引起一些微妙的錯誤(函數會繼續執行),因為函數不需要一個返回值,編譯器也不會警告你。
- 不容易傳遞額外的參數(例如,數據庫連接池,配置)。你最后不得不實用一系列的全局變量(不算太壞,但是跟蹤他們會導致難以擴展)或者將他們存到請求上下文中,然后每次都從其取出。這樣做很笨重。
- 一直在不斷的重復同樣的語句。想要記錄數據庫包返回的錯誤?既可以再每個查詢方法中調用
log.Printf
,也可以再每個handler中返回錯誤。如果你的handler可以返回給一個集中記錄錯誤的函數,並且跑出一個500的錯誤就更好了。
我以前的方法中使用了func(http.ResponseWriter, *http.Request)
簽名。這已經被證明是一個簡介的方式,但是有個奇怪的地方是,返回一個無錯誤的狀態,例如,200,302,303往往是多余的,因為要么你已經在其他地方設置了,要么就是沒用的。例如:
func SomeHandler(w http.ResponseWriter, r *http.Request) (int, error) { db, err := someDBcall() if err != nil { // This makes sense. return 500, err } if user.LoggedIn { http.Redirect(w, r, "/dashboard", 302) // Superfluous! Our http.Redirect function handles the 302, not // our return value (which is effectively ignored). return 302, nil } }
看起來還行,但是我們可以做的更好
一些區別
那么我們應該如何改進它?我們先列出代碼:
package handler // Error represents a handler error. It provides methods for a HTTP status // code and embeds the built-in error interface. type Error interface { error Status() int } // StatusError represents an error with an associated HTTP status code. type StatusError struct { Code int Err error } // Allows StatusError to satisfy the error interface. func (se StatusError) Error() string { return se.Err.Error() } // Returns our HTTP status code. func (se StatusError) Status() int { return se.Code } // A (simple) example of our application-wide configuration. type Env struct { DB *sql.DB Port string Host string } // The Handler struct that takes a configured Env and a function matching // our useful signature. type Handler struct { *Env H func(e *Env, w http.ResponseWriter, r *http.Request) error } // ServeHTTP allows our Handler type to satisfy http.Handler. func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { err := h.H(h.Env, w, r) if err != nil { switch e := err.(type) { case Error: // We can retrieve the status here and write out a specific // HTTP status code. log.Printf("HTTP %d - %s", e.Status(), e) http.Error(w, e.Error(), e.Status()) default: // Any error types we don't specifically look out for default // to serving a HTTP 500 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } } }
上面的代碼不言自明,但是要說明一下一些突出的觀點:
- 我們自定義了一個
Error
類型(接口),他內嵌了Go的內建的error接口,同時提供了一個Status() int
方法。 - 我們提供了一個簡單的
StatusError
類型(結構體),它滿足handler.Error
的接口。StatusError接受一個HTTP的狀態碼(int類型),一個可以讓我們包裝錯誤用來記錄或者查詢的error類型。 - 我們的
ServeHTTP
方法包好了一個”e := err.(type)”的類型斷言,它可以測試我們需要處理的錯誤,允許我們處理那些特別的錯誤。在這個例子中,他是只是一個handler.Error
類型。其他的錯誤,例如其他包中的錯誤想net.Error,或者其他我們定義的額外的錯誤,如果想要檢查,同樣也可以檢查。
如果我們不想捕捉那些錯誤,那么default
將會默認捕捉到。記住一點,ServeHTTP
可以使我們的Handler類型滿足http.Handler接口,這樣他就可以在任何使用http.Handler的地方使用了,例如Go的net/http包或者所有的其他的第三方框架。這樣使得定制的handler更有用,他們用起來很靈活。
注意 net 包處理事情很簡單。它又一個net.Error的接口,內嵌了內建的error接口。一些具體的類型實現了它。函數返回的具體類型跟錯誤的類型相同(DNS錯誤,解析錯誤等)。再datastore 包中定義的DBError有一個Query() string 方法,可以很好的解釋。
所有示例
它最后是什么樣子的?我們是否可以將其分到不同的包中?
package handler import ( "net/http" ) // Error represents a handler error. It provides methods for a HTTP status // code and embeds the built-in error interface. type Error interface { error Status() int } // StatusError represents an error with an associated HTTP status code. type StatusError struct { Code int Err error } // Allows StatusError to satisfy the error interface. func (se StatusError) Error() string { return se.Err.Error() } // Returns our HTTP status code. func (se StatusError) Status() int { return se.Code } // A (simple) example of our application-wide configuration. type Env struct { DB *sql.DB Port string Host string } // The Handler struct that takes a configured Env and a function matching // our useful signature. type Handler struct { *Env H func(e *Env, w http.ResponseWriter, r *http.Request) error } // ServeHTTP allows our Handler type to satisfy http.Handler. func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { err := h.H(h.Env, w, r) if err != nil { switch e := err.(type) { case Error: // We can retrieve the status here and write out a specific // HTTP status code. log.Printf("HTTP %d - %s", e.Status(), e) http.Error(w, e.Error(), e.Status()) default: // Any error types we don't specifically look out for default // to serving a HTTP 500 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } } } func GetIndex(env *Env, w http.ResponseWriter, r *http.Request) error { users, err := env.DB.GetAllUsers() if err != nil { // We return a status error here, which conveniently wraps the error // returned from our DB queries. We can clearly define which errors // are worth raising a HTTP 500 over vs. which might just be a HTTP // 404, 403 or 401 (as appropriate). It's also clear where our // handler should stop processing by returning early. return StatusError{500, err} } fmt.Fprintf(w, "%+v", users) return nil }
package main import ( "net/http" "github.com/you/somepkg/handler" ) func main() { db, err := sql.Open("connectionstringhere") if err != nil { log.Fatal(err) } // Initialise our app-wide environment with the services/info we need. env := &handler.Env{ DB: db, Port: os.Getenv("PORT"), Host: os.Getenv("HOST"), // We might also have a custom log.Logger, our // template instance, and a config struct as fields // in our Env struct. } // Note that we're using http.Handle, not http.HandleFunc. The // latter only accepts the http.HandlerFunc type, which is not // what we have here. http.Handle("/", handler.Handler{env, handler.GetIndex}) // Logs the error if ListenAndServe fails. log.Fatal(http.ListenAndServe(":8000", nil)) }