近来使用Golang来构建Restful服务变得越发流行。我发现有些时候使用MongoDB作为持久存储,这篇文档中, 我会使用Golang和MongoDB来构建一个简单的用户管理为服务。

MongoDB

MongoDB因为极简、灵活、高可用以及面向文档的特性得到越来越多市场上的青睐。根据MongoDB之父的解释,它被用来设计组合键值对存储和关系数据库存储的最佳特性。MongoDB在两者之间做妥协,具备了二者的某些有用的功能。

MongoDB的应用场景:Web应用、分析应用的首要数据库,以及弱数据类型的数据,也就是无schema数据。

什么是文档(document)

文档就是键值对集合。文档中的键用字符串表示,文档中的值可以是基础的数据类型(字符串、数字、日期等)、数组,也可以是另一个文档。在MongDB内部以二进制JSON格式存储文档数据,也就做BSON。BSON有相似的结构,但专为文档存储而设计。

下面是一个文档数据示例:

1
2
3
4
5
{
    name: '张三',
    age: '11',
    address: '湖北省武汉市光谷一路'
}

集合(Collection)

集合是结构或者概念上相似文档的容器。例如,我们会把用户(user)文档存储到(users)集合(collection)中。这里集合的概念就非常类似于关系数据库(RDMS)中表(table)的概念。两者的不同是,集合中的数据是无schema的,是不强制数据结构的,可以是任意的。

查询(Query)

MongoDB不是用SQL,而是使用自己的JSON查询语言。

例如:使用SQL语句查询名叫“张三”的用户

1
2
SELECT * from users
WHERE name = '张三'

而在MongoDB中,查询的是:

1
db.users.find({name: 'hello'})

MongoDB Golang驱动

mgo(发音:mango)是一个Go语言实现的MongoDB驱动程序,这个驱动提供了一个非常简洁易于使用、并经过充分测试API。接下来,在介绍如何通过mgo来实现CRUD(create、react、update、delete)操作之前,将简单介绍下会话管理(session manager)。

session management

获取会话

1
session, err := mgo.Dial("localhsot")

单个的会话不允许进行并发处理,所以通常需要使用多个会话。新建一个会话的最快方式是从现有的session中复制一个新的会话:

1
2
newSession := session.Copy()
defer newSession.Close()

新生成的这个会话会使用相同的集群信息和连接池(connection pool)。每一个新建的session必须在生命周期结束时调用Close方法,该会话的资源会视情况而定,是被放回连接池,还是被回收。

查询文档

mgo需要和bson一同使用,bson使编写查询更加简单。

  • 获取集合中所有的文档
1
2
3
4
c := session.DB("store").C("users")

var users []User
err := c.Find(bson.M{}).All(&books)
  • 查询单个文档
1
2
3
c := session.DB("store").C("users")
var user User
err := c.Find(bson.M{"name": "张三"}).One(&user)
  • 新建文档
1
2
c := session.DB("store").C("users")
err = c.Insert(&User{"Ale"})
  • 更新文档
1
2
c := session.DB("store").C("users")
err = c.Update(bson.M{"name": "张三"}, &book)
  • 删除文档
1
2
c := session.DB("store").C("users")
err = c.Remove(bson.M{"name": "张三"})

RESTful服务(Golang)

Echo

Echo是一个高性能、极简的Go语言Web框架。

功能概览:

  • 优化的 HTTP 路由。
  • 创建可靠并可伸缩的RESTful API。
  • 基于标准的HTTP服务器。
  • 组 APIs.
  • 可扩展的middleware框架。
  • Define middleware at root, group or route level.
  • 为JSON, XML进行数据绑定,产生负荷。
  • 提供便捷的方法来发送各种HTTP相应。
  • 对HTTP错误进行集中处理。
  • Template rendering with any template engine.
  • 定义属于你的日志格式。
  • 高度个性化。
  • Automatic TLS via Let’s Encrypt
  • 支持HTTP/2

性能对比

performance.png

服务实现

具体实现中基于Echo框架来开发,代码在github.com

  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
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
package main

import (
	"log"
	"net/http"

	"fmt"

	"github.com/labstack/echo"
	"github.com/labstack/echo/middleware"
	mgo "gopkg.in/mgo.v2"
	"gopkg.in/mgo.v2/bson"
)

type User struct {
	ID    string `json:"id" bson:"_id,omitempty"`
	Name  string `json:"name,omitempty"`
	Phone string `json:"phone,omitempty"`
	Age   int    `json:"age,omitempty"`
}

var session *mgo.Session

func init() {
	s, err := mgo.Dial("localhost")
	if err != nil {
		log.Fatal(err)
	}
	session = s
}

func main() {
	defer session.Close()
	ensureIndex(session)

	session.SetMode(mgo.Monotonic, true)

	e := echo.New()
	e.Use(middleware.Logger())

	e.GET("/users", allUsers)
	e.GET("/user/:id", getUser)
	e.PUT("/user", updateUser)
	e.DELETE("/user/:id", deleteUser)
	e.POST("/user", saveUser)

	e.Logger.Fatal(e.Start(":1424"))
}

func ensureIndex(s *mgo.Session) {
	session := s.Copy()
	defer session.Close()

	c := session.DB("store").C("users")

	index := mgo.Index{
		Key:        []string{"id"},
		Unique:     true,
		DropDups:   true,
		Background: true,
		Sparse:     true,
	}
	err := c.EnsureIndex(index)
	if err != nil {
		panic(err)
	}
}

func saveUser(e echo.Context) error {
	u := new(User)
	if err := e.Bind(u); err != nil {
		return e.JSON(http.StatusBadRequest, err)
	}

	s := session.Copy()
	defer s.Close()

	c := s.DB("store").C("users")
	err := c.Insert(u)
	if err != nil {
		log.Println("Failed insert user", u)
		if mgo.IsDup(err) {
			return e.JSON(http.StatusBadRequest, "User with this id alread exists.")
		}

		return e.JSON(http.StatusInternalServerError, "Database error")
	}

	return e.JSON(http.StatusCreated, "SUCCESS")
}

func getUser(e echo.Context) error {
	s := session.Copy()
	defer s.Clone()

	c := s.DB("store").C("users")
	var u User
	id := e.Param("id")
	fmt.Println("userid", id)
	err := c.Find(bson.M{"_id": id}).One(&u)
	if err != nil {
		log.Println("Failed get user", err)
		return e.JSON(http.StatusNotFound, "Database error")
	}

	return e.JSON(http.StatusOK, u)
}

func updateUser(e echo.Context) error {
	u := new(User)
	if err := e.Bind(u); err != nil {
		return e.JSON(http.StatusBadRequest, err)
	}

	s := session.Copy()
	defer s.Close()

	c := s.DB("store").C("users")
	err := c.Update(bson.M{"_id": u.ID}, &u)
	if err != nil {
		switch err {
		default:
			log.Fatalln("Failed update user: ", err)
			return e.JSON(http.StatusInternalServerError, "Database error")
		case mgo.ErrNotFound:
			return e.JSON(http.StatusNotFound, "Not found")
		}
	}
	return e.JSON(http.StatusOK, u)
}

func deleteUser(e echo.Context) error {
	s := session.Copy()
	defer s.Close()

	id := e.Param("id")

	c := s.DB("store").C("users")
	err := c.Remove(bson.M{"_id": id})
	if err != nil {
		switch err {
		default:
			e.JSON(http.StatusInternalServerError, "Database error")
			log.Fatalln("Failed delete user: ", err)
			return err
		case mgo.ErrNotFound:
			e.JSON(http.StatusInternalServerError, "User not found")
			return err
		}
	}

	return e.JSON(http.StatusOK, "Sucess")
}

func allUsers(e echo.Context) error {
	s := session.Copy()
	defer s.Close()

	c := s.DB("store").C("users")

	var users []User
	err := c.Find(bson.M{}).All(&users)
	if err != nil {
		e.JSON(http.StatusInternalServerError, "Database Error")
		return err
	}

	return e.JSON(http.StatusOK, users)
}

使用Curl测试服务

curl对于构建和测试RESTful服务来说是一个非常好用的工具,在其他RESTful 服务API的文档中,常常可以看到curl的身影,这里也不例外。

新增用户

  • 请求
1
2
3
4
5
6
7
8
curl -X POST -H 'Content-Type: application/json' -d @body.json http://localhsot:1424/user

body.json
{
  "id": "5",
  "name": "李四",
  "age": 11
}
  • 响应
1
SUCCESS

编辑用户

  • 请求
1
2
3
4
5
6
7
8
curl -X PUT -H 'Content-Type: application/json' -d @body.json http://localhost:1424/user

body.json
{
  "id": "1",
  "title": "天一",
  "age": "-1"
}
  • 响应
1
{"id":"1","name":"天一","age":-1}

查询所有用户

  • 请求

使用python -m json.tool将服务返回的json,进行格式化处理。

1
curl http://localhost:1424/users | python -m json.tool
  • 响应
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
[
    {
        "id": "YE/\ufffd\ufffdDj\ufffd\ufffd\u0004\ufffd-",
        "name": "xiwang"
    },
    {
        "id": "2",
        "name": "1"
    },
    {
        "age": -1,
        "id": "1",
        "name": "\u5929\u4e00"
    },
    {
        "id": "YE7\u001d\ufffdDj\ufffd\ufffd\u0004\ufffd.",
        "name": "bug"
    }
]

查询指定用户

  • 请求
1
curl http://localhost:1424/user/1
  • 响应
1
{"id":"1","name":"天一","age":-1}

删除用户

  • 请求
1
curl -X DELETE http://localhost:1424/user/1 
  • 响应
1
SUCCESS