gin について
youlai-gin の規約に基づく、Gin バックエンド向け Go 開発ガイドラインのスキルです。設計、REST API、認証、エラー処理を扱います。
主な機能
- 01Handler・Service・Repository のレイヤー規約
- 02REST API のパスとレスポンス規約
- 03JWT 認証と権限制御
- 04統一されたエラー処理フロー
- 05命名・コメント・コードレビュー項目
活用シーン
- 01Gin プロジェクトでの REST API 実装
- 02GORM を使ったデータアクセス層の作成
- 03JWT ログインと権限チェックの追加
- 04プロジェクト構成やエラー処理のレビュー
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ youlaitech/ youlai-skills --skill ginインストール後、Claude Code で/ginを入力して読み込まれたか確認します。
詳細
Gin バックエンド開発規約
本規約は、現在の youlai-gin のコード(モジュール名
youlai-gin)を基準とし、Uber Go Style Guide および Go CodeReviewComments コミュニティの慣例を取り入れています。
適用条件
- Gin プロジェクトの開発
- REST API の実装
- データアクセスでの GORM の使用
- JWT 認証の実装
- 権限制御の実装
Part 1: 技術スタック
| レイヤー | 選定技術 | 説明 |
|---|---|---|
| 実行環境 | Go 1.25+ | 標準ライブラリが堅牢で、並行処理をネイティブにサポート |
| Web フレームワーク | Gin | 高性能 HTTP ルーター |
| ORM | GORM 2.0 | チェーン形式のクエリー |
| データベース | MySQL 8.x | InnoDB エンジン |
| キャッシュ | Redis 7.x (go-redis) | 分散キャッシュ/Token ストレージ |
| 認証 | golang-jwt v5 | JWT の発行/検証 |
| ログ | zap + lumberjack によるローテーション | 構造化ログ |
| API ドキュメント | swaggo/swag (swag init -o api) | OpenAPI を自動生成 |
Part 2: ディレクトリ構造
基本ディレクトリは Standard Go Project Layout を参考にします。
internal/は業務モジュールごとに分け、各モジュールには 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
設計原則:
internal/は外部からインポートできません(コンパイラーによる厳格な制約)。pkg/は再利用できますが、internal/を import してはいけませんinternal/common/はインフラストラクチャ層です。業務モジュール(internal/systemなど)を import してはいけません- 業務モジュールは Handler → Service → Repository のレイヤー構成とし、model には entity/form/query/vo を配置します
- 目標構成(新規モジュールでは必須):構造体とコンストラクター注入を使用します。例:
dept:
deptHandler.NewHandler(deptService.NewService(deptRepo.NewRepository(db))).RegisterRoutes(r)
user/role/config/notice/log などのパッケージレベル関数形式は過去の実装です。新しいコードでは踏襲しません。
| レイヤー | ディレクトリ | 責務 | 禁止事項 |
|---|---|---|---|
| Handler | handler/ | パラメーターのバインド、service の呼び出し、レスポンスの書き込み | 業務ロジックを記述しない。GORM に触れない |
| Model | model/ | entity/form/query/vo | ロジックを持たない |
| Repository | repository/ | GORM クエリー | 業務判断を記述しない |
| Service | service/ | 業務ロジック | *gin.Context を受け取らない。第 1 引数は context.Context |
Part 3: 命名規約
3.1 ファイル名
| 種類 | 規約 | 例 |
|---|---|---|
| Handler | {name}_handler.go | user_handler.go |
| Service | {name}_service.go | user_service.go |
| Repository | {name}_repo.go | user_repo.go |
| Entity | model/ 配下に entity.go | model/entity.go |
| フォーム/クエリー/ビュー | form.go / query.go / vo.go | 同上 |
3.2 命名原則
- 簡潔にし、冗長さを避ける:パッケージ名がすでにコンテキストを示すため、型名にパッケージ名を重複させません
- エクスポートする名前は大文字で始めます(
GetByID)。エクスポートしない名前は小文字で始めます(parseToken) context.Contextは常に関数の第 1 引数にします- 禁止:
pkg/modelをcommonとして別名指定する(誤解を招き、internal/commonと混同するため) - 標準ライブラリと衝突するパッケージ名は避けます(過去の実装である
internal/common/contextは、インポート時にappContextを別名として使用します)
// ❌ 冗余:调用时变成 user.UserService
package user
type UserService struct{}
// ✅ 包名已提供语义
package user
type Service struct{}
3.3 メソッド名(新しいコードでは必須)
| 操作 | 接頭辞 | 例 |
|---|---|---|
| 単件取得 | Get | Get(ctx, id) |
| 一覧取得 | List | List(ctx, query) |
| ページネーション付き取得 | Page | Page(ctx, query) |
| 新規作成 | Create | Create(ctx, form) |
| 更新 | Update | Update(ctx, form) |
| 削除 | Delete | Delete(ctx, id) |
| ドロップダウン項目 | Options | Options(ctx) |
| 存在確認 | Exists | ExistsByCode(ctx, code) |
- メソッド名にエンティティ名を含めません。レシーバーがすでに
dept.Repositoryであるため、GetDeptListではなくListと記述します - 過去の実装である
Save*(作成と更新を統合したもの)やGetXxxListは踏襲しません
3.4 変数名
| 種類 | 規約 | 例 |
|---|---|---|
| 変数 | camelCase | userList |
| 定数 | PascalCase | MaxSize |
| 非公開変数 | 小文字で開始 | internalState |
| ブール値 | Is/Has/Can を接頭辞にする | isDeleted, hasPermission |
3.5 import の並べ替え(goimports -local youlai-gin で必ず実行)
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 規約
4.1 標準 CRUD パス(ユーザーを例にします)
| 操作 | メソッド | パス |
|---|---|---|
| ページネーション付き一覧 | GET | /api/v1/users(pageNum/pageSize/キーワードは query で渡します) |
| フォーム表示 | GET | /api/v1/users/:userId/form |
| 新規作成 | POST | /api/v1/users |
| 更新 | PUT | /api/v1/users/:userId |
| 削除(複数件可) | DELETE | /api/v1/users/:ids(カンマ区切り) |
| ステータス変更 | PATCH | /api/v1/users/:userId/status |
| ドロップダウン項目 | GET | /api/v1/users/options |
| インポート/エクスポート | POST/GET | /api/v1/users/import / /api/v1/users/export |
4.2 Handler のテンプレート
// 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)
}
ポイント:バインドには validator.BindJSON/BindQuery を使用します(ラップ済みの *errs.AppError が返されます)。エラーが発生したら、必ず c.Error(err) + return を実行します。レスポンスには必ず response.Ok/OkPaged/OkMsg を使用します。
Part 5: レスポンス形式と例外処理
5.1 統一レスポンス(internal/common/response.go、パッケージ名 common)
type Result struct {
Code string `json:"code"` // 阿里错误码:00000 成功 / A0400 参数错 / B0001 系统错
Msg string `json:"msg"`
Data interface{} `json:"data"`
}
| 関数 | 用途 |
|---|---|
Ok(c, data) | 成功 + データ |
OkPage(c, list, pageNum, pageSize, total) / OkPaged(c, *model.PagedData) | ページネーション |
OkMsg(c, msg) | 成功 + メッセージのみ |
FromAppError(c, ae) | AppError に従って出力(実際の HTTP ステータスコードを使用)。エラーミドルウェア専用 |
5.2 错误流:c.Error + 全局中间件
// pkg/errs
type AppError struct {
Code string `json:"code"`
Msg string `json:"msg"`
HTTPStatus int `json:"-"`
Err error `json:"-"` // 仅日志,不返回客户端
Stack string `json:"-"`
}
- Handler 只写
c.Error(errs.BadRequest("具体信息"))/ 透传 service 返回的 error middleware.ErrorHandler()统一FromAppError输出;类型判断用errors.As,不用裸断言- 禁止 handler 直接调用
response.Fail/BadRequest(遗留函数,待删除) - 错误只在顶层 log 一次,中间层
fmt.Errorf("...: %w", err)包装
Part 6: 实体规范
// 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" }
- 软删除走
is_deleted标志位(不是gorm.DeletedAt),查询需带条件 create_by/update_by由gormx审计钩子从 ctx 填充,同一张表只允许一个实体定义
Part 7: Service 规范
// 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)
}
- 禁止 service/repo 方法接收
*gin.Context;需要操作人时由 handler 传入appContext.OperatorCtx(c)(已注入 operator,供审计钩子使用) - repo 方法统一
ctx第一参数 +r.db.WithContext(ctx) - 业务失败返回
errs.*,不用 panic
Part 8: 认证规范
8.1 TokenManager 与中间件
// 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 权限标识格式
模块:实体:操作
sys:user:create / sys:user:update / sys:user:delete / sys:role:list
8.3 取当前用户
appContext.GetCurrentUserID(c)/GetCurrentUser(c):返回(v, error),错误即errs.Unauthorized,直接c.Error- 需要写操作人:
appContext.OperatorCtx(c)转context.Context传给 service
Part 9: 注释规范
- 导出符号必须有 godoc 注释(中文可,信号字保留英文)
- Swagger 注解:
@Summary/@Tags/@Param/@Success/@Router;Tags 用01.认证接口这类带序号分组 - 注释写 Why 不写 What;不写"用于 XXX 流程"这类会腐烂的上下文
- 生成物:
swag init -o api(输出到api/,勿手改)
// 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: 代码质量检查清单
- 遵循 RESTful 路径规范(Part 4.1)
- 错误走
c.Error+ ErrorHandler,禁止 handler 直接response.Fail - service/repo 不出现
*gin.Context,ctx为第一参数 -
strconv.ParseInt/Atoi必须检查错误 - 文件上传校验大小和类型
- 实体内嵌
pkg/model.BaseEntity,软删用is_deleted - 新 API 有 Swaggo 注解,
swag init -o api重新生成 -
goimports -w -local youlai-gin+go vet通过,CI 加golangci-lint - import 三组有序(标准库 → 第三方 → 项目内)
- 导出符号有 godoc 注释
- 错误判断用
errors.Is/As,不裸类型断言
常见反模式
| 反模式 | 正确做法 |
|---|---|
fmt.Printf("debug: %v", data) | slog/zap 结构化日志 |
panic(err) 处理业务错误 | 返回 errs.*,handler c.Error |
strconv.ParseInt(s, 10, 64) 忽略 err | 必须检查错误 |
| Context 塞进结构体字段 | Context 作为函数第一个参数 |
service 收 *gin.Context | handler 传 OperatorCtx(c) / c.Request.Context() |
层层 log.Error + return err | 只在顶层 log 一次,中间 %w 包装 |
| HTTP 调用无超时 | context.WithTimeout(ctx, 30*time.Second) |
| 堆栈返回给调用方 | 错误信息脱敏,堆栈只写日志 |
新建 utils/common 杂物包 | 按域命名(如 excel、storage),工具函数放进使用它的包 |
把 pkg/model 别名为 common | 直接用包名 model |
| 同一张表多个实体定义 | 实体唯一定义,跨层复用 |
| 悬空的 DI 图 / 无人调用的导出函数 | 删掉或补全,死代码不过夜 |
コメント
Markdown 対応