Gin Backend Development Specification
This specification is based on the current youlai-gin code (module name youlai-gin) and incorporates the Uber Go Style Guide and Go CodeReviewComments community conventions.
Trigger Conditions
- Develop Gin projects
- Implement REST APIs
- Use GORM for data access
- Implement JWT authentication
- Implement permission control
Part 1: Technology Stack
| Layer |
Selection |
Description |
| Runtime Environment |
Go 1.25+ |
Robust standard library, native concurrency |
| Web Framework |
Gin |
High-performance HTTP router |
| ORM |
GORM 2.0 |
Chained queries |
| Database |
MySQL 8.x |
InnoDB engine |
| Cache |
Redis 7.x (go-redis) |
Distributed cache / Token storage |
| Authentication |
golang-jwt v5 |
JWT signing/verification |
| Logging |
zap + lumberjack rotation |
Structured logging |
| API Documentation |
swaggo/swag (swag init -o api) |
OpenAPI auto-generation |
Part 2: Directory Structure
Basic directory reference Standard Go Project Layout;internal/ Divided by business module, each module contains handler/model/service/router.
├── main.go # 程序入口(配置/日志/数据库/存储初始化 + 优雅关闭)
├── api/ # Swagger 产物(swag init -o api 生成:docs.go/swagger.json/swagger.yaml)
├── internal/ # 私有代码(Go 编译器强制不可被外部导入)
│ ├── auth/ # 认证模块(账密/短信/扫码/小程序登录)
│ │ ├── handler/ model/ service/ router.go
│ ├── system/ # 系统管理(按实体分子模块)
│ │ ├── user/ role/ menu/ dept/ dict/ config/ notice/ log/
│ │ │ └── handler/ model/ repository/ service/
│ │ └── router.go # 模块装配(构造注入或函数注册)
│ ├── codegen/ # 代码生成器
│ ├── file/ # 文件模块
│ ├── message/ # 消息模块(SSE 推送)
│ ├── common/ # 公共基础设施(禁止依赖业务模块)
│ │ ├── auth/ # JWT / Redis TokenManager + 认证/权限中间件
│ │ ├── config/ # 配置加载(viper)
│ │ ├── context/ # gin 上下文取用户信息、操作人注入
│ │ ├── database/ # GORM 初始化 + 分页
│ │ ├── excel/ json/ logger/ # 导出、序列化、结构化日志
│ │ ├── permission/ # 权限服务 + 数据权限(datascope)
│ │ ├── redis/ # Redis 客户端 + key 规范
│ │ ├── storage/ # 文件存储(local/s3/aliyun 工厂)
│ │ ├── utils/ # 文件/密码/树/验证码工具(历史包,新代码不再往里堆)
│ │ ├── validator/ # 参数绑定与校验(BindJSON/BindQuery)
│ │ └── response.go # 统一响应(包名 common,导入时别名 response)
│ ├── middleware/ # 全局中间件(error_handler/operation_log/rate_limiter/requestid)
│ └── router/register.go # 路由注册入口
├── pkg/ # 公共库(可被外部导入,禁止依赖 internal/)
│ ├── constant/ # 业务常量 + 错误码(阿里规范 "00000"/"A0400"…)
│ ├── enums/ # 操作类型 / 日志模块枚举
│ ├── errs/ # AppError 统一错误(Code/Msg/HTTPStatus/Err/Stack)
│ ├── gormx/ # GORM 扩展(审计钩子、PatchMap)
│ ├── model/ # BaseEntity/BaseQuery/PagedData/Option/KeyValue
│ └── types/ # BigInt/FlexInt/LocalTime 序列化类型
├── configs/ # dev/prod/test.yaml
├── docs/ # 项目文档与图片(非 Swagger 产物)
├── docker/docker-compose.yml
├── sql/mysql/ # 建库脚本
└── go.mod / go.sum
Design Principles:
internal/ cannot be imported externally (compiler-enforced);pkg/ reusable,must not import internal/
internal/common/ is infrastructure,must not import business modules(internal/system etc.)
- Business module layering Handler → Service → Repository, model holds entity/form/query/vo
- Target structure (new modules must follow): struct + constructor injection, e.g.
dept:
deptHandler.NewHandler(deptService.NewService(deptRepo.NewRepository(db))).RegisterRoutes(r)
The package-level function style of user/role/config/notice/log etc. is legacy; new code should no longer imitate it.
| Layer |
Directory |
Responsibility |
Forbidden Zone |
| Handler |
handler/ |
Parameter binding, calling service, writing response |
Do not write business logic, do not touch GORM |
| Model |
model/ |
entity/form/query/vo |
No logic |
| Repository |
repository/ |
GORM queries |
Do not write business judgments |
| Service |
service/ |
Business logic |
Do not accept *gin.Context, the first parameter is context.Context |
Part 3: Naming Conventions
3.1 File Naming
| Type |
Convention |
Example |
| Handler |
{name}_handler.go |
user_handler.go |
| Service |
{name}_service.go |
user_service.go |
| Repository |
{name}_repo.go |
user_repo.go |
| Entity |
entity.go under model/ |
model/entity.go |
| Form/Query/View |
form.go / query.go / vo.go |
Same as above |
3.2 Naming Principles
- Concise without redundancy: the package name already provides context, type names do not repeat the package name
- Exported uppercase
GetByID, unexported lowercase parseToken
context.Context always as a functionfirst parameter
- Prohibitedaliasing
pkg/model as common(misleading and confused with internal/common )
- Avoid package names colliding with the standard library (legacy
internal/common/context, alias on import appContext)
// ❌ 冗余:调用时变成 user.UserService
package user
type UserService struct{}
// ✅ 包名已提供语义
package user
type Service struct{}
3.3 Method Naming (new code must follow)
| Action |
Prefix |
Example |
| Query single |
Get |
Get(ctx, id) |
| Query list |
List |
List(ctx, query) |
| Paginated query |
Page |
Page(ctx, query) |
| Create |
Create |
Create(ctx, form) |
| Update |
Update |
Update(ctx, form) |
| Delete |
Delete |
Delete(ctx, id) |
| Dropdown options |
Options |
Options(ctx) |
| Existence check |
Exists |
ExistsByCode(ctx, code) |
- Method nameWithout entity name: the receiver is already
dept.Repository, write List instead of GetDeptList
- Legacy
Save*(create-or-update combined),GetXxxList no longer mimics
3.4 Variable Naming
| Type |
Convention |
Example |
| Variable |
camelCase |
userList |
| Constant |
PascalCase |
MaxSize |
| Private variable |
lowercase first letter |
internalState |
| Boolean |
Is/Has/Can prefix |
isDeleted, hasPermission |
3.5 import ordering (goimports -local youlai-gin enforced)
import (
// 1. 标准库
"context"
"time"
// 2. 第三方库
"github.com/gin-gonic/gin"
"gorm.io/gorm"
// 3. 本项目内部
"youlai-gin/internal/common/response"
"youlai-gin/pkg/errs"
)
Part 4: RESTful API Specification
4.1 Standard CRUD paths (using user as an example)
| Operation |
Method |
Path |
| Paginated list |
GET |
/api/v1/users(pageNum/pageSize/keyword go in query) |
| Form echo |
GET |
/api/v1/users/:userId/form |
| Create |
POST |
/api/v1/users |
| Update |
PUT |
/api/v1/users/:userId |
| Delete (batch supported) |
DELETE |
/api/v1/users/:ids(comma-separated) |
| Status change |
PATCH |
/api/v1/users/:userId/status |
| Dropdown options |
GET |
/api/v1/users/options |
| Import/Export |
POST/GET |
/api/v1/users/import / /api/v1/users/export |
4.2 Handler Template
// Page 用户分页列表
// @Summary 用户分页列表
// @Tags 02.用户接口
// @Param query query model.UserQuery false "查询参数"
// @Success 200 {object} common.Result
// @Router /api/v1/users [get]
func (h *Handler) Page(c *gin.Context) {
var query model.UserQuery
if err := validator.BindQuery(c, &query); err != nil {
c.Error(err)
return
}
result, err := h.svc.Page(c.Request.Context(), &query)
if err != nil {
c.Error(err)
return
}
response.OkPaged(c, result)
}
Key points: for binding use validator.BindJSON/BindQuery(returns the already wrapped *errs.AppError); errors always c.Error(err) + return; responses always response.Ok/OkPaged/OkMsg。
Part 5: Response Format and Exception Handling
5.1 Unified response (internal/common/response.go, package name common)
type Result struct {
Code string `json:"code"` // 阿里错误码:00000 成功 / A0400 参数错 / B0001 系统错
Msg string `json:"msg"`
Data interface{} `json:"data"`
}
| Function |
Purpose |
Ok(c, data) |
Success + data |
OkPage(c, list, pageNum, pageSize, total) / OkPaged(c, *model.PagedData) |
Pagination |
OkMsg(c, msg) |
Success + message only |
FromAppError(c, ae) |
By AppError output (real HTTP status code), for error middleware only |
5.2 Error flow:c.Error + global middleware
// pkg/errs
type AppError struct {
Code string `json:"code"`
Msg string `json:"msg"`
HTTPStatus int `json:"-"`
Err error `json:"-"` // 仅日志,不返回客户端
Stack string `json:"-"`
}
- Handler only writes
c.Error(errs.BadRequest("具体信息")) / passes through the error returned by service
middleware.ErrorHandler() unified FromAppError output; for type checking use errors.As, not bare assertions
- Forbidden handler directly calls
response.Fail/BadRequest(legacy function, to be deleted)
- Errors are logged only once at the top level; intermediate layers
fmt.Errorf("...: %w", err) wrap
Part 6: Entity Specification
// pkg/model.BaseEntity:公共审计字段 + 手动软删除
type BaseEntity struct {
CreateBy *types.BigInt `gorm:"column:create_by" json:"createBy,omitempty"`
CreateTime types.LocalTime `gorm:"column:create_time;autoCreateTime" json:"createTime,omitempty"`
UpdateBy *types.BigInt `gorm:"column:update_by" json:"updateBy,omitempty"`
UpdateTime types.LocalTime `gorm:"column:update_time;autoUpdateTime" json:"updateTime,omitempty"`
IsDeleted int `gorm:"column:is_deleted;default:0" json:"-"`
}
// 业务实体:主键用 types.BigInt(解决 JS 大数精度),显式 column 标签
type User struct {
ID types.BigInt `gorm:"primaryKey;autoIncrement" json:"id"`
Username string `gorm:"column:username;not null" json:"username"`
Password string `gorm:"column:password;not null" json:"-"`
DeptID types.BigInt `gorm:"column:dept_id" json:"deptId"`
common.BaseEntity
}
func (User) TableName() string { return "sys_user" }
- Soft delete uses the
is_deleted flag (not gorm.DeletedAt), queries must include the condition
create_by/update_by is populated by the gormx audit hook from ctx,only one entity definition is allowed per table
Part 7: Service Specification
// Repository 接口定义在消费方(service 包),便于替换与测试
type Repository interface {
Page(ctx context.Context, query *model.UserQuery) ([]model.User, int64, error)
Create(ctx context.Context, user *model.User) error
}
type Service struct {
repo Repository
}
func NewService(repo Repository) *Service { return &Service{repo: repo} }
func (s *Service) Create(ctx context.Context, form *model.UserForm) error {
exists, err := s.repo.ExistsByUsername(ctx, form.Username)
if err != nil {
return err
}
if exists {
return errs.BadRequest("用户名已存在")
}
user := &model.User{Username: form.Username, Password: utils.HashPassword(form.Password)}
return s.repo.Create(ctx, user)
}
- Forbidden service/repo methods receive
*gin.Context; when an operator is needed, the handler passes in appContext.OperatorCtx(c)(operator already injected, for use by the audit hook)
- repo methods uniformly use
ctx as the first parameter + r.db.WithContext(ctx)
- Business failures return
errs.*,do not use panic
Part 8: Authentication Specification
8.1 TokenManager and Middleware
// internal/common/auth:TokenManager 接口(JWT/Redis 双实现),由 config.Security 决定
tokenManager, _ := auth.CreateTokenManager(&config.Cfg.Security)
authorized.Use(pkgAuth.Middleware(tokenManager)) // 验签 + 写入用户上下文
r.GET("...", auth.RequirePermission("sys:user:list"), handler) // 权限点
8.2 Permission Identifier Format
模块:实体:操作
sys:user:create / sys:user:update / sys:user:delete / sys:role:list
8.3 Getting the Current User
appContext.GetCurrentUserID(c) / GetCurrentUser(c): returns (v, error), an error means errs.Unauthorized, directly c.Error
- When an operator is needed:
appContext.OperatorCtx(c) convert to context.Context and pass to service
Part 9: Comment Specification
- Exported symbols must have godoc comments(Chinese is fine; keep signal words in English)
- Swagger annotations:
@Summary / @Tags / @Param / @Success / @Router; use Tags with 01.认证接口 numbered grouping like this
- Comments should state Why not What; do not write perishable context like "used for XXX flow"
- Generated artifacts:
swag init -o api(output to api/, do not edit by hand)
// Page 分页查询用户列表。
//
// 根据关键词模糊搜索用户名/昵称/手机号,支持状态筛选。
//
// @Summary 用户分页列表
// @Tags 02.用户接口
// @Param query query model.UserQuery false "查询参数"
// @Success 200 {object} common.Result
// @Router /api/v1/users [get]
func (h *Handler) Page(c *gin.Context) { ... }
Part 10: Code Quality Checklist
- Follow RESTful path conventions (Part 4.1)
- Errors go through
c.Error + ErrorHandler; handlers must not directly response.Fail
- service/repo must not contain
*gin.Context,ctx as the first parameter
-
strconv.ParseInt/Atoi Errors must be checked
- File uploads must validate size and type
- Entities embed
pkg/model.BaseEntity, soft delete uses is_deleted
- New APIs have Swaggo annotations,
swag init -o api regenerate
-
goimports -w -local youlai-gin + go vet passes, CI adds golangci-lint
- imports in three ordered groups (standard library → third-party → project-internal)
- Exported symbols have godoc comments
- Error checks use
errors.Is/As, no bare type assertions
Common Anti-Patterns
| Anti-Pattern |
Correct Practice |
fmt.Printf("debug: %v", data) |
slog/zap Structured logging |
panic(err) Handle business errors |
Return errs.*,handler c.Error |
strconv.ParseInt(s, 10, 64) ignore err |
Errors must be checked |
| Context stuffed into a struct field |
Context as the function's first parameter |
service receives *gin.Context |
handler passes OperatorCtx(c) / c.Request.Context() |
layer by layer log.Error + return err |
log only once at the top level; in between %w wrap |
| HTTP calls without timeout |
context.WithTimeout(ctx, 30*time.Second) |
| Stack traces returned to the caller |
Error messages sanitized; stack traces only written to logs |
Create a new utils/common junk package |
Name by domain (e.g. excel、storage), put utility functions in the package that uses them |
Alias pkg/model as common |
Use the package name directly model |
| Multiple entity definitions for the same table |
Single entity definition, reused across layers |
| Dangling DI graph / exported functions nobody calls |
Delete or complete them; no dead code left overnight |