Gin+Mysql simple Restful style API

The premise of using Gin is installation. We need to install the drivers of gin and mysql. The specific installation method will not be repeated here. You can refer to the introduction to the Golang micro-framework Gin and Golang persistence .

Create a folder for the project, and create a new file main.go:

☁  newland  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")
}

compile and run

☁  newland  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

Visit  /to see the string we returnedIt works

database

After installing the framework and completing a request response. The next step is to install the database driver and initialize the data related operations. First, we need to create a new data table. A very simple data table:

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;

After creating the data table, initialize the database connection pool:

func main() {

 db, err := sql.Open("mysql", "root:@tcp(127.0.0.1:3306)/test?parseTime=true")
 defer db.Close()
 if err != nil{
  log.Fatalln(err)
 }


 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")
}

Using the sql.Open method creates a database connection pool db. This db is not a database connection, it is a connection pool that only creates a connection when the real database communicates. Such as db.Pingthe operation here. db.SetMaxIdleConns(20)and db.SetMaxOpenConns(20)respectively set the idle connection and the maximum open connection of the database, that is, the maximum number of all connections issued to the Mysql server.

If not set, the default is 0, which means that there are no restrictions on open connections. During the stress test, I found that there will be a large number of connections in the TIME_WAIT state, although the number of mysql connections has not increased. After setting these two parameters, there are no more connections in the TIME_WAIT state. And qps has not changed significantly. For the protection of the database, it is best to set this parameter.

CURD CRUD

The basics of Restful are curd operations on resources. Let's open our first api interface and add a resource.

increase

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,
  })
 })

 ...
}

To perform non-query operations, use the Exec method of db and use ?it as a placeholder in mysql. Finally, we return the inserted id to the client. The result of the request is as follows:

☁  ~  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"
}

Feel free to add a few more records below.

check

Query list Query

We have added a record above, and we will get this record below. There are generally two operations for checking, one is to query the list, and the other is to query a specific record. The two are very similar.

In order to bind the query result to a variable or object in golang, we need to define a structure to bind the object. Define the Person structure above the main function:

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"`
}

Then query our data list

 router.GET("/persons", func(c *gin.Context) {
  rows, err := db.Query("SELECT id, first_name, last_name FROM person")
  defer rows.Close()

  if err != nil {
   log.Fatalln(err)
  }

  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,
  })

 })

Reading mysql data requires a binding process. The db.Query method returns a rows object, and the database connection is also transferred to this object, so we need to define the row.Close operation. Then create a []Personslice of it.

Use make instead var persons []Personof the declarative way of using it directly. There is still a difference. Using the make method, when the array slice has no elements, Json will return []. If declared directly, json will be returned null.

The next step is to use the Next method of the rows object to traverse the queried data, bind to the person object one by one, and finally append to the persons slice.

☁  ~  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"
        }
    ]
}
Query a single record QueryRow

Querying the list requires using the iterative rows object, and querying a single record is not so troublesome. Although it is also possible to iterate over the result set of a record. Because the operation of querying a single record is so common, golang's database/sql also provides a query method.

 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,
  })

 })

The query result is:

☁  ~  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"
    }
}

There is a small problem with querying a single record, which also throws an error when the data doesn't exist. Rough use of log exit is a bit inappropriate. When returning a nil, in case it is really because of an error, such as an sql error. How to solve this situation. A specific scene design program is also required.

change

Add, delete, modify, check, and update the operation below. We used the urlencode method to submit the previous record, and we automatically matched the binding content-type for the updated api.

 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,
  })
 })

Update using 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"
}

Update using 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"
}

delete

The last operation is to delete, delete the required features, the above examples are covered. Implementing deletion is particularly simple:

 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,
  })
 })

We can use the delete interface to delete all the data, and then verify that when the post interface above obtains the list, when there is no record, the slice is serialized by json []or not.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)Change it to persons []Person. Compile and run:

☁  ~  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,并在文件夹内创建文件。此时我们的目录结果如下:

☁  newland  tree
.
├── apis
│   └── person.go
├── database
│   └── mysql.go
├── main.go
├── models
│   └── person.go
└── router.go

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 "newland/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"
 . "newland/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"
 . "newland/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
}

app入口

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

main.go

package main

import (
 db "newland/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实战:Gin+Mysql简单的Restful风格的API

 

https://studygolang.com/articles/9630

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=326539931&siteId=291194637