Go 是近年來非常熱門的程式語言。它有精簡語法與高效的執行效率。現今,Go 已經被大量地用來開發後端程式。本章將介紹如何用 Go 建立一個後端程式,並且實作 REST APIs,和串接資料庫。
Table of Contents
建立 Go 專案
首先,我們先用以下的指令來建立一個 Go 專案,叫 GoRestDbExample。專案的 package 是 github.com/xhhuango/waynestalk/gorestdbexample
。
% mkdir GoRestDbExample % cd GoRestDbExample GoRestDbExample % go mod init github.com/xhhuango/waynestalk/gorestdbexample
新增 user.go,並宣告 User struct
。程式中的 json
是一個 struct field tag。在將 struct 編碼成 JSON 時,指定此欄位在 JSON 中的名稱。如,我們希望 Username 欄位在 JSON 中是 username 而不是大寫 U。另外,”-” 是指忽略此欄位。
package main type User struct { Username string `json:"username"` Password string `json:"-"` Name string `json:"name"` Age uint `json:"age"` }
新增 main.go 主程式,並印出一個 user。
package main import "fmt" func main() { fmt.Printf("%v", User{ Username: "bruce@gmail.com", Password: "123456", Name: "Bruce", Age: 24, }) }
用以下指令執行此專案。
GoRestDbExample % go run github.com/xhhuango/waynestalk/gorestdbexample {bruce@gmail.com 123456 Bruce 24}
建立 RESTful Web Services – Gin
Go 內建的 net/http 套件就可以實作 REST APIs。不過,有很多的第三方套件還提供了 router 功能。本文章中,我們將使用 gin-gonic/gin 來建立一個 RESTful Web Service。用以下指令安裝 Gin。
GoRestDbExample % go get -u github.com/gin-gonic/gin
只需兩三行,Gin 就可以建立一個 RESTful Web Service。下方為修改後的 main.go,它在本地的 port 7788 建立一個 web service,不過還沒有實作任何 REST APIs。
package main import "github.com/gin-gonic/gin" func main() { r := gin.Default() if err := r.Run("localhost:7788"); err != nil { panic(err) } }
HTTP Requests
新增 handler.go,其程式碼如下。JSON() 將 user
slice 轉為 JSON 後輸出到 response body。
package main import ( "github.com/gin-gonic/gin" "net/http" ) var users = []User{ {Username: "bruce@gmail.com", Password: "123", Name: "Bruce", Age: 24}, {Username: "monika@gmail.com", Password: "123", Name: "Monika", Age: 18}, } func ListUsers(c *gin.Context) { c.JSON(http.StatusOK, users) }
然後在 main.go 中,將 ListUsers() 加入 router 中。程式中,我們呼叫 GET(),表示這是一個 GET Request,路徑為 /users。Gin 還提供了 POST(), PUT(), DELETE() 等。
package main import "github.com/gin-gonic/gin" func main() { r := gin.Default() r.GET("/users", ListUsers) if err := r.Run("localhost:7788"); err != nil { panic(err) } }
執行專案後,用 curl 指令發出 HTTP GET Request。
% curl --location --request GET 'localhost:7788/users' [{"username":"bruce@gmail.com","name":"Bruce","age":24},{"username":"monika@gmail.com","name":"Monika","age":18}]
Path Variables
用 Gin 取得 path variables 是很簡單的。在 handler.go 中,新增 GetUser()。我們可以用 ShouldBindUri() 來取得 path variable。變數 param.Name
要使用 field tag uri
來指定要綁定的路徑名稱。
func GetUser(c *gin.Context) { var param struct { Name string `uri:"name"` } if err := c.ShouldBindUri(¶m); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": err.Error(), }) return } for _, u := range users { if u.Name == param.Name { c.JSON(http.StatusOK, u) return } } c.JSON(http.StatusNotFound, gin.H{ "error": fmt.Sprintf("user %s not found", param.Name), }) }
記得要在 main.go 中,將 GetUser() 加到 router 裡。在設定 router 時,path variable 會有一個分號的前綴。
package main import "github.com/gin-gonic/gin" func main() { r := gin.Default() r.GET("/users", ListUsers) r.GET("/users/:name", GetUser) if err := r.Run("localhost:7788"); err != nil { panic(err) } }
Query Parameters
取得 query parameters 的方式和取得 path variables 相差不多。
將 ListUsers() 改為如下。在這程式碼中,我們要取得 query parameter age
。與 path variables 用 uri
不同,query parameters 用 field tag form
來指定要綁定的名稱,並且呼叫 ShouldBindQuery() 來取得 query parameters。
func ListUsers(c *gin.Context) { var query struct { Age uint `form:"age"` } if err := c.ShouldBindQuery(&query); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": err.Error(), }) return } for _, u := range users { if u.Age == query.Age { c.JSON(http.StatusOK, u) return } } c.JSON(http.StatusNotFound, gin.H{ "error": fmt.Sprintf("user with age %d not found", query.Age), }) }
% curl --location --request GET 'localhost:7788/users?age=18' {"username":"monika@gmail.com","name":"Monika","age":18}
Request Body
最後一種是 POST Request 傳遞過來的 response body。在 handler.go 中,我們加入 CreateUser() 來新增 User。Gin 是用 field tag json
來指定要綁定的名稱,然後用 ShouldBindJSON() 來取得資料。
func CreateUser(c *gin.Context) { var req struct { Username string `json:"username"` Password string `json:"password"` Name string `json:"name"` Age uint `json:"age"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": err.Error(), }) return } users = append(users, User{ Username: req.Username, Password: req.Password, Name: req.Name, Age: req.Age, }) c.JSON(http.StatusOK, users) }
將 CreateUser() 加到 router 裡。
package main import "github.com/gin-gonic/gin" func main() { r := gin.Default() r.GET("/users", ListUsers) r.GET("/users/:name", GetUser) r.POST("/users", CreateUser) if err := r.Run("localhost:7788"); err != nil { panic(err) } }
用 curl 測試 POST /users。
% curl --location --request POST 'localhost:7788/users' \ --header 'Content-Type: application/json' \ --data-raw '{ "username": "peter@gmail.com", "password": "123", "name": "Peter", "age": 30 }' [{"username":"bruce@gmail.com","name":"Bruce","age":24},{"username":"monika@gmail.com","name":"Monika","age":18},{"username":"peter@gmail.com","name":"Peter","age":30}]
Validation
Gin 也有提供資料驗證的功能。可以在綁定時,檢查資料是否存在,或是格式是否正確。要驗證的事項是由 field tag binding
來指定。如,要指定某個資料必須要提供,可以加上 binding:"required"
,如下。
func GetUser(c *gin.Context) { var param struct { Name string `uri:"name" binding:"required"` } ... }
另外,還可以指定資料的格式,如 email,或是數值必須要大於某的數值。
func CreateUser(c *gin.Context) { var req struct { Username string `json:"username" binding:"required,email"` Password string `json:"password" binding:"required"` Name string `json:"name" binding:"required"` Age uint `json:"age" binding:"required,min=10"` } ... }
以上只是列出一些 binding 的設定,其實還有非常多的設定。不過,這些設定在 Gin 的官網是找不到的,因為 Gin 的內部是使用 go-playground/validator 套件來實作 validation 的。所以,我們可以在 validator 的官網上查詢可用設定的列表。
串接資料庫 – GORM
大部分的後端程式都會串接資料庫。就讓我們來為我們的 RESTful web service 串接資料庫吧。Go 有很多第三方的資料庫套件,本文章用 go-gorm/gorm 來串接資料庫。GORM 是一個相當多人使用的 ORM library。此外,根據選用的資料庫,還要安裝對應的 driver。
在連接資料庫之前,我們先修改一下 User
。在 User
裡加入 gorm.Model
。
package main import "gorm.io/gorm" type User struct { gorm.Model Username string `json:"username"` Password string `json:"-"` Name string `json:"name"` Age uint `json:"age"` }
gorm.Model
包含了四個 fields,其中 ID
被標註為 primary key。
package gorm import "time" type Model struct { ID uint `gorm:"primarykey"` CreatedAt time.Time UpdatedAt time.Time DeletedAt DeletedAt `gorm:"index"` }
也可以不使用 gorm.Model
,自己的定義 primary key 欄位,如下。
package main import "gorm.io/gorm" type User struct { UserID uint64 `json:"userId" gorm:"primarykey"` Username string `json:"username"` Password string `json:"-"` Name string `json:"name"` Age uint `json:"age"` }
SQLite
SQLite 是一個簡易但相當方便的資料庫。先用以下指令安裝 GORM 和 SQLite driver。
GoRestDbExample % go get -u gorm.io/gorm GoRestDbExample % go get -u gorm.io/driver/sqlite
修改 main.go 如下。呼叫 gorm.Open() 來打開 ./test.db 資料庫。如果檔案不存在,它會自動建立。之後,呼叫 AutoMigrate() 來根據 User
的欄位建立資料表。
package main import ( "github.com/gin-gonic/gin" "gorm.io/driver/sqlite" "gorm.io/gorm" ) var db *gorm.DB func main() { _db, err := gorm.Open(sqlite.Open("./test.db"), &gorm.Config{}) if err != nil { panic(err) } db = _db if err := db.AutoMigrate(&User{}); err != nil { panic(err) } r := gin.Default() r.GET("/users", ListUsers) r.GET("/users/:name", GetUser) r.POST("/users", CreateUser) if err := r.Run("localhost:7788"); err != nil { panic(err) } }
另外,SQLite 還支援 In-Memory 資料庫。它不會建立檔案,而是在記憶體中建立一個臨時的資料庫。在開發階段時,資料表欄位還不確定時,相當地好用。此外,在寫測試程式的時候,也很好用。只要將檔案路徑改為 :memory:
即可。
func main() { _db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) if err != nil { panic(err) } db = _db if err := db.AutoMigrate(&User{}); err != nil { panic(err) } ... }
Query
在 GORM 中搜尋資料是相當地簡單。新增 repository.go,並貼上以下程式碼。呼叫 Where() 設定 WHERE 的條件,再呼叫 Find() 就可以了。
func findUsers(age uint) ([]User, error) { var users []User if result := db.Where("Age > ?", age).Find(&users); result.Error != nil { return nil, result.Error } return users, nil }
除了用 Where() 來設定條件,GORM 也支援用 struct 來設定條件,如下。
func findByName(name string) (*User, error) { var user User if result := db.Where(&User{Name: name}).First(&user); result.Error != nil { return nil, result.Error } return &user, nil }
Insertion
用 GORM 來插入一筆資料也是非常地簡單。建立一個 User
,並將要新增的資料都填入,再呼叫 Create() 插入一筆資料即可。插入成功的話,剛剛建立的 User 裡的 primary key 欄位,會自動被指定此筆的 primary key。
func insertUser(user *User) error { if result := db.Create(user); result.Error != nil { return result.Error } return nil }
結語
本文章只是粗略地介紹 Gin 與 GORM,它們功能遠遠不止這些。想要深入地了解,還必須要自己去看它們的文件。好在它們的文件相很完整,而且網路上有很多的相關的資源。