RESTful API在Web項目開發中普遍使用,本文針對Go語言如何一步步實現RESTful JSON API進行講解, 另外也會涉及到RESTful設計方面的話題。 html
也許咱們以前有使用過各類各樣的API, 當咱們遇到設計很糟糕的API的時候,簡直感受崩潰至極。但願經過本文以後,能對設計良好的RESTful API有一個初步認識。git
JSON以前,不少網站都經過XML進行數據交換。若是在使用過XML以後,再接觸JSON, 毫無疑問,你會以爲世界多麼美好。這裏不深刻JSON API的介紹,有興趣能夠參考jsonapi。github
從根本上講,RESTful服務首先是Web服務。 所以咱們能夠先看看Go語言中基本的Web服務器是如何實現的。下面例子實現了一個簡單的Web服務器,對於任何請求,服務器都響應請求的URL回去。web
package main import ( "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)) }
上面基本的web服務器使用Go標準庫的兩個基本函數HandleFunc和ListenAndServe。數據庫
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { DefaultServeMux.HandleFunc(pattern, handler) } func ListenAndServe(addr string, handler Handler) error { server := &Server{Addr: addr, Handler: handler} return server.ListenAndServe() }
運行上面的基本web服務,就能夠直接經過瀏覽器訪問http://localhost:8080來訪問。編程
> go run basic_server.go
雖然標準庫包含有router, 可是我發現不少人對它的工做原理感受很困惑。 我在本身的項目中使用過各類不一樣的第三方router庫。 最值得一提的是Gorilla Web ToolKit的mux router。json
另一個流行的router是來自Julien Schmidt的叫作httprouter的包。api
package main import ( "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)) }
要運行上面的代碼,首先使用go get獲取mux router的源代碼:數組
> go get github.com/gorilla/mux
上面代碼建立了一個基本的路由器,給請求"/"賦予Index處理器,當客戶端請求http://localhost:8080/的時候,就會執行Index處理器。 瀏覽器
若是你足夠細心,你會發現以前的基本web服務訪問http://localhost:8080/abc能正常響應: 'Hello, "/abc"', 可是在添加了路由以後,就只能訪問http://localhost:8080了。 緣由很簡單,由於咱們只添加了對"/"的解析,其餘的路由都是無效路由,所以都是404。
既然咱們加入了路由,那麼咱們就能夠再添加更多路由進來了。
假設咱們要建立一個基本的ToDo應用, 因而咱們的代碼就變成下面這樣:
package main import ( "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) }
在這裏咱們添加了另外兩個路由: todos和todos/{todoId}。
這就是RESTful API設計的開始。
請注意最後一個路由咱們給路由後面添加了一個變量叫作todoId。
這樣就容許咱們傳遞id給路由,而且能使用具體的記錄來響應請求。
路由如今已經就緒,是時候建立Model了,能夠用model發送和檢索數據。在Go語言中,model可使用結構體來實現,而其餘語言中model通常都是使用類來實現。
package main import ( "time" ) type Todo struct { Name string Completed bool Due time.Time } type Todos []Todo
上面咱們定義了一個Todo結構體,用於表示待作項。 另外咱們還定義了一種類型Todos, 它表示待作列表,是一個數組,或者說是一個分片。
稍後你就會看到這樣會變得很是有用。
咱們有了基本的模型,那麼咱們能夠模擬一些真實的響應了。咱們能夠爲TodoIndex模擬一些靜態的數據列表。
package main import ( "encoding/json" "fmt" "log" "net/http" "github.com/gorilla/mux" ) // ... func TodoIndex(w http.ResponseWriter, r *http.Request) { todos := Todos{ Todo{Name: "Write presentation"}, Todo{Name: "Host meetup"}, } json.NewEncoder(w).Encode(todos) } // ...
如今咱們建立了一個靜態的Todos分片來響應客戶端請求。注意,若是你請求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" } ]
對於經驗豐富的老兵來講,你可能已經發現了一個問題。響應JSON的每一個key都是首字母答寫的,雖然看起來微不足道,可是響應JSON的key首字母大寫不是習慣的作法。 那麼下面教你如何解決這個問題:
type Todo struct { Name string `json:"name"` Completed bool `json:"completed"` Due time.Time `json:"due"` }
其實很簡單,就是在結構體中添加標籤屬性, 這樣能夠徹底控制結構體如何編排(marshalled)成JSON。
到目前爲止,咱們全部代碼都在一個文件中。顯得雜亂, 是時候拆分代碼了。咱們能夠將代碼按照功能拆分紅下面多個文件。
咱們準備建立下面的文件,而後將相應代碼移到具體的代碼文件中:
package main import ( "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) }
package main import ( "net/http" "github.com/gorilla/mux" ) type Route struct { Name string Method string Pattern string HandlerFunc http.HandlerFunc } type Routes []Route func 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, }, }
package main import "time" type Todo struct { Name string `json:"name"` Completed bool `json:"completed"` Due time.Time `json:"due"` } type Todos []Todo
package main import ( "log" "net/http" ) func main() { router := NewRouter() log.Fatal(http.ListenAndServe(":8080", router)) }
咱們重構的過程當中,咱們建立了一個更多功能的routes文件。 這個新文件利用了一個包含多個關於路由信息的結構體。 注意,這裏咱們能夠指定請求的類型,例如GET, POST, DELETE等等。
在拆分的路由文件中,我也包含有一個不可告人的動機。稍後你就會看到,拆分以後很容易使用另外的函數來修飾http處理器。
首先咱們須要有對web請求打日誌的能力,就像不少流行web服務器那樣的。 在Go語言中,標準庫裏邊沒有web日誌包或功能, 所以咱們須要本身建立。
package logger import ( "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), ) }) }
上面咱們定義了一個Logger函數,能夠給handler進行包裝修飾。
這是Go語言中很是標準的慣用方式。其實也是函數式編程的慣用方式。 很是有效,咱們只須要將Handler傳入該函數, 而後它會將傳入的handler包裝一下,添加web日誌和耗時統計功能。
要應用Logger修飾符, 咱們能夠建立router, 咱們只須要簡單的將咱們全部的當前路由都包到其中, NewRouter函數修改以下:
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 }
如今再次運行咱們的程序,咱們就能夠看到日誌大概以下:
2014/11/19 12:41:39 GET /todos TodoIndex 148.324us
路由routes文件如今已經變得稍微大了些, 下面咱們將它分解成多個文件:
package main import "net/http" type Route struct { Name string Method string Pattern string HandlerFunc http.HandlerFunc } type Routes []Route var routes = Routes{ Route{ "Index", "GET", "/", Index, }, Route{ "TodoIndex", "GET", "/todos", TodoIndex, }, Route{ "TodoShow", "GET", "/todos/{todoId}", TodoShow, }, }
package main import ( "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 }
到目前爲止,咱們已經有了一些至關好的樣板代碼(boilerplate), 是時候從新審視咱們的處理器了。咱們須要稍微多的責任。 首先修改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) } }
這裏發生了兩件事。 首先,咱們設置了響應類型並告訴客戶端指望接受JSON。第二,咱們明確的設置了響應狀態碼。
Go語言的net/http服務器會嘗試爲咱們猜想輸出內容類型(然而並非每次都準確的), 可是既然咱們已經確切的知道響應類型,咱們老是應該本身設置它。
很明顯,若是咱們要建立RESTful API, 咱們須要一些用於存儲和檢索數據的地方。然而,這個是否是本文的範圍以內, 所以咱們將簡單的建立一個很是簡陋的模擬數據庫(非線程安全的)。
咱們建立一個repo.go文件,內容以下:
package main import "fmt" var currentId int var todos Todos // Give us some seed data func 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) }
咱們建立了模擬數據庫,咱們使用並賦予id, 所以咱們相應的也須要更新咱們的Todo結構體。
package main import "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中檢索數據。修改代碼以下:
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) } }
到目前爲止,咱們只是輸出JSON, 如今是時候進入存儲一些JSON了。
在routes.go文件中添加以下路由:
Route{ "TodoCreate", "POST", "/todos", TodoCreate, },
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。 注意咱們使用io.LimitReader。這樣是保護服務器免受惡意攻擊的好方法。假設若是有人想要給你服務器發送500GB的JSON怎麼辦?
咱們讀取body之後,咱們解構Todo結構體。 若是失敗,咱們做出正確的響應,使用恰當的響應碼422, 可是咱們依然使用json響應回去。 這樣能夠容許客戶端理解有錯發生了, 並且有辦法知道到底發生了什麼錯誤。
最後,若是全部都經過了,咱們就響應201狀態碼,表示請求建立的實體已經成功建立了。 咱們一樣仍是響應回表明咱們建立的實體的json, 它會包含一個id, 客戶端可能接下來須要用到它。
咱們如今有了僞repo, 也有了create路由,那麼咱們須要post一些數據。 咱們使用curl經過下面的命令來達到這個目的:
curl -H "Content-Type: application/json" -d '{"name": "New Todo"}' http://localhost:8080/todos
若是你再次經過http://localhost:8080/todos訪問,大概會獲得下面的響應:
[ { "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" } ]
雖然咱們已經有了很好的開端,可是還有不少事情沒有作:
eTag - 若是你正在構建一些須要擴展的東西,你可能須要實現eTag。
對於全部項目來講,開始都很小,可是很快就變得失控了。可是若是咱們想要將它帶到另一個層次, 讓他生產就緒, 還有一些額外的事情須要作:
https://github.com/corylanou/...
對我來講,最重要的,須要記住的是咱們要創建一個負責任的API。 發送適當的狀態碼,header等,這些是API普遍採用的關鍵。我但願本文能讓你儘快開始本身的API。