每日一Go-24、Go语言实战-综合项目:规划与搭建

今天我们结合前面23天学习的内容做一个"商品后端管理"的项目,项目中会包括:需求分析、项目结构设计、关键依赖、组件说明、配置与环境、核心代码示例,部署到Docker以及K8s上。目标是利用Gin+GORM+zap+swagger+Redis+JWT+Viper快速搭建一个可扩展的后端骨架。

1、需求分析

  • 用户模块:注册、登录、查看和更新用户信息。

  • 商品模块:商品列表、详情、增删改查(管理员权限)。

  • 文档管理:Swagger自动生成API文档。

  • 配置管理:利用Viper加载和管理yaml文件和env文件。

  • 日志:使用zap结构化日志。

  • 性能和缓存:Redis用于会话以及缓存热点数据。

  • 安全:JWT验证、参数校验、基础限流、错误处理中间件。

2、目录结构

复制代码
2.1 创建项目目录

基础目录的创建主要利用Cobra客户端工具来创建,具体说明请访问https://github.com/spf13/cobra-cli,后期会专门出一篇文章来介绍
bash 复制代码
$ mkdir golang_per_day_24
$ cd golang_per_day_24 
$ go mod init golang_per_day_24
go: creating new go.mod: module golang_per_day_24
//使用眼镜蛇工具Cobra来初始化工程
$ cobra-cli init
Your Cobra application is ready at
C:\Users\CodeeJun\dev\golang\golang_per_day_24
$ cobra-cli add server
server created at 
C:\Users\CodeeJun\dev\golang\golang_per_day_24
bash 复制代码
// 创建内部目录
$ mkdir internal
// 创建测试目录 
$ mkdir test 
复制代码
2.2 目录结构
bash 复制代码
$ tree 
.
|-- cmd             <- cobra-cli生成
|-- configs         <- 程序配置文件夹
|-- docs            <- swagger生成的API文档
|-- go.mod
|-- internal        <- 内部文件
|   |-- components  <- 组件
|   |-- config      <- 配置加载
|   |-- controllers <- 控制器
|   |-- errors      <- 业务错误定义
|   |-- interfaces  <- 定义服务层、数据层、模型层、过滤器接口
|   |-- middlewares <- 中间件
|   |-- migrates    <- 自动迁移
|   |-- models      <- 模型
|   |-- repos       <- 数据仓库
|   |-- routers     <- 路由
|   |-- server      <- web服务入口
|   |	`-- http.go <- http server
|   |-- services    <- 服务层
|   `-- utils       <- 工具
|-- main.go         <- 程序入口
|-- test            <- 测试文件夹

3、关键依赖

bash 复制代码
github.com/gin-contrib/requestid
github.com/gin-gonic/gin
github.com/go-playground/validator/v10
github.com/golang-jwt/jwt/v5
github.com/redis/go-redis/v9
github.com/spf13/cast
github.com/spf13/cobra
github.com/spf13/viper
go.uber.org/zap
golang.org/x/crypto
gorm.io/driver/mysql
gorm.io/gorm

4、配置文件(config.yaml示例)

bash 复制代码
#configs/config.yaml
server:
  port: 8080
  host: "127.0.0.1"
  readtimeout: 60
  writetimeout: 60
log:
  level: "debug"
mysql:
  dsn: root:123456@tcp(localhost:3306)/golang_per_day?charset=utf8&parseTime=True&loc=Local&timeout=10000ms
redis:
  addr: "localhost:6379"
  password: "123456"
  db: 0  
jwt:
  secret: "golang_per_day_secret_key"
  # 24h表示24小时
  expire: 24h

5、配置加载

bash 复制代码
//internal/config/config.go
package config
import (
    "fmt"
    "strings"
    "github.com/spf13/viper"
)
func Init(cfgFile, env string) error {
    viper.SetConfigType("yaml")
    if cfgFile != "" {
        viper.SetConfigFile(cfgFile)
    } else {
        viper.AddConfigPath("./configs/")
        viper.SetConfigName("config." + env)
    }
    viper.SetEnvPrefix("APP")
    viper.AutomaticEnv()
    viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
    if err := viper.ReadInConfig(); err != nil {
        return fmt.Errorf("读取配置失败:%w", err)
    }
    fmt.Println("加载配置文件:", viper.ConfigFileUsed())
    return nil
}

6、zap日志初始化

bash 复制代码
//internal/components/log.go
func InitLog() {
    var logger *zap.Logger
    var err error
    if gin.Mode() == gin.DebugMode {
        logger, err = zap.NewDevelopment()
    } else {
        logger, err = zap.NewProduction()
    }
    if err != nil {
        panic("初始化Zap日志失败:" + err.Error())
    }
    zap.ReplaceGlobals(logger)
}

7、GORM初始化

bash 复制代码
//internal/components/db.go
var DB *gorm.DB
func InitDb() {
    dsn := viper.GetString("mysql.dsn")
    if dsn == "" {
        panic("请在配置文件里配置【mysql.dsn】")
    }
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        zap.L().Fatal("连接接数据库失败:", zap.Error(err))
        return
    }
    zap.L().Info("数据库连接成功:", zap.String("dsn", dsn))
    sqlDb, err := db.DB()
    if err == nil {
        sqlDb.SetMaxIdleConns(10)
        sqlDb.SetMaxOpenConns(100)
        sqlDb.SetConnMaxLifetime(time.Hour)
    }
    // 开启debug模式
    DB = db.Debug()
}

8、Redis客户端

bash 复制代码
//internal/components/redis.go
var Redis *redis.Client
func InitRedis() {
    addr := viper.GetString("redis.addr")
    if addr == "" {
        panic("请在配置文件里配置【redis.addr")
    }
    pass := viper.GetString("redis.password")
    if pass == "" {
        panic("请在配置文件里配置【redis.password")
    }
    db := viper.GetString("redis.db")
    if db == "" {
        panic("请在配置文件里配置【redis.db")
    }
    Redis = redis.NewClient(&redis.Options{
        Addr:     addr,
        Password: pass,
        DB:       cast.ToInt(db),
        PoolSize: 10,
    })
    zap.L().Info("Redis连接地址:", zap.String("addr", addr))
}

9、中间件

bash 复制代码
//internal/middlewares/auth.middleware.go
func AuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        authHeader := c.GetHeader("Authorization")
        if authHeader == "" {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
                `error`: "Authorization header 不能为空",
            })
            return
        }
        parts := strings.Fields(authHeader)
        if len(parts) != 2 || strings.ToLower(parts[0]) != `bearer` {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
                `error`: "Authorization header 的格式错误,必须是 Bearer {token}",
            })
            return
        }
        token := parts[1]
        claims, err := utils.ParseToken(token)
        if err != nil {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
                `error`:  "非法token",
                `detail`: err.Error(),
            })
            return
        }
        //设定当前登录的用户id,方便传下去
        c.Set(`user_id`, claims.UserID)
        c.Next()
    }
}
bash 复制代码
//internal/middlewares/ratelimit.middleware.go
package middlewares
import (
    "github.com/juju/ratelimit"
)
func RateLimitMiddleware() gin.HandlerFunc {
    bucket := ratelimit.NewBucketWithQuantum(time.Second, viper.GetInt64(`ratelimit.cap`), viper.GetInt64(`ratelimit.quantum`))
    return func(c *gin.Context) {
        if bucket.TakeAvailable(1) < 1 {
            response.Error(`rate limit ...`, http.StatusForbidden, c)
            c.Abort()
            return
        }
        c.Next()
    }
}

10、定义接口

复制代码
10.1 查询过滤器接口
bash 复制代码
// internal/interfaces/filter.interface.go
package interfaces
import (
    "golang_per_day_24/internal/utils/request"
    "github.com/gin-gonic/gin"
    "gorm.io/gorm"
)
// 过滤器接口
type IFilter interface {
    GetPage() int64
    SetPage(page int64)
    GetPageSize() int64
    SetPageSize(pageSize int64)
    GetSearchKey() string
    SetSearchKey(searchKey string)
    BuildPageListFilter(c *gin.Context, db *gorm.DB) *gorm.DB
}
// 过滤器接口实现
type Filter struct {
    request.PageList
}
func (f *Filter) GetPage() int64 {
    return f.Page
}
func (f *Filter) SetPage(page int64) {
    f.Page = page
}
func (f *Filter) GetPageSize() int64 {
    return f.PageSize
}
func (f *Filter) SetPageSize(pageSize int64) {
    f.PageSize = pageSize
}
func (f *Filter) GetSearchKey() string {
    return f.SearchKey
}
func (f *Filter) SetSearchKey(searchKey string) {
    f.SearchKey = searchKey
}
// 分页查询过滤器构建方法
func (f *Filter) BuildPageListFilter(c *gin.Context, db *gorm.DB) *gorm.DB {
    return db
}
复制代码
10.2 模型接口
bash 复制代码
// internal/interfaces/model.interface.go
package interfaces
// 模型接口
type IModel interface {
    GetId() uint
    SetId(uint)
    TableName() string
}
复制代码
10.3 数据层接口,利用泛型实现

这是一个非常标准和优雅的 Go 泛型实践。运用了泛型接口、泛型结构体和类型约束,构建了一个类型安全且高度可复用的数据访问层。这正是 Go 语言引入泛型旨在解决的典型问题之一。
bash 复制代码
// internal/interfaces/repo.interface.go
package interfaces
import (
    "errors"
    "golang_per_day_24/internal/utils/response"
    "github.com/gin-gonic/gin"
    "gorm.io/gorm"
)
// 数据资源接口
type IRepo[T IModel] interface {
    // 分页查询
    PageList(c *gin.Context, query *IFilter) (res *response.PageListT[T], err error)
    // 分页查询
    PageListWithSelectOption(c *gin.Context, query *IFilter, selectOpt []string) (res *response.PageListT[T], err error)
    // 查询一个
    One(c *gin.Context, id uint) (res T, err error)
}
// 数据资源接口实现
type Repo[T IModel] struct {
    DB *gorm.DB
}
// 新建一个数据资源
func NewRepo[T IModel](db *gorm.DB) *Repo[T] {
    return &Repo[T]{
        DB: db,
    }
}
// 分页查询数据
func (r *Repo[T]) PageList(c *gin.Context, f *IFilter) (res *response.PageListT[T], err error) {
    db := r.DB
    db = (*f).BuildPageListFilter(c, db)
    offset := ((*f).GetPage() - 1) * (*f).GetPageSize()
    db = db.Model(new(T)).Offset(int(offset)).Limit(int((*f).GetPageSize()))
    objs := make([]T, 0)
    err = db.Find(&objs).Error
    var count int64
    db.Offset(-1).Limit(-1).Select("count(id)").Count(&count)
    res = &response.PageListT[T]{
        List:  objs,
        Pages: response.MakePages(count, (*f).GetPage(), (*f).GetPageSize()),
    }
    return
}
复制代码
10.4 服务层接口
bash 复制代码
// internal/interfaces/service.interface.go
package interfaces
// 服务层接口
type IService[T IModel] interface {
    // 分页查询
    PageList(c *gin.Context, filter *IFilter) (res *response.PageListT[T], err error)
    // 分页查询
    PageListWithSelectOption(c *gin.Context, filter *IFilter, selectOpt []string) (res *response.PageListT[T], err error)
    // 查询一个
    One(c *gin.Context, id uint) (res T, err error)
    // 查询一个
    OneWithSelectOption(c *gin.Context, id uint, selectOpt []string) (res T, err error)
    // 根据名称查询
    OneByName(c *gin.Context, name string) (res T, err error)
    // 根据名称查询
    OneByNameWithSelectOption(c *gin.Context, name string, selectOpt []string) (res T, err error)
    // 添加
    Add(c *gin.Context, model T) (newId uint, err error)
    // 更新,传什么就更新什么
    Update(c *gin.Context, updateFields map[string]any, id uint) (updated bool, err error)
    // 删除
    Delete(c *gin.Context, id uint) (deleted bool, err error)
}
// 服务层接口实现
type Service[T IModel] struct {
    Repo *IRepo[T]
}
// 新建服务
func NewService[T IModel](r IRepo[T]) *Service[T] {
    return &Service[T]{
        Repo: &r,
    }
}
// 分页查询
func (s *Service[T]) PageList(c *gin.Context, filter *IFilter) (res *response.PageListT[T], err error) {}
// 分页查询
func (s *Service[T]) PageListWithSelectOption(c *gin.Context, filter *IFilter, selectOpt []string) (res *response.PageListT[T], err error) {
    repo := *s.Repo
    return repo.PageListWithSelectOption(c, filter, selectOpt)
}
// 查一条,根据id
func (s *Service[T]) One(c *gin.Context, id uint) (res T, err error) {}
// 查一条,根据id
func (s *Service[T]) OneWithSelectOption(c *gin.Context, id uint, selectOpt []string) (res T, err error) {}
// 查一条,根据名字
func (s *Service[T]) OneByName(c *gin.Context, name string) (res T, err error) {}
// 新建资源
func (s *Service[T]) Add(c *gin.Context, model T) (newId uint, err error) {}
// 更新资源
func (s *Service[T]) Update(c *gin.Context, updateFields map[string]any, id uint) (updated bool, err error) {}
// 删除资源,根据id
func (s *Service[T]) Delete(c *gin.Context, id uint) (deleted bool, err error) {}

11、创建模型models,模型要实现模型接口的方法才能被服务层和数据层接口通用

bash 复制代码
type Goods struct {
    gorm.Model
    Name  string  `json:"name" gorm:"column:name;type:varchar(100);not null;comment:'商品名称'"`
    Price float64 `json:"price" gorm:"column:price;type:decimal(10,2);not null;comment:'商品价格'"`
    Stock int     `json:"stock" gorm:"column:stock;type:int;not null;comment:'库存数量'"`
}
func (m *Goods) GetId() uint {
    return m.ID
}
func (m *Goods) SetId(id uint) {
    m.ID = id
}
func (m *Goods) TableName() string {
    return "goods"
}
bash 复制代码
type User struct {
    gorm.Model
    // gorm:"uniqueIndex" 是唯一索引,not null表示不为空
    Username string `json:"username" gorm:"type:varchar(30);uniqueIndex;not null"`
    // json:"-", - 表示不输出到json
    PassWd string `json:"-" gorm:"not null"`
}

12、创建数据仓库repos,直接嵌入接口定义实现的结构interfaces.Repo,就拥有了其所有的方法,主要是为了减少代码的编写,把所有相同的方法放在一个地方,新的数据层结构体可以直接复用

bash 复制代码
var Goods *GoodsRepo
type GoodsRepo struct {
    interfaces.Repo[*models.Goods]
}
func NewGoodsRepo() {
    Goods = &GoodsRepo{
        Repo: *interfaces.NewRepo[*models.Goods](components.DB),
    }
}
func init() {
    RegisterRepos(NewGoodsRepo)
}

13、创建服务逻辑services

bash 复制代码
// internal/services/goods.service.go
var Goods *GoodsService
type GoodsService struct {
    redis *redis.Client
    interfaces.Service[*models.Goods]
}
func NewGoodsService(r *repos.GoodsRepo, redisClient *redis.Client) *GoodsService {
    return &GoodsService{
        redis:   components.Redis,
        Service: *interfaces.NewService(repos.Goods),
    }
}
func init() {
    RegisterServices(func() {
        Goods = NewGoodsService(repos.Goods, components.Redis)
    })
}

14、创建控制器controllers

复制代码
14.1 增加swagger文档,便于自动生成swagger文档
bash 复制代码
// internal/controllers/goods.controller.go

// @Summary 新增(add)
// @Tags        goods
// @Accept      application/json
// @Produce application/json
// @Param       Authorization   header  string  true    "Bearer 用户令牌"
// @Param       {object} body  models.Goods true "body"
// @Success 200
// @Failure 400                        "请求错误"
// @Failure 401                        "token验证失败"
// @Failure 500                         "内部错误"
// @Router      /api/v1/goods [post]
func GoodsAdd(c *gin.Context) {
    model := &models.Goods{}
    err := c.ShouldBindBodyWith(&model, binding.JSON)
    if err != nil {
        response.Error(err.Error(), http.StatusBadRequest, c)
        return
    }
    newId, err := services.Goods.Add(c, model)
    if err != nil {
        response.Error(err.Error(), http.StatusBadRequest, c)
        return
    }
    response.OK(newId, c)
}
复制代码
14.2 在main函数前增加swagger注释
bash 复制代码
// @title           GolangPerDay24 API
// @version         1.0
// @description     This is a demo server.
func main() {
    cmd.Execute()
}

15、新建路由routers

bash 复制代码
// internal/routers/goods.router.go
func init() {
    RegisterRoute(func(e *gin.Engine) {
        // 路由分组
        v1 := e.Group("/api/v1")
        v1.Use(middlewares.AuthMiddleware())//必须登录
        goods := v1.Group("/goods")
        {
            goods.GET("", controllers.GoodsPageList)   //分页
            goods.GET("/:id", controllers.GoodsOne)    //一个
            goods.POST("", controllers.GoodsAdd)       //新增
            goods.PUT("/:id", controllers.GoodsUpdate) //更新
            goods.DELETE("/:id", controllers.GoodsDel) //删除
        }
    })
}

16、运行项目

复制代码
16.1 添加debug配置
bash 复制代码
// .vscode/launch.json
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "LaunchServer",
            "type": "go",
            "request": "launch",
            "mode": "debug",
            "program": "${workspaceFolder}/main.go",
            "output": "tmp/golang_per_day_24",
            "env": {},
            "args": ["server", "-c", "configs/config.yaml"]
        },
    ]
}
复制代码
16.2生成API文档

swag的使用参考https://github.com/swaggo/swag/blob/master/README_zh-CN.md,后期做详细介绍

bash 复制代码
init
true
复制代码
16.3运行项目
复制代码
16.4 访问API 文档 ,http://localhost:8080/swagger/index.html

17、支持Docker

bash 复制代码
#Dockerfile
#FROM 基础镜像
FROM golang:1.23-alpine AS builder
#LABEL 指令用来给镜像添加一些元数据(metadata),以键值对的形式
LABEL maintainer="Codee君"
#设置容器环境变量
ENV GO111MODULE=on
ENV GOPROXY=https://goproxy.cn,direct
#为 RUN、CMD、ENTRYPOINT、COPY 和 ADD 设置工作目录,就是切换目录
WORKDIR /go/release
#COPY 拷贝文件或目录到容器中,跟ADD类似,但不具备自动下载或解压的功能
COPY . .
#RUN 构建镜像时运行的指令
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
RUN apk update && apk add tzdata
RUN go install github.com/swaggo/swag/cmd/swag@v1.8.10 && swag init  --parseDependency=true
RUN CGO_ENABLED=0 GOOS=linux go build -p 1 -ldflags="-w -s" -a -installsuffix cgo -o golang_per_day .
FROM alpine:latest
COPY --from=builder /go/release/golang_per_day /
COPY --from=builder /go/release/configs /configs
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=builder /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
#EXPOSE 声明容器的服务端口(仅仅是声明)
EXPOSE 8080
#CMD 运行容器时执行的shell环境
CMD ["/golang_per_day","server","-c", "/configs/config.yaml"]
bash 复制代码
//docker-compose.yml
version: "3"
services:
  golang_per_day_24:
    container_name: golang_per_day_24
    build: .
    ports:
      - 8080:8080
    volumes:
      - ./configs:/configs
    restart: always

18、发布到K8s

复制代码
18.1 编写配置deploy.yaml
bash 复制代码
apiVersion: apps/v1 #定义应用部署的API版本
kind: Deployment #定义部署应用
metadata: #元数据部分
  name: golang-per-day-24 #这里填写部署名称
  namespace: codee_jun #这里填写命名空间
spec:
  #replicas表示期望运行的Pod副本数量
  replicas: 1 #这里填写副本数量
  #选择器,定义如何选择Pod
  selector:
    matchLabels: #选择标签,匹配具有特定标签的Pod
      app: golang-per-day-24 #这里填写标签名称
  #模板,定义Pod的规格
  template:
    metadata: #Pod的元数据
      labels: #标签部分
        app: golang-per-day-24 #这里填写标签名称
    spec: #Pod的规格部分
      containers: #定义容器
        - name: golang-per-day-24 #这里填写容器名称
          image: golang_per_day_24:latest #这里填写镜像名称
          ports: #定义容器端口
            - containerPort: 80 #容器内部端口
          volumeMounts: #定义卷挂载
            - name: runtime-dir
              mountPath: /configs #容器内挂载路径
              subPath: runtime/golang_per_day_24/configs #子路径
          resources: #定义资源请求和限制
            requests: #资源请求
              cpu: 35m #请求的CPU资源
              memory: 380Mi #请求的内存资源
            limits: #资源限制
              cpu: 70m #限制的CPU资源
              memory: 500Mi #限制的内存资源
      volumes:
        - name: runtime-dir #卷名称
          persistentVolumeClaim: 
            claimName: nfs-pvc #引用的持久卷声明名称
---
apiVersion: v1
kind: Service #定义服务
metadata:
  name: golang-per-day-24 #这里填写服务名称
  namespace: codee_jun #这里填写命名空间
spec: #服务规格
  ports:
    - protocol: TCP 
      port: 80 #服务端口
      targetPort: 80 #目标端口(容器端口)
  sessionAffinity: ClientIP #会话亲和性设置为ClientIP
  selector: 
    app: golang-per-day-24  #选择标签,匹配具有特定标签的Pod
---
apiVersion: autoscaling/v2 
kind: HorizontalPodAutoscaler #定义水平Pod自动扩缩容器
metadata:
  name: golang-per-day-24-hpa #这里填写HPA名称
  namespace: codee_jun #这里填写命名空间
spec: 
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment 
    name: golang-per-day-24
  minReplicas: 1 #最小副本数
  maxReplicas: 10 #最大副本数
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization #CPU利用率目标
          averageUtilization: 90 #CPU利用率目标值
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization #内存利用率目标
          averageUtilization: 90 #内存利用率目标值
复制代码
18.2 发布
bash 复制代码
kubectl apply -f deploy.yaml
复制代码
18.3 删除
bash 复制代码
kubectl delete -f deploy.yaml

19、源码地址

https://pan.baidu.com/s/1B6pgLWfSgMngVeFfSTcPdg?pwd=jc1s


如果您喜欢这篇文章,请点赞、推荐+分享给更多朋友,万分感谢!

相关推荐
weiabc1 小时前
cout << fixed << setprecision(2) << v; fixed 为什么不用括号,它是函数吗
开发语言·c++·算法
我爱娃哈哈1 小时前
SpringBoot + MQTT + EMQX:物联网设备上行数据实时接入与指令下发平台
spring boot·后端·物联网
昱宸星光1 小时前
spring cloud gateway内置路由断言工厂
java·开发语言·前端
m0_531237171 小时前
C语言-内存函数
c语言·开发语言·算法
迷之程序员1 小时前
llama-cpp-python用法,模型加载gpu踩坑全记录
开发语言·python·llama
yongui478341 小时前
基于C#实现视频文件解封装与媒体流读取方案
开发语言·c#·媒体
froginwe111 小时前
JavaScript、HTML 与 DOM 实例解析
开发语言
摸鱼的春哥1 小时前
春哥的Agent通关秘籍11:本地RAG实战(中上)
前端·javascript·后端
GetcharZp1 小时前
谁是OpenClaw?这个一夜爆火的“AI打工人”,正在悄悄接管你的电脑!
人工智能·后端