RESTful API在Web項目開發(fā)中廣泛使用,本文針對Go語言如何一步步實現(xiàn)RESTful JSON API進行講解, 另外也會涉及到RESTful設(shè)計方面的話題。
也許我們之前有使用過各種各樣的API, 當我們遇到設(shè)計很糟糕的API的時候,簡直感覺崩潰至極。希望通過本文之后,能對設(shè)計良好的RESTful API有一個初步認識。
JSON API是什么?
JSON之前,很多網(wǎng)站都通過XML進行數(shù)據(jù)交換。如果在使用過XML之后,再接觸JSON, 毫無疑問,你會覺得世界多么美好。這里不深入JSON API的介紹,有興趣可以參考jsonapi。
基本的Web服務(wù)器
從根本上講,RESTful服務(wù)首先是Web服務(wù)。 因此我們可以先看看Go語言中基本的Web服務(wù)器是如何實現(xiàn)的。下面例子實現(xiàn)了一個簡單的Web服務(wù)器,對于任何請求,服務(wù)器都響應(yīng)請求的URL回去。
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服務(wù)器使用Go標準庫的兩個基本函數(shù)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服務(wù),就可以直接通過瀏覽器訪問http://localhost:8080來訪問。
添加路由
雖然標準庫包含有router, 但是我發(fā)現(xiàn)很多人對它的工作原理感覺很困惑。 我在自己的項目中使用過各種不同的第三方router庫。 最值得一提的是Gorilla Web ToolKit的mux router。
另外一個流行的router是來自Julien Schmidt的叫做httprouter的包。
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
上面代碼創(chuàng)建了一個基本的路由器,給請求"/"賦予Index處理器,當客戶端請求http://localhost:8080/的時候,就會執(zhí)行Index處理器。
如果你足夠細心,你會發(fā)現(xiàn)之前的基本web服務(wù)訪問http://localhost:8080/abc能正常響應(yīng): 'Hello, "/abc"', 但是在添加了路由之后,就只能訪問http://localhost:8080了。 原因很簡單,因為我們只添加了對"/"的解析,其他的路由都是無效路由,因此都是404。
創(chuàng)建一些基本的路由
既然我們加入了路由,那么我們就可以再添加更多路由進來了。
假設(shè)我們要創(chuàng)建一個基本的ToDo應(yīng)用, 于是我們的代碼就變成下面這樣:
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設(shè)計的開始。
請注意最后一個路由我們給路由后面添加了一個變量叫做todoId。
這樣就允許我們傳遞id給路由,并且能使用具體的記錄來響應(yīng)請求。
基本模型
路由現(xiàn)在已經(jīng)就緒,是時候創(chuàng)建Model了,可以用model發(fā)送和檢索數(shù)據(jù)。在Go語言中,model可以使用結(jié)構(gòu)體來實現(xiàn),而其他語言中model一般都是使用類來實現(xiàn)。
package main
import (
"time"
)
type Todo struct {
Name string
Completed bool
Due time.Time
}
type Todos []Todo
上面我們定義了一個Todo結(jié)構(gòu)體,用于表示待做項。 另外我們還定義了一種類型Todos, 它表示待做列表,是一個數(shù)組,或者說是一個分片。
稍后你就會看到這樣會變得非常有用。
返回一些JSON
我們有了基本的模型,那么我們可以模擬一些真實的響應(yīng)了。我們可以為TodoIndex模擬一些靜態(tài)的數(shù)據(jù)列表。
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)
}
// ...
現(xiàn)在我們創(chuàng)建了一個靜態(tài)的Todos分片來響應(yīng)客戶端請求。注意,如果你請求http://localhost:8080/todos, 就會得到下面的響應(yīng):
[
{
"Name": "Write presentation",
"Completed": false,
"Due": "0001-01-01T00:00:00Z"
},
{
"Name": "Host meetup",
"Completed": false,
"Due": "0001-01-01T00:00:00Z"
}
]
更好的Model
對于經(jīng)驗豐富的老兵來說,你可能已經(jīng)發(fā)現(xiàn)了一個問題。響應(yīng)JSON的每個key都是首字母答寫的,雖然看起來微不足道,但是響應(yīng)JSON的key首字母大寫不是習慣的做法。 那么下面教你如何解決這個問題:
type Todo struct {
Name string `json:"name"`
Completed bool `json:"completed"`
Due time.Time `json:"due"`
}
其實很簡單,就是在結(jié)構(gòu)體中添加標簽屬性, 這樣可以完全控制結(jié)構(gòu)體如何編排(marshalled)成JSON。
拆分代碼
到目前為止,我們所有代碼都在一個文件中。顯得雜亂, 是時候拆分代碼了。我們可以將代碼按照功能拆分成下面多個文件。
我們準備創(chuàng)建下面的文件,然后將相應(yīng)代碼移到具體的代碼文件中:
- main.go: 程序入口文件。
- handlers.go: 路由相關(guān)的處理器。
- routes.go: 路由。
- todo.go: todo相關(guān)的代碼。
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))
}
更好的Routing
我們重構(gòu)的過程中,我們創(chuàng)建了一個更多功能的routes文件。 這個新文件利用了一個包含多個關(guān)于路由信息的結(jié)構(gòu)體。 注意,這里我們可以指定請求的類型,例如GET, POST, DELETE等等。
輸出Web日志
在拆分的路由文件中,我也包含有一個不可告人的動機。稍后你就會看到,拆分之后很容易使用另外的函數(shù)來修飾http處理器。
首先我們需要有對web請求打日志的能力,就像很多流行web服務(wù)器那樣的。 在Go語言中,標準庫里邊沒有web日志包或功能, 因此我們需要自己創(chuàng)建。
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函數(shù),可以給handler進行包裝修飾。
這是Go語言中非常標準的慣用方式。其實也是函數(shù)式編程的慣用方式。 非常有效,我們只需要將Handler傳入該函數(shù), 然后它會將傳入的handler包裝一下,添加web日志和耗時統(tǒng)計功能。
應(yīng)用Logger修飾器
要應(yīng)用Logger修飾符, 我們可以創(chuàng)建router, 我們只需要簡單的將我們所有的當前路由都包到其中, NewRouter函數(shù)修改如下:
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
}
現(xiàn)在再次運行我們的程序,我們就可以看到日志大概如下:
2014/11/19 12:41:39 GET /todos TodoIndex 148.324us
這個路由文件太瘋狂...讓我們重構(gòu)它吧
路由routes文件現(xiàn)在已經(jīng)變得稍微大了些, 下面我們將它分解成多個文件:
- routes.go
- router.go
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
}
另外再承擔一些責任
到目前為止,我們已經(jīng)有了一些相當好的樣板代碼(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)
}
}
這里發(fā)生了兩件事。 首先,我們設(shè)置了響應(yīng)類型并告訴客戶端期望接受JSON。第二,我們明確的設(shè)置了響應(yīng)狀態(tài)碼。
Go語言的net/http服務(wù)器會嘗試為我們猜測輸出內(nèi)容類型(然而并不是每次都準確的), 但是既然我們已經(jīng)確切的知道響應(yīng)類型,我們總是應(yīng)該自己設(shè)置它。
稍等片刻,我們的數(shù)據(jù)庫在哪里?
很明顯,如果我們要創(chuàng)建RESTful API, 我們需要一些用于存儲和檢索數(shù)據(jù)的地方。然而,這個是不是本文的范圍之內(nèi), 因此我們將簡單的創(chuàng)建一個非常簡陋的模擬數(shù)據(jù)庫(非線程安全的)。
我們創(chuàng)建一個repo.go文件,內(nèi)容如下:
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)
}
給Todo添加ID
我們創(chuàng)建了模擬數(shù)據(jù)庫,我們使用并賦予id, 因此我們相應(yīng)的也需要更新我們的Todo結(jié)構(gòu)體。
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
要使用數(shù)據(jù)庫,我們需要在TodoIndex中檢索數(shù)據(jù)。修改代碼如下:
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)
}
}
POST JSON
到目前為止,我們只是輸出JSON, 現(xiàn)在是時候進入存儲一些JSON了。
在routes.go文件中添加如下路由:
Route{
"TodoCreate",
"POST",
"/todos",
TodoCreate,
},
Create路由
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。這樣是保護服務(wù)器免受惡意攻擊的好方法。假設(shè)如果有人想要給你服務(wù)器發(fā)送500GB的JSON怎么辦?
我們讀取body以后,我們解構(gòu)Todo結(jié)構(gòu)體。 如果失敗,我們作出正確的響應(yīng),使用恰當?shù)捻憫?yīng)碼422, 但是我們依然使用json響應(yīng)回去。 這樣可以允許客戶端理解有錯發(fā)生了, 而且有辦法知道到底發(fā)生了什么錯誤。
最后,如果所有都通過了,我們就響應(yīng)201狀態(tài)碼,表示請求創(chuàng)建的實體已經(jīng)成功創(chuàng)建了。 我們同樣還是響應(yīng)回代表我們創(chuàng)建的實體的json, 它會包含一個id, 客戶端可能接下來需要用到它。
POST一些JSON
我們現(xiàn)在有了偽repo, 也有了create路由,那么我們需要post一些數(shù)據(jù)。 我們使用curl通過下面的命令來達到這個目的:
復制代碼 代碼如下:
curl -H "Content-Type: application/json" -d '{"name": "New Todo"}' http://localhost:8080/todos
如果你再次通過http://localhost:8080/todos訪問,大概會得到下面的響應(yīng):
[
{
"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"
}
]
我們還沒有做的事情
雖然我們已經(jīng)有了很好的開端,但是還有很多事情沒有做:
- 版本控制: 如果我們需要修改API, 結(jié)果完全改變了怎么辦? 可能我們需要在我們的路由開頭加上/v1/prefix?
- 授權(quán): 除非這些都是公開/免費API, 我們可能還需要授權(quán)。 建議學習JSON web tokens的東西。
eTag - 如果你正在構(gòu)建一些需要擴展的東西,你可能需要實現(xiàn)eTag。
還有什么?
對于所有項目來說,開始都很小,但是很快就變得失控了。但是如果我們想要將它帶到另外一個層次, 讓他生產(chǎn)就緒, 還有一些額外的事情需要做:
- 大量重構(gòu)(refactoring).
- 為這些文件創(chuàng)建幾個包,例如一些JSON助手、修飾符、處理器等等。
- 測試, 使得,你不能忘記這點。這里我們沒有做任何測試。對于生產(chǎn)系統(tǒng)來說,測試是必須的。
源代碼:https://github.com/corylanou/tns-restful-json-api
總結(jié)
對我來說,最重要的,需要記住的是我們要建立一個負責任的API。 發(fā)送適當?shù)臓顟B(tài)碼,header等,這些是API廣泛采用的關(guān)鍵。我希望本文能讓你盡快開始自己的API。
參考鏈接
Go語言RESTful JSON API實現(xiàn)
JSON API
Gorilla Web Toolkit
httprouter
JSON Web Tokens
eTag
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
您可能感興趣的文章:- Django開發(fā)RESTful API實現(xiàn)增刪改查(入門級)
- go 原生http web 服務(wù)跨域restful api的寫法介紹
- Django restful framework生成API文檔過程詳解
- Django JWT Token RestfulAPI用戶認證詳解
- Python利用Django如何寫restful api接口詳解
- 詳解Django rest_framework實現(xiàn)RESTful API
- 基于Go語言構(gòu)建RESTful API服務(wù)