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

相关推荐

  • TS珠峰 001【学习笔记】

    课程大纲 搭建 TypeScript 开发环境。 掌握 TypeScript 的基础类型,联合类型和交叉类型。 详细类型断言的作用和用法。 掌握 TypeScript 中函数、类的类型声明方式。 掌握类型别名、接口的作用和定义。 掌握泛型的应用场景,熟练应用泛型。 灵活运用条件类型、映射类型与内置类型。 创建和使用自定义类型。 理解命名空间、模块的概念已经使…

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

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

    个人 2025年3月8日
    1.1K00
  • 从0到1落地微前端架构 001【学习笔记】

    微前端 js 隔离css 隔离元素隔离生命周期预加载数据通信应用跳转多层嵌套 说明 使用的是 Mermaid 的 flowchart 语法,Markdown 渲染器如 Typora、VitePress、一些 Git 平台都支持。 保留了: 基座应用 main-vue3 各子应用:child-nuxt2-home、child-vue2-job、child-vu…

    2025年4月20日
    1.5K00
  • TS珠峰 002【学习笔记】

    泛型 /* * @Author: error: error: git config user.name & please set dead value or install git && error: git config user.email & please set dead value or install git &a…

    个人 2025年3月27日
    1.5K00
  • Node深入浅出(圣思园教育) 002【学习笔记】

    node 的包管理机制和加载机制 npm search xxxnpm view xxxnpm install xxx nodejs 文件系统操作的 api Node.js 的 fs 模块提供同步(Sync)与基于回调/Promise 的异步 API,可以操作本地文件与目录。日常开发中常用的能力包括读取、写入、追加、删除、遍历目录、监听变化等。以下示例基于 C…

    个人 2025年11月24日
    18900

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

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

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