此篇筆記適用於紀錄學習使用 go 作 web service 作練習。是參考這篇影片逐步練習和調整,會有點不一樣但值得參考。

初始化專案

$ go mod init gin-curd # 初始 go 專案模塊
$ go install github.com/cosmtrek/air@latest # 安裝監聽服務,類似 live reload 功能
$ go get -u github.com/gin-gonic/gin # 有名的 routing pkg
$ go get github.com/joho/godotenv # .env 環境變數 pkg
$ go get -u gorm.io/gorm # DB ORM
$ go get -u gorm.io/driver/mysql # mysql driver

設計

  1. Load .env variables
  2. Initial database connection and connected
  3. Definition routes
    1. create
    2. update
    3. destroy
    4. index (plural)
    5. show (singular)

實作

  1. controllers
  2. models
  3. migrations
  4. initializers (load .env and connect with database)

initializers/loadEnvVariables.go

package initializers

import (
	"log"
	"github.com/joho/godotenv"
)

func LoadEnvVariables() { // 確保大寫暴露使用接口
	err := godotenv.Load()
	if err != nil {
		log.Fatal("Error load .env file.")
	}
}

initializers/database.go

package initializers

import (
	"log"
	"os"

	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

var DB *gorm.DB

func Connection() {
	var err error
	dsn := os.Getenv("APP_CONNECTION") // 取得 .env 內的 APP_CONNECTION 變數
	DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{}) //參考 gorm 文件 https://gorm.io/docs/connecting_to_the_database.html#MySQL

	if err != nil {
		log.Fatal("Connection failed.")
	}
}

models/postModel.go

package models

import (
	"time"
	"gorm.io/plugin/soft_delete" // go mod tidy 抓 plugin pkg, 這邊我想自訂數據庫欄位順序
)

type Post struct {
	ID        uint `gorm:"primarykey"`
	Title string
	Body string
	CreatedAt time.Time
	UpdatedAt time.Time
	DeletedAt soft_delete.DeletedAt `gorm:"index"`
	// gorm.Model, 或是可以方便使用這個
}

migrations/migate.go

package main

import (
	"go-curd/initializers"
	"go-curd/models"
)

func init() {
	initializers.LoadEnvVariables() //載入環境變數
	initializers.Connection() //建立連線
}

func main() {
	initializers.DB.AutoMigrate(&models.Post{}) //參考文件 https://gorm.io/docs/migration.html#Auto-Migration
}

配置 environment variable,這邊要注意先建立 database:

.env

APP_CONNECTION=root@tcp(127.0.0.1:3306)/go-curd?charset=utf8mb4&parseTime=True&loc=Local

接著執行進行 migrate DB schema:

$ go run migrations/migrate.go

controllers/postController.go

package controllers

import (
	"errors"
	"go-curd/initializers"
	"go-curd/models"

	"github.com/gin-gonic/gin"
	"gorm.io/gorm"
)

func PostCreate(c *gin.Context) {
	var content struct { // 宣告接收傳入參數
		Title string
		Body string
	}
	c.Bind(&content)

	post := models.Post{Title: content.Title, Body: content.Body} // model 指定寫入
	result := initializers.DB.Create(&post) // 建立
	if result.Error != nil {
		c.Status(400)
		return
	}

	c.JSON(200, gin.H{
		"post": post,
	})
}

func PostIndex(c *gin.Context) {
	var posts []models.Post
	initializers.DB.Find(&posts)

	c.JSON(200, gin.H{
		"posts": posts,
	})
}

func PostShow(c *gin.Context) {
	id := c.Param("id") // 接收 uri 參數
	var post models.Post
	if err := initializers.DB.First(&post, id).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) { //如果錯誤訊息是 not found 則出現 post not found json 回應訊息
			c.JSON(404, gin.H{
				"error": "Post not found",
			})

			return
		}

		c.JSON(422, gin.H{
			"error": err,
		})
	}

	c.JSON(200, gin.H{
		"post": post,
	})
}

func PostUpdate(c *gin.Context) {
	id := c.Param("id")

	var content struct {
		Title string
		Body string
	}
	c.Bind(&content)

	var post models.Post
	if err := initializers.DB.First(&post, id).Error; err != nil { //取得 post
		if errors.Is(err, gorm.ErrRecordNotFound) {
			c.JSON(404, gin.H{
				"error": "Post not found",
			})

			return
		}

		c.JSON(422, gin.H{
			"error": err,
		})
	}

	initializers.DB.Model(&post).Updates(models.Post{ //更新 &post
		Title: content.Title,
		Body: content.Body,
	})

	c.JSON(200, gin.H{
		"post": post,
	})
}

func PostDestory(c *gin.Context) {
	id := c.Param("id")

	initializers.DB.Delete(&models.Post{}, id)

	c.Status(200)
}

main.go

package main

import (
	"go-curd/initializers"
	"go-curd/controllers"
	"github.com/gin-gonic/gin"
)

func init() {
	initializers.LoadEnvVariables()
	initializers.Connection()
}

func main() {
	r := gin.Default()
	r.POST("/posts", controllers.PostCreate)
	r.GET("/posts", controllers.PostIndex)
	r.GET("/posts/:id", controllers.PostShow)
	r.PUT("/posts/:id", controllers.PostUpdate)
	r.DELETE("/posts/:id", controllers.PostDestory)
	r.Run()
}

Running

go run main.go
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] POST   /posts                    --> go-curd/controllers.PostCreate (3 handlers)
[GIN-debug] GET    /posts                    --> go-curd/controllers.PostIndex (3 handlers)
[GIN-debug] GET    /posts/:id                --> go-curd/controllers.PostShow (3 handlers)
[GIN-debug] PUT    /posts/:id                --> go-curd/controllers.PostUpdate (3 handlers)
[GIN-debug] DELETE /posts/:id                --> go-curd/controllers.PostDestory (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080