← 返回
后端开发 2026.03.06

Go工程師體系課 004

Go工程師體系課 004
  • 後臺管理系統
  • 商品管理
    • 商品列表
    • 商品分類
    • 品牌管理
    • 品牌分類
  • 訂單管理
    • 訂單列表
  • 用戶信息管理
    • 用戶列表
    • 用戶地址
    • 用戶留言
  • 輪播圖管理
  • 電商系統
  • 登錄頁面
  • 首頁
    • 商品搜索
    • 商品分類導航
    • 輪播圖展示
    • 推薦商品展示
  • 商品詳情頁
    • 商品圖片展示
    • 商品描述
    • 商品規格選擇
    • 加入購物車
  • 購物車
    • 商品列表
    • 數量調整
    • 刪除商品
    • 結算功能
  • 用戶中心
    • 訂單中心
    • 我的訂單
    • 收貨地址管理
    • 用戶信息
    • 用戶資料修改
    • 我的收藏
    • 我的留言

單體應用的微服務的演進

single_serve.png micro_serve1.png micro_serve2.png

分層的微服務架構 web 服務 srv 服務 micro_serve3.png

  • 註冊中心 服務發現 配置中心 鏈路追蹤
  • 服務網關(路由,服務發現 鑑權 熔斷, ip 黑白名單 負載均衡)

接口管理

前後端分離的系統接口管理文檔工具 drf swagger,yapi(我覺得 apifox 更好用一些)

git clone https://github.com/Ryan-Miao/docker-yapi.git
cd docker-yapi
docker-compose up

orm 學習

1. 什麼是 ORM

ORM 全稱是:Object Relational Mapping(對象關係映射),其主要作用是在編程中,把面向對象的概念跟數據庫中表的概念對應起來。舉例來說就是,我定義一個對象,那就對應着一張表,這個對象的實例,就對應着表中的一條記錄。

2. 常用 ORM

3. ORM 的優缺點

優點

  1. 提高了開發效率。
  2. 屏蔽 SQL 細節,可以自動對實體對象(Entity)與數據庫中的表(Table)進行字段與屬性的映射;不用直接 SQL 編碼。
  3. 屏蔽各種數據庫之間的差異。

缺點

  1. ORM 會犧牲程序的執行效率和會固定思維模式。
  2. 過於依賴 ORM 會導致 SQL 理解不夠。
  3. 對於固定的 ORM 依賴過重,導致切換到其他的 ORM 代價高。

4. 如何正確看待 ORM 和 SQL 之間的關係

  1. SQL 爲主,ORM 爲輔。
  2. ORM 主要目的是爲了增加代碼可維護性和開發效率。

gorm 入門學習

https://gorm.io/docs/

package main

import (
 "fmt"
 "gorm.io/gorm/logger"
 "log"
 "os"
 "time"

 "github.com/bwmarrin/snowflake"
 "gopkg.in/yaml.v3"
 "gorm.io/driver/mysql"
 "gorm.io/gorm"
)

type Config struct {
 Database struct {
  User     string `yaml:"user"`
  Password string `yaml:"password"`
  Host     string `yaml:"host"`
  Port     int    `yaml:"port"`
  Name     string `yaml:"name"`
 } `yaml:"database"`
}

var node *snowflake.Node

func init() {
 var err error
 // 初始化一個 Node (你可以根據不同服務實例,設置不同的node number)
 node, err = snowflake.NewNode(1)
 if err != nil {
  panic(err)
 }
}

// Product模型
type Product struct {
 Code      string `gorm:"primaryKey"`
 Price     uint
 CreatedAt time.Time
 UpdatedAt time.Time
 DeletedAt gorm.DeletedAt `gorm:"index"`
}

// 插入前自動生成Code
func (p *Product) BeforeCreate(tx *gorm.DB) (err error) {
 if p.Code == "" {
  p.Code = node.Generate().String()
 }
 return
}

func main() {
 // 讀取配置
 cfg := loadConfig("config/db/db.yaml")

 // URL編碼密碼
 //encodedPassword := url.QueryEscape(cfg.Database.Password)
 //fmt.Println(encodedPassword)

 // 拼接DSN
 dsn := fmt.Sprintf(
  "%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
  cfg.Database.User,
  cfg.Database.Password,
  cfg.Database.Host,
  cfg.Database.Port,
  cfg.Database.Name,
 )
 // 設置打印日誌
 newLogger := logger.New(
  log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
  logger.Config{
   SlowThreshold: time.Second * 10,
   LogLevel:      logger.Info,
   Colorful:      true,
  },
 )

 // 連接數據庫
 db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
  Logger: newLogger,
 })
 if err != nil {
  log.Fatal("failed to connect database:", err)
 }
 // 設置全局的logger,這個logger在我們執行每個sql語句時都會打印出來

 fmt.Println("數據庫連接成功!", db)
 // 定義表結構,將表結構直接生成對應的表 自動建表
 //db.AutoMigrate(&Product{})
 // 創建一條記錄測試
 //product := Product{Price: 100}
 //result := db.Create(&product)
 //if result.Error != nil {
 // panic(result.Error)
 //}
 //
 //fmt.Println("Product Created:", product)
 // read
 var productFind Product
 result := db.First(&productFind, "code = ?", "1916479347485577216")
 if result.Error != nil {
  panic(result.Error)
 }
 fmt.Println("ProductFind Read:", productFind)
 // update
 productFind.Price = 300
 result = db.Save(&productFind)
 // delete 邏輯刪除
 db.Delete(&productFind, 1)
}

func loadConfig(path string) Config {
 data, err := os.ReadFile(path)
 if err != nil {
  log.Fatal("failed to read config file:", err)
 }

 var cfg Config
 if err := yaml.Unmarshal(data, &cfg); err != nil {
  log.Fatal("failed to parse config file:", err)
 }

 return cfg
}

聲明模型

模型是標準的 struct,由 Go 的基本數據類型、實現了 Scanner 和 Valuer 接口的自定義類型及其指針或別名組成

// 模型定義
type User struct {
    ID            uint           // 主鍵
    Name          string         // 用戶名
    Email         *string        // 郵箱
    Age           uint8          // 年齡
    Birthday      *time.Time     // 生日
    MemberNumber  sql.NullString // 會員編號
    ActivedAt     sql.NullTime   // 激活時間
    CreatedAt     time.Time      // 創建時間
    UpdatedAt     time.Time      // 更新時間
}

通過sql.NullString解決 0 或空值 值不更新的問題

字段標籤

聲明 model 時,tag 是可選的, GORM 支持以下 tag: tag 大小寫不敏感,但建議使用cameCase風格

標籤名說明
column指定 db 列名
type列數據類型,推薦使用兼容性好的通用類型,例如:所有數據庫都支持 bool、int、uint、float、string、time、bytes,並且可以和其他標籤一起使用,例如:not null、size、autoIncrement。也可以使用數據庫原生類型,但需要是完整的,如:MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT
size指定列大小,例如:size:256
primaryKey指定列爲主鍵
unique指定列爲唯一
default指定列的默認值
precision指定列的精度
scale指定列大小
not null指定列爲 NOT NULL
autoIncrement指定列爲自動增長
autoIncrementIncrement設置自增步長,控制列的自增間隔值
embedded將字段嵌入(embed the field)
embeddedPrefix爲嵌入字段的列名添加前綴
autoCreateTime創建時記錄當前時間,針對 int 字段,可使用 nano/milli 單位。例如:autoCreateTime:nano
autoUpdateTime創建/更新時記錄當前時間,針對 int 字段,可使用 nano/milli 單位。例如:autoUpdateTime:milli
index創建索引,可使用相同名字爲多個字段創建組合索引,參考 Indexes 說明
uniqueIndexindex,但創建唯一索引
check創建 Check 約束,例如:check:age > 13,參考 Constraints
<-設置字段的寫權限,例如 <-:create 創建時寫入,<-:update 更新時寫入,<-:false 禁止寫入
->設置字段的讀權限,例如 ->:false 禁止讀取
-忽略該字段,不讀寫
comment在遷移時爲字段添加註釋

查詢

// 第一條記錄,主鍵排序的第一條
db.First
db.Take
db.Last
db.Where("name=?","jinzhu").First(&user) // user指定找哪張表
db.Where(&user{Name:"jinzhu",Age:20}).First(&user)
//主鍵切片
db.Where([]int64{20,21,22}).Find(&users)

gin(web 框架)

go get -u github.com/gin-gonic/gin

啓動一個簡單的應用

package main

import "github.com/gin-gonic/gin"

func main() {
    r := gin.Default()

    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "pong",
        })
    })

    r.Run() // listen and serve on 0.0.0.0:8080
}
package main

import "github.com/gin-gonic/gin"

func main() {

 router := gin.Default()

 // restful 的開發中
 router.GET("/someGet", getting)
 router.POST("/somePost", posting)
 router.PUT("/somePut", putting)
 router.DELETE("/someDelete", deleting)
 router.PATCH("/somePatch", patching)
 router.HEAD("/someHead", head)
 router.OPTIONS("/someOptions", options)

 // 默認啓動的是 8080 端口,也可以通過 router.Run(":端口號") 自定義
 router.Run()

}

url 和路由分組

package main

import "github.com/gin-gonic/gin"

func main() {

 router := gin.Default()

 // Simple group: v1
 v1 := router.Group("/v1")
 {
  v1.POST("/login", loginEndpoint)
  v1.POST("/submit", submitEndpoint)
  v1.POST("/read", readEndpoint)
 }

 // Simple group: v2
 v2 := router.Group("/v2")
 {
  v2.POST("/login", loginEndpoint)
  v2.POST("/submit", submitEndpoint)
  v2.POST("/read", readEndpoint)
 }

 router.Run() // listen and serve on

}

url 中的參數是從 context 中的Param("key")來獲取

package main

import (
 "github.com/gin-gonic/gin"
 "net/http"
)

type Person struct {
 ID   int    `uri:"id" binding:"required,uuid"`
 Name string `uri:"name" binding:"required"`
}

func main() {
 r := gin.Default()

 r.GET("/ping", func(c *gin.Context) {
  c.JSON(200, gin.H{
   "message": "pong",
  })
 })

 r.GET("/user/:name/:action/", func(c *gin.Context) {
  name := c.Param("name")
  action := c.Param("action")
  c.String(http.StatusOK, "%s is %s", name, action)
 })

 r.GET("/user/:name/*action", func(c *gin.Context) {
  name := c.Param("name")
  action := c.Param("action")
  c.String(http.StatusOK, "%s is %s", name, action)
 })
//  通過struct來約束參數的取值
 r.GET("/:name/:id", func(c *gin.Context) {
  var person Person
  if err := c.ShouldBindUri(&person); err != nil {
   c.Status(http.StatusBadRequest)
  }
  c.JSON(http.StatusOK, person)
 })

 r.Run(":8082")
}

獲取 get 和 post 中的參數

get

package main

import (
 "github.com/gin-gonic/gin"
 "net/http"
)

func main() {
 router := gin.Default()

 // 匹配的 URL 格式:/welcome?firstname=Jane&lastname=Doe
 router.GET("/welcome", func(c *gin.Context) {
  firstname := c.DefaultQuery("firstname", "Guest")
  lastname := c.Query("lastname") // 等價於 c.Request.URL.Query().Get("lastname")

  c.String(http.StatusOK, "Hello %s %s", firstname, lastname)
 })

  router.POST("/welcome", func(c *gin.Context) {
  firstname := c.DefaultPostForm("firstname", "Guest")
  lastname := c.PostForm("lastname") // 等價於 c.Request.URL.Query().Get("lastname")

  c.String(http.StatusOK, "Hello %s %s", firstname, lastname)
 })

 router.Run(":8080")
}

post

package main

import (
 "github.com/gin-gonic/gin"
 "net/http"
)

func main() {
 router := gin.Default()

 // 匹配的 URL 格式:/welcome?firstname=Jane&lastname=Doe
 router.GET("/welcome", func(c *gin.Context) {
  firstname := c.DefaultQuery("firstname", "Guest")
  lastname := c.Query("lastname") // 等價於 c.Request.URL.Query().Get("lastname")

  c.JSON(http.StatusOK, gin.H{
   "firstname": firstname,
   "lastname":  lastname,
  })
 })

 router.POST("/welcome", func(c *gin.Context) {
  job := c.DefaultPostForm("job", "Guest")
  salary := c.PostForm("salary") // 等價於 c.Request.URL.Query().Get("lastname")

  //c.String(http.StatusOK, "Hello %s %s", job, salary)
  c.JSON(http.StatusOK, gin.H{
   "job":    job,
   "salary": salary,
  })

 })

 router.Run(":8083")
}

返回 json 和 protobuf 值

package main

import (
 "github.com/gin-gonic/gin"

 "net/http"

 "GormStart/gin06/proto"
)

func main() {
 router := gin.Default()
 router.GET("/moreJSON", func(c *gin.Context) {
  var msg struct {
   Name    string `json:"user"`
   Message string
   Number  int
  }
  msg.Name = "gin"
  msg.Message = "hello"
  msg.Number = 123

  c.JSON(http.StatusOK, msg)
 })

 //protobuf
 router.GET("/moreProtoBuf", func(c *gin.Context) {
  c.ProtoBuf(http.StatusOK, &proto.Teacher{
   Name:   "gin",
   Course: []string{"go", "python"},
  })
 })

 router.Run(":8083")
}
// 返回pureJson

1. 表單的基本驗證

若要將請求主體綁定到結構體體中,請使用模型綁定,目前支持 JSON、XML、YAML 和標準表單值(foo=bar&boo=baz)的綁定。

Gin 使用 go-playground/validator 驗證參數,查看完整文檔

需要在綁定的字段上設置 tag,比如,綁定格式爲 json,需要這樣設置:json:"fieldname"

此外,Gin 還提供了兩套綁定方法:


Must bind

  • MethodsBindBindJSONBindXMLBindQueryBindYAML
  • Behavior:這些方法底層使用 MustBindWith,如果存在綁定錯誤,請求將被以下指令中止:

go c.AbortWithError(400, err).SetType(ErrorTypeBind)

  • 影響狀態代碼會被設置爲 400

  • Content-Type 被設置爲 text/plain; charset=utf-8

  • 注意:如果你在此之後設置響應的代碼,會發出一個警告:

    [GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 422

  • 如果你希望更好地控制行爲,請使用 ShouldBind 相關的方法


Should bind

  • MethodsShouldBindShouldBindJSONShouldBindXMLShouldBindQueryShouldBindYAML
  • Behavior:這些方法底層使用 ShouldBindWith,如果存在綁定錯誤,則返回錯誤,開發人員可以正確處理請求和錯誤。

當我們使用綁定方法時,Gin 會根據 Content-Type 推斷出使用哪種綁定器,如果你確定你綁定的是什麼, 你可以使用 MustBindWith 或者 BindingWith。 你還可以給字段指定特定規則的修飾符,如果一個字段用 binding:"required" 修飾, 並且在綁定時該字段的值爲空,那麼將返回一個錯誤。


示例代碼(綁定 JSON)

// 綁定爲 json
type Login struct {
    User     string `form:"user" json:"user" xml:"user" binding:"required"`
    Password string `form:"password" json:"password" xml:"password" binding:"required"`
}

type SignUpParam struct {
    Age        uint8  `json:"age" binding:"gte=1,lte=130"`
    Name       string `json:"name" binding:"required"`
    Email      string `json:"email" binding:"required,email"`
    Password   string `json:"password" binding:"required"`
    RePassword string `json:"re_password" binding:"required,eqfield=Password"`
}

func main() {
    router := gin.Default()
    // Example for binding JSON ({"user":"manu", "password":"123"})
}
package main

import (
 "fmt"
 "github.com/gin-gonic/gin"
 "github.com/gin-gonic/gin/binding"
 "github.com/go-playground/locales/en"
 "github.com/go-playground/locales/zh"
 ut "github.com/go-playground/universal-translator"
 "github.com/go-playground/validator/v10"
 en_translator "github.com/go-playground/validator/v10/translations/en"
 zh_translator "github.com/go-playground/validator/v10/translations/zh"
 "net/http"
 "reflect"
 "strings"
)

// 綁定爲 json
type LoginForm struct {
 User     string `form:"user" json:"user" xml:"user" binding:"required"`
 Password string `form:"password" json:"password" xml:"password" binding:"required"`
}

type RegisterForm struct {
 Age        uint8  `json:"age" binding:"gte=1,lte=130"`
 Name       string `json:"name" binding:"required,min=3"`
 Email      string `json:"email" binding:"required,email"`
 Password   string `json:"password" binding:"required,min=6"`
 RePassword string `json:"re_password" binding:"required,eqfield=Password"` // 跨字段了
}

var trans ut.Translator

// 翻譯
func InitTrans(locale string) (err error) {
 if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
  v.RegisterTagNameFunc(func(fld reflect.StructField) string {
   name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
   if name == "-" {
    return ""
   }
   return name
  })
  zhT := zh.New()
  enT := en.New()
  uni := ut.New(enT, zhT)
  trans, ok = uni.GetTranslator(locale)
  if !ok {
   return fmt.Errorf("locale %s not found", locale)
  }
  switch locale {
  case "en":
   en_translator.RegisterDefaultTranslations(v, trans)
  case "zh":
   zh_translator.RegisterDefaultTranslations(v, trans)
  default:
   en_translator.RegisterDefaultTranslations(v, trans)
  }
  return
 }
 return
}

func main() {
 if err := InitTrans("zh"); err != nil {
  fmt.Println(err.Error())
  return
 }
 r := gin.Default()
 r.POST("/login", func(c *gin.Context) {
  var form LoginForm
  if err := c.ShouldBind(&form); err != nil {
   errs, ok := err.(validator.ValidationErrors)
   if !ok {
    c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
    return
   }
   c.JSON(http.StatusBadRequest, gin.H{"error": errs.Translate(trans)})
   return
  }
  c.JSON(http.StatusOK, gin.H{
   "msg": "登錄成功",
  })
 })
 r.POST("/register", func(c *gin.Context) {
  var signUpForm RegisterForm
  if err := c.ShouldBind(&signUpForm); err != nil {
   errs, ok := err.(validator.ValidationErrors)
   if !ok {
    c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
    return
   }
   c.JSON(http.StatusBadRequest, gin.H{"error": errs.Translate(trans)})
   return
  }
  c.JSON(http.StatusOK, gin.H{
   "msg": "註冊成功",
  })
 })
 r.Run(":8083")
}

中間件

package main

import (
 "fmt"
 "github.com/gin-gonic/gin"
 "net/http"
 "time"
)

func MyLogger() gin.HandlerFunc {
 return func(context *gin.Context) {
  t := time.Now()
  context.Set("example", "12345")
  context.Next()
  latency := time.Since(t)
  fmt.Printf("%s - %s - %s - %s\n", context.ClientIP(), context.Request.Method, context.Request.URL, latency)
  fmt.Printf("%d\n", context.Writer.Status())
 }
}

func main() {
 //router := gin.New()
 // 使用logger中間件
 //router.Use(gin.Logger())
 // 使用recovery中間件
 //router.Use(gin.Recovery())
 router := gin.Default()
 // 以上是使用default的方式是一樣的
 authrized := router.Group("/auth")
 authrized.Use(MyLogger())
 authrized.GET("/ping", func(context *gin.Context) {
  example := context.MustGet("example").(string)
  context.JSON(http.StatusOK, gin.H{
   "message": example,
  })
 })
 router.GET("/ping", func(context *gin.Context) {
  context.JSON(http.StatusOK, gin.H{
   "message": "pong",
  })
 })
 router.Run(":8083")
}

優雅的退出程序

// 優雅退出,當我們關閉程序的時候應該做的後續處理
// 微服務 啓動之前 或者啓動之後會做一件事:將當前的服務的 ip 地址和端口號註冊到註冊中心
// 我們當前的服務停止了以後並沒有告知註冊中心
package main

import (
 "context"
 "fmt"
 "github.com/gin-gonic/gin"
 "net/http"
 "os"
 "os/signal"
 "syscall"
 "time"
)

func main() {
 router := gin.Default()

 router.GET("/", func(c *gin.Context) {
  c.JSON(http.StatusOK, gin.H{
   "msg": "pong",
  })
 })

 // 創建一個 HTTP 服務器
 srv := &http.Server{
  Addr:    ":8083",
  Handler: router,
 }

 // 啓動服務放到協程中
 go func() {
  if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
   fmt.Printf("listen: %s\n", err)
  }
 }()

 // 創建退出通道監聽系統信號
 quit := make(chan os.Signal, 1)
 // 監聽中斷信號或終止信號
 signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)

 <-quit // 等待信號

 fmt.Println("正在優雅退出服務器...")

 // 創建上下文設置最大超時時間
 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 defer cancel()

 // 優雅關閉服務
 if err := srv.Shutdown(ctx); err != nil {
  fmt.Println("服務器強制關閉:", err)
 } else {
  fmt.Println("服務器優雅退出")
 }
}