golang创建RESTful JSON API

2019年3月15日 发表评论 阅读评论

一、创建基础http server

使用golang的net/http模块,可以很容易的创建一个http server服务器,如下:

// from www.361way.com 运维之路
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))
}

使用go run 运维该代码时,可以通过 curl http://127.0.0.1:8080 访问到该web server 。

二、带路由的http server

官方提供的库里关于路由的配置是比较不友好的,在golang下常用的路由模块有两个:

这里我们使用前者,代码如下:

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))

不过上面的代码运行的时候,当访问 http://localhost:8080/foo 这样的URL时并不会成功,因为其定义里只给匹配了/ (其对应的函数Index)。当有多个不同的路径的URL需要访问的时候 ,可以使用router函数配合不同的函数进行处理,如下:

// by www.361way.com
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)
}

上面的代码我们增加了两个类型的方法,具体访问的URL类型是:

http://localhost:8080/todos
http://localhost:8080/todos/{todoId

三、增加json数据类型

我们增加一个基础的数据模型:

package main
import "time"
type Todo struct {
    Name      string
    Completed bool
    Due       time.Time
}
type Todos []Todo

上面的数据类型可以通过如下的方法进行调用:

func TodoIndex(w http.ResponseWriter, r *http.Request) {
    todos := Todos{
        Todo{Name: "Write presentation"},
        Todo{Name: "Host meetup"},
    }
    json.NewEncoder(w).Encode(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"
    }
]

上面最初开始定义的数据类型,我们还可以再进行下优化,直接指定其数据类型为json格式,如下:

package main
import "time"
type Todo struct {
    Name      string    `json:"name"`
    Completed bool      `json:"completed"`
    Due       time.Time `json:"due"`
}
type Todos []Todo

四、按功能进行代码切分

在实际写代码时,我们不可能把所有的代码全堆积在一到两个文件里,根据MVC模型,一般我们会根据功能进行细分代码,这里将代码细分为了如下四个:

  • main.go
  • handlers.go
  • routes.go
  • todo.go

1、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)
}

2、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,
    },
}

3、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

4、Main.go代码

package main
import (
    "log"
    "net/http"
)
func main() {
    router := NewRouter()
    log.Fatal(http.ListenAndServe(":8080", router))
}

根据上面拆分后,我们也可以很容易的进行 GET, POST, DELETE请求类型的变化。

五、增加web日志输出

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),
        )
    })
}

在代码中应用该logger生成器,如下:

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进行页面访问时,在屏幕上会有相应的访问日志打印输出,具体格式和上面定义的一致。

六、重构路由

将路由文件分割为router.go和 routes.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
}

接下来再修改下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)
    }
}

七、数据库处理

该演示中并未中增加数据加处理,只不过在代码里增加了数据自增和查询的处理部分。我们创建一个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)
}

对最初的数据结构里增加id项,如下:

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

更新TodoIndex函数,如下:

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)
    }
}

routes.go 文件中增加post data方法,如下:

Route{
    "TodoCreate",
    "POST",
    "/todos",
    TodoCreate,
},

在handlers 文件中增加TodoCreate方法,如下:

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)
    }
}

接下来进行post数据测试,如下:

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"
    }
]

从上面的代码可以看到,增加了一条记录。

八、其他

当然上面的代码是一个公供的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




本站的发展离不开您的资助,金额随意,欢迎来赏!

You can donate through PayPal.
My paypal id: itybku@139.com
Paypal page: https://www.paypal.me/361way

  1. 本文目前尚无任何评论.
  1. 本文目前尚无任何 trackbacks 和 pingbacks.