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 指定 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 - %sn", context.ClientIP(), context.Request.Method, context.Request.URL, latency)
  fmt.Printf("%dn", 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: %sn", 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
上一篇 13小时前
下一篇 2025年3月8日 12:52

相关推荐

  • Go资深工程师讲解(慕课) 000_课程目录索引

    Google资深工程师深度讲解Go语言 - 课程目录索引 课程来源:慕课网(百度网盘备份)讲师风格:从 Google 工程实践出发,注重底层原理和工程规范 完整视频章节与笔记对照表 章节 视频文件 笔记位置 状态 Ch1 课程介绍 1-1 课程导读 — 跳过 1-2 安装与环境 001.md > GOPATH、环境变量 已覆盖 Ch2 基础语法 2-1…

    后端开发 22小时前
    100
  • 编程基础 0007_并发模式

    Go 并发模式 常见的 Go 并发设计模式,每个模式都有完整可运行示例和适用场景说明 1. Worker Pool 模式 固定数量的 worker goroutine 从共享的任务队列中取任务执行,控制并发度。 package main import ( "fmt" "sync" "time" ) …

    后端开发 18小时前
    400
  • 编程基础 0009_testing详解

    Go testing 详解 目录 testing 包基础 表格驱动测试 子测试 t.Run 基准测试 Benchmark 测试覆盖率 TestMain httptest 包 Mock 和接口测试技巧 模糊测试 Fuzz 1. testing 包基础 1.1 测试文件和函数命名规则 Go 测试遵循严格的命名约定: 测试文件以 _test.go 结尾(如 use…

    后端开发 19小时前
    100
  • 编程基础 0002_名库讲解

    名库讲解 goconfig go 语言针对 windows 下常见的 ini 格式的配置文件解析器,该解析器在涵盖了所有 ini 文件操作的基础上,又针对 go 语言实际开发过程中遇到的一些需求进行了扩展。该解析器最大的优势在于对注释的极佳支持,除此之外,支持多个配置文件覆盖加载也是非常特别但好用的功能。 提供与 windows api 一模一样的操作 支持…

    16小时前
    400
  • Go工程师体系课 011

    查询的倒排索引 1. 什么是倒排索引? 倒排索引(Inverted Index)是一种数据结构,用于快速查找包含特定词汇的文档。它是搜索引擎的核心技术之一。 1.1 基本概念 正排索引:文档 ID → 文档内容(词列表) 倒排索引:词 → 包含该词的文档 ID 列表 1.2 为什么叫"倒排"? 倒排索引将传统的"文档包含哪些词"的关系倒转为"词出现在哪些文档…

简体中文 繁体中文 English