Dawn's Blogs

分享技术 记录成长

0%

在本节,最终的代码目录结构如下:

1
2
3
4
5
6
7
8
9
dain/
|--context.go
|--dain.go
|--logger.go
|--router.go
|--trie.go
|--go.mod
main.go
go.mod

实现目标

实现中间件的添加以及 Logger 中间件(用于记录请求处理时间和响应码)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
"DawnGin/dain"
"net/http"
)

func main() {
e := dain.New()

// 使用 Logger 中间件
e.Use(dain.Logger())

e.Get("/hello/:name", func(c *dain.Context) {
name := c.Param("name")
c.String(http.StatusOK, "Hello, you are %v, URL path = %v", name, c.Path)
})

// 分组路由
v1 := e.Group("/v1")
{
v1.Get("/video/:name", func(c *dain.Context) {
videoName := c.Param("name")
c.String(http.StatusOK, "Hello, this is v1 group, video name = %v, path = %v", videoName, c.Path)
})
}

e.Run(":9000")
}

中间件

Context

dain/context

在上下文 Context 中需要保存中间件信息,以及需要保存执行到第几个中间件了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type Context struct {
// HTTP 请求 响应
Writer http.ResponseWriter
Req *http.Request
// 请求信息
Path string // 请求路径
Method string // 请求方法
Params map[string]string // 路由参数,如 /hello/:user 匹配 /hello/dawn,则 Params["user"]=dawn
// 响应信息
StatusCode int // 响应码
// 中间件
handlers []HandlerFunc // 存储中间件
index int // 执行的中间件下标
}

func NewContext(w http.ResponseWriter, r *http.Request) *Context {
return &Context{
Writer: w,
Req: r,
Path: r.URL.Path,
Method: r.Method,
index: -1,
}
}

c.Next 方法可以执行下一个中间件,实现在执行下一个中间件之后再进行一些额外的操作:

1
2
3
4
5
6
7
// Next 执行下一个中间件
func (c *Context) Next() {
c.index++
for ; c.index < len(c.handlers); c.index++ {
c.handlers[c.index](c)
}
}

将中间件应用到 Group

dain/dain.go

定义 Use 函数,用于将中间件添加到 Group 中。

1
2
3
4
// Use 为分组添加中间件
func (group *RouterGroup) Use(middlewares ...HandlerFunc) {
group.middleware = append(group.middleware, middlewares...)
}

同时,需要重写 ServeHTTP 函数,当我们接收到一个具体请求时,要判断该请求适用于哪些中间件,在这里我们简单通过 URL 的前缀来判断,将对应的中间件加入到 context 中。

1
2
3
4
5
6
7
8
9
10
// 实现 http.Handler 接口,自定义路由器
func (e *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
c := NewContext(w, r)
for _, group := range e.groups {
if strings.HasPrefix(r.URL.Path, group.prefix) {
c.handlers = append(c.handlers, group.middleware...)
}
}
e.router.handle(c)
}

修改 router

dain/router.go

需要修改 router.handle 方法,将最后的请求处理逻辑 Handler 添加在中间件的最后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// handle 实现路由功能
func (r *router) handle(c *Context) {
// 在前缀树种查找路由,获取路由参数
n, params := r.getRoute(c.Method, c.Path)

if n != nil {
// 匹配路由
key := c.Method + "-" + n.pattern
c.Params = params
c.handlers = append(c.handlers, r.handlers[key])
} else {
c.handlers = append(c.handlers, func(c *Context) {
c.String(http.StatusNotFound, "404 NOT FOUND FOR PATH: %v", c.Path)
})
}

c.Next()
}

Logger 中间件

dain/logger.go

预定义一个 Logger 中间件,用于记录每一个请求的处理时间、响应码

1
2
3
4
5
6
7
8
9
10
func Logger() HandlerFunc {
return func(c *Context) {
// 开始时间
startTime := time.Now()
// 向后处理请求
c.Next()
// 处理结束,输出日志
log.Printf("[%d] %s in %v", c.StatusCode, c.Req.RequestURI, time.Since(startTime))
}
}

在本节,最终的代码目录结构如下:

1
2
3
4
5
6
7
8
dain/
|--context.go
|--dain.go
|--router.go
|--trie.go
|--go.mod
main.go
go.mod

实现目标

可以对路由进行分组,每一组内的路由都可以有相似用途,也对分组定义中间件。

main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"DawnGin/dain"
"net/http"
)

func main() {
e := dain.New()

// 分组路由
v1 := e.Group("/v1")
{
v1.Get("/video/:name", func(c *dain.Context) {
videoName := c.Param("name")
c.String(http.StatusOK, "Hello, this is v1 group, video name = %v, path = %v", videoName, c.Path)
})
}

e.Run(":9000")
}

分组控制

dain/dain.go

结构体

我们需要通过前缀来区分分组路由,同时还需要记录当前分组的上一层、应用于当前分组的中间件以及最顶层的 Engine

1
2
3
4
5
6
7
// RouterGroup 分组路由
type RouterGroup struct {
prefix string // 当前分组的公共前缀
parent *RouterGroup // 记录当前分组的上一层
middleware []HandlerFunc // 记录中间件
engine *Engine // 记录所属的 Engine
}

可以将 Engine 作为最顶层的分组,也就是说 Engine 具有 RouterGroup 的所有能力:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Engine struct {
// 路由器
router *router
// 继承 RouterGroup,把根也看作是一个分组
*RouterGroup
// 记录所有的路由分组
groups []*RouterGroup
}

// New 返回一个 Engine 指针
func New() *Engine {
engine := &Engine{router: NewRouter()}
engine.RouterGroup = &RouterGroup{engine: engine}
engine.groups = []*RouterGroup{engine.RouterGroup}

return engine
}

添加分组

通过 Group 函数可以添加分组:

1
2
3
4
5
6
7
8
9
10
func (group *RouterGroup) Group(prefix string) *RouterGroup {
engine := group.engine
newGroup := &RouterGroup{
prefix: group.prefix + prefix,
parent: group,
engine: engine,
}
engine.groups = append(engine.groups, newGroup) // 在 Engine 中保存新的分组
return newGroup
}

注册路由

因为分组也可以注册路由,所以将 Engine 的路由注册方法更改为 RouterGroup 的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// addRouter 实现路由注册功能
func (group *RouterGroup) addRouter(method, pattern string, handler HandlerFunc) {
pattern = group.prefix + pattern
group.engine.router.addRouter(method, pattern, handler)
}

// Get 路由注册 GET 请求方式
func (group *RouterGroup) Get(pattern string, handler HandlerFunc) {
group.addRouter("GET", pattern, handler)
}

// Post 路由注册 POST 请求方式
func (group *RouterGroup) Post(pattern string, handler HandlerFunc) {
group.addRouter("POST", pattern, handler)
}

在本节,最终的代码目录结构如下:

1
2
3
4
5
6
7
8
dain/
|--context.go
|--dain.go
|--router.go
|--trie.go
|--go.mod
main.go
go.mod

实现目标

利用前缀树(Trie 树)实现动态路由解析,并且支持 :name*filename 两种模式。

main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main() {
e := dain.New()

e.Get("/hello/:name", func(c *dain.Context) {
name := c.Param("name")
c.String(http.StatusOK, "Hello, you are %v, URL path = %v", name, c.Path)
})

e.Get("/file/*filename", func(c *dain.Context) {
filename := c.Param("filename")
c.JSON(http.StatusOK, dain.H{
"filename": filename,
"msg": "OK",
})
})

e.Run(":9000")
}

Trie 树

dain/trie.go

在之前的版本种,使用了 map 来存储路由与处理函数的映射,但是这不能支持动态路由。

Trie 树(前缀树)可以实现动态路由,一个节点的所有子节点都有相同的前缀。

/:user/info/:user/doc/p/video/p/book/file/*filepath 对应的前缀树如下所示:

1
2
3
4
5
6
7
                      /
_______________|______________
| | |
:user p file
___|___ ___|___ |
| | | | |
info doc video book *filepath

节点结构

首先需要知道前缀树的节点的结构。

其中,matchChild 匹配第一个节点,用于将剩余路由信息插入到匹配的节点之下。matchChildren 用于匹配所有的节点,用于查找请求的路径信息是否匹配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
type node struct {
pattern string // 待匹配的路由,只在pattern定义的最后一个节点存储
part string // 路由中的一部分
children []*node // 叶子节点
isWild bool // 若不是精确匹配则为 true;否则为 false
}

// matchChild 匹配第一个节点,用于插入
func (n *node) matchChild(part string) *node {
for _, child := range n.children {
if child.part == part || child.isWild {
return child
}
}

return nil
}

// matchChildren 匹配所有的节点,用于查找
func (n *node) matchChildren(part string) []*node {
children := make([]*node, 0)
for _, child := range n.children {
if child.part == part || child.isWild {
children = append(children, child)
}
}

return children
}

插入和搜索

添加路由时,需要在前缀树中插入节点。

匹配路由时,需要查找当前请求的路径是否能匹配到节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// insert 插入节点
func (n *node) insert(pattern string, parts []string, height int) {
if len(parts) == height {
// 到达叶子节点
// 仅仅在pattern定义的最后一个节点存储
n.pattern = pattern
return
}

part := parts[height]
child := n.matchChild(part)
if child == nil {
// 若当前层没有匹配,新建一个节点,并插入到孩子节点中
child = &node{part: part, isWild: part[0] == ':' || part[0] == '*'}
n.children = append(n.children, child)
}
// 向下一层继续插入
child.insert(pattern, parts, height+1)
}

// search 查找节点
func (n *node) search(parts []string, height int) *node {
if len(parts) == height || strings.HasPrefix(n.part, "*") {
// 到达叶子节点 或者 匹配到“*”
if n.pattern == "" {
return nil
}
return n
}

part := parts[height]
children := n.matchChildren(part)

for _, child := range children {
// 在下一层中继续查找
result := child.search(parts, height+1)
if result != nil {
// 找到节点
return result
}
}

return nil
}

路由器 Router

dain/router.go

router 结构

在 router 中记录前缀树的根节点 roots,用于记录和匹配路由;handlers 用于记录 pattern 和 HandlerFunc 的映射关系:

1
2
3
4
5
6
7
8
9
10
11
type router struct {
roots map[string]*node // 保存 trie 树的根,key为 Method,value为树根
handlers map[string]HandlerFunc // 保存 pattern 与 HandlerFunc 的映射关系
}

func NewRouter() *router {
return &router{
roots: make(map[string]*node),
handlers: make(map[string]HandlerFunc),
}
}

添加路由

因为增加了前缀树用来保存路由 pattern,所以需要在添加路由的同时在前缀树中插入节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// parsePattern 解析 pattern,返回对应的 parts(路由中的一部分)
// 如 pattern 为 /hello/world,那么对应的 parts 为 []{"hello", "world"}
func parsePattern(pattern string) []string {
vs := strings.Split(pattern, "/")

parts := make([]string, 0, len(vs))
for _, item := range vs {
if item != "" {
// 不为空,加入到 parts 中
parts = append(parts, item)
if item[0] == '*' {
// 遇到通配符直接退出
break
}
}
}

return parts
}

// addRouter 实现路由注册功能
func (r *router) addRouter(method, pattern string, handler HandlerFunc) {
log.Printf("Router %v - %v\n", method, pattern)
key := method + "-" + pattern

_, ok := r.roots[method]
if !ok {
r.roots[method] = &node{}
}

parts := parsePattern(pattern)
r.roots[method].insert(pattern, parts, 0) // 插入到前缀树中
r.handlers[key] = handler
}

路由功能

添加一个函数 getRouter 用于查找对应的叶子节点和路由参数,并且重新实现 handle 路由功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// getRoute 根据请求的 method 和 path,找到对应的前缀树叶子节点和路由参数
func (r *router) getRoute(method, path string) (*node, map[string]string) {
searchParts := parsePattern(path) // 查找的 parts
params := make(map[string]string)
root, ok := r.roots[method]
if !ok {
// 方法没有定义路由,直接返回
return nil, nil
}

n := root.search(searchParts, 0) // 查找叶子节点
if n != nil {
// 可以找到
parts := parsePattern(n.pattern)
for index, part := range parts {
if part[0] == ':' {
params[part[1:]] = searchParts[index]
}
if part[0] == '*' && len(part) > 1 {
params[part[1:]] = strings.Join(searchParts[index:], "/")
break
}
}

return n, params
}

return nil, nil
}

// handle 实现路由功能
func (r *router) handle(c *Context) {
// 在前缀树种查找路由,获取路由参数
n, params := r.getRoute(c.Method, c.Path)

if n != nil {
// 匹配路由
key := c.Method + "-" + n.pattern
c.Params = params
r.handlers[key](c)
} else {
fmt.Fprintf(c.Writer, "404 NOT FOUND FOR PATH: %v", c.Path)
}
}

Context

dain/context.go

Context 结构

为了访问到路由参数,所以需要修改 Context 结构体,向其中添加 Params,用来记录路由参数:

1
2
3
4
5
6
7
8
9
10
11
type Context struct {
// HTTP 请求 响应
Writer http.ResponseWriter
Req *http.Request
// 请求信息
Path string // 请求路径
Method string // 请求方法
Params map[string]string // 路由参数,如 /hello/:user 匹配 /hello/dawn,则 Params["user"]=dawn
// 响应信息
StatusCode int // 响应码
}

获取路由参数

增加可以用过键值获取相应路由参数的函数:

1
2
3
4
5
// Param 获取路由参数
func (c *Context) Param(key string) string {
value, _ := c.Params[key]
return value
}

在本节,最终的代码目录结构如下:

1
2
3
4
5
6
7
dain/
|--context.go
|--dain.go
|--router.go
|--go.mod
main.go
go.mod

实现目标

最终实现得效果如下,此时更加接近于 GIN:

  • Handler 得参数变为 dain.Context,同时提供了对表单和 URL 的查询 PostForm/Query
  • dain.Context 同 GIN 一样,封装了 HTML/String/JSON/Data 函数,快速构建 HTTP 响应。

main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
"DawnGin/dain"
"net/http"
)

func main() {
e := dain.New()

e.Get("/", func(c *dain.Context) {
c.HTML(http.StatusOK, "<h1>Hello Dawn</h1>")
})

e.Get("/hello", func(c *dain.Context) {
c.String(http.StatusOK, "Hello World, URL path = %v", c.Path)
})

e.Post("/login", func(c *dain.Context) {
c.JSON(http.StatusOK, dain.H{
"username": c.PostForm("username"),
"password": c.PostForm("password"),
})
})

e.Run(":9000")
}

构建上下文

dain/context.go

上下文 Context 即一个请求的上下文,它随着每一个请求的出现而产生,响应的结束而销毁。

Context 结构

Context 可以对一些代码进行封装,使用起来更加简便。首先看 Context 的结构:

  • dain.H 同 gin.H 的作用一样,是 map[string]interface{} 的简便写法。
  • 在 Context 中,保存了一次HTTP的请求 Req 和响应 Writer,同时保存了一些请求和响应信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type H map[string]interface{}

type Context struct {
// HTTP 请求 响应
Writer http.ResponseWriter
Req *http.Request
// 请求信息
Path string // 请求路径
Method string // 请求方法
// 响应信息
StatusCode int // 响应码
}

func NewContext(w http.ResponseWriter, r *http.Request) *Context {
return &Context{
Writer: w,
Req: r,
Path: r.URL.Path,
Method: r.Method,
}
}

获取请求数据

封装关于获取请求数据的函数 PostForm/Query

1
2
3
4
5
6
7
8
9
// PostForm 根据 key 获取第一个表单数据
func (c *Context) PostForm(key string) string {
return c.Req.FormValue(key)
}

// Query 根据 key 获取请求的 query 数据
func (c *Context) Query(key string) string {
return c.Req.URL.Query().Get(key)
}

改变 HTTP 响应头

封装改变 HTTP 响应的函数 Status/SetHeader

1
2
3
4
5
6
7
8
9
10
// Status 设置响应状态码
func (c *Context) Status(code int) {
c.StatusCode = code
c.Writer.WriteHeader(code)
}

// SetHeader 设置响应头
func (c *Context) SetHeader(key, value string) {
c.Writer.Header().Set(key, value)
}

快速响应

封装快速响应的函数 String/JSON/Data/HTML

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
func (c *Context) String(code int, format string, values ...interface{}) error {
c.SetHeader("Content-Type", "text/plain")
c.Status(code)
_, err := c.Writer.Write([]byte(fmt.Sprintf(format, values...)))
return err
}

func (c *Context) JSON(code int, obj interface{}) error {
c.SetHeader("Content-Type", "application/json")
c.Status(code)
encoder := json.NewEncoder(c.Writer)
if err := encoder.Encode(obj); err != nil {
return err
}

return nil
}

func (c *Context) Data(code int, data []byte) error {
c.Status(code)
_, err := c.Writer.Write(data)
return err
}

func (c *Context) HTML(code int, html string) error {
c.SetHeader("Content-Type", "text/html")
c.Status(code)
_, err := c.Writer.Write([]byte(html))
return err
}

路由器

dain/router.go

router 结构

于此同时,将路由器从 dain/dain.go 中分离出来。路由器的结构为:

1
2
3
4
5
6
7
8
9
type router struct {
handlers map[string]HandlerFunc
}

func NewRouter() *router {
return &router{
handlers: make(map[string]HandlerFunc),
}
}

路由功能

需要稍微更改路由注册和路由功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// addRouter 实现路由注册功能
func (r *router) addRouter(method, pattern string, handler HandlerFunc) {
log.Printf("Router %v - %v\n", method, pattern)
key := method + "-" + pattern
r.handlers[key] = handler
}

// handle 实现路由功能
func (r *router) handle(c *Context) {
key := c.Method + "-" + c.Path
if handler, ok := r.handlers[key]; ok {
handler(c)
} else {
fmt.Fprintf(c.Writer, "404 NOT FOUND FOR PATH: %v", c.Path)
}
}

框架入口

dain/dain.go

与此同时,也要更改框架入口。

首先需要将 HandlerFunc 的参数改变为 Context:

1
2
// HandlerFunc handler 函数类型
type HandlerFunc func(c *Context)

改变 Engine 的内部结构,使之内嵌 router 结构体

1
2
3
4
5
6
7
8
9
10
11
type Engine struct {
// 路由器
router *router
}

// New 返回一个 Engine 指针
func New() *Engine {
return &Engine{
router: NewRouter(),
}
}

更改路由注册的内部逻辑为,调用 router 的路由注册:

1
2
3
4
// addRouter 实现路由注册功能
func (e *Engine) addRouter(method, pattern string, handler HandlerFunc) {
e.router.addRouter(method, pattern, handler)
}

最后,需要更改 http.Handler 接口函数 ServeHTTP 的实现:

1
2
3
4
5
// 实现 http.Handler 接口,自定义路由器
func (e *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
c := NewContext(w, r)
e.router.handle(c)
}

本系列参考于 极客兔兔-7天用Go从零实现Web框架Gee教程,将从零开始实现一个简易的仿 GIN 的框架,称为 Dawn’s Gin 简称 dain

在本节,最终的代码目录结构如下:

1
2
3
4
5
dain/
|--dain.go
|--go.mod
main.go
go.mod

实现目标

我们最终的实现效果如下,可以看到自实现的框架与 GIN 十分相似:

  • 通过 e.GETe.POST 注册路由
  • 最后调用 e.RUN 运行 Web 服务器。

main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"DawnGin/dain"
"fmt"
"net/http"
)

func main() {
e := dain.New()

e.Get("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World, URL path = %v", r.URL.Path)
})

e.Post("/", func(w http.ResponseWriter, r *http.Request) {
for k, v := range r.Header {
fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
}
})

e.Run(":9000")
}

引擎

dain/dain.go

在 GIN 中,添加路由等的操作都是通过引擎 Engine 完成的,所以自定义 Engine。

其中 Engine.router 是一个路由器,类型为 map。key 记录路由注册时的 pattern,value 则为相应的处理逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// HandlerFunc handler 函数类型
type HandlerFunc func(w http.ResponseWriter, r *http.Request)

type Engine struct {
// 路由器
router map[string]HandlerFunc
}

// New 返回一个 Engine 指针
func New() *Engine {
return &Engine{
router: make(map[string]HandlerFunc),
}
}

静态路由注册

我们需要实现通过 POST 和 GET 注册静态路由,首先编写同一的路由注册入口,将路由注册到 Engine.router 中:

1
2
3
4
5
// AddRouter 实现路由注册功能
func (e *Engine) AddRouter(method, pattern string, handler HandlerFunc) {
key := method + "-" + pattern
e.router[key] = handler
}

添加对外暴露的与 HTTP Method 相关的路由注册方法,共实现了 GET 和 POST 方法:

1
2
3
4
5
6
7
8
9
// Get 路由注册 GET 请求方式
func (e *Engine) Get(pattern string, handler HandlerFunc) {
e.AddRouter("GET", pattern, handler)
}

// Post 路由注册 POST 请求方式
func (e *Engine) Post(pattern string, handler HandlerFunc) {
e.AddRouter("POST", pattern, handler)
}

实现 http.Handler 接口

需要使得自定义的路由器工作,首先需要实现 http.Handler 接口,接口定义如下:

1
2
3
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}

所以只需要实现 ServeHTTP 方法即可:

1
2
3
4
5
6
7
8
9
// 实现 http.Handler 接口,自定义路由器
func (e *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
key := r.Method + "-" + r.URL.Path
if handler, ok := e.router[key]; ok {
handler(w, r)
} else {
fmt.Fprintf(w, "404 NOT FOUND FOR PATH: %v", r.URL.Path)
}
}

最后,包装一层执行函数即可:

1
2
3
4
// Run 运行服务器
func (e *Engine) Run(addr string) error {
return http.ListenAndServe(addr, e)
}

Go 语言中可以使用第三方日志系统:logrus 和 seelog,它们实现了很强大的日志功能。

logrus

介绍

logrus 是用 Go 语言实现的一个日志系统,安装 logrus:

1
go get -u github.com/sirupsen/logrus

六个日志等级(从低到高):

  • Debug
  • Info
  • Warn
  • Error
  • Fatal,随后触发 os.Exit(1)
  • Panic,随后触发 panic()

使用

设置 logrus 参数

可以设置 logrus 的参数:

1
2
3
4
5
6
7
8
9
10
func init() {
// 日志格式化为 JSON,而不是默认的 ASCII
logrus.SetFormatter(&logrus.JSONFormatter{})

// 输出 stdout 而不是默认的 stderr,也可以是一个文件
logrus.SetOutput(os.Stdout)

// 只记录 Warn 以及以上的错误等级
logrus.SetLevel(logrus.WarnLevel)
}

Feilds

可以添加 Fields 来自定义输出:

1
2
3
4
5
6
7
logrus.WithFields(logrus.Fields{
"name": "zh",
"age": 23,
}).Warn("This is a warn level log.")

// output:
// {"age":23,"level":"warning","msg":"This is a warn level log.","name":"zh","time":"2022-05-20T13:11:48+08:00"}

有时候我们需要固定的 Fields,只需要生成一个 log.Entry 就可以复用 Fields

1
2
3
4
5
6
7
// 生成 log.Entry 复用 Feilds
contextLogger := logrus.WithFields(logrus.Fields{
"common": "this is a common field",
})

contextLogger.Info("I'll be logged with common and other field")
contextLogger.Info("Me too")

Logger

如果想在一个应用里面向多个地方记录日志,可以创建 Logger实例,Logger 结构如下:

1
2
3
4
5
6
7
8
9
10
11
type Logger struct {
Out io.Writer
Hooks LevelHooks
Formatter Formatter
//最小级别
Level Level
//被用来同步写入,比如两个地方同时log.默认是被锁住的
mu MutexWrap
// Reusable empty entry
entryPool sync.Pool
}

使用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main
import (
"os"
"github.com/sirupsen/logrus"
)

// 你可以创建很多instance
var log = logrus.New()

func main() {
file, err := os.OpenFile("logrus.log", os.O_CREATE|os.O_WRONLY, 0666)
if err == nil {
log.Out = file
} else {
log.Info("Failed to log to file, using default stderr")
}
log.WithFields(logrus.Fields{
"filename": "123.txt",
}).Info("打开文件失败")
}

seelog

介绍

seelog 主要有以下特性:

  • XML 的动态配置,支持动态改变配置而不需要重新启动应用。
  • 支持多输出流,能够同时把日志输出到多种流中、例如文件流、网络流等。
  • 日志级别:trace、debug、info、warn、error、critical、off

安装 seelog:

1
go get -u github.com/cihub/seelog

使用

基本使用

1
2
3
4
5
6
7
8
9
package main
import (
log "github.com/cihub/seelog"
)

func main() {
defer log.Flush()
log.Info("hello world")
}

配置文件说明

seelog 的配置文件通过 XML 进行配置,其说明如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 
1. type: 日志输出类型,有4中类型,分别是:sync,asyncloop(default),asynctimer,adaptive
type=“sync”:在同一个go中处理日志消息,仅当调用日志函数时才被执行。
type=“asyncloop”:在单独的go中独自处理日志消息,循环从日志队列中读取日志并消费(输出到控制台或者文件)。
type="asynctimer":在单独的go中独自处理日志消息,在指定时间间隔去读取日志队列消息,所以该类型还需要配置一个间隔时间(纳秒)。
type="adaptive":在单独的go中独自处理日志消息,但是不是固定的每隔指定时间去读取日志消息,间隔时间与队列剩余的日志量有关,如果剩余日志量多,则间隔时间短,反之亦然
2. minlevel: 全局最低输出日志级别
3. maxlevel: 全局最高输出日志级别
4. exceptions: 日志的特殊处理情况,可根据指定文件或者函数进行日志输出
5. formatid: 输出格式标签,可以在formats中找到对应的标签
6. console: 将日志输出到控制台
7. splitter: 用于细分outputs日志格式,支持: file(文件), rollingfile(滚动文件), buffered(缓存到内存再输出到文件), smtp(发送日志邮件), con(网络转发)
8. rollingfile: 滚动文件,可基于日期(type="date")或者文件大小(type="size")进行日志切割,maxsize: 单个日志文件最大size,如果设置为100M,则maxsize=100*1024*1024,maxrolls: 最大文件数量,超出的日志文件数量会被滚动删除
9. buffered: 将日志先存在内存中,定期写入文件,适合日志并发量较大或 IO 比较紧张的场合,size:缓存大小, flushperiod:缓存时间
10. filter: 单独处理某级别的日志
11. formats: 日志输出格式
-->

自定义配置文件

seelog 支持自定义日志处理,可以通过配置文件进行配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package logs

import (
"fmt"

seelog "github.com/cihub/seelog"
)

var Logger seelog.LoggerInterface

func loadAppConfig() {
appConfig := `
<seelog minlevel="warn">
<outputs formatid="common">
<rollingfile type="size" filename="/data/logs/roll.log" maxsize="100000" maxrolls="5"/>
<filter levels="critical">
<file path="/data/logs/critical.log" formatid="critical"/>
<smtp formatid="criticalemail" senderaddress="astaxie@gmail.com" sendername="ShortUrl API" hostname="smtp.gmail.com" hostport="587" username="mailusername" password="mailpassword">
<recipient address="xiemengjun@gmail.com"/>
</smtp>
</filter>
</outputs>
<formats>
<format id="common" format="%Date/%Time [%LEV] %Msg%n" />
<format id="critical" format="%File %FullPath %Func %Msg%n" />
<format id="criticalemail" format="Critical error on our server!\n %Time %Date %RelFile %Func %Msg \nSent by Seelog"/>
</formats>
</seelog>
`
logger, err := seelog.LoggerFromConfigAsBytes([]byte(appConfig))
if err != nil {
fmt.Println(err)
return
}
UseLogger(logger)
}

func init() {
DisableLog()
loadAppConfig()
}

// DisableLog disables all library log output
func DisableLog() {
Logger = seelog.Disabled
}

// UseLogger uses a specified seelog.LoggerInterface to output library log.
// Use this func if you are using Seelog logging system in your app.
func UseLogger(newLogger seelog.LoggerInterface) {
Logger = newLogger
}

上面实现了三个函数:

  • DisableLog 函数:初始化全局变量 Logger 为 seelog 的禁用状态,主要为了防止 Logger 被多次初始化
  • loadAppConfig 函数:根据配置文件初始化 seelog 的配置信息,配置文件说明如下:
    • seelog:minlevel 参数可选,如果被配置,高于或等于此级别的日志会被记录,同理 maxlevel。
    • outputs:输出信息的目的地,这里分成了两份数据,一份记录到 log rotate 文件里面。另一份设置了 filter,如果这个错误级别是 critical,那么将发送报警邮件。
    • formats:定义了日志的格式。
  • UseLogger 函数:设置当前的日志器为相应的日志处理。

错误处理

在 Go 语言中,定义了 error 类型,来显示的表达错误,错误作为函数的返回值进行返回。

Error 类型

error 类型是一个接口类型:

1
2
3
type error interface {
Error() string
}

可以通过 errors.New 把一个字符串转化为 errorString,以得到一个 error 对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
// errorString is a trivial implementation of error.
type errorString struct {
s string
}

func (e *errorString) Error() string {
return e.s
}

// New returns an error that formats as the given text.
func New(text string) error {
return &errorString{text}
}

自定义 Error

error 因为是一个接口,所以可以通过实现 error 接口,自定义错误类型。如 json.SyntaxError 类型,除了错误描述外,还定义了错误发生的位置:

1
2
3
4
5
6
type SyntaxError struct {
msg string // 错误描述
Offset int64 // 错误发生的位置
}

func (e *SyntaxError) Error() string { return e.msg }

再如 net.Error 类型,定义了更复杂的错误处理,判断是否超时、或者临时性错误:

1
2
3
4
5
type Error interface {
error
Timeout() bool // Is the error a timeout?
Temporary() bool // Is the error temporary?
}

可以通过类型断言,获取自定义的错误类型:

1
2
3
serr, ok := err.(*json.SyntaxError)

nerr, ok := err.(net.Error)

异常

Go 语言中,将异常与错误区分开来:

  • 错误指可以预期的错误,这时可以将错误当作函数返回值返回。
  • 异常指无法预期、无法继续执行的严重程序错误,如数组地址越界。

异常恢复机制

Go 语言中可以使用 recover 机制来恢复异常:

1
2
3
4
5
6
defer func(){
if x := recover(); x != nil {
// 如果发生了异常
// ....
}
}()

测试

编写测试用例

测试文件必须遵循如下原则:

  • 文件名必须是 _test.go 结尾的。
  • 必须 import testing
  • 所有的测试函数的格式为 func TestXxx (t *testing.T)
  • 通过调用 testing.TError, Errorf, FailNow, Fatal, FatalIf 方法,说明测试不通过,调用 Log 方法用来记录测试的信息。

如编写 go 文件 gotest.go

1
2
3
4
5
6
7
8
9
10
package gotest

import "errors"

func Divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("被除数不为0")
}
return a / b, nil
}

编写相应的测试文件 gotest_test.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package gotest

import "testing"

func TestDivide_1(t *testing.T) {
if i, e := Divide(6, 2); i != 3 || e != nil {
t.Error("除法函数测试没通过")
}
t.Log("第一个测试通过")
}

func TestDivide_2(t *testing.T) {
if _, e := Divide(6, 0); e == nil {
t.Error("除数为0测试未通过")
}
t.Log("第二个测试通过")
}

执行测试文件可以得到以下结果:

1
2
3
4
5
6
7
=== RUN   TestDivide_1
gotest_test.go:9: 第一个测试通过
--- PASS: TestDivide_1 (0.00s)
=== RUN TestDivide_2
gotest_test.go:16: 第二个测试通过
--- PASS: TestDivide_2 (0.00s)
PASS

压力测试

压力测试用来测试函数的性能,需要注意:

  • 压力测试函数的格式为 func BenchmarkXXX(b *testing.B)
  • go test 不会默认执行压力测试的函数,如果要执行压力测试需要带上参数 -test.bench,语法: -test.bench="test_name_regex"

对上述编写的除法函数进行压力测试:

1
2
3
4
5
6
7
8
9
10
11
12
func BenchmarkDivide(b *testing.B) {
b.StopTimer() // 调用该函数停止压力测试的时间计数

// 做一些初始化的工作,例如读取文件数据,数据库连接之类的,
// 这样这些时间不影响我们测试函数本身的性能

b.StartTimer() // 重新开始时间

for i := 0; i < b.N; i++ {
Divide(10, 3)
}
}

可以通过调用 b.StopTimer()b.StartTimer() 暂停和开始时间计数。执行结果如下:

1
2
3
4
5
6
7
goos: windows
goarch: amd64
pkg: go-web-demo/ch11/gotest
cpu: Intel(R) Pentium(R) CPU G4560 @ 3.50GHz
BenchmarkDivide
BenchmarkDivide-4 1000000000 0.4220 ns/op
PASS

CSRF

CSRF 原理

CSRF(Cross-Site Request Forgery)跨站请求伪造,可以伪装为受害者的身份,向服务器发送各种请求。原理如下:

  • 受害者登录受信任网站 A,并在本地生成 Cookie
  • 在不退出 A 的情况下,访问危险网站 B。此时访问 B 时会发送请求给受信任网站 A 并且会附上 Cookie 信息。

img

CSRF 防御

在服务器端防御 CSRF,主要有两个方面:

  • 正确使用 GET、POST 请求。
  • 在非 GET 请求中增验证。

正确使用 GET POST 请求

使用 REST 方式可以限制请求的类型:

  • GET 常用在查看,列举,展示等不需要改变资源属性的时候。

  • POST 常用在改变一个资源的属性或者状态。

增加验证

在非 GET 请求中增加验证,可以有三个思路:

  • 为每个用户生成一个唯一的 token,所有表单都包含同一个伪随机值。这种方法最简单,因为攻击者(理论上)不能获取到第三方的 Cookie,所以表单中的数据也就构造失败,但是 XSS 可以窃取到第三方 Cookie,所以这个方案在没有 XSS 时是安全的。

  • 为每一个请求使用验证码,用户体验很差。

  • 每个用户生成的 token 随时更新,实现如下:

    • 生成随机 token:
    1
    2
    3
    4
    5
    6
    7
    h := md5.New()
    io.WriteString(h, strconv.FormatInt(crutime, 10))
    io.WriteString(h, "salt")
    token := fmt.Sprintf("%x", h.Sum(nil))

    t, _ := template.ParseFiles("xxx.gtpl")
    t.Execute(w, token)
    • 表单中的 token:
    1
    <input type="hidden" name="token" value="{{.}}">
    • 验证 token:
    1
    2
    3
    4
    5
    6
    7
    r.ParseForm()
    token := r.Form.Get("token")
    if token != "" {
    // 验证 token 的合法性
    } else {
    // 不存在 token 报错
    }

XSS

XSS 原理

XSS(Cross-Site Scripting)跨站脚本攻击,原理是一段恶意的 JavaScript 代码在用户客户端上被执行,导致信息泄露( Cookie 泄露)。XSS 主要用于攻击用户端的。

主要分为两类:

  • 存储型 XSS:恶意 XSS 代码被服务器存储到了服务器中,应用程序从数据库中查询出来并在客户端中显示,造成 XSS 攻击。
  • 反射型 XSS:将恶意 XSS 代码加入到 URL 的请求参数中,请求参数在页面上直接输出。

XSS 防御

XSS防御可以有如下方式:

  • 过滤特殊字符:text/template 包下面的 HTMLEscapeString、JSEscapeString 等函数可以对敏感字符进行转义。
  • 输入内容长度控制:对于不受信任的输入,都应该限定一个合理的长度,这样可以增加攻击难度。
  • HTTP-Only:禁止从客户端脚本中读取 Cookie 信息,使得攻击者无法窃取 Cookie。
  • 等一系列防御手段。。。

SQL 注入

SQL 注入原理

SQL 注入的原理就是因为用户输入的数据被当作 SQL 语句执行。

SQL 注入防御

SQL 注入可以有如下防御方式:

  • 限制 Web 应用数据库的操作权限,给予用户最低的操作权限。

  • 检查输入的数据,对进入数据库的字符进行转义、过滤。html/template 包的 HTMLEscapeString 函数可以对字符串进行转义处理。

  • 所有的查询语句建议使用数据库提供的参数化查询接口,避免直接拼接 SQL 语句。

存储密码

普通方案:哈希

目前利用最多的方案就是对明文密码进行哈希之后,进行存储。常用的单向哈希算法包括 SHA-256, SHA-1, MD5 等。

缺点:考虑到多数人所使用的密码为常见的组合,攻击者可以将所有密码的常见组合进行单向哈希,得到一个摘要组合(彩虹表),然后与数据库中的摘要进行比对即可获得对应的密码。

进阶方案:哈希+盐

可以采用加盐的方式来存储密码,常用的方式:

  • 对用户的明文密码进行一次哈希运算
  • 将得到的摘要加上随机串(盐),这个随机串中可以包括某些固定的串,也可以包括用户名(用来保证每个用户加密使用的密钥都不一样)。
  • 再进行一次哈希运算后,放入数据库中存储起来。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 假设用户名 abc,密码 123456
h := md5.New()
io.WriteString(h, "需要加密的密码")

// pwmd5 等于 e10adc3949ba59abbe56e057f20f883e
pwmd5 :=fmt.Sprintf("%x", h.Sum(nil))

// 指定两个 salt: salt1 = @#$% salt2 = ^&*()
salt1 := "@#$%"
salt2 := "^&*()"

// salt1 + 用户名 + salt2 + MD5 拼接
io.WriteString(h, salt1)
io.WriteString(h, "abc")
io.WriteString(h, salt2)
io.WriteString(h, pwmd5)

last :=fmt.Sprintf("%x", h.Sum(nil))

专家方案:Scrypt

故意增加密码计算所需耗费的资源和时间,使得任何人都不可获得足够的资源建立所需的 rainbow table

Scrypt 算法使得并行计算多个摘要异常困难,因此利用rainbow table(彩虹表)进行暴力攻击的难度增加。

在 Go 的 golang.org/x/crypto/scrypt 包中支持 scrypt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"encoding/base64"
"fmt"
"log"

"golang.org/x/crypto/scrypt"
)

func main() {
// DO NOT use this salt value; generate your own random salt. 8 bytes is
// a good length.
salt := []byte{0xc8, 0x28, 0xf2, 0x58, 0xa7, 0x6a, 0xad, 0x7b}

dk, err := scrypt.Key([]byte("some password"), salt, 1<<15, 8, 1, 32)
if err != nil {
log.Fatal(err)
}
fmt.Println(base64.StdEncoding.EncodeToString(dk))
}

加密和解密数据

Go 语言中 crypto 及其子包提供了多种加密算法。

Socket 编程

常用的 Socket 有两种类型:

  • 流式 Socket(SOCK_STREAM):一种面向连接的 Socket,针对于面向连接的 TCP 服务应用。

  • 数据包式 Socket (SOCK_DGRAM)无连接的 Socket,对应于无连接的 UDP 服务应用。

对于在网络上的应用程序来说,(协议类型,IP 地址,端口号)这个三元组可以唯一确定一个进程。

在 Go 的 net 包中,定义了 IP 地址类型,net 包的函数都可以接收 IPv4 和 IPv6 的 IP 地址作为输入。其中 ParseIP(s string) IP 函数会把一个 IPv4 或者 IPv6 的地址转化成 IP 类型。

1
2
3
type IP []byte

func ParseIP(s string) IP

TCP Socket

在 net 包中,有一个类型为 TCPConn,它用来作为客户端和服务器端交互的通道,主要有两个函数,分别可以读写数据:

1
2
func (c *TCPConn) Write(b []byte) (int, error)
func (c *TCPConn) Read(b []byte) (int, error)

另外,还有一个 TCPAddr 类型,用于表示 TCP 的地址信息,通过 ResolveTCPAddr 函数可以获取一个 TCPAddr:

1
2
3
4
5
6
7
type TCPAddr struct {
IP IP
Port int
Zone string // IPv6 scoped addressing zone
}

func ResolveTCPAddr(net, addr string) (*TCPAddr, os.Error)
  • net 参数是 “tcp4”、”tcp6”或”tcp” 中的任意一个,分别表示 TCP (IPv4-only), TCP (IPv6-only) 或者 TCP (IPv4, IPv6 的任意一个)。
  • addr 表示域名或者 IP 地址

TCP client

客户端通过 net 包中的 DialTCP 函数来建立一个 TCP 连接,并返回一个 TCPConn 类型的对象:

1
func DialTCP(network string, laddr, raddr *TCPAddr) (*TCPConn, error)
  • net 必须是**”tcp”、”tcp4”、”tcp6”**。
  • laddr 为本地地址,如果 laddr 不是 nil,将使用它作为本地地址,否则自动选择一个本地地址。通常为 nil
  • raddr 为远程的服务器地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 服务器远程地址
serviceAddr = "127.0.0.1:9617"
raddr, err := net.ResolveTCPAddr("tcp", serviceAddr)
if err != nil {
// ...
}
// 与服务器建立连接
conn, err := net.DialTCP("tcp", nil, raddr)
if err != nil {
// ...
}
// 向服务器端发送数据
_, err = conn.Write([]byte("hello world"))
if err != nil {
// ...
}
// 读取数据
result, err := ioutil.ReadAll(conn)
if err != nil {
// ...
}

TCP server

服务器端需要:

  • 绑定服务到指定的非激活端口,并监听此端口。
1
func ListenTCP(network string, laddr *TCPAddr) (*TCPListener, error)
  • 当有客户端请求到达的时候可以接收到来自客户端连接的请求。
1
func (l *TCPListener) Accept() (Conn, error)

当 Accept 之后,为每一个 conn 开启一个 goroutine 来处理与客户端的通信,这样可以支持多并发:

1
2
3
4
5
6
7
8
for {
conn, err := listener.Accept()
if err != nil {
continue
}
// 开启协程,支持多并发
go handleClient(conn)
}d

控制 TCP 连接

TCP 有很多连接控制函数,常用如下:

  • 设置建立连接的超时时间,当超过设置时间时,连接自动关闭。
1
func DialTimeout(net, addr string, timeout time.Duration) (Conn, error)
  • 设置 写入 / 读取 一个连接的超时时间
1
2
func (c *TCPConn) SetReadDeadline(t time.Time) error
func (c *TCPConn) SetWriteDeadline(t time.Time) error
  • 设置 keepAlive 属性,操作系统在 TCP 上没有数据和 ACK 时,会间隔性的发送 keepalive 包,以此判断这个 TCP 连接是否已经断开。
1
func (c *TCPConn) SetKeepAlive(keepalive bool) os.Error

UDP Socket

UDP Socket 在服务器端没有 Accept 函数,其他几乎一样。

Web Socket

在 WebSocket 出现之前,为了实现即时通信,采用的技术都是 “”轮询”,即在特定的时间间隔内,由浏览器对服务器发出 HTTP Request,服务器在收到请求后,返回最新的数据给浏览器刷新,“轮询” 使得浏览器需要对服务器不断发出请求,这样会占用大量带宽。

Web Socket 采用了特殊的报头,使得浏览器和服务器只需要做一个握手的动作,就可以在浏览器和服务器之间建立一条连接通道。URI 以 ws 或者 wss(SSL) 开头。

原理

Web Socket 协议本质上是一个基于 TCP 的协议。Web Socket 的大致流程如下:

  • 握手过程:为了建立一个 Web Socket 连接,客户端浏览器首先要向服务器发起一个 HTTP 请求,这个请求和通常的 HTTP 请求不同,包含了一些附加头信息,其中附加头信息 Upgrade: WebSocket 表明这是一个申请协议升级的 HTTP 请求,服务器端解析这些附加的头信息然后产生应答信息返回给客户端,客户端和服务器端的 WebSocket 连接就建立起来了。

  • 数据传输:双方就可以通过这个连接通道自由的传递信息,并且这个连接会持续存在直到客户端或者服务器端的某一方主动的关闭连接。

Web Socket在第一次握手之后,连接便建立成功,其后的通讯数据都是以 \x00 开头,以 \xFF 结尾。在上层应用中,这是透明的,Web Socket 组件会自动的去掉头部和尾部。

实现

Go 语言官方标准库中没有对于 Web Socket 的支持,但是可以通过以下命令获取:

1
go get golang.org/x/net/websocket

客户端

客户端示例如下,并且客户端绑定了4个事件:

  • onopen 建立连接后触发。
  • onmessage 收到消息后触发。
  • onerror 发生错误时触发。
  • onclose 关闭连接时触发。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<html>
<head></head>
<body>
<script type="text/javascript">
var sock = null;
var wsuri = "ws://127.0.0.1:9617";

window.onload = function() {

console.log("onload");

sock = new WebSocket(wsuri);

sock.onopen = function() {
console.log("connected to " + wsuri);
}

sock.onclose = function(e) {
console.log("connection closed (" + e.code + ")");
}

sock.onmessage = function(e) {
console.log("message received: " + e.data);
}
};

function send() {
var msg = document.getElementById('message').value;
sock.send(msg);
};
</script>
<h1>WebSocket Echo Test</h1>
<form>
<p>
Message: <input id="message" type="text" value="Hello, world!">
</p>
</form>
<button onclick="send();">Send Message</button>
</body>
</html>

服务器端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package main

import (
"fmt"
"golang.org/x/net/websocket"
"html/template"
"log"
"net/http"
)

func Index(w http.ResponseWriter, r *http.Request) {
t, _ := template.ParseFiles("demo.html")
t.Execute(w, nil)
}

func Echo(ws *websocket.Conn) {
var err error

for {
var reply string

if err = websocket.Message.Receive(ws, &reply); err != nil {
fmt.Println("Can't receive")
break
}

fmt.Println("Received back from client: " + reply)

msg := "Received: " + reply
fmt.Println("Sending to client: " + msg)

if err = websocket.Message.Send(ws, msg); err != nil {
fmt.Println("Can't send")
break
}
}
}

func main() {
http.HandleFunc("/", Index)
http.Handle("/ws", websocket.Handler(Echo))
if err := http.ListenAndServe(":9617", nil); err != nil {
log.Fatal("ListenAndServe:", err)
}
}

REST

REST ( REpresentatianal State Transfer ) 表现层状态转化。

基本概念

要理解什么是 REST,需要理解以下几个概念:

  • 资源(Resource):REST 是 表现层状态转化,实际上 表现层指的是 资源的表现层。平常上网访问的一张图片、一个文档、一个视频等就是资源。这些资源我们通过 URI 来定位,也就是一个 URI 表示一个资源。
  • 表现层(Representation):把资源实体展现出来的方式,就是表现层。比如一段文本信息,可以输出为 HTML,JSON,XML 等。URI 确定一个资源,但是如何确定它的具体表现形式呢?应该在 HTTP 请求的头信息中用 AcceptContent-Type 字段指定,这两个字段才是对 “表现层” 的描述。
  • 状态转换(State Transfer):在访问服务器的过程中,服务器与客户端进行交互,这就涉及到了数据和状态的变化。HTTP 中四个方法 GET、POST、PUT、DELETE,分别对应于四种基本操作:GET 用来获取资源,POST 用来新建资源(也可以用于更新资源),PUT 用来更新资源,DELETE 用来删除资源。

RESTful 架构

RESTful 架构就是:

  • 每一个 URI 代表一种资源
  • 客户端和服务器之间,传递这种资源的某种表现层
  • 客户端通过四个 HTTP 动词,对服务器端资源进行操作,实现 表现层状态转化

Web 应用要满足 REST 最重要的原则是:客户端和服务器之间的交互在请求之间是无状态的,即从客户端到服务器的每个请求都必须包含理解请求所必需的信息。如果服务器在请求之间的任何时间点重启,客户端不会得到通知。此外此请求可以由任何可用服务器回答,这十分适合云计算之类的环境。因为是无状态的,所以客户端可以缓存数据以改进性能。

另一个重要的 REST 原则是:系统分层,这表示组件无法了解除了与它直接交互的层次以外的组件。

REST 架构图:

img

RPC

见文章 GO语言杂谈 2 RPC

Web 开发中需要对输入、输出进行处理。

XML 处理

示例 XML 文件如下:

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="utf-8"?>
<servers version="1">
<server>
<serverName>Shanghai_VPN</serverName>
<serverIP>127.0.0.1</serverIP>
</server>
<server>
<serverName>Beijing_VPN</serverName>
<serverIP>127.0.0.2</serverIP>
</server>
</servers>

读取 XML

读取 XML 选择 xml.Unmarshal 函数:

1
func Unmarshal(data []byte, v interface{}) error

将 XML 写入结构体为例,上述 XML 文件 对应的结构体为:

1
2
3
4
5
6
7
8
9
10
11
12
type Servers struct {
XMLName xml.Name `xml:"servers"`
Version string `xml:"version,attr"`
Svs []Server `xml:"server"`
Description string `xml:",innerxml"`
}

type Server struct {
XMLName xml.Name `xml:"server"`
ServerName string `xml:"serverName"`
ServerIP string `xml:"serverIP"`
}

读取操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
func main() {
// 打开 XML 文件
file, err := os.Open("servers.xml")
if err != nil {
fmt.Printf("error: %v", err)
return
}
defer file.Close()
// 读取文件内容
data, err := ioutil.ReadAll(file)
if err != nil {
fmt.Printf("error: %v", err)
return
}
// 将 XML 转为结构体
v := Servers{}
err = xml.Unmarshal(data, &v)
if err != nil {
fmt.Printf("error: %v", err)
return
}
fmt.Println(v)
}
// 输出:
// {{ servers} 1 [{{ server} Shanghai_VPN 127.0.0.1} {{ server} Beijing_VPN 127.0.0.2}]
// <server>
// <serverName>Shanghai_VPN</serverName>
// <serverIP>127.0.0.1</serverIP>
// </server>
// <server>
// <serverName>Beijing_VPN</serverName>
// <serverIP>127.0.0.2</serverIP>
// </server>
}

输出 XML

xml 包提供了两个函数 Marshal 和 MarshalIndent 来输出 XML,二者的区别在于 MarshalIndent 会增加前缀和缩进:

1
2
func Marshal(v interface{}) ([]byte, error)
func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error)

构建结构体:

1
2
3
4
5
6
7
8
9
10
11
12
type Servers struct {
XMLName xml.Name `xml:"servers"`
Version string `xml:"version,attr"`
Svs []Server `xml:"server"`
Description string `xml:",innerxml"`
}

type Server struct {
XMLName xml.Name `xml:"server"`
ServerName string `xml:"serverName"`
ServerIP string `xml:"serverIP"`
}

解析结构体并输出 XML :

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
// 新建结构体
v := Servers{Version: "1"}
v.Svs = append(v.Svs, Server{ServerName: "Shanghai_VPN", ServerIP: "127.0.0.1"})
v.Svs = append(v.Svs, Server{ServerName: "Beijing_VPN", ServerIP: "127.0.0.2"})
// 转为 XML 数据
data, err := xml.MarshalIndent(&v, "", " ")
if err != nil {
fmt.Printf("error: %v\n", err)
}
os.Stdout.Write([]byte(xml.Header))
os.Stdout.Write(data)
}

注意:

之所以会有 os.Stdout.Write([]byte(xml.Header)) 这句代码的出现,是因为 xml.MarshalIndent 或者 xml.Marshal 输出的信息都是不带 XML 头的,为了生成正确的 xml 文件,我们使用了 xml 包预定义的 Header 变量。

JSON 处理

示例 JSON 格式数据如下:

1
2
3
4
5
6
7
8
9
10
11
12
{
"servers": [
{
"serverName": "Shanghai_VPN",
"serverIP": "127.0.0.1"
},
{
"serverName": "Beijing_VPN",
"serverIP": "127.0.0.2"
}
]
}

解析 JSON

json 包中有如下函数可以解析 JSON 数据:

1
func Unmarshal(data []byte, v interface{}) error

解析到结构体

上述 JSON 数据对应的结构体如下:

1
2
3
4
5
6
7
8
type Server struct {
ServerName string `json:"serverName"`
ServerIP string `json:"serverIP"`
}

type ServerSlice struct {
Servers []Server `json:"servers"`
}

解析 JSON 数据到结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main() {
// 打开文件
file, err := os.Open("servers.json")
if err != nil {
fmt.Errorf("open file err: %v", err)
return
}
// 读取内容
data, _ := ioutil.ReadAll(file)
// 解析 JSON
v := ServerSlice{}
err = json.Unmarshal(data, &v)
if err != nil {
fmt.Errorf("open file err: %v", err)
return
}
fmt.Println(v)
}

解析到 interface

如果知道 JSON 数据的格式,可以解析到结构体中。如果不知道 JSON 数据格式,可以利用 map [string] interface {}[] interface {} 结构来存储任意的 JSON 对象。

1
2
var f interface{}
err := json.Unmarshal(b, &f)

此时,空接口 f 实际上是一个 map[string] interface{},其结构如下:

1
2
3
4
5
6
7
8
9
10
11
map[string]interface {}{
"servers":[]interface {}{
map[string]interface {}{
"serverIP":"127.0.0.1",
"serverName":"Shanghai_VPN"},
map[string]interface {}{
"serverIP":"127.0.0.2",
"serverName":"Beijing_VPN"
}
}
}

可以通过类型断言将**空接口转为 map[string] interface{}**,接着就可以利用 for range 对 map 进行遍历了:

1
m := f.(map[string]interface{})

目前,simplejson 包可以更加容易的处理未知结构的 JSON 数据,其 github 地址如下:

github.com/bitly/go-simplejson


生成 JSON

json 包提供了生成 JSON 数据的函数:

1
func Marshal(v interface{}) ([]byte, error)

这里不再举例说明具体操作。

模板处理

使用模板

解析模板

html/template 包中有两个函数可以解析模板:

  • ParseFiles 函数创建一个模板并解析filenames指定的文件里的模板定义。返回的模板的名字是第一个文件的文件名(不含扩展名),内容为解析后的第一个文件的内容。
1
func ParseFiles(filenames ...string) (*Template, error)
  • ParseGlob 函数创建一个模板并解析匹配 pattern 的文件里的模板定义。返回的模板的名字是第一个匹配的文件的文件名(不含扩展名),内容为解析后的第一个文件的内容。
1
func ParseGlob(pattern string) (*Template, error)

模板执行

html/template 包中有两个函数可以执行模板:

  • Execute 方法将解析好的模板应用到 data 上,并将输出写入 wr。
1
func (t *Template) Execute(wr io.Writer, data interface{}) error
  • ExecuteTemplate 方法类似 Execute,但是使用名为 nam e的 t 关联的模板产生输出。
1
func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error

模板中插入数据

  • 字段操作{{.}} 代表当前对象,可以通过 {{.FieldName}} 访问当前对象的字段

  • 输出嵌套字段

    • {{with …}}…{{end}} 可以指定当前对象的值,如子结构体。
    • {{range …}}{{end}} 可以循环操作数据。
  • 条件判断{{if …}}…{{else if …}}…{{else}}…{{end}} 语句可以进行条件判断。

  • pipelines:在 {{}} 中的都是 pipeline,如 {{. | html}} 可以将当前对象进行 HTML 转义,变为 HTML 实体。

  • 模板变量:可以通过 $variable := pipeline 方式声明模板局部变量。

  • 模板嵌套

    • 声明:{{define "子模板名称"}}内容{{end}}
    • 调用:{{template "子模板名称"}}
  • 模板函数

    • 每一个模板函数都有一个唯一的名字,可以与一个 Go 函数相关联。FuncMap 类型定义了函数名字符串到函数的映射,每个函数都必须有 1 到 2 个返回值,如果有 2 个则后一个必须是 error 接口类型;如果有 2 个返回值的方法返回的 error 非 nil ,模板执行会中断并返回给调用者该错误。:
    1
    type FuncMap map[string]interface{}
    • 使用 t.Funcs 函数在模板中注册函数:
    1
    t = t.Funcs(template.FuncMap{"TmplFuncName": FuncName})
    • 在模板包内部已经有内置的实现函数:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    var builtins = FuncMap{
    "and": and,
    "call": call,
    "html": HTMLEscaper,
    "index": index,
    "js": JSEscaper,
    "len": length,
    "not": not,
    "or": or,
    "print": fmt.Sprint,
    "printf": fmt.Sprintf,
    "println": fmt.Sprintln,
    "urlquery": URLQueryEscaper,
    }

Must 操作

Must 函数用来检查模板是否正确:

1
func Must(t *Template, err error) *Template

一般用于变量初始化:

1
var t = template.Must(template.New("name").Parse("html"))