# 前言 這篇是我看到這篇文章 https://github.com/880831ian/go-restful-api-repository-messageboard?tab=readme-ov-file 跟著實作練習的紀錄,是使用 Go 來寫一個 Repository Restful API 的留言板,並且會使用 gin 以及 gorm (使用 Mysql)套件。 另外有加入 docker-compose設定跟mysql 連線調整。 # 開發環境 ## Go ![image](https://hackmd.io/_uploads/Hy_t7sx5a.png) >https://go.dev/ ## GIN框架 ![image](https://hackmd.io/_uploads/B1ZiXjl5p.png) >https://gin-gonic.com/ ## Mysql ![image](https://hackmd.io/_uploads/Sks3mogcT.png) ![image](https://hackmd.io/_uploads/SJXmVogc6.png) ## Docker ![image](https://hackmd.io/_uploads/SyXkVjl56.png) # 檔案結構 ``` . ├── controller │ └── controller.go ├── go.mod ├── go.sum ├── main.go ├── model │ └── model.go ├── repository │ └── repository.go ├── router │ └── router.go └── sql ├── connect.yaml └── sql.go ``` 資料夾個別功能與作用: sql:放置連線資料庫檔案。 controller:商用邏輯控制。 model:定義資料表資料型態。 repository:處理與資料庫進行交握。 router:設定網站網址路由。 ## 設定 go.mod 到開發資料夾底下 ``` cd message_board ``` 初始化設定 go.mod 的 module ``` go mod init message ``` 接著使用 go get 來引入 gin、gorm、mysql、yaml 套件 ``` $ go get -u github.com/gin-gonic/gin $ go get -u gorm.io/gorm $ go get -u gorm.io/driver/mysql $ go get -u gopkg.in/yaml.v2 ``` ## main.go ``` package main import ( "fmt" "message/model" "message/router" "message/sql" ) func main() { //連線資料庫 if err := sql.InitMySql(); err != nil { panic(err) } //連結模型 sql.Connect.AutoMigrate(&model.Message{}) //sql.Connect.Table("message") //也可以使用連線已有資料表方式 //註冊路由 r := router.SetRouter() //啟動埠為8082的專案 fmt.Println("開啟127.0.0.0.1:8082...") r.Run("127.0.0.1:8082") } ``` 引入我們 Repository 架構,將 config、model、router 導入,先測試是否可以連線資料庫,使用 AutoMigrate 來新增資料表(如果沒有才新增),或是使用 Table 來連線已有資料表,註冊網址路由,最後啟動專案,我們將 Port 設定成 8082。 ## sql 設定 connect.yaml ``` host: 127.0.0.1 username: root password: "密碼" dbname: "資料庫名稱" port: 3306 ``` 我們把 mysql 連線的資訊寫在此處。 (專案正式環境可能要加入gitignore 比較安全) sql.go (下面為一個檔案,但長度有點長,分開說明) ``` package sql import ( "io/ioutil" "fmt" "gopkg.in/yaml.v2" "gorm.io/gorm" "gorm.io/driver/mysql" ) ``` import 會使用到的套件。 ``` var Connect *gorm.DB type conf struct { Host string `yaml:"host"` UserName string `yaml:"username"` Password string `yaml:"password"` DbName string `yaml:"dbname"` Port string `yaml:"port"` } func (c *conf) getConf() *conf { //讀取config/connect.yaml檔案 yamlFile, err := ioutil.ReadFile("sql/connect.yaml") //若出現錯誤,列印錯誤訊息 if err != nil { fmt.Println(err.Error()) } //將讀取的字串轉換成結構體conf err = yaml.Unmarshal(yamlFile, c) if err != nil { fmt.Println(err.Error()) } return c } ``` 設定資料庫連線的 conf 來讀取 yaml 檔案。 ``` //初始化連線資料庫 func InitMySql() (err error) { var c conf //獲取yaml配置引數 conf := c.getConf() //將yaml配置引數拼接成連線資料庫的url dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", conf.UserName, conf.Password, conf.Host, conf.Port, conf.DbName, ) //連線資料庫 Connect, err = gorm.Open(mysql.New(mysql.Config{DSN: dsn}), &gorm.Config{}) return } ``` 初始化資料庫,會把剛剛讀取 yaml 的 conf 串接成可以連接資料庫的 url ,最後連線資料庫。 ## 路由設定 router.go ``` package router import ( "message/controller" "github.com/gin-gonic/gin" ) func SetRouter() *gin.Engine { //顯示 debug 模式 gin.SetMode(gin.ReleaseMode) r := gin.Default() v1 := r.Group("api/v1") { //新增留言 v1.POST("/message", controller.Create) //查詢全部留言 v1.GET("/message", controller.GetAll) //查詢 {id} 留言 v1.GET("/message/:id", controller.Get) //修改 {id} 留言 v1.PATCH("/message/:id", controller.Update) //刪除 {id} 留言 v1.DELETE("/message/:id", controller.Delete) } return r } ``` 設定路由,版本 v1 網址是 api/v1 ,分別是新增留言、查詢全部留言、查詢 {id} 留言、修改 {id} 留言、刪除 {id} 留言,連接到不同的 controller function 。 ## 資料表設定 model.go ``` package model import "gorm.io/gorm" func (Message) TableName() string { return "message" } type Message struct { Id int `gorm:"primary_key,type:INT;not null;AUTO_INCREMENT"` User_Id int `json:"User_Id" binding:"required"` Content string `json:"Content" binding:"required"` Version int `gorm:"default:0"` // 包含 CreatedAt 和 UpdatedAt 和 DeletedAt 欄位 gorm.Model } ``` 設定資料表的結構,使用 gorm.Model 預設裡面會包含 CreatedAt 和 UpdatedAt 和 DeletedAt 欄位。 ## controller 設定 controller.go (下面為一個檔案,但長度有點長,分開說明) ``` package controller import ( "message/model" "message/repository" "net/http" "unicode/utf8" "github.com/gin-gonic/gin" ) ``` import 會使用到的套件。 查詢留言功能 ``` func GetAll(c *gin.Context) { message, err := repository.GetAllMessage() if err != nil { c.JSON(http.StatusBadRequest, gin.H{"message": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": message}) } func Get(c *gin.Context) { var message model.Message if err := repository.GetMessage(&message, c.Param("id")); err != nil { c.JSON(http.StatusNotFound, gin.H{"message": "找不到留言"}) return } c.JSON(http.StatusOK, gin.H{"message": message}) } ``` GetAll() 會使用到 repository.GetAllMessage() 查詢並回傳顯示查詢的資料。 c.Param("id") 是網址讀入後的 id,網址是http://127.0.0.1:8081/api/v1/message/{id} ,將輸入的 id 透過 repository.GetMessage() 查詢並回傳顯示查詢的資料。 新增留言功能 ``` func Create(c *gin.Context) { var message model.Message if c.PostForm("Content") == "" || utf8.RuneCountInString(c.PostForm("Content")) >= 20 { c.JSON(http.StatusBadRequest, gin.H{"message": "沒有輸入內容或長度超過20個字元"}) return } c.Bind(&message) repository.CreateMessage(&message) c.JSON(http.StatusCreated, gin.H{"message": message}) } ``` 使用 Gin 框架中的 Bind 函數,可以將 url 的查詢參數 query parameter,http 的 Header、body 中提交的數據給取出,透過 repository.CreateMessage() 將要新增的資料帶入,如果失敗就顯示 http.StatusBadRequest,如果成功就顯示 http.StatusCreated 以及新增的資料。 修改留言功能 ``` func Update(c *gin.Context) { var message model.Message if c.PostForm("Content") == "" || utf8.RuneCountInString(c.PostForm("Content")) >= 20 { c.JSON(http.StatusBadRequest, gin.H{"message": "沒有輸入內容或長度超過20個字元"}) return } if err := repository.UpdateMessage(&message, c.PostForm("Content"), c.Param("id")); err != nil { c.JSON(http.StatusNotFound, gin.H{"message": "找不到留言"}) return } c.JSON(http.StatusOK, gin.H{"message": message}) } ``` 先使用 repository.GetMessage() 以及 c.Param("id") 來查詢此 id 是否存在,再帶入要修改的 Content ,透過 repository.UpdateMessage() 將資料修改,,如果失敗就顯示 http.StatusNotFound 以及找不到留言,如果成功就顯示 http.StatusOK 以及修改的資料。 刪除留言功能 ``` func Delete(c *gin.Context) { var message model.Message if err := repository.DeleteMessage(&message, c.Param("id")); err != nil { c.JSON(http.StatusNotFound, gin.H{"message": "找不到留言"}) return } c.JSON(http.StatusNoContent, gin.H{"message": "刪除留言成功"}) } ``` 透過 repository.DeleteMessage() 將資料刪除,如果失敗就顯示 http.StatusNotFound 以及找不到留言,如果成功就顯示 http.StatusNoContent。 ## repository.go (下面為一個檔案,但長度有點長,分開說明) 所有的邏輯判斷都要在 controller 處理,所以 repository.go 就單純對資料庫就 CRUD: ``` package repository import ( "message/model" "message/sql" ) ``` import 會使用到的套件。 查詢留言資料讀取 ``` //查詢全部留言 func GetAllMessage() (message []*model.Message, err error) { err = sql.Connect.Find(&message).Error return } //查詢 {id} 留言 func GetMessage(message *model.Message, id string) (err error) { err = sql.Connect.Where("id=?", id).First(&message).Error return } ``` 新增留言資料讀取 ``` //新增留言 func CreateMessage(message *model.Message) (err error) { err = sql.Connect.Create(&message).Error return } ``` 修改留言資料讀取 ``` //更新 {id} 留言 func UpdateMessage(message *model.Message, content, id string) (err error) { err = sql.Connect.Where("id=?", id).First(&message).Update("content", content).Error return } ``` 刪除留言資料讀取 ``` //刪除 {id} 留言 func DeleteMessage(message *model.Message, id string) (err error) { err = sql.Connect.Where("id=?", id).First(&message).Delete(&message).Error return } ``` # Mysql docker-compose 設定 因為我在實作時發現沒有做 mysql server的設定,我這邊作了以下調整 ## Dockerfile ``` # 使用 Go 官方映像作為基礎映像 FROM golang:latest # 設定工作目錄 WORKDIR /app # 複製 go.mod 和 go.sum 文件 COPY go.mod go.sum ./ # 下載依賴 RUN go mod download # 複製源代碼文件到工作目錄 COPY . . # 構建應用程序 RUN go build -o main . # 暴露端口 EXPOSE 8082 # 執行應用程序 CMD ["./main"] ``` ## docker-compose.yml ``` version: "2.1" services: db: image: mysql:5.7 command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: go ports: - "3306:3306" restart: always phpmyadmin: image: phpmyadmin/phpmyadmin environment: PMA_HOST: db PMA_PORT: 3306 ports: - "8084:80" depends_on: - db go-app: build: . ports: - "8082:8082" depends_on: - db ``` 這邊設定了 phpmyadmin跟 mysql 服務,另外有一個go-app 的服務運行這個專案 ## 啟用docker-compose ``` docker-compose up -d ``` ![image](https://hackmd.io/_uploads/HkpcFix56.png) >運行結果 ![image](https://hackmd.io/_uploads/ByzLhixca.png) >phpmyadmin連線 # Postman 測試 ## 查詢全部留言 - 成功(無資料) ![image](https://hackmd.io/_uploads/Hy_Ja5x96.png) ## 查詢全部留言 - 成功(有資料) ![image](https://hackmd.io/_uploads/HkqgWig5T.png) ## 查詢{id}留言 - 成功 ![image](https://hackmd.io/_uploads/rkPUWig56.png) ## 查詢{id}留言 - 失敗 ![image](https://hackmd.io/_uploads/rJeKWilqp.png) ## 新增留言 - 成功 ![image](https://hackmd.io/_uploads/By2w1jxqT.png) ## 修改{id}留言 - 成功 ![image](https://hackmd.io/_uploads/SyYGejec6.png) ## 刪除{id}留言 - 成功 ![image](https://hackmd.io/_uploads/ByDofsxcp.png) ## 執行結果 ![image](https://hackmd.io/_uploads/SJv2GoxcT.png) ## 練習結果Repo https://github.com/gahgah147/go-restful-api-repository-messageboard ## 測試postmain 內容 export ``` { "info": { "_postman_id": "957a0737-049b-4605-b718-07ca8e13d683", "name": "GO Repository Restful API 留言板", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", "_exporter_id": "4951341", "_collection_link": "https://martian-escape-400870.postman.co/workspace/GO-Repository-Restful-API-%25E7%259A%2584%25E7%2595%2599%25E8%25A8%2580%25E6%259D%25BF~4f66f5ef-49b8-47c3-918f-cf7a14414aa4/collection/4951341-957a0737-049b-4605-b718-07ca8e13d683?action=share&source=collection_link&creator=4951341" }, "item": [ { "name": "查詢全部留言", "request": { "method": "GET", "header": [], "url": { "raw": "http://127.0.0.1:8082/api/v1/message/", "protocol": "http", "host": [ "127", "0", "0", "1" ], "port": "8082", "path": [ "api", "v1", "message", "" ] } }, "response": [] }, { "name": "查詢留言", "request": { "method": "GET", "header": [], "url": { "raw": "http://127.0.0.1:8082/api/v1/message/", "protocol": "http", "host": [ "127", "0", "0", "1" ], "port": "8082", "path": [ "api", "v1", "message", "" ] } }, "response": [] }, { "name": "新增留言", "request": { "method": "POST", "header": [], "body": { "mode": "raw", "raw": "{\r\n \"message\":{\r\n \"id\":1,\r\n \"User_Id\": 2,\r\n \"Content\": \"早安\",\r\n \"Version\": 0,\r\n \"ID\": 0,\r\n \"CreatedAt\": \"2022-03-29T17:39:33.014+08:00\",\r\n \"UpdatedAt\": \"2022-03-29T17:39:33.014+08:00\",\r\n \"DeletedAt\": null\r\n }\r\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "http://127.0.0.1:8082/api/v1/message", "protocol": "http", "host": [ "127", "0", "0", "1" ], "port": "8082", "path": [ "api", "v1", "message" ] } }, "response": [] }, { "name": "修改留言", "request": { "method": "POST", "header": [], "body": { "mode": "raw", "raw": "{\r\n \"message\":{\r\n \"id\":1,\r\n \"User_Id\": 2,\r\n \"Content\": \"早安\",\r\n \"Version\": 0,\r\n \"ID\": 0,\r\n \"CreatedAt\": \"2022-03-29T17:39:33.014+08:00\",\r\n \"UpdatedAt\": \"2022-03-29T17:39:33.014+08:00\",\r\n \"DeletedAt\": null\r\n }\r\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "http://127.0.0.1:8082/api/v1/message", "protocol": "http", "host": [ "127", "0", "0", "1" ], "port": "8082", "path": [ "api", "v1", "message" ] } }, "response": [] }, { "name": "刪除留言", "request": { "method": "DELETE", "header": [], "body": { "mode": "raw", "raw": "{\r\n \"message\":{\r\n \"id\":1,\r\n \"User_Id\": 2,\r\n \"Content\": \"早安\",\r\n \"Version\": 0,\r\n \"ID\": 0,\r\n \"CreatedAt\": \"2022-03-29T17:39:33.014+08:00\",\r\n \"UpdatedAt\": \"2022-03-29T17:39:33.014+08:00\",\r\n \"DeletedAt\": null\r\n }\r\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "http://127.0.0.1:8082/api/v1/message/1", "protocol": "http", "host": [ "127", "0", "0", "1" ], "port": "8082", "path": [ "api", "v1", "message", "1" ] } }, "response": [] } ] } ``` # 加入Container 內容持久化設定 ``` volumes: - ./db_data:/var/lib/mysql ``` 在 yaml 中新增 Volumes,Volumes 會將資料存放於 Container 之外,範例中就是會把資料存放於 db_data 這個資料夾 ``` db: image: mysql:5.7 container_name: db command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: go volumes: - ./db_data:/var/lib/mysql ports: - "3306:3306" restart: always networks: - default ``` # 設定container之間的連線 加入network設定 ``` networks: default: ``` 並調整 docker-compose 檔案 ``` version: "2.1" services: db: image: mysql:5.7 container_name: db command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: go volumes: - ./db_data:/var/lib/mysql ports: - "3306:3306" restart: always networks: - default phpmyadmin: image: phpmyadmin/phpmyadmin container_name: phpmyadmin environment: PMA_HOST: db PMA_PORT: 3306 ports: - "8084:80" depends_on: - db networks: - default go-app: build: . container_name: go-app ports: - "8082:8082" depends_on: - db networks: - default networks: default: ``` 調整 connect.yaml ``` host: db username: root password: root dbname: go port: 3306 ``` 這邊設定 db連線到db這個 container # 設定UUID 為 PRIMARY_KEY 調整資料表設定 model.go ``` package model import "gorm.io/gorm" func (Message) TableName() string { return "message" } type Message struct { Id int `gorm:"primary_key,type:INT;not null;AUTO_INCREMENT"` User_Id int `json:"User_Id" binding:"required"` Content string `json:"Content" binding:"required"` Version int `gorm:"default:0"` // 包含 CreatedAt 和 UpdatedAt 和 DeletedAt 欄位 gorm.Model } ``` 其中的這一行 ``` Id int `gorm:"primary_key,type:INT;not null;AUTO_INCREMENT"` ``` 改成這樣 ``` Id uuid.UUID `gorm:"primary_key;type:varchar(100);not null"` ``` 並且加上以下這段程式碼 ``` func (u *UploadImage) BeforeCreate(tx *gorm.DB) (err error) { u.Id = uuid.New() return nil } ``` 這樣就能使用UUID了 # 專案練習 Repo https://github.com/gahgah147/go-restful-api-repository-messageboard # 參考資料 https://github.com/880831ian/go-restful-api-repository-messageboard?tab=readme-ov-file