9.4 KiB
9.4 KiB
自包含 Common Runtime
当目标项目没有等价的本地基础能力时,使用这些模板。不要依赖外部 common 仓库或某台机器上的固定路径。应用项目优先使用 internal/common;只有当模块确实需要向其他模块暴露这些辅助函数时,才使用 pkg/common。
包结构
internal/common/
app_error.go
codes.go
response.go
time.go
logging.go
request_context.go
错误码与 AppError
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)
}
统一响应
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
}
}
请求上下文
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 时间
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)
}
结构化日志
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
}