使用Golang基于MongoDB构建Restful服务
近来使用Golang来构建Restful服务变得越发流行。我发现有些时候使用MongoDB作为持久存储,这篇文档中, 我会使用Golang和MongoDB来构建一个简单的用户管理为服务。
MongoDB
MongoDB因为极简、灵活、高可用以及面向文档的特性得到越来越多市场上的青睐。根据MongoDB之父的解释,它被用来设计组合键值对存储和关系数据库存储的最佳特性。MongoDB在两者之间做妥协,具备了二者的某些有用的功能。
MongoDB的应用场景:Web应用、分析应用的首要数据库,以及弱数据类型的数据,也就是无schema数据。
什么是文档(document)
文档就是键值对集合。文档中的键用字符串表示,文档中的值可以是基础的数据类型(字符串、数字、日期等)、数组,也可以是另一个文档。在MongDB内部以二进制JSON格式存储文档数据,也就做BSON。BSON有相似的结构,但专为文档存储而设计。
下面是一个文档数据示例:
{
name: '张三',
age: '11',
address: '湖北省武汉市光谷一路'
}
集合(Collection)
集合是结构或者概念上相似文档的容器。例如,我们会把用户(user)文档存储到(users)集合(collection)中。这里集合的概念就非常类似于关系数据库(RDMS)中表(table)的概念。两者的不同是,集合中的数据是无schema的,是不强制数据结构的,可以是任意的。
查询(Query)
MongoDB不是用SQL,而是使用自己的JSON查询语言。
例如:使用SQL语句查询名叫“张三”的用户
SELECT * from users
WHERE name = '张三'
而在MongoDB中,查询的是:
db.users.find({name: 'hello'})
MongoDB Golang驱动
mgo(发音:mango)是一个Go语言实现的MongoDB驱动程序,这个驱动提供了一个非常简洁易于使用、并经过充分测试API。接下来,在介绍如何通过mgo来实现CRUD(create、react、update、delete)操作之前,将简单介绍下会话管理(session manager)。
session management
获取会话
session, err := mgo.Dial("localhsot")
单个的会话不允许进行并发处理,所以通常需要使用多个会话。新建一个会话的最快方式是从现有的session中复制一个新的会话:
newSession := session.Copy()
defer newSession.Close()
新生成的这个会话会使用相同的集群信息和连接池(connection pool)。每一个新建的session必须在生命周期结束时调用Close方法,该会话的资源会视情况而定,是被放回连接池,还是被回收。
查询文档
mgo需要和bson一同使用,bson使编写查询更加简单。
- 获取集合中所有的文档
c := session.DB("store").C("users")
var users []User
err := c.Find(bson.M{}).All(&books)
- 查询单个文档
c := session.DB("store").C("users")
var user User
err := c.Find(bson.M{"name": "张三"}).One(&user)
- 新建文档
c := session.DB("store").C("users")
err = c.Insert(&User{"Ale"})
- 更新文档
c := session.DB("store").C("users")
err = c.Update(bson.M{"name": "张三"}, &book)
- 删除文档
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
性能对比
服务实现
具体实现中基于Echo框架来开发,代码在github.com。
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的身影,这里也不例外。
新增用户
- 请求
curl -X POST -H 'Content-Type: application/json' -d @body.json http://localhsot:1424/user
body.json
{
"id": "5",
"name": "李四",
"age": 11
}
- 响应
SUCCESS
编辑用户
- 请求
curl -X PUT -H 'Content-Type: application/json' -d @body.json http://localhost:1424/user
body.json
{
"id": "1",
"title": "天一",
"age": "-1"
}
- 响应
{"id":"1","name":"天一","age":-1}
查询所有用户
- 请求
使用python -m json.tool将服务返回的json,进行格式化处理。
curl http://localhost:1424/users | python -m json.tool
- 响应
[
{
"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"
}
]
查询指定用户
- 请求
curl http://localhost:1424/user/1
- 响应
{"id":"1","name":"天一","age":-1}
删除用户
- 请求
curl -X DELETE http://localhost:1424/user/1
- 响应
SUCCESS