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 說明
uniqueIndex index,但創建唯一索引
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("服務器優雅退出")
 }
}

主題測試文章,只做測試使用。發佈者:Walker,轉轉請注明出處:https://walker-learn.xyz/archives/6770

(0)
Walker的頭像Walker
上一篇 2026年3月8日 15:11
下一篇 2026年3月9日 12:56

相關推薦

  • 編程基礎 0005_錯誤處理進階

    Go 錯誤處理進階 目錄 Go 錯誤處理哲學 error 接口本質 自定義錯誤類型 fmt.Errorf 與 %w 包裝錯誤 errors.Is 和 errors.As 哨兵錯誤模式 錯誤處理最佳實踐 實際項目中的錯誤處理模式 1. Go 錯誤處理哲學 1.1 與 try-catch 的根本區別 在 Java、Python、C++ 等語言中,異常處理依賴 t…

    後端開發 2026年3月6日
    6500
  • 編程基礎 0003_Web_beego開發

    Web 開發之 Beego 使用 go get 安裝 bee 工具與 beego Bee Beego 使用 bee 工具初始化 Beego 項目 在$GOPATH/src 目錄下執行 bee create myapp 使用 bee 工具熱編譯 Beego 項目 在$GOPATH/src/myapp 目錄下執行 bee start myapp // hello…

    後端開發 2026年3月6日
    6600
  • 編程基礎 0008_標準庫進階

    Go 標準庫進階 系統整理 Go 標準庫中最常用的包,重點覆蓋 io、os、bufio、strings、time、fmt 等 1. io 包核心接口 Go 的 I/O 設計圍繞幾個核心接口展開,幾乎所有 I/O 操作都基於它們。 // 最基礎的兩個接口 type Reader interface { Read(p []byte) (n int, err er…

    後端開發 2026年3月6日
    8100
  • 編程基礎 0012_Go_Web與網絡編程精華

    Go Web 與網絡編程精華 知識來源:- 《Building Web Apps with Go》- 《Go API 編程》- 《Go Web 編程》(Go Web Programming, Sau Sheong Chang)- 《Go 網絡編程》(Network Programming with Go)- 《Mastering Go Web Service…

    後端開發 2026年3月6日
    5700
  • Go工程師體系課 protoc-gen-validate

    protoc-gen-validate 簡介與使用指南 ✅ 什麼是 protoc-gen-validate protoc-gen-validate(簡稱 PGV)是一個 Protocol Buffers 插件,用於在生成的 Go 代碼中添加結構體字段的驗證邏輯。 它通過在 .proto 文件中添加 validate 規則,自動爲每個字段生成驗證代碼,避免你手…

    後端開發 2026年3月6日
    7300
簡體中文 繁體中文 English