使用Go构建RESTful的JSON API
来源:互联网 发布:慢走丝编程招聘信息 编辑:程序博客网 时间:2024/04/30 05:09
原文地址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
注:
最后一行定义的类型
Todos
是Todo
的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
- go restful json api
- 使用flask 构建基本的 restful api
- SpringMVC+Json构建基于Restful api
- 使用Swagger2构建强大的RESTful API文档
- Spring boot构建RESTFul API+使用Swagger2构建API文档
- 使用Node.js + MongoDB 构建restful API
- springMVC+json构建restful风格的服务
- 使用ASP.NET Web API构建RESTful API
- Kylin的RESTful API使用
- Spring Boot中使用Swagger2构建强大的RESTful API文档
- Spring Boot中使用Swagger2构建强大的RESTful API文档
- Spring Boot中使用Swagger2构建强大的RESTful API文档
- Spring Boot中使用Swagger2构建强大的RESTful API文档
- Spring Boot中使用Swagger2构建强大的RESTful API文档
- Spring Boot中使用Swagger2构建强大的RESTful API文档
- Spring Boot中使用Swagger2构建强大的RESTful API文档
- Spring Boot中使用Swagger2构建强大的RESTful API文档
- WinForm多线程编程与Control.Invoke的应用
- hadoop环境搭建遇到的问题:-bash: jps: command not found
- Studio 更新的时候提示Connection failed (connect timed out). Please check network connection and try again.
- 【Linux】常见进程调度
- ERROR 1010 (HY000): Error dropping database (can't rmdir '.\qpweb', errno: 41) MySQL删库失败问题的解决
- 使用Go构建RESTful的JSON API
- 新手整合springmvc+velocity过程
- Rectangle Area
- x & (-x)
- [Linux]生产者与消费者 三种模型 C
- 将eclipse的代码提交到Github
- [解决]UserLibrary中的jar包不会自动发布到Tomcat的lib目录下,而出现的tomcat开启服务报错问题
- jQuery选择器-表单对象属性选择器示例
- 清空缓存的四个函数