原文地址http://thenewstack.io/make-a-restful-json-api-go/
这篇文章不只仅讨论如何使用Go构建RESTful的JSON API,同时也会讨论如何设计好的RESTful API。若是你曾经遭遇了未遵循良好设计的API,那么你最终将写烂代码来使用这些垃圾API。但愿阅读这篇文章后,你可以对好的API应该是怎样的有更多的认识。html
在JSON前,XML是一种主流的文本格式。笔者有幸XML和JSON都使用过,毫无疑问,JSON是明显的赢家。本文不会深刻涉及JSON API的概念,在jsonapi.org能够找到的详细的描述。git
Sponsor Note
SpringOne2GX是一个专门面向App开发者、解决方案和数据架构师的会议。议题都是专门针对程序猿(媛),架构师所使用的流行的开源技术,如:Spring IO Projects,Groovy & Grails,Cloud Foundry,RabbitMQ,Redis,Geode,Hadoop and Tomcat等。github
一个RESTful服务本质上首先是一个Web service。下面的是示例是一个最简单的Web server,对于任何请求都简单的直接返回请求连接: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)) }
编译执行这个示例将运行这个server,监听8080端口。尝试使用http://localhost:8080访问server。spring
当大多数标准库开始支持路由,我发现大多数人都搞不清楚它们是如何工做的。我在项目中使用过几个第三方的router。印象最深的是Gorilla Web Toolkit中的mux router.数据库
另外一个比较流行的router是Julien Schmidt贡献的httprouterjson
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)) }
运行上面的示例,首先须要安装包“github.com/gorilla/mux”.能够直接使用命令go get
遍历整个source code安装全部未安装的依赖包。api
译者注:
也可使用
go get "github.com/gorilla/mux"
直接安装包。安全
上面的示例建立了一个简单的router,增长了一个“/”路由,并分配Index handler响应针对指定的endpoint的访问。这是你会发如今第一个示例中还能访问的如http://localhost:8080/foo这类的连接在这个示例中再也不工做了,这个示例将只能响应连接http://localhost:8080.服务器
上一节咱们已经有了一个路由,是时候建立更多的路由了。假设咱们将要建立一个基本的TODO app。
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) }
如今咱们又在上一个示例的基础上增长了两个routes,分别是:
这就是一个RESTful设计的开始。注意,最后一个路由咱们增长了一个名为todoId
的变量。这将容许咱们向route传递变量,而后得到合适的响应记录。
有了路由后,就能够建立一些基本的TODO样式用于发送和检索数据。在一些其余语言中使用类(class)来达到这个目的,Go中使用struct。
package main import “time” type Todo struct { Name string Completed tool Due time.time } type Todos []Todo
注:
最后一行定义的类型
Todos
是Todo
的slice。稍后你将会看到怎么使用它。
基于上面的基本样式,咱们能够模拟真实的响应,并基于静态数据列出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 main import "time" type Todo struct { Name string `json:"name"` Completed bool `json:"completed"` Due time.Time `json:"due"` } type Todos []Todo
上面的代码示例在原来的基础上增长了struct tags,这样能够指定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)) }
上面重构的一部分就是建立了一个更详细的route文件,新文件中使用了一个struct包含了更多的有关路由的详细信息。尤为是,咱们能够经过这个struct指定请求的动做,如GET、POST、DELETE等。
前面的拆分文件中,我还有一个更长远的考虑。稍后你将会看到,拆分后我将可以很轻松的使用其余函数装饰个人http handlers。这一节咱们将使用这个功能让咱们的web可以像其余现代的网站同样为web访问请求记Log。在Go中,目前尚未一个web logging package,也没有标准库提供相应的功能。因此咱们不得不本身实现一个。
在前面拆分文件的基础上,咱们建立一个叫logger.go
的新文件,并在文件中添加以下代码:
package main 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), ) }) }
这样,若是你访问http://localhost:8080/todos
,你将会看到console中有以下log输出。
2014/11/19 12:41:39 GET /todos TodoIndex 148.324us
基于上面的拆分,你会发现继续照着这个节奏发展,routes.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 }
如今咱们已经有了一个不错的模板,是时候从新考虑咱们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 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,用于标识和见识Todo item。数据结构更新以下:
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
数据存储在数据库后,没必要在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) } }
前面全部的API都是相应GET请求的,只能输出JSON。这节将增长一个上传和存储JSON的API。在routes.go
文件中增长以下route:
Route{ "TodoCreate", "POST", "/todos", TodoCreate, },
上面建立了一个新的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,同时咱们还向客户端返回建立的实体内容,这些信息客户端在后面的操做中可能会用到。
全部的工做的完成后,咱们就能够上传下json string测试一下了。Sample及返回结果以下所示:
curl -H "Content-Type: application/json" -d '{"name":"New Todo"}' http://localhost:8080/todos Now, 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" } ]
如今咱们已经有了一个好的开头,后面还有不少事情要作。下面是咱们还未作的事情:
全部的项目都是开始的时候很小,可是很快就会发展开始变得失控。若是我想把这件事带到下一个层级,并准备使其投入生产,则还有以下这些额外的事情须要作:
若是你想获取本文示例的源代码,repo地址在这里:https://github.com/corylanou/tns-restful-json-api