原文出处:https://thenewstack.io/make-a-restful-json-api-go/
在这篇文章中,我们不仅将介绍如何使用Go来创建RESTful JSON API,而且还将讨论好的RESTful设计。如果你以前曾经消费过一个不遵循良好设计的API,那么你最终写不好可供使用的API服务。希望在本文之后,您将更好地了解一个行为良好的API应该是什么样的。
什么是JSON API?
在JSON之前,有XML。使用XML和JSON两者,毫无疑问,JSON是明确的赢家。我不会深入讨论一个JSON API的概念,因为它具有很详细的说明在这个网站上:jsonapi.org
一个简单的Web Server
RESTful服务首先从根本上成为一个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)) }
运行此示例将启动端口8080上的服务器,并可以访问 http://localhost:8080
访问效果如下图所示:
添加路由功能
Go的标准库也附带了路由器,但我发现大多数人对它的工作感到困惑。我在项目中使用了几个第三方路由器。最值得注意的是,我使用了Gorilla Web Toolkit的mux路由器。
另外一个非常著名的路由是httprouter,作者Julien Schmidt.
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
这将从GitHub“github.com/gorilla/mux”中检索Gorilla Mux软件包.
上述示例创建一个基本路由器,添加route /并分配在处理该端点时要运行的Index处理程序。您还会注意到,如果你想访问 http://localhost:8080/foo由于没有定义路由,它将没有任何响应。目前的代码只能响应 http://localhost:8080/
创建一些基本路由
现在我们有路由器到位,现在是创建更多路由的时候了。 我们假设我们要创建一个基本的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) }
我们现在已经添加了两个端点(或路由)。
一个是todo程序的首页路由: http://localhost:8080/todos
另一个是todo程序的展示路由:http://localhost:8080/todos/{todoId}
这是RESTful设计的开始。
请查看todo程序的展示路由:http://localhost:8080/todos/{todoId} ,我们可以通过给路由传入 id 然后获取对应id的数据记录。
一个基础的Model
现在我们有了路由,现在是创建一个基本的Todo模型,我们可以发送和检索数据。在Go中,结构体通常用作您的模型。许多其他语言也一样使用类来定义模型。
package main import "time" type Todo struct { Name string Completed bool Due time.Time } type Todos []Todo
请注意,在最后一行,我们创建另一个类型,称为Todos,它是Todo的切片(有序集合)。你会看到这在哪里变得很有用。
返回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的静态片段(模拟数据)来发送给客户端。打开浏览器访问:,你应该得到以下响应:
[ { "Name": "Write presentation", "Completed": false, "Due": "0001-01-01T00:00:00Z" }, { "Name": "Host meetup", "Completed": false, "Due": "0001-01-01T00:00:00Z" } ]
一个更好的Model
对于任何经验的开发人员,很容易发现一个问题,json字符串,是建议不该出现大写字母的,所以,为了修改全部键名为小写,我们使用下面的model定义:
package main import "time" type Todo struct { Name string `json:"name"` Completed bool `json:"completed"` Due time.Time `json:"due"` } type Todos []Todo
通过添加结构标签,您可以准确控制如何将结构体编组为JSON。
组织项目文件结构
在这个时候,项目需要一些重构。我们的文件如果不能清晰的放置,代码将会很乱,难以维护。
我们现在要创建以下文件并相应地移动代码:
- main.go
- handlers.go
- routes.go
- todo.go
Handlers.go:
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) }
Routes.go
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, }, }
Todo.go
package main import "time" type Todo struct { Name string `json:"name"` Completed bool `json:"completed"` Due time.Time `json:"due"` } type Todos []Todo
Main.go
package main import ( "log" "net/http" ) func main() { router := NewRouter() log.Fatal(http.ListenAndServe(":8080", router)) }
更加全面的路由
作为我们重构的一部分,我们创建了更加全面的路由,Routes.go 文件中的路由包含了更多的路由信息。比如我们现在可以指定请求路由的方法,比如GET, POST, DELETE等等。
web日志
在重构过程中,我发现在Handlers.go文件中,我们是非常容易加上一些额外的功能,比如日志功能。像大多数流行的服务器提供日志输出那样,我们可以用Go来实现一个日志功能。当然在Go的标准库里是没有日志库,我们需要自己实现。
我将创建一个文件 logger.go 并且加入下面的代码:
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), ) }) }
这是Go中非常标准的有效的输出log方法,我们将把我们的处理程序传递给这个函数,然后它将使用记录和定时功能包装所传递的处理程序。
接下来,我们将需要在我们的路由中使用该方法。
使用Log装饰器
要应用装饰器,当我们创建路由器时,我们将通过更新我们的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 }
然后,当我们访问 http://localhost:8080/todos 的时候,你将会看到下面的log输出:
但是我们发现这个路由文件太大了,代码又很乱,继续重构,我们把Routes.go文件拆分为以下两个文件:
- Routes.go
- Router.go
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, }, }
Router.go:
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 }
那么现在我们便有了很不错的模板了。现在我们重新访问我们的网站,我们添加一些任务,然后我们在额外的添加一些代码。
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) } }
上面代码我们做了两件事情。第一,我们输出了我们的content type,告诉客户端我们输出的是json文本。第二我们明确的设置了http的状态码。
Go的net / http服务器会尝试猜测我们的输出内容类型(但是并不总是准确的),但是由于我们明确地知道类型,所以我们应该始终设置它们。
那么,涉及到数据库操作呢?
显然,如果我们要创建一个RESTful API,我们需要采用持久化方法,比如数据库(mysql, mongodb...)然而,这超出了本文的范围,所以我们将简单地创建一个非常粗糙的(而不是线程安全的)模拟数据库。
创建一个文件: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属性
现在我们便有了一个模拟的数据库,我们如果想使用的话,最好分配id属性,这样子方便我们进行CRUD的操作。
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
更新ToDo显示方法
要使用数据库,我们现在需要通过修改以下函数来检索Todo中的数据:
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) } }
上面做的都是显示Todo的方法,下面我们要增加一个插入Todo的方法。
POST JSON方法
添加如下的路由在Routes.go文件中:
Route{ "TodoCreate", "POST", "/todos", TodoCreate, },
然后紧接着创建我们的处理方法:TodoCreate
在Handlers.go中增加该方法:
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) } }
这个方法主要做了如下工作:
我们首先做的是打开请求的正文。请注意,我们使用io.LimitReader。这是防止您的服务器上的恶意攻击的好方法。想象一下,如果有人想给你发送500GB的json!
在我们阅读了消息体之后,我们再将它分配到我们的Todo结构体上。如果失败了,我们将做正确的事情,不仅仅是以适当的状态代码422进行响应,而且我们也会在json字符串中发回错误。这样可以让客户了解不仅出现问题,而且我们有能力沟通出什么问题。
最后,如果一切顺利,我们发回201的状态码,这意味着实体成功创建。我们还发回我们创建的实体的json表示,因为它包含客户端可能需要下一步的id。
测试发送一些json
现在我们用模拟的数据库以及我们创建的路由来测试下。我使用curl通过以下命令来执行:
curl -H "Content-Type: application/json" -d '{"name":"New Todo"}' http://localhost:8080/todos
如果访问网站的话,我们会看到新增加的数据过来了:
我们未涉及到的内容
虽然我们是一个开始,但还有很多事要做。我们没有解决的事情是:
- api版本控制。比如我们需要增加/api/v1/, /api/v2/等路由
- 权限认证。除非这是一个免费/公开的API,否则我们可能需要一些身份验证。我推荐使用JSON Web Token
还有一些什么呢?
与所有项目一样,从小到大过程中慢慢就会失控。如果我要把它提升到一个新的水平,并且准备好生产,这些只是一些额外的事情:
- 大量的重构
- 为几个这些文件创建包,例如一些JSON助手,装饰器,处理程序等等。
- 测试...哦,是的,你不能忘记。我们没有在这里做任何测试。对于生产系统来说,这是必须的。
我可以获取本博客的源码么?
源码地址:
CSDN:http://download.csdn.net/detail/yuan8222/9818336
github:https://github.com/corylanou/tns-restful-json-api
总结
对我来说最重要的是记住要建立一个负责任的API。发送正确的状态代码,头部标识等对于广泛采用API至关重要。我希望这篇文章可以让你开始自己的API!
文章的脚注信息由WordPress的wp-posturl插件自动生成