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,它們功能遠遠不止這些。想要深入地了解,還必須要自己去看它們的文件。好在它們的文件相很完整,而且網路上有很多的相關的資源。




