前言
本文通過一步一步的設計,最終實現一個完善的todo應用。
我們使用GO框架Gin用戶路由控制和返回數據。使用Gorm用於操作數據庫。
讀者可根據本教程操作,最終實現列出的各項功能。
技術清單
本文中所涉及的技術內容主要有以下幾種:
- Gin:輕量高效性能爆棚的WEB框架
- Gorm:一個關係型數據庫的ORM工具包,避免直接SQL語句操作
- MySQL:數據庫
- curl工具,用於API接口數據測試
另外,使用的GO版本是
go version go1.13.5 windows/amd64
對於第2,3條內容,可使用以下指令安裝
go get -u github.com/gin-gonic/gin
go get -u github.com/jinzhu/gorm
對於工具curl,我們使用的是 git bash內自帶的指令。如果是linux下用戶,開箱即用。
創建數據庫
本文使用MySQL數據庫裝載數據。本節我們僅需創建一個空的數據庫,就可以了。表結構在下一節使用gorm遷移功能創建。
使用Navicat工具新建界面如下圖。
需要特別留意數據庫字符集編碼使用 utf8mb4,這個是MySQL真正的utf8,用於中文字符支持。
創建表模型
gorm中的Automigrate()操作,用於刷新數據庫中的表,使其保持最新。即讓數據庫之前存儲的記錄的表字段和程序中最新使用的表字段保持一致(只增不減)。
我們先建一個todos表模型。
<code>type (
\ttodoModel struct {
\t\tgorm.Model
\t\tTitle string `json:"title"`
\t\tCompleted int `json:"completed`
\t}
\ttransformedTodo struct {
\t\tID uint `json:"id"`
\t\tTitle string `json:"title"`
\t\tCompleted bool `json:"completed"`
\t}
)/<code>
其中 todoModel用於數據庫todos表。我們默認繼承使用了gorm.Model的字段,主要包括以下幾個:
<code>// gorm.Model 定義
type Model struct {
ID uint `gorm:"primary_key"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
}/<code>
數據庫表會自動生成上述4個字段,並追加 title,completed兩個字段。模型名與表名不一致,我們手動指定表名:
<code>// 指定表名
func (todoModel) TableName() string {
\treturn "todos"
}/<code>
然後在代碼初始化過程中執行遷移。
<code>var db *gorm.DB
// 初始化
func init() {
\tvar err error
\tvar constr string
\tconstr = fmt.Sprintf("%s:%s@(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", "root", "111213", "localhost", 3306, "05-gin-gorm-todo")
\tdb, err = gorm.Open("mysql", constr)
\tif err != nil {
\t\tpanic("數據庫連接失敗")
\t}
\tdb.AutoMigrate(&todoModel{})
}/<code>
首先聲明一個 gorm.DB,用於數據庫操作。
在使用gorm包之前,需要導入。
<code>import (
\t"github.com/jinzhu/gorm"
\t_ "github.com/jinzhu/gorm/dialects/mysql"
)/<code>
第二項我們僅導入而不使用。這個導入操作,gorm執行了下述操作
<code>
這樣我們無需重新手動處理包依賴關係。
MySQL的連接字符串是有特定格式的,這也是由底層 go-sql-driver/mysql 決定的參數配置項。其含義如下:
<code>user:password@(hostname:port)/database_name?charset=utf8mb4&parseTime=True&loc=Local/<code>
此處我們使用的本地數據庫3306端口,root用戶,和 05-gin-gorm-todo 數據庫。
在執行完整的程序之後,todos表會被自動遷移創建,其詳細參數如下圖。
這張表也是我們本文所操作的數據基礎。
規劃路由
依照restful風格的API設計標準,我們規劃了5個路由,涵蓋了一個todo列表清單的增刪改查功能。
<code>func main() {
\tr := gin.Default()
\tv1 := r.Group("/api/v1/todo")
\t{
\t\tv1.POST("/", add) // 添加新條目
\t\tv1.GET("/", all) // 查詢所有條目
\t\tv1.GET("/:id", take) // 獲取單個條目
\t\tv1.PUT("/:id", update) // 更新單個條目
\t\tv1.DELETE("/:id", del) // 刪除單個條目
\t}
\tr.Run(":9089")
}/<code>
這個是最終會使用到的main函數,使用了gin提供的路由功能。為了擴展方便,我們使用了gin路由的Group功能,將版本v1的所有路由集中處理。
其中,訪問的方法跟別使用 POST表示添加,GET表示查詢,PUT表是更新,DELETE表示刪除,這是restful API設計的一般性方法。
API功能
上一節規劃的路由中,我們聲明瞭5個函數,本節逐一實現這5個函數。注意API返回數據都是JSON格式。
為了統一返回狀態碼,對於正確響應的,返回HTTP CODE = 200。JSON數據的正常與否,使用兩個常量表示,如下:
<code>const (
\tJSON_SUCCESS int = 1
\tJSON_ERROR int = 0
)/<code>
成功返回1,失敗返回0。
1 - 添加條目 add
主要的功能,是拿到POST表單提交的數據,並寫入數據庫,成功則返回信息通知,失敗則給出相應提示。
<code>// 創建TODO條目
func add(c *gin.Context) {
\tcompleted, _ := strconv.Atoi(c.PostForm("completed"))
\ttodo := todoModel{Title: c.PostForm("title"), Completed: completed}
\tdb.Save(&todo)
\tc.JSON(http.StatusOK, gin.H{
\t\t"status": JSON_SUCCESS,
\t\t"message": "創建成功",
\t\t"resourceId": todo.ID,
\t})
}/<code>
注意我們對於POST提供的字段 completed 進行了轉換,因為表單數據默認都是字符串類型,而todoModel內Completed是int類型,所以需要進行轉換。
2 - 獲取所有條目
不接受任何參數,默認給出所有的條目內容。
<code>// 獲取所有條目
func all(c *gin.Context) {
\tvar todos []todoModel
\tvar _todos []fmtTodo
\tdb.Find(&todos)
\t// 沒有數據
\tif len(todos) <= 0 {
\t\tc.JSON(http.StatusOK, gin.H{
\t\t\t"status": JSON_ERROR,
\t\t\t"message": "沒有數據",
\t\t})
\t\treturn
\t}
\t// 格式化
\tfor _, item := range todos {
\t\tcompleted := false
\t\tif item.Completed == 1 {
\t\t\tcompleted = true
\t\t} else {
\t\t\tcompleted = false
\t\t}
\t\t_todos = append(_todos, fmtTodo{
\t\t\tID: item.ID,
\t\t\tTitle: item.Title,
\t\t\tCompleted: completed,
\t\t})
\t}
\tc.JSON(http.StatusOK, gin.H{
\t\t"status": JSON_SUCCESS,
\t\t"message": "ok",
\t\t"data": _todos,
\t})
}/<code>
為了API返回數據的便於使用,我們使用了一個結構體 fmtTodo 用於將結構體 todoModel 的數據進行格式化。如果沒有查詢到任何數據,返回狀態碼 status = 0。
3 - 獲取單個條目
在路由中附加的id,可以調用此路由,用於返回單條數據。
<code>// 根據id獲取一個條目
func take(c *gin.Context) {
\tvar todo todoModel
\ttodoID := c.Param("id")
\tdb.First(&todo, todoID)
\tif todo.ID == 0 {
\t\tc.JSON(http.StatusNotFound, gin.H{
\t\t\t"status": JSON_ERROR,
\t\t\t"message": "條目不存在",
\t\t})
\t\treturn
\t}
\tcompleted := false
\tif todo.Completed == 1 {
\t\tcompleted = true
\t} else {
\t\tcompleted = false
\t}
\t_todo := fmtTodo{
\t\tID: todo.ID,
\t\tTitle: todo.Title,
\t\tCompleted: completed,
\t}
\tc.JSON(http.StatusOK, gin.H{
\t\t"status": JSON_SUCCESS,
\t\t"message": "ok",
\t\t"data": _todo,
\t})
}/<code>
根據ID查詢數據,如果存在就返回,並使用fmtTodo進行格式化;如果不存在,狀態碼等於0。
4 - 更新單個條目
已經存在的數據,根據ID對其內容進行修改。如果ID不存在,返回錯誤信息。
<code>// 更新一個條目
func update(c *gin.Context) {
\tvar todo todoModel
\ttodoID := c.Param("id")
\tdb.First(&todo, todoID)
\tif todo.ID == 0 {
\t\tc.JSON(http.StatusNotFound, gin.H{
\t\t\t"status": JSON_ERROR,
\t\t\t"message": "條目不存在",
\t\t})
\t\treturn
\t}
\tdb.Model(&todo).Update("title", c.PostForm("title"))
\tcompleted, _ := strconv.Atoi(c.PostForm("completed"))
\tdb.Model(&todo).Update("completed", completed)
\tc.JSON(http.StatusOK, gin.H{
\t\t"status": JSON_SUCCESS,
\t\t"message": "更新成功",
\t})
}/<code>
5 - 刪除單個條目
根據ID查詢是否存在,如果存在就進行刪除。
<code>// 刪除條目
func del(c *gin.Context) {
\tvar todo todoModel
\ttodoID := c.Param("id")
\tdb.First(&todo, todoID)
\tif todo.ID == 0 {
\t\tc.JSON(http.StatusOK, gin.H{
\t\t\t"status": JSON_ERROR,
\t\t\t"message": "條目不存在",
\t\t})
\t\treturn
\t}
\tdb.Delete(&todo)
\tc.JSON(http.StatusOK, gin.H{
\t\t"status": JSON_SUCCESS,
\t\t"message": "刪除成功!",
\t})
}/<code>
以上就是5個方法的具體實現,只能用作demo,而不能用於生產。因為表單數據的有效性檢測,我們在代碼中並沒有實現。這在線上是絕對不允許的。
還有一些數據的鑑權,用戶身份權限鑑定,本示例中都沒有。
使用curl測試
完成以上步驟,該todo清單功能基本完善,我們使用
<code>go build main.go/<code>
進行編譯,如果不出錯,編譯通過後,會生成 main.exe 文件。我們在命令行直接運行該文件,結果如下圖。
下面我們對五個url分別進行測試。
首先是獲取所有的條目,
<code>curl -s -X GET http://localhost:9089/api/v1/todo/ /<code>
這會命中第二條路由規則,返回值如下:
<code>{"message":"沒有數據","status":0}/<code>
暫時沒有數據。接著添加一個條目:
<code>curl -s -X POST -d "title=Talk is cheap, show me the code." -d "completed=0" http://localhost:9089/api/v1/todo/ /<code>
執行成功返回數據
<code>{"message":"創建成功","resourceId":8,"status":1}/<code>
接著我們再查詢所有的條目,看看能否找到:
<code>
返回結果如下:
<code>{"data":[{"id":8,"title":"Talk is cheap, show me the code.","completed":false}],"message":"ok","status":1}/<code>
為了演示方便,我們再隨機寫入幾條數據,然後測試單條數據查詢,修改,和刪除。
<code>curl -s -X POST -d "title=Life is short, I use python." -d "completed=0" http://localhost:9089/api/v1/todo/
curl -s -X POST -d "title=Zen of Golang." -d "completed=0" http://localhost:9089/api/v1/todo/
curl -s -X POST -d "title=Do not repeat yourself." -d "completed=0" http://localhost:9089/api/v1/todo/ /<code>
然後獲取所有的條目
<code>curl -s -X GET http://localhost:9089/api/v1/todo/ /<code>
結果JSON數組格式化輸出如下:
測試獲取單條數據,這裡使用id=10這一條,執行如下指令
<code>curl -s -X GET http://localhost:9089/api/v1/todo/10 /<code>
返回結果如下:
<code>{"data":{"id":10,"title":"Zen of Golang.","completed":false},"message":"ok","status":1}/<code>
測試修改該條數據,設置completed=1,為已完成狀態。
<code>curl -s -X PUT -d "title=Zen of Golang." -d "completed=1" http://localhost:9089/api/v1/todo/10 /<code>
執行成功返回:
<code>
注意更新操作使用的method = PUT。會命中第4條路由規則。
下面刪除id=10的條目,使用以下指令:
<code>curl -s -X DELETE http://localhost:9089/api/v1/todo/10 /<code>
執行成功後返回結果:
<code>{"message":"刪除成功!","status":1}/<code>
OK,上面所有的路由都已經測試完畢,看服務器端的訪問歷史,大致如下圖:
注意到有一條是數據庫連接的自動釋放,這是由MySQL設置的連接超時時間決定的,超期閒置則釋放。如果有新的連接請求,重新建立。這可以節約資源。
關鍵點總結
在測試上述功能的時候,列出一些初學者可能會犯的錯。
1 - 數據庫連接失敗
一定要確保連接字符串書寫正確,賬號密碼書寫正確,數據庫IP地址和端口號正確,還有數據庫名稱對應。如果始終不能連接成長,可以嘗試單獨拿出來數據庫連接進行測試,直到通過。
2 - 路由地址
根據設定的路由規則,正確地書寫路由地址,還有傳送參數方法,這樣才能在程序中獲取到提交的數據。
比如使用POST,傳送的表單數據使用
c.PostForm 可以獲取到。而 c.Param 則用於獲取路由中 “/:id” id 這個位置參數。3 - curl測試工具使用
注意使用請求方式 -X 參數,還有POST中使用的 -d 參數選項。
結語
以上內容使用兩個成熟的包,快速地創建了一個待辦清單的微服務。可以看到Go語言生態日臻完善,優秀的框架頻出,給開發帶來了很高的效率。
另外,GO語言的易於書寫特性,接近與腳本語言的表達力,還有嚴格的數據類型檢測,將不少低級的錯誤排除在編譯階段。
Happy coding :-)
【本文由 發佈,持續分享編程與程序員成長相關的內容,歡迎關注】
閱讀更多 程序員小助手 的文章