正文
Go语言开发RESTFul JSON API
RESTful API在Web项目开发中广泛使用,本文针对Go语言如何一步步实现RESTful JSON API进行讲解, 另外也会涉及到RESTful设计方面的话题。
也许我们之前有使用过各种各样的API, 当我们遇到设计很糟糕的API的时候,简直感觉崩溃至极。希望通过本文之后,能对设计良好的RESTful API有一个初步认识。
JSON API是什么?
JSON之前,很多网站都通过XML进行数据交换。如果在使用过XML之后,再接触JSON, 毫无疑问,你会觉得世界多么美好。这里不深入JSON API的介绍,有兴趣可以参考
jsonapi
。
基本的Web服务器
从根本上讲,RESTful服务首先是Web服务。 因此我们可以先看看Go语言中基本的Web服务器是如何实现的。下面例子实现了一个简单的Web服务器,对于任何请求,服务器都响应请求的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服务器使用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。
另外一个流行的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
上面代码创建了一个基本的路由器,给请求"/"赋予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, 它表示待做列表,是一个数组,或者说是一个分片。
稍后你就会看到这样会变得非常有用。
返回一些JSON
我们有了基本的模型,那么我们可以模拟一些真实的响应了。我们可以为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"
}
]
更好的Model
对于经验丰富的老兵来说,你可能已经发现了一个问题。响应JSON的每个key都是首字母答写的,虽然看起来微不足道,但是响应JSON的key首字母大写不是习惯的做法。 那么下面教你如何解决这个问题:
type Todo struct {
Name string `json:"name"`
Completed bool `json:"completed"`
Due time.Time `json:"due"`
}
其实很简单,就是在结构体中添加标签属性, 这样可以完全控制结构体如何编排(marshalled)成JSON。
拆分代码
到目前为止,我们所有代码都在一个文件中。显得杂乱, 是时候拆分代码了。我们可以将代码按照功能拆分成下面多个文件。
我们准备创建下面的文件,然后将相应代码移到具体的代码文件中:
-
main.go: 程序入口文件。
-
handlers.go: 路由相关的处理器。
-
routes.go: 路由。
-
todo.go: todo相关的代码。
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
我们重构的过程中,我们创建了一个更多功能的routes文件。 这个新文件利用了一个包含多个关于路由信息的结构体。 注意,这里我们可以指定请求的类型,例如GET, POST, DELETE等等。
输出Web日志
在拆分的路由文件中,我也包含有一个不可告人的动机。稍后你就会看到,拆分之后很容易使用另外的函数来修饰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进行包装修饰。