Golang:使用 httprouter 構建 API 伺服器

lcc發表於2021-09-09


作者: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

如果您有一個大的單體伺服器,您還可以將 handlersmodels 和所有路由功能都放在另一個名為 app 的包中。只要記住,go 不像 Java 或 Scala 那樣可以有迴圈的包呼叫。因此你必須格外小心您的包結構。

這就是全部內容,希望本教程能對您有用。乾杯!

  • [1]

  • [2]

  • [3]

  • [4]

  • [Gorilla mux]

  • [httprouter]

  • [bone]



來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2331/viewspace-2805176/,如需轉載,請註明出處,否則將追究法律責任。

相關文章