使用Go構建RESTful的JSON API

來源:互聯網
上載者:User

標籤:

原文地址http://thenewstack.io/make-a-restful-json-api-go/
這篇文章不僅僅討論如何使用Go構建RESTful的JSON API,同時也會討論如何設計好的RESTful API。如果你曾經遭遇了未遵循良好設計的API,那麼你最終將寫爛代碼來使用這些垃圾API。希望閱讀這篇文章後,你能夠對好的API應該是怎樣的有更多的認識。

JSON API是啥?

在JSON前,XML是一種主流的文字格式設定。筆者有幸XML和JSON都使用過,毫無疑問,JSON是明顯的贏家。本文不會深入涉及JSON API的概念,在jsonapi.org可以找到的詳細的描述。

Sponsor Note

SpringOne2GX是一個專門面向App開發人員、解決方案和資料架構師的會議。議題都是專門針對程式猿(媛),架構師所使用的流行的開源技術,如:Spring IO Projects,Groovy & Grails,Cloud Foundry,RabbitMQ,Redis,Geode,Hadoop and Tomcat等。

一個基本的Web Server

一個RESTful服務本質上首先是一個Web service。下面的是樣本是一個最簡單的Web server,對於任何請求都簡單的直接返回請求連結:

package mainimport (        "fmt"        "html"        "log"        "net/http")func main() {        http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {            fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))    })    log.Fatal(http.ListenAndServe(":8080", nil))}

編譯執行這個樣本將運行這個server,監聽8080連接埠。嘗試使用http://localhost:8080訪問server。

增加一個路由

當大多數標準庫開始支援路由,我發現大多數人都搞不清楚它們是如何工作的。我在項目中使用過幾個第三方的router。印象最深的是Gorilla Web Toolkit中的mux router.

另一個比較流行的router是Julien Schmidt貢獻的httprouter

package mainimport (        "fmt"        "html"        "log"        "net/http"        "github.com/gorilla/mux")func main() {        router := mux.NewRouter().StrictSlash(true)        router.HandleFunc("/", Index)        log.Fatal(http.ListenAndServe(":8080", router))}func Index(w http.ResponseWriter, r *http.Request) {        fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))}

運行上面的樣本,首先需要安裝包“github.com/gorilla/mux”.可以直接使用命令go get遍曆整個source code安裝所有未安裝的依賴包。

譯者註:

也可以使用go get "github.com/gorilla/mux"直接安裝包。

上面的樣本建立了一個簡單的router,增加了一個“/”路由,並分配Index handler響應針對指定的endpoint的訪問。這是你會發現在第一個樣本中還能訪問的如http://localhost:8080/foo這類的連結在這個樣本中不再工作了,這個樣本將只能響應連結http://localhost:8080.

建立更多的基本路由

上一節我們已經有了一個路由,是時候建立更多的路由了。假設我們將要建立一個基本的TODO app。

package mainimport (    "fmt"    "log"    "net/http"    "github.com/gorilla/mux")func main() {    router := mux.NewRouter().StrictSlash(true)    router.HandleFunc("/", Index)    router.HandleFunc("/todos", TodoIndex)    router.HandleFunc("/todos/{todoId}", TodoShow)    log.Fatal(http.ListenAndServe(":8080", router))}func Index(w http.ResponseWriter, r *http.Request) {    fmt.Fprintln(w, "Welcome!")}func TodoIndex(w http.ResponseWriter, r *http.Request) {    fmt.Fprintln(w, "Todo Index!")}func TodoShow(w http.ResponseWriter, r *http.Request) {    vars := mux.Vars(r)    todoId := vars["todoId"]    fmt.Fprintln(w, "Todo show:", todoId)}

現在我們又在上一個樣本的基礎上增加了兩個routes,分別是:

  • ToDo index route:http://localhost:8080/todos
  • ToDo show route: http://localhost:8080/todos/{todoId}

這就是一個RESTful設計的開始。注意,最後一個路由我們增加了一個名為todoId的變數。這將允許我們向route傳遞變數,然後獲得合適的響應記錄。

基本樣式

有了路由後,就可以建立一些基本的TODO樣式用於發送和檢索資料。在一些其他語言中使用類(class)來達到這個目的,Go中使用struct。

package mainimport “time”type Todo struct {    Name        string    Completed   tool    Due         time.time}type Todos []Todo   
註:

最後一行定義的類型TodosTodo的slice。稍後你將會看到怎麼使用它。

返回JSON

基於上面的基本樣式,我們可以類比真實的響應,並基於待用資料列出TodoIndex。

func TodoIndex(w http.ResponseWriter, r *http.Request) {    todos := Todos{    Todo{Name: "Write presentation"},    Todo{Name: "Host meetup"},    }    json.NewEncoder(w).Encode(todos)}

這樣就建立了一個Todos的靜態slice,並被編碼響應使用者請求。如果這時你訪問http://localhost:8080/todos,你將得到如下響應:

[    {        "Name": "Write presentation",        "Completed": false,        "Due": "0001-01-01T00:00:00Z"    },    {    "Name": "Host meetup",        "Completed": false,        "Due": "0001-01-01T00:00:00Z"    }]
一個稍微好點的樣式

可能你已經發現了,基於前面的樣式,todos返回的並不是一個標準的JSON資料包(JSON格式定義中不包含大寫字母)。雖然這個問題有那麼一點微不足道,但是我們還是可以解決它:

package mainimport "time"type Todo struct {    Name      string    `json:"name"`    Completed bool      `json:"completed"`    Due       time.Time `json:"due"`}type Todos []Todo

上面的程式碼範例在原來的基礎上增加了struct tags,這樣可以指定JSON的編碼格式。

檔案拆分

到此我們需要對這個項目稍微做下重構。現在一個檔案包含了太多的內容。我們將建立如下幾個檔案,並重新組織檔案內容:

  • main.go
  • handlers.go
  • routes.go
  • todo.go
handlers.go
package mainimport (    "encoding/json"    "fmt"    "net/http"    "github.com/gorilla/mux")func Index(w http.ResponseWriter, r *http.Request) {    fmt.Fprintln(w, "Welcome!")}func TodoIndex(w http.ResponseWriter, r *http.Request) {    todos := Todos{        Todo{Name: "Write presentation"},        Todo{Name: "Host meetup"},    }    if err := json.NewEncoder(w).Encode(todos); err != nil {        panic(err)    }}func TodoShow(w http.ResponseWriter, r *http.Request) {    vars := mux.Vars(r)    todoId := vars["todoId"]    fmt.Fprintln(w, "Todo show:", todoId)}
routes.go
package mainimport (    "net/http"    "github.com/gorilla/mux")type Route struct {    Name        string    Method      string    Pattern     string    HandlerFunc http.HandlerFunc}type Routes []Routefunc NewRouter() *mux.Router {    router := mux.NewRouter().StrictSlash(true)    for _, route := range routes {        router.        Methods(route.Method).            Path(route.Pattern).            Name(route.Name).            Handler(route.HandlerFunc)    }    return router}var routes = Routes{    Route{        "Index",        "GET",        "/",        Index,    },    Route{        "TodoIndex",        "GET",        "/todos",        TodoIndex,    },    Route{        "TodoShow",        "GET",        "/todos/{todoId}",        TodoShow,    },}
todo.go
package mainimport "time"type Todo struct {    Name      string    `json:"name"`    Completed bool      `json:"completed"`    Due       time.Time `json:"due"`}type Todos []Todo
main.go
package mainimport (        "log"        "net/http")func main() {    router := NewRouter()    log.Fatal(http.ListenAndServe(":8080", router))}
更好的路由

上面重構的一部分就是建立了一個更詳細的route檔案,新檔案中使用了一個struct包含了更多的有關路由的詳細資料。尤其是,我們可以通過這個struct指定請求的動作,如GET、POST、DELETE等。

記錄Web Log

前面的拆分檔案中,我還有一個更長遠的考慮。稍後你將會看到,拆分後我將能夠很輕鬆的使用其他函數裝飾我的http handlers。這一節我們將使用這個功能讓我們的web能夠像其他現代的網站一樣為web訪問請求記Log。在Go中,目前還沒有一個web logging package,也沒有標準庫提供相應的功能。所以我們不得不自己實現一個。

在前面拆分檔案的基礎上,我們建立一個叫logger.go的新檔案,並在檔案中添加如下代碼:

package mainimport (    "log"    "net/http"    "time")func Logger(inner http.Handler, name string) http.Handler {    return http.HandlerFunc(func(w http.ResponseWriter, r       *http.Request) {        start := time.Now()        inner.ServeHTTP(w, r)        log.Printf(            "%s\t%s\t%s\t%s",            r.Method,            r.RequestURI,            name,            time.Since(start),        )    })}

這樣,如果你訪問http://localhost:8080/todos,你將會看到console中有如下log輸出。

2014/11/19 12:41:39 GET /todos  TodoIndex       148.324us
Routes file開始瘋狂…繼續重構

基於上面的拆分,你會發現繼續照著這個節奏發展,routes.go檔案將變得越來越龐大。所以我們繼續拆分這個檔案。將其拆分為如下兩個檔案:

  • router.go
  • routes.go
routes.go 迴歸
package mainimport "net/http"type Route struct {    Name        string    Method      string    Pattern     string    HandlerFunc http.HandlerFunc}type Routes []Routevar routes = Routes{    Route{        "Index",        "GET",        "/",        Index,    },    Route{        "TodoIndex",        "GET",        "/todos",        TodoIndex,    },    Route{        "TodoShow",        "GET",        "/todos/{todoId}",        TodoShow,    },}
router.go
package mainimport (    "net/http"    "github.com/gorilla/mux")func NewRouter() *mux.Router {    router := mux.NewRouter().StrictSlash(true)    for _, route := range routes {        var handler http.Handler        handler = route.HandlerFunc        handler = Logger(handler, route.Name)        router.            Methods(route.Method).            Path(route.Pattern).            Name(route.Name).            Handler(handler)    }    return router}
做更多的事情

現在我們已經有了一個不錯的模板,是時候重新考慮我們handlers了,讓handler能做更多的事情。首先我們在TodoIndex中增加兩行代碼。

func TodoIndex(w http.ResponseWriter, r *http.Request) {    todos := Todos{        Todo{Name: "Write presentation"},        Todo{Name: "Host meetup"},    }    w.Header().Set("Content-Type", "application/json; charset=UTF-8")    w.WriteHeader(http.StatusOK)    if err := json.NewEncoder(w).Encode(todos); err != nil {        panic(err)    }}

新增的兩行代碼讓TodoIndex handler多做兩件事。首先返回client期望的json,並告知內容類型。然後明確的設定一個狀態代碼。

Go的net/http server在Header中沒有顯示的說明內容類型時將嘗試為我們猜測內容類型,但是並不是總是那麼準確。所以在我們知道content類型的情況下,我們應該總是自己設定類型。

等等,資料庫在哪兒?

如果我們繼續構造RESTful API,我們需要考慮一個地方用於儲存和檢索資料。但是這超出了本文所討論的範疇,所以這裡簡單的實現了一個粗糙的資料存放區(粗糙到甚至都沒安全執行緒機制)。

建立一個名為repo.go的檔案,代碼如下:

package mainimport "fmt"var currentId intvar todos Todos// Give us some seed datafunc init() {    RepoCreateTodo(Todo{Name: "Write presentation"})    RepoCreateTodo(Todo{Name: "Host meetup"})}func RepoFindTodo(id int) Todo {    for _, t := range todos {        if t.Id == id {            return t        }    }    // return empty Todo if not found    return Todo{}}func RepoCreateTodo(t Todo) Todo {    currentId += 1    t.Id = currentId    todos = append(todos, t)    return t}func RepoDestroyTodo(id int) error {    for i, t := range todos {        if t.Id == id {            todos = append(todos[:i], todos[i+1:]...)            return nil        }    }    return fmt.Errorf("Could not find Todo with id of %d to delete", id)}
為Todo添加一個ID

現在我們已經有了一個粗糙的資料庫。我們可以為Todo建立一個ID,用於標識和見識Todo item。資料結構更新如下:

package mainimport "time"type Todo struct {    Id        int       `json:"id"`    Name      string    `json:"name"`    Completed bool      `json:"completed"`    Due       time.Time `json:"due"`}type Todos []Todo
更新TodoIndex handler

資料存放區在資料庫後,不必在handler中產生資料,直接通過ID檢索資料庫即可得到相應內容。修改handler如下:

func TodoIndex(w http.ResponseWriter, r *http.Request) {    w.Header().Set("Content-Type", "application/json; charset=UTF-8")    w.WriteHeader(http.StatusOK)    if err := json.NewEncoder(w).Encode(todos); err != nil {        panic(err)    }}
Posting JSON

前面所有的API都是相應GET請求的,只能輸出JSON。這節將增加一個上傳和儲存JSON的API。在routes.go檔案中增加如下route:

Route{    "TodoCreate",    "POST",    "/todos",    TodoCreate,},
The Create endpoint

上面建立了一個新的router,現在為這個新的route建立一個endpoint。在handlers.go檔案增加TodoCreate handler。代碼如下:

func TodoCreate(w http.ResponseWriter, r *http.Request) {    var todo Todo    body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1048576))    if err != nil {        panic(err)    }    if err := r.Body.Close(); err != nil {        panic(err)    }    if err := json.Unmarshal(body, &todo); err != nil {        w.Header().Set("Content-Type", "application/json;   charset=UTF-8")        w.WriteHeader(422) // unprocessable entity        if err := json.NewEncoder(w).Encode(err); err != nil {            panic(err)        }    }    t := RepoCreateTodo(todo)    w.Header().Set("Content-Type", "application/json;   charset=UTF-8")    w.WriteHeader(http.StatusCreated)    if err := json.NewEncoder(w).Encode(t); err != nil {        panic(err)    }}

上面的代碼中,首先我們擷取使用者請求的body。注意,在擷取body時我們使用了io.LimitReader,這是一個防止你的伺服器被惡意攻擊的好方法。試想如果有人給你發送了一個500GB的json。

讀取body後,將其內容解碼到Todo struct中。如果解碼失敗,我們要做的事情不僅僅是返回一個‘422’這樣的狀態代碼,同時還會返回一段包含錯誤資訊的json。這能夠使用戶端不僅知道有錯誤發生,還能瞭解錯誤發生在哪兒。

最後,如果一切順利,我們將向用戶端返回狀態代碼201,同時我們還向用戶端返回建立的實體內容,這些資訊用戶端在後面的操作中可能會用到。

Post JSON

所有的工作的完成後,我們就可以上傳下json string測試一下了。Sample及返回結果如下所示:

curl -H "Content-Type: application/json" -d ‘{"name":"New Todo"}‘ http://localhost:8080/todosNow, if you go to http://localhost/todos we should see the following response:[    {        "id": 1,        "name": "Write presentation",        "completed": false,        "due": "0001-01-01T00:00:00Z"    },    {        "id": 2,        "name": "Host meetup",        "completed": false,        "due": "0001-01-01T00:00:00Z"    },    {        "id": 3,        "name": "New Todo",        "completed": false,        "due": "0001-01-01T00:00:00Z"    }]
我們未做的事情

現在我們已經有了一個好的開頭,後面還有很多事情要做。下面是我們還未做的事情:

  • 版本控制 - 如果我們需要修改API,並且這將導致重大的更改?也許我們可以從為所有的routes添加/v1這樣的首碼開始。
  • 身份認證 - 除非這是一個自由/公開的API,否則我們可能需要添加一些認證機制。建議學習JSON web tokens
  • eTags - 如果你的構建需要擴充,你可能需要實現eTags
還剩些啥?

所有的項目都是開始的時候很小,但是很快就會發展開始變得失控。如果我想把這件事帶到下一個層級,並準備使其投入生產,則還有如下這些額外的事情需要做:

  • 很多的重構
  • 將這些檔案封裝成一些package,如JSON helpers,decorators,handlers等等。
  • 測試…是的,這個不能忽略。目前我們還沒有做任何的測試,但是對於一個產品,這個是必須的。
如何擷取原始碼

如果你想擷取本文樣本的原始碼,repo地址在這裡:https://github.com/corylanou/tns-restful-json-api

使用Go構建RESTful的JSON API

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.