這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。我 10 個月前開始成為一名 Gopher,沒有回頭。像許多其他 gopher 一樣,我很快發現簡單的語言特性對於快速構建快速、可擴充的軟體非常有用。當我剛開始學習 Go 時,我正在玩不同的多工器(multiplexer),它可以作為 API 伺服器使用。如果您像我一樣有 Rails 背景,你可能也會在構建 Web 架構提供的所有功能方面遇到困難。回到多工器,我發現了 3 個是非常有用的好東西,即 [Gorilla mux](https://github.com/gorilla/mux)、[httprouter](https://github.com/julienschmidt/httprouter) 和 [bone](https://github.com/go-zoo/bone)(按效能從低到高排列)。即使 bone 有最佳效能和更簡單的 handler 簽名,但對於我來說,它仍然不夠成熟,無法用於生產環境。因此,我最終使用了 httprouter。在本教程中,我將使用 httprouter 構建一個簡單的 REST API 伺服器。<!-- more -->如果你想偷懶,只想擷取源碼,你可以在[這裡](https://github.com/gsingharoy/httprouter-tutorial/tree/master/part4)[4]直接檢出我的 github 倉庫。讓我們開始吧。首先建立一個基本端點:```gopackage 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 伺服器。點擊[這裡](https://github.com/gsingharoy/httprouter-tutorial/tree/master/part1)[1]擷取當前代碼。現在我們可以讓 API 變得複雜一點。我們現在有一個名為 `Book` 的實體,可以把 `ISDN` 欄位作為唯一標識。讓我們建立更多的動作,即分表代表著 Index 和 Show 動作的 GET `/books` 和 GET `/books/:isdn`。 我們的 `main.go` 檔案此時如下:```gopackage 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 responseMeta 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 foundw.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 entriesbookstore["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`,您將得到以下響應:```go{ "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 實體。點擊[這裡](https://github.com/gsingharoy/httprouter-tutorial/tree/master/part2)[2]擷取當前階段的代碼。讓我們來重構一下代碼。 到目前為止,我們所有的代碼都放置在同一個檔案中:`main.go`。我們可以把它們移到各個單獨的檔案中。此時我們有一個目錄:```.├── handlers.go├── main.go├── models.go└── responses.go```我們把所有與 `JSON` 響應相關的結構體移動到 `responses.go`,將 handler 函數移動到 `Handlers.go`,且將 `Book` 結構體移動到 `models.go`。點擊[這裡](https://github.com/gsingharoy/httprouter-tutorial/tree/master/part3)[3]查看當前階段的代碼。 現在,我們跳過來寫一些測試。在 Go 中,`*_test.go` 檔案是用於測試的。因此讓我們建立一個 `handlers_test.go`。```gopackage 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 maptestBook := &Book{ISDN: "111",Title: "test title",Author: "test author",Pages: 42,}bookstore["111"] = testBook// A request with an existing isdnreq1, 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 responseer1 := "{\"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`。```gopackage 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] = bookwriteOKResponse(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 foundwriteErrorResponse(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 函數之前和之後列印日誌訊息。```gopackage 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`。```gopackage mainimport "github.com/julienschmidt/httprouter"/*Define all the routes here.A new Route entry passed to the routes slice will be automaticallytranslated to a handler with the NewRouter() function*/type Route struct {Name stringMethod stringPath stringHandlerFunc 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。```gopackage 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.Handlehandle = route.HandlerFunchandle = 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```在[這裡](https://github.com/gsingharoy/httprouter-tutorial/tree/master/part4)[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] https://github.com/gsingharoy/httprouter-tutorial/tree/master/part1- [2] https://github.com/gsingharoy/httprouter-tutorial/tree/master/part2- [3] https://github.com/gsingharoy/httprouter-tutorial/tree/master/part3- [4] https://github.com/gsingharoy/httprouter-tutorial/tree/master/part4- [Gorilla mux] https://github.com/gorilla/mux- [httprouter] https://github.com/julienschmidt/httprouter- [bone] https://github.com/go-zoo/bone> [https://medium.com/@gauravsingharoy/build-your-first-api-server-with-httprouter-in-golang-732b7b01f6ab](https://medium.com/@gauravsingharoy/build-your-first-api-server-with-httprouter-in-golang-732b7b01f6ab)> 作者:Gaurav Singha Roy> 譯者:oopsguy.com2618 次點擊 ∙ 1 贊