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://joyjs.cn/archives/4774

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

相关推荐

  • 深入理解ES6 005【学习笔记】

    解构:使用数据访问更便捷 如果使用var、let或const解构声明变量,则必须要提供初始化程序(也就是等号右侧的值)如下会导致错误 // 语法错误 var {tyep,name} // 语法错误 let {type,name} // 语法错误 const {type,name} 使用解构给已经声明的变量赋值,哪下 let node = { type:&qu…

    个人 2025年3月8日
    1.1K00
  • 深入理解ES6 012【学习笔记】

    代理(Proxy)和反射(Reflection)API 代理是一种可以拦截并改变底层javascript引擎操作的包装器,在新语言中通过它暴露内部运作对象,从而让开发者可以创建内建的对象。 代理陷阱 覆写的特性 默认特性 get 读取一个属性值 Reflect.get() set 写入一个属性值 Reflect.set() has in操作符 Reflect…

    个人 2025年3月8日
    96900
  • Go工程师体系课 011【学习笔记】

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

    个人 2025年11月25日
    5200
  • 热爱运动,挑战极限,拥抱自然

    热爱 在这个快节奏的时代,我们被工作、生活的压力所包围,常常忽略了身体的需求。而运动,不仅仅是一种健身方式,更是一种释放自我、挑战极限、与自然共舞的生活态度。无论是滑雪、攀岩、冲浪,还是跑步、骑行、瑜伽,每一种运动都能让我们找到内心的激情,感受到生命的跃动。 运动是一场自我挑战 挑战极限,不仅仅是职业运动员的专属,而是每一个热爱运动的人都可以追求的目标。它可…

    个人 2025年2月26日
    1.1K00
  • 【开篇】

    我是Walker,生于八十年代初,代码与生活的旅者。全栈开发工程师,游走于前端与后端的边界,执着于技术与艺术的交汇点。 代码,是我编织梦想的语言;项目,是我刻画未来的画布。在键盘的敲击声中,我探索技术的无尽可能,让灵感在代码里永恒绽放。 深度咖啡爱好者,迷恋每一杯手冲的诗意与仪式感。在咖啡的醇香与苦涩中,寻找专注与灵感,亦如在开发的世界中追求极致与平衡。 骑…

    2025年2月6日 个人
    1.9K00

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信
欢迎🌹 Coding never stops, keep learning! 💡💻 光临🌹