
简介Go语言后端脚手架资源面向有一定Gin基础、希望快速搭建整合MySQL与Redis的Web服务的中级开发者。资源演示了Gin框架与MySQL、Redis的完整整合流程涵盖数据访问层、Redis缓存服务、路由分组、参数校验及日志模块等通用设计可直接作为项目起步模板或学习参考。压缩包共25个文件以Go源码为主共13个.go文件辅以go.mod依赖配置、YAML配置、XML项目文件及少量日志、gitignore等整体仅44KB轻量易读适合快速梳理工程结构。已有1351人学习下载。从内容预览看包含main.go、routes、controller、service、dao、config等分层目录并附带snowflake雪花算法、微信支付工具类、zap日志封装等实用组件便于理解企业级项目组织方式。通过对照学习可掌握Gin中间件使用、数据库连接池管理、Redis缓存策略与依赖注入思路减少后端初始化踩坑成本。1. 为什么我最终决定维护一套自己的Go脚手架先交代一下背景。我在团队里见过太多“从零搭项目”的场面每次新服务启动大家做的第一件事不是写业务而是把上一套项目的配置搬过来、把数据库连接复制一份、Redis客户端再粘一次。东西是能跑但每份代码里都残留着上个项目的“历史包袱”连接参数写死在代码里Redis的key命名跟着心情走启动顺序完全看运气。这样的项目堆得越多维护成本就越失控。后来我开始整理一套基于Go语言的通用脚手架核心组合是Gin框架、MySQL和Redis——这三样几乎覆盖了绝大多数后端服务的全部基础需求。这个脚手架现在已经是团队内部新建项目的默认起点也是我个人写技术演示、做外包交付、带新人上手时的标准模板。它的价值说白了就一句话把“每个项目都要重复做的那70%”固定下来让你只需要关心剩下30%的业务逻辑。这篇文章会把这套脚手架从目录结构、配置加载、数据库接入到Redis整合的完整链路拆开讲清楚。里面会有完整的代码块、可复用的配置模板以及我在实际迭代过程中踩过的坑。不管你是刚学Go想搭一套自己的工具集还是工作中需要快速起一个后端服务这篇文章都值得对照着抄一遍。2. 选型不是拍脑袋Gin、GORM、go-redis 为什么能凑成一套很多初学者问我为什么脚手架选了Gin而不是Echo或者Fiber为什么ORM选了GORM而不是sqlx或者直接裸写SQL。我的回答是脚手架不是炫技场它的唯一使命是稳定、通用、好替换。选型要考虑的不是“哪个框架更酷”而是“哪个组合能让团队里经验最浅的人也能快速上手同时不会在未来成为瓶颈”。2.1 Web框架Gin赢在生态和心智Gin是目前Go社区使用率最高的Web框架之一它的中间件生态极其丰富Logger、Recovery、CORS、JWT这些常见需求都有成熟实现。和Echo相比Gin的文档和社区案例要多得多遇到问题搜索一下基本就有答案。和Fiber相比Gin基于net/http标准库没有fasthttp那种对象池和零拷贝的特殊内存模型踩坑概率更低。对于脚手架项目来说易上手、生态全、坑少这三点就是全部理由。package main import ( github.com/gin-gonic/gin ) func main() { r : gin.New() r.Use(gin.Logger(), gin.Recovery()) r.GET(/health, func(c *gin.Context) { c.JSON(200, gin.H{status: ok}) }) r.Run(:8080) }这段代码就是Gin脚手架的最小单位。gin.New()和gin.Default()的区别在于Default自带Logger和Recovery中间件New是空白实例。我习惯用New然后自己挂中间件这样在测试环境可以直接去掉Logger避免日志刷屏在线上再按需开启。2.2 ORMGORM适合作为通用默认值GORM是Go里最流行的ORM库支持MySQL、PostgreSQL、SQLite等多种数据库提供了自动迁移、Hook、关联查询、事务、软删除等开箱即用的能力。选择GORM有一个很现实的原因招人容易。大部分做Go业务的开发都接触过GORM新人接手项目时学习的不是“这套ORM怎么用”而是“业务表怎么建”。两次阅读成本完全不是一个量级。import ( gorm.io/driver/mysql gorm.io/gorm ) func InitDB(dsn string) (*gorm.DB, error) { db, err : gorm.Open(mysql.Open(dsn), gorm.Config{ SkipDefaultTransaction: true, PrepareStmt: true, }) if err ! nil { return nil, err } sqlDB, _ : db.DB() sqlDB.SetMaxIdleConns(10) sqlDB.SetMaxOpenConns(100) sqlDB.SetConnMaxLifetime(time.Hour) return db, nil }注意gorm.Config里两个参数SkipDefaultTransaction表示不开启默认事务——单条SQL语句本身就有原子性GORM默认给每次写操作包一层事务反而会带来额外开销业务需要多表更新的地方再显式用TransactionPrepareStmt开启预编译缓存重复执行的SQL会复用statement减少MySQL服务端的解析压力。2.3 Redis客户端go-redis是社区的事实标准Redis客户端可选的不多go-redis基本是唯一值得考虑的。它支持Redis 6/7的几乎所有新特性包括Redis Cluster、Pipeline、事务、发布订阅、分布式锁等。相比老牌的redigogo-redis的API设计更现代并且对连接池、超时控制有更细粒度的配置。import ( github.com/redis/go-redis/v9 context time ) func InitRedis(addr, password string, db int) *redis.Client { rdb : redis.NewClient(redis.Options{ Addr: addr, Password: password, DB: db, DialTimeout: 5 * time.Second, ReadTimeout: 3 * time.Second, WriteTimeout: 3 * time.Second, PoolSize: 50, MinIdleConns: 10, }) ctx, cancel : context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err : rdb.Ping(ctx).Err(); err ! nil { panic(redis connect failed: err.Error()) } return rdb }这里有个新手容易忽略的地方DialTimeout、ReadTimeout、WriteTimeout必须区分配置不要一个值走天下。很多线上卡顿问题的根源就是读超时和写超时设置不合理——比如某个操作只是写入慢结果读操作也被同一个Timeout卡死整个服务跟着雪崩。2.4 选型组合的整体评判标准如果你问我这套组合是不是最优解我诚实的回答是它是在“可维护性”、“学习成本”和“功能完备度”之间取得平衡的选择。选型的核心标准不是“单点最强”而是“组合不冲突”。Gin管HTTP、GORM管MySQL、go-redis管Redis三者职责清晰没有重叠边界配合Go的context机制可以做到统一的超时控制和链路取消。这种组合对脚手架再合适不过。3. 目录结构一打开就能上手的布局一个脚手架好不好用第一眼就看目录。如果打开项目要翻半天才找到main.go或者在config、common、utils这类抽象目录里绕圈子那就算代码写得再漂亮也会被团队嫌弃。我最终定的目录结构是下面这样的简单、直接、按职责划分。my-project/ ├── api/ │ ├── handler/ # HTTP处理器路由绑定的具体逻辑 │ ├── middleware/ # 中间件鉴权、日志、CORS等 │ └── route/ # 路由注册将URL和handler绑定 ├── config/ │ ├── config.go # 配置加载逻辑 │ └── settings/ # 配置结构体定义 ├── internal/ │ ├── cache/ # Redis缓存操作封装 │ ├── database/ # 数据库连接与自动迁移 │ ├── model/ # 数据模型数据库表映射 │ ├── repository/ # 数据访问层数据库读写 │ └── service/ # 业务逻辑层 ├── pkg/ │ ├── response/ # 统一API响应封装 │ └── logger/ # 日志初始化 ├── config.yaml # 本地配置文件 ├── go.mod └── main.go3.1 明确的分层规则在API层handler只做HTTP层面的解析和响应不写任何业务规则service层承载核心业务逻辑repository层只做数据库访问不在service里看到SQLmodel层定义表结构由GORM自动迁移。cache层独立存在所有Redis操作都通过这一层进出这样以后把单机Redis换成Cluster只需要改动cache包的内部实现业务代码完全不用动。这套分层是我经过几个项目的历练才定下来的。早期我为了“省事”把数据库操作直接写在handler里后期业务复杂起来发现根本没法测试每个接口都依赖数据库状态mock成本高到怀疑人生。后来改成repository模式service层可以注入mock数据源单测才好写起来。3.2 入口文件main.go要薄main.go的唯一职责是“组装”加载配置、初始化数据库、初始化Redis、注册路由、启动服务。它不包含任何业务逻辑所有初始化工作都拆到对应的包里去。package main import ( my-project/config my-project/internal/cache my-project/internal/database my-project/api/route go.uber.org/zap ) func main() { cfg : config.Load(config.yaml) logger : logger.Init(cfg.LogLevel) db : database.InitMySQL(cfg.MySQL) rdb : cache.InitRedis(cfg.Redis) r : route.SetupRouter(db, rdb, logger) r.Run(cfg.App.Port) }这个文件的理想状态是“三个月不修改”每次新起项目只需要复制完改配置即可。如果有新的中间件或初始化组件加一行调用不要在这里写额外逻辑。跑通之后你会发现脚手架的核心价值不是快速启动而是“大半年之后你还能记得住代码在哪”。4. 配置管理从yaml到运行时读取的完整链路脚手架里配置管理是最容易被人忽略、但拆起来最痛苦的模块。很多项目的配置文件五花八门有的用json有的用env有的直接硬编码在代码里。我这边统一用yaml因为yaml支持注释和嵌套结构拿来写数据库连接、Redis、日志等级这类配置比json直观得多。4.1 配置结构体定义app: name: my-project port: :8080 mode: debug mysql: host: 127.0.0.1 port: 3306 user: root password: 123456 dbname: test_db max_idle_conns: 10 max_open_conns: 100 conn_max_lifetime: 3600 redis: addr: 127.0.0.1:6379 password: db: 0 pool_size: 50 min_idle_conns: 10对应的Go结构体package settings type Config struct { App AppConfig yaml:app MySQL MySQLConfig yaml:mysql Redis RedisConfig yaml:redis } type AppConfig struct { Name string yaml:name Port string yaml:port Mode string yaml:mode }加载逻辑用Viper实现这是Go生态里最主流的配置库。package config import ( github.com/spf13/viper my-project/config/settings ) func Load(path string) *settings.Config { viper.SetConfigFile(path) viper.AutomaticEnv() viper.SetEnvPrefix(APP) viper.SetDefault(app.mode, debug) if err : viper.ReadInConfig(); err ! nil { panic(read config failed: err.Error()) } cfg : settings.Config{} if err : viper.Unmarshal(cfg); err ! nil { panic(unmarshal config failed: err.Error()) } return cfg }4.2 环境变量覆盖的坑上面代码里最关键的一行是viper.SetEnvPrefix(APP)和viper.AutomaticEnv()它实现了“yaml里写默认值环境变量覆盖特定项”的效果。比如线上部署时不想把数据库密码写在配置文件里可以直接设置环境变量APP_MYSQL_PASSWORDxxxxViper会自动把它映射到mysql.password这个配置项上。这里有个隐藏的坑Viper的环境变量映射默认按点号分隔比如配置项mysql.password对应环境变量APP_MYSQL_PASSWORD这个规则在viper.Unmarshal之前是生效的但如果你用viper.GetString(mysql.password)单独取值却不对应环境变量必须用viper.BindEnv显式绑定。在使用Viper时建议把读取配置整体封装到一个包内通过结构体传递避免到处直接调用viper.GetXX()否则环境变量覆盖很容易在最意想不到的地方失效。4.3 配置校验的土办法我对脚手架配置还有一个习惯启动时强制校验关键字段。比如MySQL的host和dbname不能为空Redis的addr不能为空。这部分我用的是最土的方式——直接判断结构体字段不为空就往下走为空就panic附上“请检查config.yaml”的提示。虽然不优雅但比引入一整套validator要省事得多脚手架要的是快速定位问题不是完备的配置校验机制。5. 数据库接入与生命周期MySQL连接池的“活法”MySQL接入是脚手架的核心环节。很多项目在数据库连接上出的问题根本不在SQL写得怎样而在连接池的配置和生命周期管理上。连接池的参数不是越大越好它的设置直接决定了数据库在高并发下的表现。5.1 GORM接入完整代码package database import ( fmt time gorm.io/driver/mysql gorm.io/gorm my-project/config/settings ) func InitMySQL(cfg settings.MySQLConfig) *gorm.DB { dsn : fmt.Sprintf(%s:%stcp(%s:%d)/%s?charsetutf8mb4parseTimeTruelocLocal, cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.DBName) db, err : gorm.Open(mysql.Open(dsn), gorm.Config{ SkipDefaultTransaction: true, PrepareStmt: true, }) if err ! nil { panic(mysql connect failed: err.Error()) } sqlDB, err : db.DB() if err ! nil { panic(get sql.DB failed: err.Error()) } sqlDB.SetMaxIdleConns(cfg.MaxIdleConns) sqlDB.SetMaxOpenConns(cfg.MaxOpenConns) sqlDB.SetConnMaxLifetime(time.Duration(cfg.ConnMaxLifetime) * time.Second) return db }这段代码里charsetutf8mb4值得单独强调。很多从老项目迁过来的库还在用utf8MySQL的utf8其实不是真正的全字符集它最多存3字节遇到emoji或生僻字直接报错。utf8mb4才是完整的UTF-8。另外parseTimeTrue必须开否则GORM读出来的DATETIME字段是字符串后续处理非常尴尬。5.2 连接池参数背后的逻辑MaxIdleConns空闲连接数建议保持10到20。开的太多浪费数据库内存太少则每次请求都重新建连接延迟会增加。MaxOpenConns最大连接数这个是整个数据库的“限流器”。我见过不少人直接设成0表示不限然后线上数据库被打爆。100以内是一个比较稳妥的起步压测后再根据实际情况调整。ConnMaxLifetime连接最大存活时间建议小于MySQL的wait_timeout值。如果MySQL连接8小时被服务端断开而本地连接池还认为它活着就会出现诡异的“间歇性连接失败”但其实你查下来SQL没问题、网络也没问题。设置ConnMaxLifetime可以让GORM自动在超时后重建连接。早期的踩坑回忆我维护过一个老服务MySQL连接数一直涨DBA查了发现会话里大量Sleep状态最后定位到就是连接池没有设置MaxLifetime连接被MySQL侧回收后客户端不知道还在复用旧连接。加上ConnMaxLifetime之后这个问题彻底消失。脚手架里加上这个配置就是为了让后人不再踩同样的坑。5.3 自动迁移的使用边界GORM的AutoMigrate是脚手架里很有用的能力启动时自动根据model创建或更新表结构。它适合开发环境快速同步也适合表格结构不复杂的内部系统。但要注意它的边界AutoMigrate只会增加列不会删除列也不会处理大数据量下的索引创建。生产环境我建议关闭自动迁移改用SQL迁移脚本比如golang-migrate管理否则一旦表数据量大起来启动时自动建索引会锁表线上会有秒级甚至分钟级的阻塞。6. Redis整合落地连接、锁与缓存治理Redis在Go后端里的定位不只是“缓存”它同时还要承担分布式锁、限流、会话存储等功能。脚手架层面要做的是把这些基础能力封装好业务调用时像用工具函数一样简单而不是每个业务方自己去操作Redis连接。6.1 连接池与超时配置go-redis的连接池配置比GORM简单但有几个参数仍然值得注意。PoolSize默认是10 * runtime.GOMAXPROCS如果是8核机器就是80个连接这对于大多数场景其实偏高。对于一个小服务池子设50已经足够过大反而会占用Redis服务端资源。MinIdleConns是保持的最小空闲连接数建议设10左右避免突发流量下连接池从0开始创建连接导致首波请求的高延迟。这个和GORM的MaxIdleConns是同一个逻辑。package cache import ( context time github.com/redis/go-redis/v9 my-project/config/settings ) func InitRedis(cfg settings.RedisConfig) *redis.Client { rdb : redis.NewClient(redis.Options{ Addr: cfg.Addr, Password: cfg.Password, DB: cfg.DB, DialTimeout: 5 * time.Second, ReadTimeout: 3 * time.Second, WriteTimeout: 3 * time.Second, PoolSize: cfg.PoolSize, MinIdleConns: cfg.MinIdleConns, }) ctx, cancel : context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err : rdb.Ping(ctx).Err(); err ! nil { panic(redis ping failed: err.Error()) } return rdb }6.2 缓存操作封装读缓存与回源Redis最常用的场景是缓存穿透和缓存击穿。如果直接在每个handler里写GET缓存、查库、写缓存三次操作的逻辑会散落得到处都是。我的做法是在cache包里封装一个辅助函数把“读缓存 - 未命中 - 回源 - 写缓存”的链路收拢业务方只需要往里面塞一个回调函数。package cache import ( context encoding/json time github.com/redis/go-redis/v9 ) // GetOrSet 从缓存读取未命中时调用getData回源并写入缓存 func GetOrSet[T any](ctx context.Context, rdb *redis.Client, key string, ttl time.Duration, getData func() (T, error)) (T, error) { var zero T val, err : rdb.Get(ctx, key).Bytes() if err nil { var result T if e : json.Unmarshal(val, result); e nil { return result, nil } } data, err : getData() if err ! nil { return zero, err } bytes, _ : json.Marshal(data) if e : rdb.Set(ctx, key, bytes, ttl).Err(); e ! nil { // 缓存写入失败不阻断业务 return data, nil } return data, nil }GetOrSet这个函数的泛型设计在Go 1.18之后才可用1.27自然没问题。如果还在用老版本改成interface{}然后断言即可。使用方看起来长这样type UserProfile struct { ID int64 json:id Name string json:name } func GetUser(ctx context.Context, id int64) (*UserProfile, error) { key : fmt.Sprintf(user:profile:%d, id) return cache.GetOrSet(ctx, rdb, key, 30*time.Minute, func() (*UserProfile, error) { return repo.FindUserByID(ctx, id) }) }业务方完全不需要感知Redis操作细节只需要提供回源函数。这里还有一个防御点单条数据的缓存空值也要设计进去也就是缓存穿透的常见解法——查库结果为空时在Redis里写一个短TTL的空占位避免大量请求直接打到数据库。6.3 分布式锁的正确写法Redis实现分布式锁有很多坑网上讨论也很多。最稳的方案其实是Redlock的多节点实现但大部分业务场景用单节点加一个合理的死锁兜底就够了。go-redis自带的SetNX配合过期时间就能实现一个足够健壮的锁。package cache import ( context time github.com/redis/go-redis/v9 github.com/google/uuid ) // TryLock 尝试获取分布式锁 func TryLock(ctx context.Context, rdb *redis.Client, key string, ttl time.Duration) (string, bool, error) { token : uuid.NewString() ok, err : rdb.SetNX(ctx, key, token, ttl).Result() if err ! nil { return , false, err } if !ok { return , false, nil } return token, true, nil } // Unlock 使用token安全释放锁防止误删他人的锁 func Unlock(ctx context.Context, rdb *redis.Client, key, token string) error { script : if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end return rdb.Eval(ctx, script, []string{key}, token).Err() }这里有几个关键点值得展开token必须是唯一值用来标记当前持锁方。释放锁时先校验token、再DEL避免业务执行超时后锁已经自动过期然后别的请求拿到锁结果第一个请求的Del把第二个请求的锁删掉了。释放锁的CheckAndDelete操作必须用Lua脚本保证原子性不能先GET再DEL否则并发场景下就有竞态窗口。TTL的取值不能拍脑袋。锁时间必须大于业务操作耗时否则锁提前失效多个请求同时进入临界区。如果没法准确预估可以开一个后台协程做锁续期这个功能对于业务复杂的场景非常重要。6.4 Redis key命名规范脚手架里还要统一一套key的命名规则这一点在所有项目里都容易被忽略。我建议遵循业务名:实体名:id的风格比如user:profile:1001、order:detail:20240101。redis-cli里用KEYS user:*能直接看出项目里有哪些user相关的缓存排查问题的时候能省很多时间。另外所有key都应该通过一个常量或函数来构建禁止散落在业务代码里的字符串拼接这样后续改前缀、加版本号都只需要改一处。7. 启动编排与优雅关停让进程走得不狼狈服务启动的时候要按依赖顺序初始化这个顺序很简单配置文件 - 日志 - MySQL - Redis - 路由 - 启动HTTP服务。但服务的“关停”其实比启动更有讲究。很多初学Go的同学写的服务都是直接r.Run()CtrlC之后进程瞬间结束正在处理的请求被直接掐断。对脚手架来说这不是可接受的行为——生产环境发布重启时优雅关停应该成为默认能力。7.1 基于信号量的优雅关停Go标准库的signal包可以捕获SIGINT和SIGTERMGin的http.Server自带Shutdown方法两者配合就能实现优雅关停。package main import ( context errors net/http os os/signal syscall time github.com/gin-gonic/gin ) func main() { r : gin.New() server : http.Server{ Addr: :8080, Handler: r, ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, } go func() { if err : server.ListenAndServe(); err ! nil !errors.Is(err, http.ErrServerClosed) { panic(server error: err.Error()) } }() quit : make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) -quit ctx, cancel : context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err : server.Shutdown(ctx); err ! nil { panic(shutdown error: err.Error()) } }Shutdown的机制是先停止接收新请求然后等待处理中的请求全部完成超时后强制退出。超时时间按业务请求的最大耗时来定一般10秒足够。如果你在服务里还挂了MySQL连接池和Redis连接池也要在Shutdown之后主动关闭它们释放底层连接资源。7.2 启动时的依赖健康检查服务的健康检查接口我一般放在/health它不只返回一个“ok”而是把MySQL和Redis的Ping结果一起返回。这样在容器编排或负载均衡配置健康检查时能真实反映服务的依赖状态。r.GET(/health, func(c *gin.Context) { ctx, cancel : context.WithTimeout(c.Request.Context(), 2*time.Second) defer cancel() mysqlOK : database.Ping(ctx) redisOK : cache.Ping(ctx) if !mysqlOK || !redisOK { c.JSON(http.StatusServiceUnavailable, gin.H{ status: error, mysql: mysqlOK, redis: redisOK, }) return } c.JSON(http.StatusOK, gin.H{status: ok, mysql: true, redis: true}) })这个接口在联调阶段的价值非常明显前端或运维发现服务不可用第一件事就是打这个接口如果MySQL挂了立刻就能定位是数据库问题而不是业务代码问题。省去了一堆“帮我看看服务为什么不返回数据”的沟通。8. 踩过的坑与收尾经验脚手架跑通是一回事跑稳是另一回事。最后分享几个我在迭代过程中真实遇到的坑每个都让我花过不少时间定位。8.1 Go版本与依赖版本的兼容性Go的版本迭代很快依赖库也跟着更新。最典型的一次是go-redis从v8升级到v9Context参数加入到了几乎所有API的方法签名里。如果照抄网上的旧教程编译直接报错。所以脚手架项目一定要在README里写清楚Go版本和关键依赖版本升级依赖时优先看官方迁移指南不要无脑go get latest。另外Go 1.27这个版本在网上被讨论得很多虽然还没正式发布但核心方向已经明确了——模块体验优化和泛型进一步成熟。脚手架项目建议保持Go的次新版本既能体验新特性又不会被前沿版本的不稳定坑到。8.2 MySQL时区问题MySQL的time_zone配置和Go的loc设置如果不一致写入的时间会偏移8小时。GORM连接串里的locLocal就是让驱动和Go进程使用同一个时区。但要注意如果你的MySQL服务器默认时区是UTC建议在应用层统一使用UTC存储展示的时候再转换。最怕的是“一半代码用了本地时区、一半用了UTC”时间问题的排查会让你怀疑人生。8.3 Redis空值缓存的设计缓存穿透的经典解法是缓存空值但很多人在实现时只设置了TTL没有区分“空值”和“真数据”。我的建议是空值也用同一个key缓存但TTL缩短到30到60秒。同时可以在值前面加一个类型前缀比如用Empty字符串标识空值读取时判断前缀后直接返回空结果。这个设计在后端有大量无效ID请求时非常有用。8.4 日志要结构化不搞文本拼接脚手架里我默认配置了zap作为日志库结构化日志是排查线上问题的基础设施。不要用fmt.Println来输出日志不要拼字符串来组装字段。zap用起来很简单logger.Info(user created, zap.Int64(id, id), zap.Duration(cost, cost))JSON格式的日志直接给日志平台消费检索效率比文本高一个量级。最后再分享一个我自己的习惯每次新建项目先把脚手架跑起来然后用wrk做一轮简单的压测确认QPS、P99延迟在合理范围再开始写业务。这个动作能提前发现不少基础层的问题——比如连接池开太小、Redis超时设置太短。很多“感觉”能跑的代码压一压就露馅了。脚手架的意义就在这里让基础设施的稳定性成为可默认的常量而不是每个项目都要重新验证一次的不确定变量。本文还有配套的精品资源点击获取