Go工程師體系課 004

8次閱讀

需求分析

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

單體應用的微服務的演進

Go 工程師體系課 004
Go 工程師體系課 004
Go 工程師體系課 004

分層的微服務架構
web 服務
srv 服務
Go 工程師體系課 004

  • 註冊中心 服務發現 配置中心 鏈路追蹤
  • 服務網關(路由,服務發現 鑑權 熔斷,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(" 服務器優雅退出 ")
 }
}
正文完
 0