使用Golang基于MongoDB构建Restful服务

11 min read

近来使用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

性能对比

performance.png

服务实现

具体实现中基于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
Last updated on 2017-06-17