golang创建RESTful JSON API
一、创建基础http server
使用golang的net/http模块,可以很容易的创建一个http server服务器,如下:
1// from www.361way.com 运维之路
2package main
3import (
4 "fmt"
5 "html"
6 "log"
7 "net/http"
8)
9func main() {
10 http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
11 fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
12 })
13 log.Fatal(http.ListenAndServe(":8080", nil))
14}
使用go run 运维该代码时,可以通过 curl http://127.0.0.1:8080 访问到该web server 。
二、带路由的http server
官方提供的库里关于路由的配置是比较不友好的,在golang下常用的路由模块有两个:
- mux router (from the Gorilla Web Toolkit)
- httprouter
这里我们使用前者,代码如下:
1package main
2import (
3 "fmt"
4 "html"
5 "log"
6 "net/http"
7 "github.com/gorilla/mux"
8)
9func main() {
10 router := mux.NewRouter().StrictSlash(true)
11 router.HandleFunc("/", Index)
12 log.Fatal(http.ListenAndServe(":8080", router))
13}
14func Index(w http.ResponseWriter, r *http.Request) {
15 fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
不过上面的代码运行的时候,当访问 http://localhost:8080/foo 这样的URL时并不会成功,因为其定义里只给匹配了/ (其对应的函数Index)。当有多个不同的路径的URL需要访问的时候 ,可以使用router函数配合不同的函数进行处理,如下:
1// by www.361way.com
2package main
3import (
4 "fmt"
5 "log"
6 "net/http"
7 "github.com/gorilla/mux"
8)
9func main() {
10 router := mux.NewRouter().StrictSlash(true)
11 router.HandleFunc("/", Index)
12 router.HandleFunc("/todos", TodoIndex)
13 router.HandleFunc("/todos/{todoId}", TodoShow)
14 log.Fatal(http.ListenAndServe(":8080", router))
15}
16func Index(w http.ResponseWriter, r *http.Request) {
17 fmt.Fprintln(w, "Welcome!")
18}
19func TodoIndex(w http.ResponseWriter, r *http.Request) {
20 fmt.Fprintln(w, "Todo Index!")
21}
22func TodoShow(w http.ResponseWriter, r *http.Request) {
23 vars := mux.Vars(r)
24 todoId := vars["todoId"]
25 fmt.Fprintln(w, "Todo show:", todoId)
26}
上面的代码我们增加了两个类型的方法,具体访问的URL类型是:
1http://localhost:8080/todos
2http://localhost:8080/todos/{todoId
三、增加json数据类型
我们增加一个基础的数据模型:
1package main
2import "time"
3type Todo struct {
4 Name string
5 Completed bool
6 Due time.Time
7}
8type Todos []Todo
上面的数据类型可以通过如下的方法进行调用:
1func TodoIndex(w http.ResponseWriter, r *http.Request) {
2 todos := Todos{
3 Todo{Name: "Write presentation"},
4 Todo{Name: "Host meetup"},
5 }
6 json.NewEncoder(w).Encode(todos)
7}
当我们通过http://localhost:8080/todos进行访问时,将返回如下类型的数据:
1[
2 {
3 "Name": "Write presentation",
4 "Completed": false,
5 "Due": "0001-01-01T00:00:00Z"
6 },
7 {
8 "Name": "Host meetup",
9 "Completed": false,
10 "Due": "0001-01-01T00:00:00Z"
11 }
12]
上面最初开始定义的数据类型,我们还可以再进行下优化,直接指定其数据类型为json格式,如下:
1package main
2import "time"
3type Todo struct {
4 Name string `json:"name"`
5 Completed bool `json:"completed"`
6 Due time.Time `json:"due"`
7}
8type Todos []Todo
四、按功能进行代码切分
在实际写代码时,我们不可能把所有的代码全堆积在一到两个文件里,根据MVC模型,一般我们会根据功能进行细分代码,这里将代码细分为了如下四个:
- main.go
- handlers.go
- routes.go
- todo.go
1、Handlers.go代码如下:
1package main
2import (
3 "encoding/json"
4 "fmt"
5 "net/http"
6 "github.com/gorilla/mux"
7)
8func Index(w http.ResponseWriter, r *http.Request) {
9 fmt.Fprintln(w, "Welcome!")
10}
11func TodoIndex(w http.ResponseWriter, r *http.Request) {
12 todos := Todos{
13 Todo{Name: "Write presentation"},
14 Todo{Name: "Host meetup"},
15 }
16 if err := json.NewEncoder(w).Encode(todos); err != nil {
17 panic(err)
18 }
19}
20func TodoShow(w http.ResponseWriter, r *http.Request) {
21 vars := mux.Vars(r)
22 todoId := vars["todoId"]
23 fmt.Fprintln(w, "Todo show:", todoId)
24}
2、Routes.go代码
1package main
2import (
3 "net/http"
4 "github.com/gorilla/mux"
5)
6type Route struct {
7 Name string
8 Method string
9 Pattern string
10 HandlerFunc http.HandlerFunc
11}
12type Routes []Route
13func NewRouter() *mux.Router {
14 router := mux.NewRouter().StrictSlash(true)
15 for _, route := range routes {
16 router.
17 Methods(route.Method).
18 Path(route.Pattern).
19 Name(route.Name).
20 Handler(route.HandlerFunc)
21 }
22 return router
23}
24var routes = Routes{
25 Route{
26 "Index",
27 "GET",
28 "/",
29 Index,
30 },
31 Route{
32 "TodoIndex",
33 "GET",
34 "/todos",
35 TodoIndex,
36 },
37 Route{
38 "TodoShow",
39 "GET",
40 "/todos/{todoId}",
41 TodoShow,
42 },
43}
3、Todo.go代码
1package main
2import "time"
3type Todo struct {
4 Name string `json:"name"`
5 Completed bool `json:"completed"`
6 Due time.Time `json:"due"`
7}
8type Todos []Todo
4、Main.go代码
1package main
2import (
3 "log"
4 "net/http"
5)
6func main() {
7 router := NewRouter()
8 log.Fatal(http.ListenAndServe(":8080", router))
9}
根据上面拆分后,我们也可以很容易的进行 GET, POST, DELETE请求类型的变化。
五、增加web日志输出
1package main
2import (
3 "log"
4 "net/http"
5 "time"
6)
7func Logger(inner http.Handler, name string) http.Handler {
8 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
9 start := time.Now()
10 inner.ServeHTTP(w, r)
11 log.Printf(
12 "%s\t%s\t%s\t%s",
13 r.Method,
14 r.RequestURI,
15 name,
16 time.Since(start),
17 )
18 })
19}
在代码中应用该logger生成器,如下:
1func NewRouter() *mux.Router {
2 router := mux.NewRouter().StrictSlash(true)
3 for _, route := range routes {
4 var handler http.Handler
5 handler = route.HandlerFunc
6 handler = Logger(handler, route.Name)
7 router.
8 Methods(route.Method).
9 Path(route.Pattern).
10 Name(route.Name).
11 Handler(handler)
12 }
13 return router
14}
当使用 http://localhost:8080/todos 进行页面访问时,在屏幕上会有相应的访问日志打印输出,具体格式和上面定义的一致。
六、重构路由
将路由文件分割为router.go和 routes.go两个。Routes.go代码如下:
1package main
2import "net/http"
3type Route struct {
4 Name string
5 Method string
6 Pattern string
7 HandlerFunc http.HandlerFunc
8}
9type Routes []Route
10var routes = Routes{
11 Route{
12 "Index",
13 "GET",
14 "/",
15 Index,
16 },
17 Route{
18 "TodoIndex",
19 "GET",
20 "/todos",
21 TodoIndex,
22 },
23 Route{
24 "TodoShow",
25 "GET",
26 "/todos/{todoId}",
27 TodoShow,
28 },
29}
Router.go代码如下:
1package main
2import (
3 "net/http"
4 "github.com/gorilla/mux"
5)
6func NewRouter() *mux.Router {
7 router := mux.NewRouter().StrictSlash(true)
8 for _, route := range routes {
9 var handler http.Handler
10 handler = route.HandlerFunc
11 handler = Logger(handler, route.Name)
12 router.
13 Methods(route.Method).
14 Path(route.Pattern).
15 Name(route.Name).
16 Handler(handler)
17 }
18 return router
19}
接下来再修改下TodoIndex 函数,增加响应头,增加了两行数据,代码如下:
1func TodoIndex(w http.ResponseWriter, r *http.Request) {
2 todos := Todos{
3 Todo{Name: "Write presentation"},
4 Todo{Name: "Host meetup"},
5 }
6 w.Header().Set("Content-Type", "application/json; charset=UTF-8")
7 w.WriteHeader(http.StatusOK)
8 if err := json.NewEncoder(w).Encode(todos); err != nil {
9 panic(err)
10 }
11}
七、数据库处理
该演示中并未中增加数据加处理,只不过在代码里增加了数据自增和查询的处理部分。我们创建一个repo.go文件,代码如下:
1package main
2import "fmt"
3var currentId int
4var todos Todos
5// Give us some seed data
6func init() {
7 RepoCreateTodo(Todo{Name: "Write presentation"})
8 RepoCreateTodo(Todo{Name: "Host meetup"})
9}
10func RepoFindTodo(id int) Todo {
11 for _, t := range todos {
12 if t.Id == id {
13 return t
14 }
15 }
16 // return empty Todo if not found
17 return Todo{}
18}
19func RepoCreateTodo(t Todo) Todo {
20 currentId += 1
21 t.Id = currentId
22 todos = append(todos, t)
23 return t
24}
25func RepoDestroyTodo(id int) error {
26 for i, t := range todos {
27 if t.Id == id {
28 todos = append(todos[:i], todos[i+1:]...)
29 return nil
30 }
31 }
32 return fmt.Errorf("Could not find Todo with id of %d to delete", id)
33}
对最初的数据结构里增加id项,如下:
1package main
2import "time"
3type Todo struct {
4 Id int `json:"id"`
5 Name string `json:"name"`
6 Completed bool `json:"completed"`
7 Due time.Time `json:"due"`
8}
9type Todos []Todo
更新TodoIndex函数,如下:
1func TodoIndex(w http.ResponseWriter, r *http.Request) {
2 w.Header().Set("Content-Type", "application/json; charset=UTF-8")
3 w.WriteHeader(http.StatusOK)
4 if err := json.NewEncoder(w).Encode(todos); err != nil {
5 panic(err)
6 }
7}
routes.go 文件中增加post data方法,如下:
1Route{
2 "TodoCreate",
3 "POST",
4 "/todos",
5 TodoCreate,
6},
在handlers 文件中增加TodoCreate方法,如下:
1func TodoCreate(w http.ResponseWriter, r *http.Request) {
2 var todo Todo
3 body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1048576))
4 if err != nil {
5 panic(err)
6 }
7 if err := r.Body.Close(); err != nil {
8 panic(err)
9 }
10 if err := json.Unmarshal(body, &todo); err != nil {
11 w.Header().Set("Content-Type", "application/json; charset=UTF-8")
12 w.WriteHeader(422) // unprocessable entity
13 if err := json.NewEncoder(w).Encode(err); err != nil {
14 panic(err)
15 }
16 }
17 t := RepoCreateTodo(todo)
18 w.Header().Set("Content-Type", "application/json; charset=UTF-8")
19 w.WriteHeader(http.StatusCreated)
20 if err := json.NewEncoder(w).Encode(t); err != nil {
21 panic(err)
22 }
23}
接下来进行post数据测试,如下:
1curl -H "Content-Type: application/json" -d '{"name":"New Todo"}' http://localhost:8080/todos
2Now, if you go to http://localhost/todos we should see the following response:
3[
4 {
5 "id": 1,
6 "name": "Write presentation",
7 "completed": false,
8 "due": "0001-01-01T00:00:00Z"
9 },
10 {
11 "id": 2,
12 "name": "Host meetup",
13 "completed": false,
14 "due": "0001-01-01T00:00:00Z"
15 },
16 {
17 "id": 3,
18 "name": "New Todo",
19 "completed": false,
20 "due": "0001-01-01T00:00:00Z"
21 }
22]
从上面的代码可以看到,增加了一条记录。
八、其他
当然上面的代码是一个公供的API,如果是非公共的API,还需要增加认证处理,比如常见的JWT模板(JSON web tokens)。
该篇翻译自:https://thenewstack.io/make-a-restful-json-api-go/
该篇涉及的相关代码我已上传github: https://github.com/361way/golang/tree/master/http/restful-json-api
捐赠本站(Donate)
如您感觉文章有用,可扫码捐赠本站!(If the article useful, you can scan the QR code to donate))
- Author: shisekong
- Link: https://blog.361way.com/go-restful-json-api/6049.html
- License: This work is under a 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议. Kindly fulfill the requirements of the aforementioned License when adapting or creating a derivative of this work.