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

It works

使用Gin的前提是安装,我们需要安装gin和mysql的驱动,具体的安装方式就不在赘述。可以参考Golang 微框架Gin简介和Golang持久化。

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

☁  newgin  tree
.
└── main.go

main.go

package mainimport ("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  CurrentDload  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 Personrows.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  CurrentDload  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 Personerr := 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  CurrentDload  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  CurrentDload  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  CurrentDload  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  CurrentDload  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  CurrentDload  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.DBfunc main() {var err errordb, 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 Personrows.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 databaseimport ("database/sql"_ "github.com/go-sql-driver/mysql""log"
)var SqlDB *sql.DBfunc init() {var err errorSqlDB, 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 modelsimport ("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 Personrows.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 apisimport ("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 mainimport ("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 mainimport (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实战:Gin+Mysql简单的Restful风格的API相关推荐

  1. mysql例子 restful_Gin实战:Gin+Mysql简单的Restful风格的API

    我们已经了解了Golang的Gin框架.对于Webservice服务,restful风格几乎一统天下.Gin也天然的支持restful.下面就使用gin写一个简单的服务,麻雀虽小,五脏俱全.我们先以一 ...

  2. 简单介绍RESTful风格

    文章目录 前言 一.RESTful风格是什么? 二.搭建环境: 1. get.html 2. post.html 3. Controller.java 三.@PathVariable简单介绍 前言 学 ...

  3. 【译】使用python创建一个简单的restful风格的webservice应用

    2019独角兽企业重金招聘Python工程师标准>>> 这是一个如何使用python快速构建简单restful风格webservice的应用教程. 1.分析rest路由规则 rest ...

  4. php yii2 api框架,Yii2框架制作RESTful风格的API快速入门教程

    先给大家说下什么是REST restful REST全称是Representational State Transfer,中文意思是表述(编者注:通常译为表征)性状态转移. 它首次出现在2000年Ro ...

  5. RESTful风格的API设计

    RESTful风格的API设计 1.你怎么理解 RESTful 2000 年,Roy Thomas Fielding 博士在他那篇著名的博士论文<Architectural Styles and ...

  6. restful风格_什么是RESTful风格的API设计?

    随着移动互联网的兴起,RESTful风格的API设计也随之流行起来,但我们说了那么多RESTful设计,它到底是什么?本篇文章带大家来了解一下它的真实面目. RESTful概念 首先,我们需要明确的是 ...

  7. 什么是RESTful风格的API设计?

    随着移动互联网的兴起,RESTful风格的API设计也随之流行起来,但我们说了那么多RESTful设计,它到底是什么?本篇文章带大家来了解一下它的真实面目. RESTful概念 首先,我们需要明确的是 ...

  8. 简单认识restful风格

    Rest风格 restful风格只是一种风格[或者习惯],不是标准 使用请求方式 使用RESTful操作资源 GET-----------查询用户信息 POST---------新建用户信息 PUT- ...

  9. restful 风格 web api规范

    协议:http/https 域名 : http://api.example.com/xxx/xxx api: 标明api接口服务 xxx: 服务 xxx: 资源 版本控制: 一.使用MediaType ...

最新文章

  1. SpringBoot 学习 | raibaby Halo v0.4.3 漏洞分析-Ali0th
  2. 如何把讨厌的人踢出局域网(kickthemout)
  3. MySQL 性能 细节 考量 (更新中......)
  4. LeetCode Algorithm 6. Z 字形变换
  5. 在Eclipse中导入dtd和xsd文件,使XML自动提示
  6. linux下libpcap抓包分析
  7. 离职证明电子版_离职证明中说劳动者因违纪离职的怎么办?
  8. scrapy初始化selenium,防止网站反爬虫策略监测自动化控件
  9. 华为麦芒5云空间升级_云+AI+5G 华为云联手中软国际引爆线下门店智能化升级
  10. jenkins:集成sonar代码扫描+发送邮件
  11. DAY102 - Rest Framework(七)- 手动编写配置文件、分页器和版本控制
  12. 【Spring 工厂】反转控制与依赖注入、Spring工厂创建复杂对象3种方式
  13. [Linux] DSO missing from command line
  14. python输入一个整数、输出该整数的所有素数因子_【401】Python 求合数的所有质数因子...
  15. 拓端tecdat|R语言贝叶斯非参数模型:密度估计、非参数化随机效应meta分析心肌梗死数据
  16. 数值范围_涉及数值范围的答复及撰写建议
  17. 【Https】Spring RestTemplete支持Https安全请求
  18. 零基础 Java 学习笔记
  19. Windows PE的作用
  20. oracle rac 12514,ORA-12514: 错误解决一例

热门文章

  1. 写一个判别素数的函数,在主函数中输入一个整数,输出是否为素数的信息。
  2. C语言创建函数案例:求长方体体积
  3. 哈密顿系统_Matlab
  4. 【论文阅读笔记】Automatic Liver and Lesion Segmentation in CT Using Cascaded Fully Convolutional Neural Net
  5. IBM朱辉:大数据分析的5个高复制使用场景及案例分享(含PPT)
  6. Python的7大就业方向,你知道几个?
  7. JAVA 类声明中关键字public的作用
  8. MDK编译全过程及数据存储
  9. 京东“毕业”下的众生相:房贷、二胎,这失业后的日子怎么办?
  10. mysql连接密码加密_数据库连接用户名和密码加密