21 KiB
21 KiB
Shared Package Layer - CLAUDE.md
本文件为 Claude Code 在共享包模块中工作时提供指导。
🎯 模块概览
pkg 包提供可复用的工具和组件,供整个应用使用,遵循 Go 语言的包设计哲学。
主要职责
- 📦 提供通用工具和实用函数
- 📝 提供日志记录组件
- 🔄 提供统一响应格式
- ✅ 提供数据验证工具
- 🛠️ 提供辅助功能函数
📁 模块结构
pkg/
├── CLAUDE.md # 📋 当前文件 - 公共工具包指导
├── logger/ # 📝 日志工具
│ ├── logger.go # 日志接口和实现
│ ├── zap_logger.go # Zap 日志实现
│ ├── config.go # 日志配置
│ └── logger_test.go # 日志测试
├── response/ # 🔄 响应格式
│ ├── response.go # 统一响应结构
│ ├── error.go # 错误响应
│ ├── success.go # 成功响应
│ └── pagination.go # 分页响应
├── validator/ # ✅ 数据验证
│ ├── validator.go # 验证器接口
│ ├── gin_validator.go # Gin 验证器实现
│ ├── custom_validators.go # 自定义验证规则
│ └── validator_test.go # 验证器测试
├── utils/ # 🛠️ 通用工具
│ ├── string.go # 字符串工具
│ ├── time.go # 时间工具
│ ├── crypto.go # 加密工具
│ ├── file.go # 文件工具
│ ├── uuid.go # UUID 生成
│ └── utils_test.go # 工具测试
├── middleware/ # 🔗 中间件
│ ├── cors.go # CORS 中间件
│ ├── rate_limit.go # 限流中间件
│ ├── request_id.go # 请求ID中间件
│ └── recovery.go # 错误恢复中间件
├── database/ # 🗄️ 数据库工具
│ ├── connection.go # 数据库连接
│ ├── migrate.go # 数据库迁移
│ └── health.go # 健康检查
└── config/ # ⚙️ 配置工具
├── config.go # 配置管理
├── env.go # 环境变量处理
└── validation.go # 配置验证
📝 日志组件
日志接口设计
// logger/logger.go - 日志接口
package logger
import (
"context"
)
// Logger 日志接口
type Logger interface {
// 基础日志方法
Debug(msg string, fields ...Field)
Info(msg string, fields ...Field)
Warn(msg string, fields ...Field)
Error(msg string, fields ...Field)
Fatal(msg string, fields ...Field)
// 上下文日志方法
DebugContext(ctx context.Context, msg string, fields ...Field)
InfoContext(ctx context.Context, msg string, fields ...Field)
WarnContext(ctx context.Context, msg string, fields ...Field)
ErrorContext(ctx context.Context, msg string, fields ...Field)
// 结构化字段方法
With(fields ...Field) Logger
WithError(err error) Logger
WithContext(ctx context.Context) Logger
// 日志级别控制
SetLevel(level Level)
GetLevel() Level
}
// Field 日志字段
type Field struct {
Key string
Value interface{}
}
// Level 日志级别
type Level int
const (
DebugLevel Level = iota
InfoLevel
WarnLevel
ErrorLevel
FatalLevel
)
// 便捷字段构造函数
func String(key, value string) Field {
return Field{Key: key, Value: value}
}
func Int(key string, value int) Field {
return Field{Key: key, Value: value}
}
func Uint(key string, value uint) Field {
return Field{Key: key, Value: value}
}
func Error(err error) Field {
return Field{Key: "error", Value: err}
}
func Any(key string, value interface{}) Field {
return Field{Key: key, Value: value}
}
Zap 日志实现
// logger/zap_logger.go - Zap 日志实现
package logger
import (
"context"
"os"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// ZapLogger Zap 日志实现
type ZapLogger struct {
logger *zap.Logger
config *Config
}
// Config 日志配置
type Config struct {
Level string `mapstructure:"level" json:"level"`
Format string `mapstructure:"format" json:"format"` // json, console
OutputPath string `mapstructure:"output_path" json:"output_path"`
ErrorPath string `mapstructure:"error_path" json:"error_path"`
MaxSize int `mapstructure:"max_size" json:"max_size"`
MaxAge int `mapstructure:"max_age" json:"max_age"`
MaxBackups int `mapstructure:"max_backups" json:"max_backups"`
Compress bool `mapstructure:"compress" json:"compress"`
}
// NewZapLogger 创建 Zap 日志实例
func NewZapLogger(config *Config) (Logger, error) {
zapConfig := zap.NewProductionConfig()
// 设置日志级别
level, err := zapcore.ParseLevel(config.Level)
if err != nil {
level = zapcore.InfoLevel
}
zapConfig.Level.SetLevel(level)
// 设置输出格式
if config.Format == "console" {
zapConfig.Encoding = "console"
zapConfig.EncoderConfig = zap.NewDevelopmentEncoderConfig()
} else {
zapConfig.Encoding = "json"
zapConfig.EncoderConfig = zap.NewProductionEncoderConfig()
}
// 设置输出路径
if config.OutputPath != "" {
zapConfig.OutputPaths = []string{config.OutputPath}
}
if config.ErrorPath != "" {
zapConfig.ErrorOutputPaths = []string{config.ErrorPath}
}
logger, err := zapConfig.Build()
if err != nil {
return nil, err
}
return &ZapLogger{
logger: logger,
config: config,
}, nil
}
// Debug 调试日志
func (l *ZapLogger) Debug(msg string, fields ...Field) {
l.logger.Debug(msg, l.convertFields(fields...)...)
}
// Info 信息日志
func (l *ZapLogger) Info(msg string, fields ...Field) {
l.logger.Info(msg, l.convertFields(fields...)...)
}
// Warn 警告日志
func (l *ZapLogger) Warn(msg string, fields ...Field) {
l.logger.Warn(msg, l.convertFields(fields...)...)
}
// Error 错误日志
func (l *ZapLogger) Error(msg string, fields ...Field) {
l.logger.Error(msg, l.convertFields(fields...)...)
}
// Fatal 致命错误日志
func (l *ZapLogger) Fatal(msg string, fields ...Field) {
l.logger.Fatal(msg, l.convertFields(fields...)...)
}
// With 添加字段
func (l *ZapLogger) With(fields ...Field) Logger {
return &ZapLogger{
logger: l.logger.With(l.convertFields(fields...)...),
config: l.config,
}
}
// WithError 添加错误字段
func (l *ZapLogger) WithError(err error) Logger {
return l.With(Error(err))
}
// convertFields 转换字段格式
func (l *ZapLogger) convertFields(fields ...Field) []zap.Field {
zapFields := make([]zap.Field, len(fields))
for i, field := range fields {
zapFields[i] = zap.Any(field.Key, field.Value)
}
return zapFields
}
// NewNoop 创建空日志实例(用于测试)
func NewNoop() Logger {
return &ZapLogger{
logger: zap.NewNop(),
config: &Config{},
}
}
🔄 响应格式
统一响应结构
// response/response.go - 统一响应结构
package response
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
)
// Response 基础响应结构
type Response struct {
Success bool `json:"success"`
Data interface{} `json:"data,omitempty"`
Message string `json:"message,omitempty"`
Error *Error `json:"error,omitempty"`
Timestamp int64 `json:"timestamp"`
RequestID string `json:"request_id,omitempty"`
}
// Error 错误信息结构
type Error struct {
Code string `json:"code"`
Message string `json:"message"`
Details string `json:"details,omitempty"`
}
// PaginatedResponse 分页响应结构
type PaginatedResponse struct {
Response
Pagination *Pagination `json:"pagination,omitempty"`
}
// Pagination 分页信息
type Pagination struct {
Page int `json:"page"`
Limit int `json:"limit"`
Total int64 `json:"total"`
TotalPages int `json:"total_pages"`
HasNext bool `json:"has_next"`
HasPrev bool `json:"has_prev"`
}
// Success 成功响应
func Success(c *gin.Context, data interface{}, message ...string) {
msg := "Success"
if len(message) > 0 {
msg = message[0]
}
response := Response{
Success: true,
Data: data,
Message: msg,
Timestamp: time.Now().Unix(),
RequestID: getRequestID(c),
}
c.JSON(http.StatusOK, response)
}
// Error 错误响应
func Error(c *gin.Context, code string, message string, details ...string) {
detail := ""
if len(details) > 0 {
detail = details[0]
}
response := Response{
Success: false,
Error: &Error{
Code: code,
Message: message,
Details: detail,
},
Timestamp: time.Now().Unix(),
RequestID: getRequestID(c),
}
c.JSON(getHTTPStatusFromCode(code), response)
}
// BadRequest 400 错误
func BadRequest(c *gin.Context, message string, details ...string) {
Error(c, "BAD_REQUEST", message, details...)
}
// Unauthorized 401 错误
func Unauthorized(c *gin.Context, message string, details ...string) {
Error(c, "UNAUTHORIZED", message, details...)
}
// Forbidden 403 错误
func Forbidden(c *gin.Context, message string, details ...string) {
Error(c, "FORBIDDEN", message, details...)
}
// NotFound 404 错误
func NotFound(c *gin.Context, message string, details ...string) {
Error(c, "NOT_FOUND", message, details...)
}
// InternalError 500 错误
func InternalError(c *gin.Context, message string, details ...string) {
Error(c, "INTERNAL_ERROR", message, details...)
}
// Paginated 分页响应
func Paginated(c *gin.Context, data interface{}, page, limit int, total int64, message ...string) {
msg := "Success"
if len(message) > 0 {
msg = message[0]
}
totalPages := int((total + int64(limit) - 1) / int64(limit))
pagination := &Pagination{
Page: page,
Limit: limit,
Total: total,
TotalPages: totalPages,
HasNext: page < totalPages,
HasPrev: page > 1,
}
response := PaginatedResponse{
Response: Response{
Success: true,
Data: data,
Message: msg,
Timestamp: time.Now().Unix(),
RequestID: getRequestID(c),
},
Pagination: pagination,
}
c.JSON(http.StatusOK, response)
}
// getRequestID 获取请求ID
func getRequestID(c *gin.Context) string {
if requestID, exists := c.Get("X-Request-ID"); exists {
if id, ok := requestID.(string); ok {
return id
}
}
return ""
}
// getHTTPStatusFromCode 根据错误码获取HTTP状态码
func getHTTPStatusFromCode(code string) int {
switch code {
case "BAD_REQUEST", "INVALID_PARAMETER", "VALIDATION_ERROR":
return http.StatusBadRequest
case "UNAUTHORIZED", "INVALID_TOKEN", "TOKEN_EXPIRED":
return http.StatusUnauthorized
case "FORBIDDEN", "PERMISSION_DENIED":
return http.StatusForbidden
case "NOT_FOUND", "USER_NOT_FOUND", "PHOTO_NOT_FOUND":
return http.StatusNotFound
case "CONFLICT", "USER_EXISTS", "DUPLICATE_KEY":
return http.StatusConflict
case "TOO_MANY_REQUESTS":
return http.StatusTooManyRequests
default:
return http.StatusInternalServerError
}
}
✅ 数据验证
自定义验证器
// validator/custom_validators.go - 自定义验证规则
package validator
import (
"regexp"
"strings"
"github.com/go-playground/validator/v10"
)
// RegisterCustomValidators 注册自定义验证器
func RegisterCustomValidators(v *validator.Validate) {
v.RegisterValidation("username", validateUsername)
v.RegisterValidation("password", validatePassword)
v.RegisterValidation("phone", validatePhone)
v.RegisterValidation("slug", validateSlug)
v.RegisterValidation("file_ext", validateFileExtension)
}
// validateUsername 验证用户名
func validateUsername(fl validator.FieldLevel) bool {
username := fl.Field().String()
// 3-50个字符,只允许字母、数字、下划线、短横线
matched, _ := regexp.MatchString(`^[a-zA-Z0-9_-]{3,50}$`, username)
return matched
}
// validatePassword 验证密码强度
func validatePassword(fl validator.FieldLevel) bool {
password := fl.Field().String()
// 至少8个字符,包含字母、数字和特殊字符
if len(password) < 8 {
return false
}
hasLetter := regexp.MustCompile(`[a-zA-Z]`).MatchString(password)
hasNumber := regexp.MustCompile(`[0-9]`).MatchString(password)
hasSpecial := regexp.MustCompile(`[!@#$%^&*()_+\-=\[\]{}|;:,.<>?]`).MatchString(password)
return hasLetter && hasNumber && hasSpecial
}
// validatePhone 验证手机号
func validatePhone(fl validator.FieldLevel) bool {
phone := fl.Field().String()
// 支持多种手机号格式
patterns := []string{
`^1[3-9]\d{9}$`, // 中国大陆
`^\+1[2-9]\d{9}$`, // 美国
`^\+44[1-9]\d{8,9}$`, // 英国
}
for _, pattern := range patterns {
if matched, _ := regexp.MatchString(pattern, phone); matched {
return true
}
}
return false
}
// validateSlug 验证 URL 友好字符串
func validateSlug(fl validator.FieldLevel) bool {
slug := fl.Field().String()
matched, _ := regexp.MatchString(`^[a-z0-9]+(?:-[a-z0-9]+)*$`, slug)
return matched
}
// validateFileExtension 验证文件扩展名
func validateFileExtension(fl validator.FieldLevel) bool {
filename := fl.Field().String()
ext := strings.ToLower(getFileExtension(filename))
// 允许的图片文件扩展名
allowedExts := []string{".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".svg"}
for _, allowedExt := range allowedExts {
if ext == allowedExt {
return true
}
}
return false
}
// getFileExtension 获取文件扩展名
func getFileExtension(filename string) string {
lastDot := strings.LastIndex(filename, ".")
if lastDot == -1 {
return ""
}
return filename[lastDot:]
}
// ValidationError 验证错误结构
type ValidationError struct {
Field string `json:"field"`
Value string `json:"value"`
Tag string `json:"tag"`
Message string `json:"message"`
}
// FormatValidationErrors 格式化验证错误
func FormatValidationErrors(err error) []ValidationError {
var errors []ValidationError
if validationErrors, ok := err.(validator.ValidationErrors); ok {
for _, e := range validationErrors {
errors = append(errors, ValidationError{
Field: e.Field(),
Value: e.Value().(string),
Tag: e.Tag(),
Message: getErrorMessage(e),
})
}
}
return errors
}
// getErrorMessage 获取错误消息
func getErrorMessage(e validator.FieldError) string {
switch e.Tag() {
case "required":
return e.Field() + " is required"
case "email":
return e.Field() + " must be a valid email"
case "min":
return e.Field() + " must be at least " + e.Param() + " characters"
case "max":
return e.Field() + " must be at most " + e.Param() + " characters"
case "username":
return e.Field() + " must be 3-50 characters and contain only letters, numbers, underscores, and hyphens"
case "password":
return e.Field() + " must be at least 8 characters and contain letters, numbers, and special characters"
case "phone":
return e.Field() + " must be a valid phone number"
case "slug":
return e.Field() + " must be a valid URL slug"
case "file_ext":
return e.Field() + " must have a valid image file extension"
default:
return e.Field() + " is invalid"
}
}
🛠️ 通用工具
字符串工具
// utils/string.go - 字符串工具
package utils
import (
"crypto/rand"
"math/big"
"regexp"
"strings"
"unicode"
)
// GenerateRandomString 生成随机字符串
func GenerateRandomString(length int) (string, error) {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
result := make([]byte, length)
for i := range result {
index, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
if err != nil {
return "", err
}
result[i] = charset[index.Int64()]
}
return string(result), nil
}
// ToSnakeCase 转换为蛇形命名
func ToSnakeCase(str string) string {
var result []rune
for i, r := range str {
if unicode.IsUpper(r) {
if i > 0 {
result = append(result, '_')
}
result = append(result, unicode.ToLower(r))
} else {
result = append(result, r)
}
}
return string(result)
}
// ToCamelCase 转换为驼峰命名
func ToCamelCase(str string) string {
parts := strings.Split(str, "_")
for i := 1; i < len(parts); i++ {
if len(parts[i]) > 0 {
parts[i] = strings.ToUpper(parts[i][:1]) + parts[i][1:]
}
}
return strings.Join(parts, "")
}
// Truncate 截断字符串
func Truncate(str string, length int) string {
if len(str) <= length {
return str
}
return str[:length] + "..."
}
// IsValidEmail 验证邮箱格式
func IsValidEmail(email string) bool {
pattern := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`
matched, _ := regexp.MatchString(pattern, email)
return matched
}
// SanitizeFilename 清理文件名
func SanitizeFilename(filename string) string {
// 移除或替换不安全的字符
reg := regexp.MustCompile(`[<>:"/\\|?*]`)
return reg.ReplaceAllString(filename, "_")
}
// Contains 检查字符串切片是否包含指定字符串
func Contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}
加密工具
// utils/crypto.go - 加密工具
package utils
import (
"crypto/md5"
"crypto/sha256"
"encoding/hex"
"golang.org/x/crypto/bcrypt"
)
// HashPassword 加密密码
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(bytes), nil
}
// CheckPassword 验证密码
func CheckPassword(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
// MD5Hash 计算MD5哈希
func MD5Hash(data string) string {
hash := md5.Sum([]byte(data))
return hex.EncodeToString(hash[:])
}
// SHA256Hash 计算SHA256哈希
func SHA256Hash(data string) string {
hash := sha256.Sum256([]byte(data))
return hex.EncodeToString(hash[:])
}
🔗 中间件
CORS 中间件
// middleware/cors.go - CORS 中间件
package middleware
import (
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
// CORS 创建CORS中间件
func CORS() gin.HandlerFunc {
config := cors.DefaultConfig()
config.AllowOrigins = []string{"*"}
config.AllowMethods = []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}
config.AllowHeaders = []string{"Origin", "Content-Type", "Accept", "Authorization", "X-Request-ID"}
config.ExposeHeaders = []string{"X-Request-ID"}
config.AllowCredentials = true
return cors.New(config)
}
请求ID中间件
// middleware/request_id.go - 请求ID中间件
package middleware
import (
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// RequestID 请求ID中间件
func RequestID() gin.HandlerFunc {
return func(c *gin.Context) {
requestID := c.GetHeader("X-Request-ID")
if requestID == "" {
requestID = uuid.New().String()
}
c.Set("X-Request-ID", requestID)
c.Header("X-Request-ID", requestID)
c.Next()
}
}
💡 最佳实践
包设计原则
- 单一职责: 每个包只负责一个明确的功能
- 接口导向: 优先定义接口,便于测试和替换
- 零依赖: 包应尽量减少外部依赖
- 文档完善: 提供清晰的使用文档和示例
- 向后兼容: 保持API的向后兼容性
代码质量
- 测试覆盖: 为所有公共函数编写测试
- 错误处理: 合理处理和传播错误
- 性能考虑: 关注内存分配和性能影响
- 线程安全: 确保并发使用的安全性
- 资源管理: 及时释放资源
使用建议
- 导入路径: 使用清晰的导入路径
- 命名规范: 遵循 Go 语言命名约定
- 版本管理: 使用语义化版本管理
- 依赖管理: 合理管理第三方依赖
- 配置化: 支持配置化使用
本模块提供了应用的基础设施和工具支持,确保代码的可复用性和一致性。