Go Engineer Systematic Course 004

Requirements Analysis

  • Backend Management System
  • Product Management
    • Product List
    • Product Categories
    • Brand Management
    • Brand Categories
  • Order Management
    • Order List
  • User Information Management
    • User List
    • User Addresses
    • User Messages
  • Carousel Management
  • E-commerce System
  • Login Page
  • Homepage
    • Product Search
    • Product Category Navigation
    • Carousel Display
    • Recommended Product Display
  • Product Details Page
    • Product Image Display
    • Product Description
    • Product Specification Selection
    • Add to Cart
  • Shopping Cart
    • Product List
    • Quantity Adjustment
    • Delete Product
    • Checkout Function
  • User Center
    • Order Center
    • My Orders
    • Shipping Address Management
    • User Information
    • Edit User Profile
    • My Favorites
    • My Messages

Evolution from Monolithic Applications to Microservices

single_serve.png
micro_serve1.png
micro_serve2.png

Layered Microservices Architecture
web service
srv service
micro_serve3.png

  • Registry Center, Service Discovery, Configuration Center, Distributed Tracing
  • API Gateway (routing, service discovery, authentication, circuit breaking, IP blacklist/whitelist, load balancing)

API Management

API management documentation tools for frontend-backend separated systems: DRF Swagger, YApi (I think Apifox is better to use)

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

ORM Learning

1. What is ORM

ORM stands for Object Relational Mapping. Its main purpose in programming is to map object-oriented concepts to database table concepts. For example, if I define an object, it corresponds to a table, and an instance of that object corresponds to a record in the table.

2. Common ORMs

3. Pros and Cons of ORM

Pros

  1. Improves development efficiency.
  2. Hides SQL details, automatically maps fields and attributes between entity objects and database tables; no direct SQL coding required.
  3. Masks differences between various databases.

Cons

  1. ORM sacrifices program execution efficiency and can lead to fixed thinking patterns.
  2. Over-reliance on ORM can lead to insufficient understanding of SQL.
  3. Heavy reliance on a specific ORM makes switching to other ORMs costly.

4. How to correctly view the relationship between ORM and SQL

  1. SQL as primary, ORM as supplementary.
  2. The main purpose of ORM is to increase code maintainability and development efficiency.

GORM Getting Started

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
}

Declaring Models

A model is a standard struct, composed of Go's basic data types, custom types that implement the Scanner and Valuer interfaces, and their pointers or aliases.

// 模型定义
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      // 更新时间
}

Usingsql.NullString to solve the problem of 0 or empty values not being updated.

Field Tags

When declaring a model, tags are optional. GORM supports the following tags: tags are case-insensitive, but `camelCase` style is recommended.

Tag Name Description
column Specify DB column name
type Column data type, it is recommended to use compatible general types, for example: all databases support bool, int, uint, float, string, time, bytes, and can be used with other tags, such as: not null, size, autoIncrement. Database native types can also be used, but they need to be complete, such as: MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT
size Specify column size, e.g.: size:256
primaryKey Specify column as primary key
unique Specify column as unique
default Specify column's default value
precision Specify column's precision
scale Specify column size
not null Specify column as NOT NULL
autoIncrement Specify column as auto-increment
autoIncrementIncrement Set auto-increment step, control the auto-increment interval value of the column
embedded Embed the field
embeddedPrefix Add prefix to column name of embedded field
autoCreateTime Record current time on creation, for int fields, nano/milli units can be used. E.g.: autoCreateTime:nano
autoUpdateTime Record current time on creation/update, for int fields, nano/milli units can be used. E.g.: autoUpdateTime:milli
index Create index, can use the same name to create composite index for multiple fields, refer to Indexes documentation
uniqueIndex Same as index, but creates a unique index
check Create Check constraint, e.g.: check:age > 13, refer to Constraints
<- Set write permission for the field, e.g. <-:create write on creation, <-:update write on update, <-:false prohibit writing
-> Set read permission for the field, e.g. ->:false prohibit reading
- Ignore this field, do not read or write
comment Add comment to field during migration

Querying

// 第一条记录,主键排序的第一条
db.First
db.Take
db.Last
db.Where("name=?","jinzhu").First(&user) // user specifies which table to find
db.Where(&user{Name:"jinzhu",Age:20}).First(&user)
// Primary key slice
db.Where([]int64{20,21,22}).Find(&users)

Gin (web framework)

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

Starting a simple application

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()

 // In RESTful development
 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)

 // The default port is 8080, but you can also customize it with router.Run(":port_number")
 router.Run()

}

URL and Route Grouping

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

}

Parameters in the URL are obtained from `Param("key")` in the context.

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)
 })
//  Constrain parameter values using a 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")
}

Getting parameters from GET and POST

GET

package main

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

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

 // Matching URL format: /welcome?firstname=Jane&lastname=Doe
 router.GET("/welcome", func(c *gin.Context) {
  firstname := c.DefaultQuery("firstname", "Guest")
  lastname := c.Query("lastname") // Equivalent to 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") // Equivalent to 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()

 // Matching URL format: /welcome?firstname=Jane&lastname=Doe
 router.GET("/welcome", func(c *gin.Context) {
  firstname := c.DefaultQuery("firstname", "Guest")
  lastname := c.Query("lastname") // Equivalent to 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") // Equivalent to 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")
}

Returning JSON and Protobuf values

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")
}
// Return pure JSON

1. Basic Form Validation

To bind the request body to a struct, use model binding, which currently supports binding JSON, XML, YAML, and standard form values (foo=bar&boo=baz).

Gin uses go-playground/validator to validate parameters, view full documentation.

You need to set tags on the bound fields. For example, if the binding format is JSON, you need to set it like this: json:"fieldname".

Additionally, Gin provides two sets of binding methods:


Must bind

  • Methods: Bind, BindJSON, BindXML, BindQuery, BindYAML
  • Behavior: These methods internally use MustBindWith. If a binding error occurs, the request will be aborted with the following instruction:

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

  • The affected status code will be set to 400
  • Content-Type will be set to text/plain; charset=utf-8
  • Note: If you set the response code after this, a warning will be issued:

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

  • If you want better control over the behavior, please use the ShouldBind related methods


Should bind

  • Methods: ShouldBind, ShouldBindJSON, ShouldBindXML, ShouldBindQuery, ShouldBindYAML
  • Behavior: These methods internally use ShouldBindWith. If a binding error occurs, an error is returned, allowing developers to correctly handle the request and error.

When we use binding methods, Gin infers which binder to use based on the Content-Type. If you are sure what you are binding,
you can use `MustBindWith` or `BindingWith`.
You can also specify modifiers with specific rules for fields. If a field is modified with binding:"required",
and its value is empty during binding, an error will be returned.


Example Code (JSON Binding)

// Bind as 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"
)

// Bind as 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"` // Cross-field
}

var trans ut.Translator

// Translation
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": "Login成功",
  })
 })
 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")
}

Middleware

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()
 // Use logger middleware
 //router.Use(gin.Logger())
 // Use recovery middleware
 //router.Use(gin.Recovery())
 router := gin.Default()
 // The above is the same as using the default method
 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")
}

Graceful Shutdown

// Graceful shutdown, subsequent processing that should be done when we shut down the program
// Before or after starting a microservice, one thing is done: registering the current service's IP address and port number with the registry center.
// Our current service did not notify the registry center after stopping.
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",
  })
 })

 // Create an HTTP server
 srv := &http.Server{
  Addr:    ":8083",
  Handler: router,
 }

 // Start the service in a goroutine
 go func() {
  if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
   fmt.Printf("listen: %s\n", err)
  }
 }()

 // Create an exit channel to listen for system signals
 quit := make(chan os.Signal, 1)
 // Listen for interrupt or termination signals
 signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)

 <-quit // Wait for signal

 fmt.Println("Gracefully shutting down server...")

 // Create context and set maximum timeout
 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 defer cancel()

 // Gracefully shut down service
 if err := srv.Shutdown(ctx); err != nil {
  fmt.Println("Server forced shutdown:", err)
 } else {
  fmt.Println("Server gracefully exited")
 }
}

主题测试文章,只做测试使用。发布者:Walker,转转请注明出处:https://walker-learn.xyz/archives/6743

(0)
Walker的头像Walker
上一篇 12 hours ago
下一篇 14 hours ago

Related Posts

EN
简体中文 繁體中文 English