一、创建基础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下常用的路由模块有两个:

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

 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