阶段2(上):后端专题 Go/Gin/PostgreSQL/MySQL/Redis/MQ/gRPC 的课程、企业场景、面试题和可运行示例

This commit is contained in:
Shikui Wang committed 2026-10-23 15:24:57 +08:00
1 parent 0bae9916a9
commit 5eb67c5d08
65 files changed
+6457 -77

No files matched your search

@@ -0,0 +1,185 @@
// Package unifiedapi 演示企业后台接口规范:统一响应、业务错误码、请求 ID、参数校验中文提示、panic 兜底。
package unifiedapi
import (
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"log/slog"
"net/http"
"reflect"
"strings"
"sync"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10"
)
// AppError 业务错误:HTTP 状态码 + 业务错误码 + 给用户看的提示
type AppError struct {
Status int
Code int
Message string
}
func (e *AppError) Error() string { return e.Message }
var (
ErrNotFound = &AppError{http.StatusNotFound, 40400, "资源不存在"}
ErrInternal = &AppError{http.StatusInternalServerError, 50000, "服务器开小差了,请稍后再试"}
)
func Invalid(msg string) *AppError { return &AppError{http.StatusBadRequest, 40000, msg} }
// Body 统一响应格式
type Body struct {
Code int `json:"code"`
Message string `json:"message"`
Data any `json:"data"`
RequestID string `json:"request_id"`
}
func OK(c *gin.Context, status int, data any) {
c.JSON(status, Body{Code: 0, Message: "ok", Data: data, RequestID: c.GetString("requestID")})
}
func writeErr(c *gin.Context, e *AppError) {
c.AbortWithStatusJSON(e.Status, Body{Code: e.Code, Message: e.Message, RequestID: c.GetString("requestID")})
}
// RequestID 沿用客户端传来的 X-Request-ID,没有就生成一个,并写回响应头
func RequestID() gin.HandlerFunc {
return func(c *gin.Context) {
id := c.GetHeader("X-Request-ID")
if id == "" || len(id) > 64 {
b := make([]byte, 8)
_, _ = rand.Read(b)
id = hex.EncodeToString(b)
}
c.Set("requestID", id)
c.Header("X-Request-ID", id)
c.Next()
}
}
// Errors 统一把处理函数里 c.Error(err) 记录的错误和 panic 转成统一响应;内部错误不把细节暴露给用户
func Errors(log *slog.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if r := recover(); r != nil {
log.Error("panic", "request_id", c.GetString("requestID"), "panic", r)
writeErr(c, ErrInternal)
}
}()
c.Next()
if len(c.Errors) == 0 || c.Writer.Written() {
return
}
err := c.Errors.Last().Err
var ae *AppError
if errors.As(err, &ae) {
writeErr(c, ae)
return
}
log.Error("internal error", "request_id", c.GetString("requestID"), "err", err)
writeErr(c, ErrInternal)
}
}
var setupOnce sync.Once
// useJSONFieldNames 让校验错误里显示 JSON 字段名(title)而不是结构体字段名(Title)
func useJSONFieldNames() {
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
v.RegisterTagNameFunc(func(f reflect.StructField) string {
name := strings.Split(f.Tag.Get("json"), ",")[0]
if name == "" || name == "-" {
return f.Name
}
return name
})
}
}
// Bind 解析并校验 JSON,失败时记录一个用户能看懂的错误
func Bind(c *gin.Context, req any) bool {
err := c.ShouldBindJSON(req)
if err == nil {
return true
}
var ve validator.ValidationErrors
if errors.As(err, &ve) {
msgs := make([]string, 0, len(ve))
for _, fe := range ve {
switch fe.Tag() {
case "required":
msgs = append(msgs, fmt.Sprintf("%s 不能为空", fe.Field()))
case "max":
msgs = append(msgs, fmt.Sprintf("%s 长度不能超过 %s", fe.Field(), fe.Param()))
case "oneof":
msgs = append(msgs, fmt.Sprintf("%s 只能是 %s 之一", fe.Field(), fe.Param()))
default:
msgs = append(msgs, fmt.Sprintf("%s 不合法", fe.Field()))
}
}
_ = c.Error(Invalid(strings.Join(msgs, ";")))
} else {
_ = c.Error(Invalid("请求体格式错误"))
}
return false
}
type Task struct {
ID int `json:"id"`
Title string `json:"title"`
Priority string `json:"priority"`
}
type CreateTask struct {
Title string `json:"title" binding:"required,max=50"`
Priority string `json:"priority" binding:"omitempty,oneof=low normal high"`
}
// New 组装路由
func New(log *slog.Logger) *gin.Engine {
setupOnce.Do(useJSONFieldNames)
var mu sync.Mutex
tasks := map[int]Task{}
next := 1
r := gin.New()
r.Use(RequestID(), Errors(log))
api := r.Group("/api")
api.POST("/tasks", func(c *gin.Context) {
var req CreateTask
if !Bind(c, &req) {
return
}
mu.Lock()
t := Task{ID: next, Title: req.Title, Priority: req.Priority}
tasks[t.ID] = t
next++
mu.Unlock()
OK(c, http.StatusCreated, t)
})
api.GET("/tasks/:id", func(c *gin.Context) {
var id int
if _, err := fmt.Sscan(c.Param("id"), &id); err != nil {
_ = c.Error(Invalid("id 必须是整数"))
return
}
mu.Lock()
t, ok := tasks[id]
mu.Unlock()
if !ok {
_ = c.Error(ErrNotFound)
return
}
OK(c, http.StatusOK, t)
})
api.GET("/boom", func(c *gin.Context) { panic("数据库连接串 postgres://secret 泄漏测试") })
api.GET("/db-error", func(c *gin.Context) { _ = c.Error(errors.New("pq: relation \"tasks\" does not exist")) })
return r
}