我們已經了解了Golang的Gin框架。對于Webservice服務,restful風格幾乎一統天下。Gin也天然的支持restful。下面就使用gin寫一個簡單的服務,麻雀雖小,五臟俱全。我們先以一個單文件開始,然后再逐步分解模塊成包,組織代碼。
It works
使用Gin的前提是安裝,我們需要安裝gin和mysql的驅動,具體的安裝方式就不在贅述。可以參考Golang 微框架Gin簡介和Golang持久化。
創建一個文件夾用來為項目,新建一個文件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")
}
編譯運行
? 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
訪問 /
即可看見我們返回的字串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=?")
if err != nil {
log.Fatalln(err)
}
defer stmt.Close()
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")
if err != nil {
return
}
defer rows.Close()
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的分離,代碼結構變得更加清晰,可是我們還是單文件。下一步將進行封裝不同的包。
數據庫處理
在項目根目錄創建下面三個文件夾,apis
,databases
和models
,并在文件夾內創建文件。此時我們的目錄結果如下:
? 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")
if err != nil {
return
}
defer rows.Close()
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增加測試代碼。
此外,更多的內容,可以閱讀別人優秀的開源項目,學習并實踐,以提升自己的編碼能力。