Gin与Mysql实现简单Restful风格API实战示例详解

目录
  • it works
    • main.go
    • 编译运行
  • 数据库
    • curd 增删改查
      •  
        • 查询列表 query
        • 查询单条记录 queryrow
        • 组织代码
          • 封装模型方法
            • handler函数
            • 组织项目
              • 数据库处理
                • 数据model封装
                  • handler
                    • 路由
                      • 分组路由
                        • app入口
                        • 总结

                          我们已经了解了golang的gin框架。对于webservice服务,restful风格几乎一统天下。gin也天然的支持restful。下面就使用gin写一个简单的服务,麻雀虽小,五脏俱全。我们先以一个单文件开始,然后再逐步分解模块成包,组织代码。

                          it works

                          使用gin的前提是安装,我们需要安装gin和mysql的驱动,具体的安装方式就不在赘述。

                          参考golang 微框架gin简介和golang持久化。

                          创建一个文件夹用来为项目,新建一个文件main.go:

                            newgin  tree
                          .
                          └── main.go

                          main.go

                          package main 
                          import (
                           "gopkg.in/gin-gonic/gin.v1"
                           "net/http"
                          ) 
                          func main() {
                           router := gin.default()
                           router.get("/", func(c *gin.context) {
                            c.string(http.statusok, "it works")
                           }) 
                           router.run(":8000")
                          }

                          编译运行

                            newgin  go run main.go
                          [gin-debug] [warning] running in "debug" mode. switch to "release" mode in production.
                           - using env: export gin_mode=release
                           - using code: gin.setmode(gin.releasemode) 
                          [gin-debug] get    /                         --> main.main.func1 (3 handlers)
                          [gin-debug] listening and serving http on :8000
                           

                          访问 /即可看见我们返回的字串it works

                          数据库

                          安装完毕框架,完成一次请求响应之后。接下来就是安装数据库驱动和初始化数据相关的操作了。首先,我们需要新建数据表。一个及其简单的数据表:

                          create table `person` (
                            `id` int(11) not null auto_increment,
                            `first_name` varchar(40) not null default '',
                            `last_name` varchar(40) not null default '',
                            primary key (`id`)
                          ) engine=innodb default charset=utf8;

                          创建数据表之后,初始化数据库连接池:

                          func main() { 
                           db, err := sql.open("mysql", "root:@tcp(127.0.0.1:3306)/test?parsetime=true")
                           if err != nil{
                            log.fatalln(err)
                           }
                          defer db.close()
                           db.setmaxidleconns(20)
                           db.setmaxopenconns(20) 
                           if err := db.ping(); err != nil{
                            log.fatalln(err)
                           } 
                           router := gin.default()
                           router.get("/", func(c *gin.context) {
                            c.string(http.statusok, "it works")
                           }) 
                           router.run(":8000")
                          }

                          使用sql.open方法会创建一个数据库连接池db。这个db不是数据库连接,它是一个连接池,只有当真正数据库通信的时候才创建连接。例如这里的db.ping的操作。db.setmaxidleconns(20)db.setmaxopenconns(20)分别设置数据库的空闲连接和最大打开连接,即向mysql服务端发出的所有连接的最大数目。

                          如果不设置,默认都是0,表示打开的连接没有限制。我在压测的时候,发现会存在大量的time_wait状态的连接,虽然mysql的连接数没有上升。设置了这两个参数之后,不在存在大量time_wait状态的连接了。而且qps也没有明显的变化,出于对数据库的保护,最好设置这连个参数。

                          curd 增删改查

                          restful的基本就是对资源的curd操作。下面开启我们的第一个api接口,增加一个资源。

                           

                          func main() { 
                           ... 
                           router.post("/person", func(c *gin.context) {
                            firstname := c.request.formvalue("first_name")
                            lastname := c.request.formvalue("last_name") 
                            rs, err := db.exec("insert into person(first_name, last_name) values (?, ?)", firstname, lastname)
                            if err != nil {
                             log.fatalln(err)
                            } 
                            id, err := rs.lastinsertid()
                            if err != nil {
                             log.fatalln(err)
                            }
                            fmt.println("insert person id {}", id)
                            msg := fmt.sprintf("insert successful %d", id)
                            c.json(http.statusok, gin.h{
                             "msg": msg,
                            })
                           }) 
                           ...
                          }
                           

                          执行非query操作,使用db的exec方法,在mysql中使用?做占位符。最后我们把插入后的id返回给客户端。请求得到的结果如下:

                            ~  curl -x post http://127.0.0.1:8000/person -d "first_name=hello&last_name=world" | python -m json.tool
                            % total    % received % xferd  average speed   time    time     time  current
                                                           dload  upload   total   spent    left  speed
                          100    62  100    30  100    32   5054   5391 --:--:-- --:--:-- --:--:--  6400
                          {
                              "msg": "insert successful 1"
                          }

                          下面可以随意增加几条记录。

                          查询列表 query

                          上面我们增加了一条记录,下面就获取这个记录,查一般有两个操作,一个是查询列表,其次就是查询具体的某一条记录。两种大同小异。

                          为了给查询结果绑定到golang的变量或对象,我们需要先定义一个结构来绑定对象。在main函数的上方定义person结构:

                          type person struct {
                           id        int    `json:"id" form:"id"`
                           firstname string `json:"first_name" form:"first_name"`
                           lastname  string `json:"last_name" form:"last_name"`
                          }

                          然后查询我们的数据列表

                           router.get("/persons", func(c *gin.context) {
                            rows, err := db.query("select id, first_name, last_name from person")  
                            if err != nil {
                             log.fatalln(err)
                            }
                           defer rows.close()
                             persons := make([]person, 0)
                            for rows.next() {
                             var person person
                             rows.scan(&person.id, &person.firstname, &person.lastname)
                             persons = append(persons, person)
                            }
                            if err = rows.err(); err != nil {
                             log.fatalln(err)
                            } 
                            c.json(http.statusok, gin.h{
                             "persons": persons,
                            }) 
                           })

                          读取mysql的数据需要有一个绑定的过程,db.query方法返回一个rows对象,这个数据库连接随即也转移到这个对象,因此我们需要定义row.close操作。然后创建一个[]person的切片。

                          使用make,而不是直接使用var persons []person的声明方式。还是有所差别的,使用make的方式,当数组切片没有元素的时候,json会返回[]。如果直接声明,json会返回null

                          接下来就是使用rows对象的next方法,遍历所查询的数据,一个个绑定到person对象上,最后append到persons切片。

                            ~  curl  http://127.0.0.1:8000/persons | python -m json.tool
                            % total    % received % xferd  average speed   time    time     time  current
                                                           dload  upload   total   spent    left  speed
                          100   113  100   113    0     0   101k      0 --:--:-- --:--:-- --:--:--  110k
                          {
                              "persons": [
                                  {
                                      "first_name": "hello",
                                      "id": 1,
                                      "last_name": "world"
                                  },
                                  {
                                      "first_name": "vanyar",
                                      "id": 2,
                                      "last_name": "elves"
                                  }
                              ]
                          }

                          查询单条记录 queryrow

                          查询列表需要使用迭代rows对象,查询单个记录,就没这么麻烦了。虽然也可以迭代一条记录的结果集。因为查询单个记录的操作实在太常用了,因此golang的database/sql也专门提供了查询方法

                           router.get("/person/:id", func(c *gin.context) {
                            id := c.param("id")
                            var person person
                            err := db.queryrow("select id, first_name, last_name from person where id=?", id).scan(
                             &person.id, &person.firstname, &person.lastname,
                            )
                            if err != nil {
                             log.println(err)
                             c.json(http.statusok, gin.h{
                              "person": nil,
                             })
                             return
                            }
                             c.json(http.statusok, gin.h{
                             "person": person,
                            })
                           
                           })

                          查询结果为:

                            ~  curl  http://127.0.0.1:8000/person/1 | python -m json.tool
                            % total    % received % xferd  average speed   time    time     time  current
                                                           dload  upload   total   spent    left  speed
                          100    60  100    60    0     0  20826      0 --:--:-- --:--:-- --:--:-- 30000
                          {
                              "person": {
                                  "first_name": "hello",
                                  "id": 1,
                                  "first_name": "world"
                              }
                          }

                          查询单个记录有一个小问题,当数据不存在的时候,同样也会抛出一个错误。粗暴的使用log退出有点不妥。返回一个nil的时候,万一真的是因为错误,比如sql错误。这种情况如何解决。还需要具体场景设计程序。

                          增删改查,下面进行更新的操作。前面增加记录我们使用了urlencode的方式提交,更新的api我们自动匹配绑定content-type

                           router.put("/person/:id", func(c *gin.context) {
                            cid := c.param("id")
                            id, err := strconv.atoi(cid)
                            person := person{id: id}
                            err = c.bind(&person)
                            if err != nil {
                             log.fatalln(err)
                            }
                            stmt, err := db.prepare("update person set first_name=?, last_name=? where id=?")
                            defer stmt.close()
                            if err != nil {
                             log.fatalln(err)
                            }
                            rs, err := stmt.exec(person.firstname, person.lastname, person.id)
                            if err != nil {
                             log.fatalln(err)
                            }
                            ra, err := rs.rowsaffected()
                            if err != nil {
                             log.fatalln(err)
                            }
                            msg := fmt.sprintf("update person %d successful %d", person.id, ra)
                            c.json(http.statusok, gin.h{
                             "msg": msg,
                            })
                           })
                           

                          使用 urlencode的方式更新:

                            ~  curl -x put http://127.0.0.1:8000/person/2 -d "first_name=noldor&last_name=elves" | python -m json.tool
                            % total    % received % xferd  average speed   time    time     time  current
                                                           dload  upload   total   spent    left  speed
                          100    72  100    39  100    33   3921   3317 --:--:-- --:--:-- --:--:--  4333
                          {
                              "msg": "update person 2 successful 1"
                          }

                          使用json的方式更新:

                            ~  curl -x put http://127.0.0.1:8000/person/2 -h "content-type: application/json"  -d '{"first_name": "vanyar", "last_name": "elves"}' | python -m json.tool
                            % total    % received % xferd  average speed   time    time     time  current
                                                           dload  upload   total   spent    left  speed
                          100    85  100    39  100    46   4306   5079 --:--:-- --:--:-- --:--:--  5750
                          {
                              "msg": "update person 2 successful 1"
                          }

                          最后一个操作就是删除了,删除所需要的功能特性,上面的例子都覆盖了。实现删除也就特别简单了:

                           router.delete("/person/:id", func(c *gin.context) {
                            cid := c.param("id")
                            id, err := strconv.atoi(cid)
                            if err != nil {
                             log.fatalln(err)
                            }
                            rs, err := db.exec("delete from person where id=?", id)
                            if err != nil {
                             log.fatalln(err)
                            }
                            ra, err := rs.rowsaffected()
                            if err != nil {
                             log.fatalln(err)
                            }
                            msg := fmt.sprintf("delete person %d successful %d", id, ra)
                            c.json(http.statusok, gin.h{
                             "msg": msg,
                            })
                           })

                          我们可以使用删除接口,把数据都删除了,再来验证上面post接口获取列表的时候,当记录没有的时候,切片被json序列化[]还是null

                            ~  curl  http://127.0.0.1:8000/persons | python -m json.tool
                            % total    % received % xferd  average speed   time    time     time  current
                                                           dload  upload   total   spent    left  speed
                          100    15  100    15    0     0  11363      0 --:--:-- --:--:-- --:--:-- 15000
                          {
                              "persons": []
                          }

                          persons := make([]person, 0)改成persons []person。编译运行:

                            ~  curl  http://127.0.0.1:8000/persons | python -m json.tool
                            % total    % received % xferd  average speed   time    time     time  current
                                                           dload  upload   total   spent    left  speed
                          100    17  100    17    0     0  13086      0 --:--:-- --:--:-- --:--:-- 17000
                          {
                              "persons": null
                          }

                          至此,基本的curd操作的restful风格的api已经完成。内容其实不复杂,甚至相当简单。完整的代码可以通过gist获取。

                          组织代码

                          实现了一个基本点restful服务,可惜我们的代码都在一个文件中。对于一个库,单文件或许很好,对于稍微大一点的项目,单文件总是有点非主流。当然,更多原因是为了程序的可读和维护,我们也需要重新组织代码,拆分模块和包。

                          封装模型方法

                          我们的handler出来函数中,对请求的出来和数据库的交互,都糅合在一起。首先我们基于创建的person结构创建数据模型,以及模型的方法。把数据库交互拆分出来。

                          创建一个单例的数据库连接池对象:

                          var db *sql.db
                          func main() {
                           var err error
                           db, err = sql.open("mysql", "root:@tcp(127.0.0.1:3306)/test?parsetime=true")
                           if err != nil {
                            log.fatalln(err)
                           } 
                           defer db.close()
                           if err := db.ping(); err != nil {
                            log.fatalln(err)
                           }
                           ...
                          }

                          这样在main包中,db就能随意使用了。

                          接下来,再把增加记录的的函数封装成person结构的方法:

                          func (p *person) addperson() (id int64, err error) {
                           rs, err := db.exec("inserts into person(first_name, last_name) values (?, ?)", p.firstname, p.lastname)
                           if err != nil {
                            return
                           }
                           id, err = rs.lastinsertid()
                           return
                          }

                          然后handler函数也跟着修改,先创建一个person结构的实例,然后调用其方法即可:

                           router.post("/person", func(c *gin.context) {
                            firstname := c.request.formvalue("first_name")
                            lastname := c.request.formvalue("last_name") 
                            person := person{firstname: firstname, lastname: lastname}
                            ra_rows, err := person.addperson()
                            if err != nil {
                             log.fatalln(err)
                            }
                            msg := fmt.sprintf("insert successful %d", ra_rows)
                            c.json(http.statusok, gin.h{
                             "msg": msg,
                            })
                           })

                          对于获取列表的模型方法和handler函数也很好改:

                          func (p *person) getpersons() (persons []person, err error) {
                           persons = make([]person, 0)
                           rows, err := db.query("select id, first_name, last_name from person")
                           defer rows.close() 
                           if err != nil {
                            return
                           } 
                           for rows.next() {
                            var person person
                            rows.scan(&person.id, &person.firstname, &person.lastname)
                            persons = append(persons, person)
                           }
                           if err = rows.err(); err != nil {
                            return
                           }
                           return
                          }

                           router.post("/person", func(c *gin.context) {
                            firstname := c.request.formvalue("first_name")
                            lastname := c.request.formvalue("last_name")
                            person := person{firstname: firstname, lastname: lastname}
                            ra_rows, err := person.addperson()
                            if err != nil {
                             log.fatalln(err)
                            }
                            msg := fmt.sprintf("insert successful %d", ra_rows)
                            c.json(http.statusok, gin.h{
                             "msg": msg,
                            })
                           })

                          剩下的函数和方法就不再一一举例了。

                          增加记录的接口中,我们使用了客户端参数和person创建实例,然后再调用其方法。而获取列表的接口中,我们直接声明了person对象。两种方式都可以。

                          handler函数

                          gin提供了router.get(url, handler func)的格式。首先我们可以把所有的handler函数从router中提取出来。

                          例如把增加记录和获取列表的handle提取出来

                          func addpersonapi(c *gin.context) {
                           firstname := c.request.formvalue("first_name")
                           lastname := c.request.formvalue("last_name") 
                           person := person{firstname: firstname, lastname: lastname} 
                           ra_rows, err := person.addperson()
                           if err != nil {
                            log.fatalln(err)
                           }
                           msg := fmt.sprintf("insert successful %d", ra_rows)
                           c.json(http.statusok, gin.h{
                            "msg": msg,
                           })
                          } 
                          func main(){
                           ...
                           router.post("/person", addpersonapi)
                           ... 
                          }

                          把modle和handler抽出来之后,我们的代码结构变得更加清晰,具体可以参考这个gist

                          组织项目

                          经过上面的model和handler的分离,代码结构变得更加清晰,可是我们还是单文件。下一步将进行封装不同的包。

                          数据库处理

                          在项目根目录创建下面三个文件夹,apisdatabasesmodels,并在文件夹内创建文件。此时我们的目录结果如下:

                          apis文件夹存放我们的handler函数,models文件夹用来存放我们的数据模型。

                          myql.go的包代码如下:

                          package database 
                          import (
                           "database/sql"
                           _ "github.com/go-sql-driver/mysql"
                           "log"
                          ) 
                          var sqldb *sql.db 
                          func init() {
                           var err error
                           sqldb, err = sql.open("mysql", "root:@tcp(127.0.0.1:3306)/test?parsetime=true")
                           if err != nil {
                            log.fatal(err.error())
                           }
                           err = sqldb.ping()
                           if err != nil {
                            log.fatal(err.error())
                           }
                          }

                          因为我们需要在别的地方使用sqldb这个变量,因此依照golang的习惯,变量名必须大写开头。

                          数据model封装

                          修改models文件夹下的person.go,把对应的person结构及其方法移到这里:

                          package models 
                          import (
                           "log"
                           db "newgin/database"
                          ) 
                          type person struct {
                           id        int    `json:"id" form:"id"`
                           firstname string `json:"first_name" form:"first_name"`
                           lastname  string `json:"last_name" form:"last_name"`
                          } 
                          func (p *person) addperson() (id int64, err error) {
                           rs, err := db.sqldb.exec("insert into person(first_name, last_name) values (?, ?)", p.firstname, p.lastname)
                           if err != nil {
                            return
                           }
                           id, err = rs.lastinsertid()
                           return
                          } 
                          func (p *person) getpersons() (persons []person, err error) {
                           persons = make([]person, 0)
                           rows, err := db.sqldb.query("select id, first_name, last_name from person")
                           defer rows.close() 
                           if err != nil {
                            return
                           } 
                           for rows.next() {
                            var person person
                            rows.scan(&person.id, &person.firstname, &person.lastname)
                            persons = append(persons, person)
                           }
                           if err = rows.err(); err != nil {
                            return
                           }
                           return
                          }
                          ....
                           

                          handler

                          然后把具体的handler函数封装到api包中,因为handler函数要操作数据库,所以会引用model包

                          package apis 
                          import (
                           "net/http"
                           "log"
                           "fmt"
                           "strconv"
                           "gopkg.in/gin-gonic/gin.v1"
                           . "newgin/models"
                          ) 
                          func indexapi(c *gin.context) {
                           c.string(http.statusok, "it works")
                          } 
                          func addpersonapi(c *gin.context) {
                           firstname := c.request.formvalue("first_name")
                           lastname := c.request.formvalue("last_name") 
                           p := person{firstname: firstname, lastname: lastname} 
                           ra, err := p.addperson()
                           if err != nil {
                            log.fatalln(err)
                           }
                           msg := fmt.sprintf("insert successful %d", ra)
                           c.json(http.statusok, gin.h{
                            "msg": msg,
                           })
                          }
                          ...

                          路由

                          最后就是把路由抽离出来,修改router.go,我们在路由文件中封装路由函数

                          package main
                          import (
                           "gopkg.in/gin-gonic/gin.v1"
                           . "newgin/apis"
                          ) 
                          func initrouter() *gin.engine {
                           router := gin.default() 
                           router.get("/", indexapi) 
                           router.post("/person", addpersonapi) 
                           router.get("/persons", getpersonsapi)
                           router.get("/person/:id", getpersonapi)
                           router.put("/person/:id", modpersonapi) 
                           router.delete("/person/:id", delpersonapi) 
                           return router
                          }
                           

                          分组路由

                          	v1 := router.group("/v1").use(middleware.authrequired())
                          	{
                          		v1.get("/", indexapi)
                           
                          		v1.get("/person", addpersonapi)
                           
                          		v1.get("/persons", getpersonsapi)
                           
                          		v1.post("/person/:id", getpersonapi)
                          		//
                          		v1.put("/person/:id", editpersonapi)
                          		//
                          		v1.delete("/person/:id", delpersonapi)
                          	}

                          app入口

                          最后就是main函数的app入口,将路由导入,同时我们要在main函数结束的时候,关闭全局的数据库连接池:

                          main.go

                          package main 
                          import (
                           db "newgin/database"
                          ) 
                          func main() {
                           defer db.sqldb.close()
                           router := initrouter()
                           router.run(":8000")
                          }
                           

                          至此,我们就把简单程序进行了更好的组织。当然,golang的程序组织依包为基础,不拘泥,根据具体的应用场景可以组织。

                          此时运行项目,不能像之前简单的使用go run main.go,因为包main包含main.go和router.go的文件,因此需要运行go run *.go命令编译运行。如果是最终编译二进制项目,则运行go build -o app

                          总结

                          通过上述的实践,我们了解了gin框架创建基本的的restful服务。并且了解了如何组织golang的代码包。我们讨论了很多内容,但是唯独缺少测试。测试很重要,考察一个框架或者三方包的时候,是否有测试文件以及测试覆盖率是一个重要的参考。因为测试的内容很多,我们这里就不做单独的测试介绍。后面会结合gofight给gin的api增加测试代码。

                          此外,更多的内容,可以阅读别人优秀的开源项目,学习并实践,以提升自己的编码能力。

                          以上就是gin与mysql实现简单restful风格api示例详解的详细内容,更多关于gin与mysql实现restful风格api的资料请关注www.887551.com其它相关文章!

                          (0)
                          上一篇 2022年3月21日
                          下一篇 2022年3月21日

                          相关推荐