
今天我们结合前面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
如果您喜欢这篇文章,请点赞、推荐+分享给更多朋友,万分感谢!