Gin與Mysql實現(xiàn)簡單Restful風(fēng)格API實戰(zhàn)示例詳解
我們已經(jīng)了解了Golang的Gin框架。對于Webservice服務(wù),restful風(fēng)格幾乎一統(tǒng)天下。Gin也天然的支持restful。下面就使用gin寫一個簡單的服務(wù),麻雀雖小,五臟俱全。我們先以一個單文件開始,然后再逐步分解模塊成包,組織代碼。
It works
使用Gin的前提是安裝,我們需要安裝gin和mysql的驅(qū)動,具體的安裝方式就不在贅述。
創(chuàng)建一個文件夾用來為項目,新建一個文件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") }
編譯運(yùn)行
☁ 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
數(shù)據(jù)庫
安裝完畢框架,完成一次請求響應(yīng)之后。接下來就是安裝數(shù)據(jù)庫驅(qū)動和初始化數(shù)據(jù)相關(guān)的操作了。首先,我們需要新建數(shù)據(jù)表。一個及其簡單的數(shù)據(jù)表:
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;
創(chuàng)建數(shù)據(jù)表之后,初始化數(shù)據(jù)庫連接池:
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方法會創(chuàng)建一個數(shù)據(jù)庫連接池db。這個db不是數(shù)據(jù)庫連接,它是一個連接池,只有當(dāng)真正數(shù)據(jù)庫通信的時候才創(chuàng)建連接。例如這里的db.Ping
的操作。db.SetMaxIdleConns(20)
和db.SetMaxOpenConns(20)
分別設(shè)置數(shù)據(jù)庫的空閑連接和最大打開連接,即向Mysql服務(wù)端發(fā)出的所有連接的最大數(shù)目。
如果不設(shè)置,默認(rèn)都是0,表示打開的連接沒有限制。我在壓測的時候,發(fā)現(xiàn)會存在大量的TIME_WAIT狀態(tài)的連接,雖然mysql的連接數(shù)沒有上升。設(shè)置了這兩個參數(shù)之后,不在存在大量TIME_WAIT狀態(tài)的連接了。而且qps也沒有明顯的變化,出于對數(shù)據(jù)庫的保護(hù),最好設(shè)置這連個參數(shù)。
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, }) }) ... }
執(zhí)行非query操作,使用db的Exec方法,在mysql中使用?
做占位符。最后我們把插入后的id返回給客戶端。請求得到的結(jié)果如下:
☁ ~ 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
上面我們增加了一條記錄,下面就獲取這個記錄,查一般有兩個操作,一個是查詢列表,其次就是查詢具體的某一條記錄。兩種大同小異。
為了給查詢結(jié)果綁定到golang的變量或?qū)ο?,我們需要先定義一個結(jié)構(gòu)來綁定對象。在main函數(shù)的上方定義Person結(jié)構(gòu):
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"` }
然后查詢我們的數(shù)據(jù)列表
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的數(shù)據(jù)需要有一個綁定的過程,db.Query方法返回一個rows對象,這個數(shù)據(jù)庫連接隨即也轉(zhuǎn)移到這個對象,因此我們需要定義row.Close操作。然后創(chuàng)建一個[]Person
的切片。
使用make,而不是直接使用var persons []Person
的聲明方式。還是有所差別的,使用make的方式,當(dāng)數(shù)組切片沒有元素的時候,Json會返回[]
。如果直接聲明,json會返回null
。
接下來就是使用rows對象的Next方法,遍歷所查詢的數(shù)據(jù),一個個綁定到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對象,查詢單個記錄,就沒這么麻煩了。雖然也可以迭代一條記錄的結(jié)果集。因為查詢單個記錄的操作實在太常用了,因此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, }) })
查詢結(jié)果為:
☁ ~ 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" } }
查詢單個記錄有一個小問題,當(dāng)數(shù)據(jù)不存在的時候,同樣也會拋出一個錯誤。粗暴的使用log退出有點不妥。返回一個nil的時候,萬一真的是因為錯誤,比如sql錯誤。這種情況如何解決。還需要具體場景設(shè)計程序。
改
增刪改查,下面進(jìn)行更新的操作。前面增加記錄我們使用了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" }
刪
最后一個操作就是刪除了,刪除所需要的功能特性,上面的例子都覆蓋了。實現(xiàn)刪除也就特別簡單了:
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, }) })
我們可以使用刪除接口,把數(shù)據(jù)都刪除了,再來驗證上面post接口獲取列表的時候,當(dāng)記錄沒有的時候,切片被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
。編譯運(yùn)行:
☁ ~ 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風(fēng)格的API已經(jīng)完成。內(nèi)容其實不復(fù)雜,甚至相當(dāng)簡單。完整的代碼可以通過GIST獲取。
組織代碼
實現(xiàn)了一個基本點restful服務(wù),可惜我們的代碼都在一個文件中。對于一個庫,單文件或許很好,對于稍微大一點的項目,單文件總是有點非主流。當(dāng)然,更多原因是為了程序的可讀和維護(hù),我們也需要重新組織代碼,拆分模塊和包。
封裝模型方法
我們的handler出來函數(shù)中,對請求的出來和數(shù)據(jù)庫的交互,都糅合在一起。首先我們基于創(chuàng)建的Person結(jié)構(gòu)創(chuàng)建數(shù)據(jù)模型,以及模型的方法。把數(shù)據(jù)庫交互拆分出來。
創(chuàng)建一個單例的數(shù)據(jù)庫連接池對象:
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就能隨意使用了。
接下來,再把增加記錄的的函數(shù)封裝成Person結(jié)構(gòu)的方法:
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函數(shù)也跟著修改,先創(chuàng)建一個Person結(jié)構(gòu)的實例,然后調(diào)用其方法即可:
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函數(shù)也很好改:
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, }) })
剩下的函數(shù)和方法就不再一一舉例了。
增加記錄的接口中,我們使用了客戶端參數(shù)和Person創(chuàng)建實例,然后再調(diào)用其方法。而獲取列表的接口中,我們直接聲明了Person對象。兩種方式都可以。
Handler函數(shù)
gin提供了router.Get(url, handler func)
的格式。首先我們可以把所有的handler函數(shù)從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抽出來之后,我們的代碼結(jié)構(gòu)變得更加清晰,具體可以參考這個GIST
組織項目
經(jīng)過上面的model和handler的分離,代碼結(jié)構(gòu)變得更加清晰,可是我們還是單文件。下一步將進(jìn)行封裝不同的包。
數(shù)據(jù)庫處理
在項目根目錄創(chuàng)建下面三個文件夾,apis
,databases
和models
,并在文件夾內(nèi)創(chuàng)建文件。此時我們的目錄結(jié)果如下:
apis文件夾存放我們的handler函數(shù),models文件夾用來存放我們的數(shù)據(jù)模型。
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的習(xí)慣,變量名必須大寫開頭。
數(shù)據(jù)model封裝
修改models文件夾下的person.go,把對應(yīng)的Person結(jié)構(gòu)及其方法移到這里:
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函數(shù)封裝到api包中,因為handler函數(shù)要操作數(shù)據(jù)庫,所以會引用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,我們在路由文件中封裝路由函數(shù)
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函數(shù)的app入口,將路由導(dǎo)入,同時我們要在main函數(shù)結(jié)束的時候,關(guān)閉全局的數(shù)據(jù)庫連接池:
main.go
package main import ( db "newgin/database" ) func main() { defer db.SqlDB.Close() router := initRouter() router.Run(":8000") }
至此,我們就把簡單程序進(jìn)行了更好的組織。當(dāng)然,golang的程序組織依包為基礎(chǔ),不拘泥,根據(jù)具體的應(yīng)用場景可以組織。
此時運(yùn)行項目,不能像之前簡單的使用go run main.go
,因為包main包含main.go和router.go的文件,因此需要運(yùn)行go run *.go
命令編譯運(yùn)行。如果是最終編譯二進(jìn)制項目,則運(yùn)行go build -o app
總結(jié)
通過上述的實踐,我們了解了Gin框架創(chuàng)建基本的的restful服務(wù)。并且了解了如何組織golang的代碼包。我們討論了很多內(nèi)容,但是唯獨缺少測試。測試很重要,考察一個框架或者三方包的時候,是否有測試文件以及測試覆蓋率是一個重要的參考。因為測試的內(nèi)容很多,我們這里就不做單獨的測試介紹。后面會結(jié)合gofight給gin的api增加測試代碼。
此外,更多的內(nèi)容,可以閱讀別人優(yōu)秀的開源項目,學(xué)習(xí)并實踐,以提升自己的編碼能力。
以上就是Gin與Mysql實現(xiàn)簡單Restful風(fēng)格API示例詳解的詳細(xì)內(nèi)容,更多關(guān)于Gin與Mysql實現(xiàn)Restful風(fēng)格API的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Win10系統(tǒng)下Golang環(huán)境搭建全過程
在編程語言的選取上,越來越多的人選擇了Golang,下面這篇文章主要給大家介紹了關(guān)于Win10系統(tǒng)下Golang環(huán)境搭建的相關(guān)資料,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-01-01golang 如何用反射reflect操作結(jié)構(gòu)體
這篇文章主要介紹了golang 用反射reflect操作結(jié)構(gòu)體的操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-04-04解析Go 標(biāo)準(zhǔn)庫 http.FileServer 實現(xiàn)靜態(tài)文件服務(wù)
http.FileServer 方法屬于標(biāo)準(zhǔn)庫 net/http,返回一個使用 FileSystem 接口 root 提供文件訪問服務(wù)的 HTTP 處理器。下面通過本文給大家介紹Go 標(biāo)準(zhǔn)庫 http.FileServer 實現(xiàn)靜態(tài)文件服務(wù)的相關(guān)知識,感興趣的朋友一起看看吧2018-08-08golang方法中receiver為指針與不為指針的區(qū)別詳析
這篇文章主要給大家介紹了關(guān)于golang方法中receiver為指針與不為指針區(qū)別的相關(guān)資料,其實最大的區(qū)別應(yīng)該是指針傳遞的是對像的引用,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面來一起看看吧。2017-10-10GO使用socket和channel實現(xiàn)簡單控制臺聊天室
今天小編給大家分享一個簡單的聊天室功能,聊天室主要功能是用戶可以加入離開聊天室,實現(xiàn)思路也很簡單明了,下面小編給大家?guī)砹送暾a,感興趣的朋友跟隨小編一起看看吧2021-12-12詳解Go語言如何實現(xiàn)類似Python中的with上下文管理器
熟悉?Python?的同學(xué)應(yīng)該知道?Python?中的上下文管理器非常好用,那么在?Go?中是否也能實現(xiàn)上下文管理器呢,下面小編就來和大家仔細(xì)講講吧2023-07-07