https://medium.com/@gauravsingharoy/build-your-first-api-server-with-httprouter-in-golang-732b7b01f6ab
做者:Gaurav Singha Roy
譯者:oopsguy.comgit
10 個月前,我成爲了一名 Gopher,沒有後悔。像許多其餘 gopher 同樣,我很快發現簡單的語言特性對於快速構建快速、可擴展的軟件很是有用。當我剛開始學習 Go 時,我正在搗鼓不一樣的路由器,它能夠當作 API 服務器使用。若是你跟我同樣有 Rails 背景,你也可能會在構建 Web 框架提供的全部功能方面遇到困難。回到路由器話題,我發現了 3 個是很是有用的好東西: Gorilla mux、httprouter 和 bone(按性能從低到高)。即使 bone 的性能最高且有更簡單的 handler 簽名,但對我來講,它仍然不夠成熟,沒法應用於生產環境中。所以,我最終使用了 httprouter。在本教程中,我將使用 httprouter 構建一個簡單的 REST API 服務器。github
若是你想偷懶,只想獲取源碼,則能夠在這裏[4]直接克隆個人 github 倉庫。golang
讓咱們開始吧。首先建立一個基本端點:json
package main import ( "fmt" "log" "net/http" "github.com/julienschmidt/httprouter" ) func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { fmt.Fprint(w, "Welcome!\n") } func main() { router := httprouter.New() router.GET("/", Index) log.Fatal(http.ListenAndServe(":8080", router)) }
在上面的代碼段中,Index
是一個 handler 函數,須要傳入三個參數。以後,該 handler 將在 main
函數中被註冊到 GET /
路徑上。如今編譯並運行你的程序,轉到 http:// localhost:8080
來查看你的 API 服務器。點擊這裏[1]獲取當前代碼。api
咱們可讓 API 變得複雜一點。如今有一個名爲 Book
的實體,能夠把 ISDN
字段做爲惟一標識。讓咱們建立更多的 handler,即分表表明着 Index 和 Show 動做的 GET /books
和 GET /books/:isdn
。main.go
文件此時以下:服務器
package main import ( "encoding/json" "fmt" "log" "net/http" "github.com/julienschmidt/httprouter" ) func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { fmt.Fprint(w, "Welcome!\n") } type Book struct { // The main identifier for the Book. This will be unique. ISDN string `json:"isdn"` Title string `json:"title"` Author string `json:"author"` Pages int `json:"pages"` } type JsonResponse struct { // Reserved field to add some meta information to the API response Meta interface{} `json:"meta"` Data interface{} `json:"data"` } type JsonErrorResponse struct { Error *ApiError `json:"error"` } type ApiError struct { Status int16 `json:"status"` Title string `json:"title"` } // A map to store the books with the ISDN as the key // This acts as the storage in lieu of an actual database var bookstore = make(map[string]*Book) // Handler for the books index action // GET /books func BookIndex(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { books := []*Book{} for _, book := range bookstore { books = append(books, book) } response := &JsonResponse{Data: &books} w.Header().Set("Content-Type", "application/json; charset=UTF-8") w.WriteHeader(http.StatusOK) if err := json.NewEncoder(w).Encode(response); err != nil { panic(err) } } // Handler for the books Show action // GET /books/:isdn func BookShow(w http.ResponseWriter, r *http.Request, params httprouter.Params) { isdn := params.ByName("isdn") book, ok := bookstore[isdn] w.Header().Set("Content-Type", "application/json; charset=UTF-8") if !ok { // No book with the isdn in the url has been found w.WriteHeader(http.StatusNotFound) response := JsonErrorResponse{Error: &ApiError{Status: 404, Title: "Record Not Found"}} if err := json.NewEncoder(w).Encode(response); err != nil { panic(err) } } response := JsonResponse{Data: book} if err := json.NewEncoder(w).Encode(response); err != nil { panic(err) } } func main() { router := httprouter.New() router.GET("/", Index) router.GET("/books", BookIndex) router.GET("/books/:isdn", BookShow) // Create a couple of sample Book entries bookstore["123"] = &Book{ ISDN: "123", Title: "Silence of the Lambs", Author: "Thomas Harris", Pages: 367, } bookstore["124"] = &Book{ ISDN: "124", Title: "To Kill a Mocking Bird", Author: "Harper Lee", Pages: 320, } log.Fatal(http.ListenAndServe(":8080", router)) }
你若是如今嘗試請求 GET https:// localhost:8080/books
,將獲得如下響應:app
{ "meta": null, "data": [ { "isdn": "123", "title": "Silence of the Lambs", "author": "Thomas Harris", "pages": 367 }, { "isdn": "124", "title": "To Kill a Mocking Bird", "author": "Harper Lee", "pages": 320 } ] }
咱們在 main
函數中硬編碼了這兩個 book 實體。點擊這裏[2]獲取當前代碼。框架
讓咱們來重構一下代碼。到目前爲止,全部的代碼都放置在同一個文件中:main.go
。咱們能夠把它們劃分移到不一樣的文件中。此時咱們有一個目錄:ide
. ├── handlers.go ├── main.go ├── models.go └── responses.go
咱們把全部與 JSON
響應相關的結構體移動到 responses.go
,將 handler 函數移動到 Handlers.go
,將 Book
結構體移動到 models.go
。點擊這裏[3]查看當前代碼。如今,咱們跳過來寫一些測試。在 Go 中,*_test.go
文件是用做測試用途。所以讓咱們建立一個 handlers_test.go
。函數
package main import ( "net/http" "net/http/httptest" "testing" "github.com/julienschmidt/httprouter" ) func TestBookIndex(t *testing.T) { // Create an entry of the book to the bookstore map testBook := &Book{ ISDN: "111", Title: "test title", Author: "test author", Pages: 42, } bookstore["111"] = testBook // A request with an existing isdn req1, err := http.NewRequest("GET", "/books", nil) if err != nil { t.Fatal(err) } rr1 := newRequestRecorder(req1, "GET", "/books", BookIndex) if rr1.Code != 200 { t.Error("Expected response code to be 200") } // expected response er1 := "{\"meta\":null,\"data\":[{\"isdn\":\"111\",\"title\":\"test title\",\"author\":\"test author\",\"pages\":42}]}\n" if rr1.Body.String() != er1 { t.Error("Response body does not match") } } // Mocks a handler and returns a httptest.ResponseRecorder func newRequestRecorder(req *http.Request, method string, strPath string, fnHandler func(w http.ResponseWriter, r *http.Request, param httprouter.Params)) *httptest.ResponseRecorder { router := httprouter.New() router.Handle(method, strPath, fnHandler) // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. rr := httptest.NewRecorder() // Our handlers satisfy http.Handler, so we can call their ServeHTTP method // directly and pass in our Request and ResponseRecorder. router.ServeHTTP(rr, req) return rr }
咱們使用 httptest
包的 Recorder 來 mock handler。一樣,你也能夠爲 handler BookShow
編寫測試用例。
讓咱們稍微作些重構。咱們仍然把全部路由都定義在了 main
函數中,handler 看起來有點臃腫,須要作點 DRY。咱們仍然在終端中輸出一些日誌消息,而且能夠添加一個 BookCreate
handler 來建立一個新的 Book。
首先先解決 handlers.go
。
package main import ( "encoding/json" "fmt" "io" "io/ioutil" "net/http" "github.com/julienschmidt/httprouter" ) func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { fmt.Fprint(w, "Welcome!\n") } // Handler for the books Create action // POST /books func BookCreate(w http.ResponseWriter, r *http.Request, params httprouter.Params) { book := &Book{} if err := populateModelFromHandler(w, r, params, book); err != nil { writeErrorResponse(w, http.StatusUnprocessableEntity, "Unprocessible Entity") return } bookstore[book.ISDN] = book writeOKResponse(w, book) } // Handler for the books index action // GET /books func BookIndex(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { books := []*Book{} for _, book := range bookstore { books = append(books, book) } writeOKResponse(w, books) } // Handler for the books Show action // GET /books/:isdn func BookShow(w http.ResponseWriter, r *http.Request, params httprouter.Params) { isdn := params.ByName("isdn") book, ok := bookstore[isdn] if !ok { // No book with the isdn in the url has been found writeErrorResponse(w, http.StatusNotFound, "Record Not Found") return } writeOKResponse(w, book) } // Writes the response as a standard JSON response with StatusOK func writeOKResponse(w http.ResponseWriter, m interface{}) { w.Header().Set("Content-Type", "application/json; charset=UTF-8") w.WriteHeader(http.StatusOK) if err := json.NewEncoder(w).Encode(&JsonResponse{Data: m}); err != nil { writeErrorResponse(w, http.StatusInternalServerError, "Internal Server Error") } } // Writes the error response as a Standard API JSON response with a response code func writeErrorResponse(w http.ResponseWriter, errorCode int, errorMsg string) { w.Header().Set("Content-Type", "application/json; charset=UTF-8") w.WriteHeader(errorCode) json. NewEncoder(w). Encode(&JsonErrorResponse{Error: &ApiError{Status: errorCode, Title: errorMsg}}) } //Populates a model from the params in the Handler func populateModelFromHandler(w http.ResponseWriter, r *http.Request, params httprouter.Params, model interface{}) error { body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1048576)) if err != nil { return err } if err := r.Body.Close(); err != nil { return err } if err := json.Unmarshal(body, model); err != nil { return err } return nil }
我建立了兩個函數,writeOKResponse
用於將 StatusOK
寫入響應,其返回一個 model 或一個 model slice,writeErrorResponse
將在發生預期或意外錯誤時將 JSON
錯誤做爲響應。像任何一個優秀的 gopher 同樣,咱們不該該 panic。我還添加了一個名爲 populateModelFromHandler
的函數,它將內容從 body 中解析成所需的 model(struct)。在這種狀況下,咱們在 BookCreate
handler 中使用它來填充一個 Book
。
如今來看看日誌。咱們簡單地建立一個 Logger
函數,它包裝了 handler 函數,並在執行 handler 函數以前和以後打印日誌消息。
package main import ( "log" "net/http" "time" "github.com/julienschmidt/httprouter" ) // A Logger function which simply wraps the handler function around some log messages func Logger(fn func(w http.ResponseWriter, r *http.Request, param httprouter.Params)) func(w http.ResponseWriter, r *http.Request, param httprouter.Params) { return func(w http.ResponseWriter, r *http.Request, param httprouter.Params) { start := time.Now() log.Printf("%s %s", r.Method, r.URL.Path) fn(w, r, param) log.Printf("Done in %v (%s %s)", time.Since(start), r.Method, r.URL.Path) } }
以後來看看路由。首先,在統一一個地方定義全部路由,好比 routes.go
。
package main import "github.com/julienschmidt/httprouter" /* Define all the routes here. A new Route entry passed to the routes slice will be automatically translated to a handler with the NewRouter() function */ type Route struct { Name string Method string Path string HandlerFunc httprouter.Handle } type Routes []Route func AllRoutes() Routes { routes := Routes{ Route{"Index", "GET", "/", Index}, Route{"BookIndex", "GET", "/books", BookIndex}, Route{"Bookshow", "GET", "/books/:isdn", BookShow}, Route{"Bookshow", "POST", "/books", BookCreate}, } return routes }
讓咱們建立了一個 NewRouter
函數,它能夠在 main
函數中調用,它讀取上面定義的全部路由,並返回一個可用的 httprouter.Router
。所以建立一個文件 router.go
。咱們還將使用新建立的 Logger
函數來包裝 handler。
package main import "github.com/julienschmidt/httprouter" //Reads from the routes slice to translate the values to httprouter.Handle func NewRouter(routes Routes) *httprouter.Router { router := httprouter.New() for _, route := range routes { var handle httprouter.Handle handle = route.HandlerFunc handle = Logger(handle) router.Handle(route.Method, route.Path, handle) } return router }
你的目錄此時應該像這樣:
. ├── handlers.go ├── handlers_test.go ├── logger.go ├── main.go ├── models.go ├── responses.go ├── router.go └── routes.go
在這裏[4]查看完整代碼。
到這裏,這些代碼應該能夠幫助你開始編寫本身的 API 服務器了。固然,你須要把你的功能放在不一樣的包中,如下目錄結構是一個參照:
. ├── LICENSE ├── README.md ├── handlers │ ├── books_test.go │ └── books.go ├── models │ ├── book.go │ └── * ├── store │ ├── * └── lib | ├── * ├── main.go ├── router.go ├── rotes.go
若是你有一個大的單體服務,你還能夠將 handlers
、models
和全部路由功能都放在另外一個名爲 app
的包中。你只要記住,go 不像 Java 或 Scala 那樣能夠有循環的包引用。所以你必須格外注意你的包結構。
這就是所有內容,但願本教程能幫助到你。🍻