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, "rn", 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 指定資料庫欄位名稱
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")
}
// 回傳 Pure JSON

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]

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

(0)
Walker的頭像Walker
上一篇 2025年11月25日 04:00
下一篇 2025年11月25日 02:00

相關推薦

  • Go 工程師體系課 012【學習筆記】

    在 Go 中整合 Elasticsearch 1. 客戶端函式庫選擇 1.1 主流 Go ES 客戶端 olivere/elastic:功能最完整,API 設計優雅,支援 ES 7.x/8.x elastic/go-elasticsearch:官方客戶端,輕量級,更接近原生 REST API go-elasticsearch/elasticsearch:社群維護的官…

    個人 2025年11月25日
    21000
  • TS珠峰 001【學習筆記】

    課程大綱 建置 TypeScript 開發環境。 掌握 TypeScript 的基礎類型、聯集類型和交集類型。 詳細類型斷言的作用和用法。 掌握 TypeScript 中函式、類別的類型宣告方式。 掌握類型別名、介面的作用和定義。 掌握泛型的應用情境,熟練應用泛型。 靈活運用條件類型、映射類型與內建類型。 建立和使用自訂義類型。 理解命名空間、模組的概念以及使…

    個人 2025年3月27日
    1.5K00
  • 向世界揮手,擁抱無限可能 🌍✨

    站得更高,看得更遠 生活就像一座座高樓,我們不斷向上攀登,不是為了炫耀高度,而是為了看到更廣闊的風景。圖中的兩位女孩站在城市之巔,伸展雙手,徬彿在迎接世界的無限可能。這不僅是一次俯瞰城市的旅程,更是對自由和夢想的禮讚。 勇敢探索,突破邊界 每個人的生活都是一場冒險,我們生而自由,就該去探索未知的風景,去經歷更多的故事。或許路途中會有挑戰,但正是那些攀爬的瞬間…

    個人 2025年2月26日
    1.3K00
  • 熱愛運動,挑戰極限,擁抱自然

    熱愛 在這個快節奏的時代,我們被工作、生活的壓力所包圍,常常忽略了身體的需求。而運動,不只是一種健身方式,更是一種釋放自我、挑戰極限、與自然共舞的生活態度。無論是滑雪、攀岩、衝浪,還是跑步、騎行、瑜伽,每一種運動都能讓我們找到內心的激情,感受到生命的躍動。 運動是一場自我挑戰。挑戰極限,不只是職業運動員的專屬,而是每一個熱愛運動的人都可以追求的目標。它可…

    個人 2025年2月26日
    1.3K00
  • Go 工程師體系課 013【學習筆記】

    訂單事務 先扣庫存 後扣庫存 都會對庫存和訂單都會有影響,所以要使用分散式事務 業務(下單不對付)業務問題 支付成功再扣減(下單了,支付時沒庫存了) 訂單扣減,不支付(訂單超時歸還)【常用方式】 事務和分散式事務 1. 什麼是事務? 事務(Transaction)是資料庫管理系統中的一個重要概念,它是一組資料庫操作的集合,這些操作要麼全部成功執行,要麼全部…

    個人 2025年11月25日
    22400
歡迎🌹 Coding never stops, keep learning! 💡💻 光臨🌹