Golang:使用 httprouter 構建 API 伺服器
作者:Gaurav Singha Roy
譯者:oopsguy.com
我 10 個月前開始成為一名 Gopher,沒有回頭。像許多其他 gopher 一樣,我很快發現簡單的語言特性對於快速構建快速、可擴充套件的軟體非常有用。當我剛開始學習 Go 時,我正在玩不同的多路複用器(multiplexer),它可以作為 API 伺服器使用。如果您像我一樣有 Rails 背景,你可能也會在構建 Web 框架提供的所有功能方面遇到困難。回到多路複用器,我發現了 3 個是非常有用的好東西,即 、 和 (按效能從低到高排列)。即使 bone 有最佳效能和更簡單的 handler 簽名,但對於我來說,它仍然不夠成熟,無法用於生產環境。因此,我最終使用了 httprouter。在本教程中,我將使用 httprouter 構建一個簡單的 REST API 伺服器。
如果你想偷懶,只想獲取原始碼,你可以在[4]直接檢出我的 github 倉庫。
讓我們開始吧。首先建立一個基本端點:
package mainimport ( "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 變得複雜一點。我們現在有一個名為 Book
的實體,可以把 ISDN
欄位作為唯一標識。讓我們建立更多的動作,即分表代表著 Index 和 Show 動作的 GET /books
和 GET /books/:isdn
。 我們的 main.go
檔案此時如下:
package mainimport ( "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 databasevar bookstore = make(map[string]*Book)// Handler for the books index action// GET /booksfunc 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/:isdnfunc 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
,您將得到以下響應:
{ "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
。我們可以把它們移到各個單獨的檔案中。此時我們有一個目錄:
. ├── 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 mainimport ( "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.ResponseRecorderfunc 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 mainimport ( "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 /booksfunc 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 /booksfunc 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/:isdnfunc 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 StatusOKfunc 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 codefunc 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 Handlerfunc 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 mainimport ( "log" "net/http" "time" "github.com/julienschmidt/httprouter")// A Logger function which simply wraps the handler function around some log messagesfunc 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 mainimport "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 []Routefunc 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 mainimport "github.com/julienschmidt/httprouter"//Reads from the routes slice to translate the values to httprouter.Handlefunc 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 那樣可以有迴圈的包呼叫。因此你必須格外小心您的包結構。
這就是全部內容,希望本教程能對您有用。乾杯!
注
[1]
[2]
[3]
[4]
[Gorilla mux]
[httprouter]
[bone]
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2331/viewspace-2805176/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Golang | HttpRouterGolangHTTP
- 使用Golang和MongoDB構建 RESTful APIGolangMongoDBRESTAPI
- 探索使用 Golang 和 Webassembly 構建一個多人遊戲伺服器GolangWeb遊戲伺服器
- 使用Golang和MongoDB構建微服務GolangMongoDB微服務
- 使用Golang快速構建WEB應用GolangWeb
- 使用Laravel構建小程式Api伺服器的準備工作LaravelAPI伺服器
- 使用ASP.NET Web API構建RESTful APIASP.NETWebAPIREST
- 使用 Mux, Go, PostgreSQL, GORM 構建 APIUXGoSQLORMAPI
- 簡單介紹如何使用Bazel構建Golang程式Golang
- 怎樣構建 Golang Dockerfiles?GolangDocker
- 使用 .NET Core 3.x 構建 RESTFUL ApiRESTAPI
- 使用 JWT 構建基本的 Api 登入介面JWTAPI
- 使用 Go-Spring 構建最小 Web APIGoSpringWebAPI
- 探索使用Nginx +Lua 構建 API 閘道器NginxAPI
- 使用JBang構建Spring Boot Rest API教程Spring BootRESTAPI
- 構建 API 應用API
- httprouter框架 (Gin使用的路由框架)HTTP框架路由
- 使用 .NET Core 3.x 構建 RESTFUL Api (續)RESTAPI
- laravel7使用JWT+Dingo構建APILaravelJWTGoAPI
- 構建微服務-使用OAuth 2.0保護API介面微服務OAuthAPI
- 使用 ES decorators 構建一致性 APIAPI
- 使用Hudson搭建自動構建伺服器伺服器
- 使用API閘道器構建移動端友好的API服務API
- 構建Web API服務WebAPI
- 用 Golang 構建 gRPC 服務GolangRPC
- [譯] 使用 Go 和 AWS Lambda 構建無服務 APIGoAPI
- 使用 Python 構建一個簡單的 RESTful APIPythonRESTAPI
- 如何使用Linux構建高效FTP伺服器LinuxFTP伺服器
- [golang]-golang呼叫gitlab api增加刪除使用者GolangGitlabAPI
- 3 行寫爬蟲 - 使用 Goribot 快速構建 Golang 爬蟲爬蟲Golang
- 構建API的7個技巧API
- 構建高效的 API 規範API
- 構建FTP伺服器FTP伺服器
- [實戰] 使用 MongoDB Go 驅動與 Iris 構建 RESTful APIMongoDBRESTAPI
- 使用 Swift 和 Vapor 構建區塊鏈伺服器SwiftVapor區塊鏈伺服器
- 如何基於 Notadd 構建 API (Laravel 寫 API)APILaravel
- Gradle 與 AGP 構建 API: 配置您的構建檔案GradleAPI
- httprouter 原始碼分析HTTP原始碼