# 自包含 Common Runtime 当目标项目没有等价的本地基础能力时,使用这些模板。不要依赖外部 common 仓库或某台机器上的固定路径。应用项目优先使用 `internal/common`;只有当模块确实需要向其他模块暴露这些辅助函数时,才使用 `pkg/common`。 ## 包结构 ```text internal/common/ app_error.go codes.go response.go time.go logging.go request_context.go ``` ## 错误码与 AppError ```go package common import ( "errors" "fmt" ) const ( CodeSuccess = 0 CodeParamError = 1001 CodeValidationFail = 1002 CodeUnauthorized = 1003 CodeForbidden = 1004 CodeNotFound = 1005 CodeTimeout = 1006 CodeServerError = 1007 CodeDuplicate = 1008 CodeOperationFail = 1009 CodeBusinessError = 2001 CodeDataNotReady = 2002 CodeStatusInvalid = 2003 CodeDependencyError = 2004 CodeExternalAPIError = 2005 CodeResourceLocked = 2006 CodeQuotaExceeded = 2007 CodeConcurrentConflict = 2008 ) var CodeMessage = map[int]string{ CodeSuccess: "success", CodeParamError: "参数错误", CodeValidationFail: "数据验证失败", CodeUnauthorized: "未授权,请先登录", CodeForbidden: "权限不足,禁止访问", CodeNotFound: "资源不存在", CodeTimeout: "请求超时", CodeServerError: "服务器内部错误", CodeDuplicate: "数据重复", CodeOperationFail: "操作失败", CodeBusinessError: "业务处理失败", CodeDataNotReady: "数据未就绪", CodeStatusInvalid: "状态不合法", CodeDependencyError: "依赖服务错误", CodeExternalAPIError: "外部服务调用失败", CodeResourceLocked: "资源被锁定", CodeQuotaExceeded: "配额超限", CodeConcurrentConflict: "并发冲突", } // AppError 是 service 层向 handler 层暴露的稳定错误契约。 // DAO 返回底层错误,service 负责转换为 AppError。 type AppError struct { Code int Message string Cause error Fields map[string]any } func NewAppError(code int, message string) *AppError { if message == "" { message = CodeMessage[code] } return &AppError{Code: code, Message: message} } func WrapAppError(code int, message string, cause error) *AppError { appErr := NewAppError(code, message) appErr.Cause = cause return appErr } func (e *AppError) Error() string { if e == nil { return "" } if e.Cause == nil { return fmt.Sprintf("%d:%s", e.Code, e.Message) } return fmt.Sprintf("%d:%s: %v", e.Code, e.Message, e.Cause) } func (e *AppError) Unwrap() error { if e == nil { return nil } return e.Cause } func AsAppError(err error) (*AppError, bool) { var appErr *AppError if errors.As(err, &appErr) { return appErr, true } return nil, false } func ToAppError(err error) *AppError { if err == nil { return nil } if appErr, ok := AsAppError(err); ok { return appErr } return WrapAppError(CodeServerError, CodeMessage[CodeServerError], err) } ``` ## 统一响应 ```go package common import ( "net/http" "github.com/gin-gonic/gin" ) // Response 是所有 API 的统一响应体。失败时 Data 必须为 nil。 type Response struct { Code int `json:"code"` Message string `json:"message"` Data any `json:"data"` Timestamp string `json:"timestamp"` RequestID string `json:"request_id"` } type PageResponse struct { List any `json:"list"` Total int64 `json:"total"` Page int `json:"page"` PageSize int `json:"page_size"` } func ResponseSuccess(c *gin.Context, data any) { respond(c, http.StatusOK, CodeSuccess, CodeMessage[CodeSuccess], data) } func ResponseSuccessWithMessage(c *gin.Context, data any, message string) { respond(c, http.StatusOK, CodeSuccess, message, data) } func ResponseError(c *gin.Context, code int, message string) { respond(c, httpStatusByCode(code), code, message, nil) } func ResponseAppError(c *gin.Context, err error) { appErr := ToAppError(err) Error(c.Request.Context(), appErr.Message, "code", appErr.Code, "error", appErr.Cause) respond(c, httpStatusByCode(appErr.Code), appErr.Code, appErr.Message, nil) } func respond(c *gin.Context, httpStatus int, code int, message string, data any) { if message == "" { message = CodeMessage[code] } c.JSON(httpStatus, Response{ Code: code, Message: message, Data: data, Timestamp: FormatTime(Now()), RequestID: RequestIDFromGin(c), }) } func httpStatusByCode(code int) int { switch code { case CodeUnauthorized: return http.StatusUnauthorized case CodeForbidden: return http.StatusForbidden case CodeParamError, CodeValidationFail: return http.StatusBadRequest case CodeNotFound: return http.StatusNotFound default: return http.StatusOK } } ``` ## 请求上下文 ```go package common import ( "context" "github.com/gin-gonic/gin" ) type contextKey string const ( ContextKeyRequestID contextKey = "request_id" ContextKeyUserID contextKey = "user_id" ContextKeyUsername contextKey = "username" ContextKeyRole contextKey = "role" ) func WithRequestID(ctx context.Context, requestID string) context.Context { return context.WithValue(ctx, ContextKeyRequestID, requestID) } func RequestIDFromContext(ctx context.Context) string { if value, ok := ctx.Value(ContextKeyRequestID).(string); ok { return value } return "" } func RequestIDFromGin(c *gin.Context) string { if value, exists := c.Get(string(ContextKeyRequestID)); exists { if requestID, ok := value.(string); ok { return requestID } } return RequestIDFromContext(c.Request.Context()) } func UserIDFromContext(ctx context.Context) int64 { if value, ok := ctx.Value(ContextKeyUserID).(int64); ok { return value } return 0 } ``` ## Asia/Shanghai 时间 ```go package common import "time" const TimeFormat = time.RFC3339 var shanghaiLocation = mustLoadShanghaiLocation() func mustLoadShanghaiLocation() *time.Location { location, err := time.LoadLocation("Asia/Shanghai") if err != nil { return time.FixedZone("Asia/Shanghai", 8*60*60) } return location } // Now 返回东八区当前时间。只有 common/time.go 允许直接调用 time.Now。 func Now() time.Time { return time.Now().In(shanghaiLocation) } func FormatTime(t time.Time) string { return t.In(shanghaiLocation).Format(TimeFormat) } func ParseTime(value string) (time.Time, error) { parsed, err := time.Parse(TimeFormat, value) if err != nil { return time.Time{}, err } return parsed.In(shanghaiLocation), nil } func StartOfDay(t time.Time) time.Time { local := t.In(shanghaiLocation) return time.Date(local.Year(), local.Month(), local.Day(), 0, 0, 0, 0, shanghaiLocation) } func EndOfDay(t time.Time) time.Time { return StartOfDay(t).Add(24*time.Hour - time.Nanosecond) } ``` ## 结构化日志 ```go package common import ( "context" "fmt" "log/slog" "os" "strings" ) var logger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})) func InitLogger(debug bool) { level := slog.LevelInfo if debug { level = slog.LevelDebug } logger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: level})) } func Debug(ctx context.Context, message string, fields ...any) { logger.DebugContext(ctx, message, appendContextFields(ctx, sanitizeFields(fields)...)...) } func Info(ctx context.Context, message string, fields ...any) { logger.InfoContext(ctx, message, appendContextFields(ctx, sanitizeFields(fields)...)...) } func Warn(ctx context.Context, message string, fields ...any) { logger.WarnContext(ctx, message, appendContextFields(ctx, sanitizeFields(fields)...)...) } func Error(ctx context.Context, message string, fields ...any) { logger.ErrorContext(ctx, message, appendContextFields(ctx, sanitizeFields(fields)...)...) } func appendContextFields(ctx context.Context, fields ...any) []any { if requestID := RequestIDFromContext(ctx); requestID != "" { fields = append(fields, "request_id", requestID) } if userID := UserIDFromContext(ctx); userID > 0 { fields = append(fields, "user_id", userID) } return fields } func sanitizeFields(fields []any) []any { sanitized := make([]any, 0, len(fields)) for i := 0; i < len(fields); i += 2 { key := fields[i] if i+1 >= len(fields) { sanitized = append(sanitized, key) break } value := fields[i+1] if isSensitiveKey(key) { value = "***REDACTED***" } sanitized = append(sanitized, key, value) } return sanitized } func isSensitiveKey(key any) bool { name := strings.ToLower(strings.TrimSpace(strings.ReplaceAll(fmt.Sprint(key), "_", ""))) sensitive := []string{"password", "passwd", "token", "authorization", "secret", "privatekey", "apikey", "cookie"} for _, item := range sensitive { if strings.Contains(name, item) { return true } } return false } ```