用 Go 和資料庫建構 REST APIs

Photo by Dušan veverkolog on Unsplash
Photo by Dušan veverkolog on Unsplash
Go 是近年來非常熱門的程式語言。它有精簡語法與高效的執行效率。現今,Go 已經被大量地用來開發後端程式。本章將介紹如何用 Go 建立一個後端程式,並且實作 REST APIs,和串接資料庫。

Go 是近年來非常熱門的程式語言。它有精簡語法與高效的執行效率。現今,Go 已經被大量地用來開發後端程式。本章將介紹如何用 Go 建立一個後端程式,並且實作 REST APIs,和串接資料庫。

本章完整的程式碼可以在 下載。

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

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

You May Also Like