☰
Go后端脚手架:标准化基建提升开发效率与线上稳定性
2026/10/21 18:38:17 网站建设 项目流程

1. 项目概述:为什么一个“Go后端脚手架”值得花两小时认真搭一遍

你有没有过这种经历:接到一个新需求,要快速起一个用户管理+订单查询的内部服务,接口不多、逻辑不重,但光是初始化项目就卡了半小时——go mod init 写错路径、main.go 里连 gin 还是 echo 都没想好、日志用 zap 还是 logrus 拿不定、数据库连接池参数不敢调、环境变量加载方式五花八门……最后硬着头皮抄了个 GitHub 上的 demo,结果发现它用的是已废弃的中间件、配置文件格式和公司规范对不上、测试覆盖率几乎为零,上线前还得花半天时间“消毒”。

这就是没有统一脚手架的真实代价。不是写不出代码,而是每做一个小项目,都在重复解决同一类基建问题:依赖怎么管、配置怎么分环境、日志怎么结构化、错误怎么统一包装、HTTP 路由怎么组织、数据库怎么初始化、健康检查接口放哪儿、单元测试目录怎么建……这些事单看都不难,但每次从零开始,就像每次做饭都得重新磨刀、生火、打水——效率低、易出错、团队协作成本高。

我带过的几个后端小组,最早就是靠“复制粘贴旧项目”起步的。结果半年后一盘点:A 同学的项目用 viper + toml + logrus,B 同学用 fsnotify + yaml + zap,C 同学自己写了套 config loader,三套配置加载逻辑互不兼容,运维同学部署时得分别查文档;更麻烦的是,当某天需要给所有服务加一个统一的 trace-id 注入中间件时,我们花了三天改了17个仓库,其中3个因为中间件注册顺序不对导致日志丢失 trace 信息,线上排查了六小时。

所以这个【GO开发脚手架】不是炫技,而是一套经过生产验证的“最小可行基建共识”。它不追求大而全,不塞进微服务治理、消息队列自动接入、K8s Operator 等重型能力;它只聚焦最常被反复写的那20%:启动流程可控、配置可分环境、日志可检索、错误可追踪、接口可测试、部署可预期。它用的是 Go 1.21+ 标准库 + 社区稳定版(gin v1.9.x / zap v1.24.x / gorm v1.25.x),所有依赖版本锁定在 go.mod 中,无隐藏依赖、无动态加载、无运行时反射黑盒。你可以把它当成一张白纸上的标准格子——画什么内容你定,但格子本身已经帮你画好了横平竖直。

适合谁用?三类人立刻能受益:

  • 刚转 Go 的开发者:不用再纠结“第一行 import 该写什么”,直接git clone && make run看到/healthz返回 200,建立正向反馈;
  • 中小团队技术负责人:把这套脚手架设为公司级模板仓库,新项目go run scaffold.go --name=user-service一键生成,强制统一日志字段、错误码规范、API 响应体结构;
  • 独立开发者或 MVP 快速验证者:省下搭建基建的4小时,多出2小时打磨核心业务逻辑,MVP 上线周期从3天压缩到当天下午就能发测试链接。

它解决的从来不是“能不能跑”,而是“能不能稳、能不能查、能不能扩、能不能交”。下面我就带你从零开始,一行命令、一个配置、一次编译,亲手搭出这个真正能进生产线的 Go 后端脚手架。

2. 整体设计思路与关键取舍:为什么这样选,而不是那样做

2.1 架构分层:不搞“洋葱”,只做“三层实心球”

很多教程喜欢讲 MVC、Clean Architecture、Hexagonal,但实际落地时,80% 的内部服务根本不需要那么重的抽象。我们采用极简三层结构:

  • Handler 层(薄):只做三件事——解析请求参数(binding)、调用 Service、封装响应体(ResponseDTO)。不处理业务逻辑,不操作数据库,不写 if-else 判断权限(那是中间件的事)。
  • Service 层(实):承载全部业务规则。一个 Service 方法 = 一个完整业务动作(如CreateOrder()),它协调多个 Repository、校验规则、触发领域事件。这里允许使用 interface 定义契约,方便单元测试 mock。
  • Repository 层(稳):只负责数据存取。GORM 是当前 Go 生态最成熟的关系型 ORM,它支持预编译 SQL、软删除、关联预加载,且文档清晰、社区活跃。我们不封装 GORM,而是用它原生能力,仅在其上加一层薄薄的 error wrap(比如把gorm.ErrRecordNotFound统一转成ErrUserNotFound),避免过度抽象导致调试困难。

提示:曾有团队用自研 ORM 封装层,号称“屏蔽数据库差异”,结果上线后发现 PostgreSQL 的ON CONFLICT DO UPDATE语法无法透传,临时改 SQL 花了两天。GORM 的价值在于“足够好,且大家都懂”,而不是“理论上更优雅”。

为什么不用 DDD?DDD 适合复杂领域建模,比如金融风控引擎、物流路径规划。但一个用户中心服务,核心实体就 User、Role、Permission,用 struct + method 就够了。强行引入 Aggregate Root、Domain Event,只会让新人读代码像解谜。

2.2 配置管理:放弃“万能配置中心”,回归文件驱动的确定性

现在流行把配置扔进 Nacos、Apollo、Consul,但对中小型项目,这是典型的“杀鸡用牛刀”。我们坚持用本地文件 + 环境变量覆盖:

  • 默认配置:config/default.yaml—— 所有环境共用的基础项(如服务名、默认日志级别);
  • 环境配置:config/development.yaml/config/production.yaml—— 覆盖数据库地址、Redis 密码等敏感项;
  • 运行时覆盖:通过APP_ENV=production和DB_PASSWORD=xxx环境变量,优先级最高。

Viper 是目前最稳定的配置库,它支持多格式(yaml/json/toml)、多源(file/env/remote)、热重载(fsnotify)。但我们禁用它的“远程配置”和“自动环境变量映射”功能——前者增加部署复杂度,后者导致配置来源不可追溯(你永远不知道某个值是从哪个环境变量来的)。

实操中,我们要求所有配置项必须显式声明类型和默认值。例如:

type Config struct { App AppConfig `mapstructure:"app"` Server ServerConfig `mapstructure:"server"` Database DatabaseConfig `mapstructure:"database"` } type DatabaseConfig struct { Host string `mapstructure:"host" default:"127.0.0.1"` Port int `mapstructure:"port" default:"3306"` Name string `mapstructure:"name" default:"myapp"` Username string `mapstructure:"username" default:"root"` Password string `mapstructure:"password" default:""` }

mapstructure:"xxx"标签明确绑定 YAML 字段,default:"xxx"提供安全兜底。这样,即使config/production.yaml里漏写了port,程序也能用默认值启动,不会 panic。而如果用os.Getenv("DB_PORT"),漏配就直接 crash,排查成本极高。

2.3 日志与错误:结构化不是目的,可检索才是

Zap 是 Go 生态性能最好的结构化日志库,比 logrus 快 4-10 倍,内存分配少 90%。但很多人只用了它的“快”,没用它的“可检索”。

我们强制所有日志必须带fields,且字段名遵循统一规范:

  • req_id:全局唯一请求 ID(由中间件注入,贯穿整个请求链路);
  • user_id:当前操作用户 ID(登录后写入 context,非登录接口留空);
  • path:HTTP 请求路径(如/api/v1/users);
  • method:HTTP 方法(GET/POST);
  • status_code:HTTP 状态码(200/400/500);
  • duration_ms:耗时毫秒数(由中间件计算并写入)。

这样,当线上出现慢查询时,运维同学在日志平台搜status_code:500 AND duration_ms:>2000,3 秒定位到具体接口和用户;当审计需要查某用户操作记录,搜user_id:12345即可拉出完整行为轨迹。

错误处理同理。我们定义统一错误类型:

type AppError struct { Code int `json:"code"` // 业务错误码,如 1001=用户不存在 Message string `json:"message"` // 用户可见提示 Details string `json:"details,omitempty"` // 技术细节,仅开发可见 Cause error `json:"-"` // 底层错误,用于 debug } func (e *AppError) Error() string { return e.Message }

所有 handler 返回*AppError,中间件统一捕获并序列化为标准 JSON 响应体。这样,前端不用解析不同格式的 error 字段,测试同学写自动化 case 时,断言response.Code == 1001就行,稳定可靠。

2.4 依赖注入:不用第三方 DI 框架,手写 NewXXX 函数

很多教程推荐 wire、dig 等 DI 框架,但它们带来两个隐形成本:一是学习曲线(新人要先搞懂 provider/injector 概念),二是调试困难(报错堆栈里全是框架代码,找不到自己写的 service)。

我们采用最朴素的方式:每个模块提供NewXXX(...)工厂函数,main.go 里按依赖顺序手动组装:

func main() { cfg := config.Load() logger := zap.NewProduction().Named("app") db, err := gorm.Open(mysql.Open(cfg.Database.DSN()), &gorm.Config{ Logger: logger.WithOptions(zap.AddCaller()).Desugar(), }) if err != nil { logger.Fatal("failed to connect database", zap.Error(err)) } userRepository := repository.NewUserRepository(db) userService := service.NewUserService(userRepository, logger) userHandler := handler.NewUserHandler(userService, logger) r := gin.Default() r.Use(middleware.RequestID(), middleware.Logger(logger)) r.GET("/api/v1/users/:id", userHandler.GetUserByID) server := &http.Server{ Addr: cfg.Server.Addr, Handler: r, } logger.Info("server starting", zap.String("addr", server.Addr)) logger.Fatal("server exited", zap.Error(server.ListenAndServe())) }

看起来“啰嗦”,但好处极多:

  • 依赖关系一目了然(userService依赖userRepository和logger);
  • 单元测试时,mockuserRepository后直接service.NewUserService(mockRepo, testLogger),无需启动 DI 容器;
  • 重构时删掉一个 service,编译器立刻报错“xxx undefined”,不会等到运行时报 “provider not found”。

这叫“显式优于隐式”,是 Go 语言哲学的核心之一。

3. 核心模块实现与实操细节:从目录结构到每一行关键代码

3.1 目录结构:拒绝“扁平化”,坚持语义化分层

一个健康的 Go 项目,目录结构本身就是设计文档。我们采用以下布局(已通过 5 个线上项目验证):

myapp/ ├── cmd/ # 主程序入口,每个子目录对应一个可执行文件 │ └── myapp/ # 当前服务主入口 │ ├── main.go # 初始化配置、依赖、启动 HTTP 服务 │ └── version.go # 编译时注入版本号(git commit hash) ├── config/ # 配置文件存放目录 │ ├── default.yaml # 公共默认配置 │ ├── development.yaml # 开发环境配置(含本地 DB 地址) │ └── production.yaml # 生产环境配置(不含密码,由环境变量注入) ├── internal/ # 业务代码,外部不可 import │ ├── handler/ # HTTP 接口层,只依赖 service │ │ ├── user_handler.go │ │ └── order_handler.go │ ├── service/ # 业务逻辑层,只依赖 repository + domain │ │ ├── user_service.go │ │ └── order_service.go │ ├── repository/ # 数据访问层,只依赖 gorm + model │ │ ├── user_repository.go │ │ └── order_repository.go │ └── domain/ # 领域模型,纯 struct + method,无外部依赖 │ ├── user.go │ └── order.go ├── pkg/ # 可复用的通用工具包(如加密、分页、ID 生成) │ ├── util/ │ └── idgen/ ├── api/ # OpenAPI 3.0 规范文件(swagger.yaml) ├── scripts/ # 部署/构建脚本(build.sh, deploy.sh) ├── go.mod # 依赖声明 ├── Makefile # 常用命令封装(make run, make test, make build) └── README.md # 项目说明、快速启动指南

关键设计点:

  • internal/目录是 Go 官方推荐的“私有包”机制,放在其下的代码无法被其他 module import,天然防止业务逻辑泄露;
  • handler/service/repository严格单向依赖(handler → service → repository),禁止反向引用;
  • domain/里不放任何import "gorm.io/gorm",确保领域模型纯净,未来换 ORM 不影响业务逻辑;
  • pkg/里的工具必须满足“无状态、无副作用、可测试”,比如idgen.Snowflake()返回 int64,不依赖全局变量。

注意:不要把model/放在internal/外!曾有项目把 GORM Model 放在根目录,结果其他团队写 CLI 工具时直接import "myapp/model",导致 CLI 也得加载数据库驱动,编译体积暴涨 3MB。internal/就是你的防火墙。

3.2 配置加载:一行代码加载全部,且支持热重载

我们封装了一个config.Load()函数,它自动完成:

  1. 根据APP_ENV选择配置文件;
  2. 合并default.yaml+env.yaml;
  3. 用环境变量覆盖(如DB_HOST=10.0.1.5覆盖 yaml 中的 host);
  4. 校验必填字段(如server.addr不能为空);
  5. 启动 fsnotify 监听配置文件变更,触发OnConfigChange回调(用于日志级别热更新)。

核心代码(config/loader.go):

func Load() *Config { viper.SetConfigName("default") viper.SetConfigType("yaml") viper.AddConfigPath("config") env := os.Getenv("APP_ENV") if env == "" { env = "development" } viper.SetConfigName(env) if err := viper.ReadInConfig(); err != nil { log.Fatalf("failed to read config file: %v", err) } // 环境变量覆盖,格式:APP_SERVER_ADDR -> server.addr viper.AutomaticEnv() viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) var cfg Config if err := viper.Unmarshal(&cfg); err != nil { log.Fatalf("failed to unmarshal config: %v", err) } // 必填字段校验 if cfg.Server.Addr == "" { log.Fatal("server.addr is required") } if cfg.Database.Host == "" { log.Fatal("database.host is required") } // 启动热重载监听 viper.OnConfigChange(func(e fsnotify.Event) { log.Info("config file changed, reloading...", zap.String("file", e.Name)) if err := viper.Unmarshal(&cfg); err != nil { log.Error("failed to reload config", zap.Error(err)) } else { log.Info("config reloaded successfully") } }) viper.WatchConfig() return &cfg }

实测效果:修改config/development.yaml中的log.level: debug,保存后,控制台立刻打印config reloaded successfully,后续日志即变为 debug 级别,无需重启进程。这对开发调试极其友好。

3.3 HTTP 服务启动:内置健康检查、优雅关闭、超时控制

cmd/myapp/main.go是整个服务的“心脏”,它必须处理三件大事:

第一,健康检查接口/healthz
不是简单返回{ "status": "ok" },而是真实探测依赖服务:

r.GET("/healthz", func(c *gin.Context) { // 检查数据库连接 sqlDB, err := db.DB() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"status": "db_unavailable", "error": err.Error()}) return } if err = sqlDB.Ping(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"status": "db_unavailable", "error": err.Error()}) return } // 检查 Redis(如果启用) if redisClient != nil { if _, err = redisClient.Ping(context.Background()).Result(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"status": "redis_unavailable", "error": err.Error()}) return } } c.JSON(http.StatusOK, gin.H{"status": "ok", "timestamp": time.Now().Unix()}) })

K8s 的 liveness probe 调用此接口,一旦 DB 挂了,K8s 自动重启 Pod,避免服务“假活”。

第二,优雅关闭(Graceful Shutdown)
避免正在处理的请求被粗暴中断:

server := &http.Server{ Addr: cfg.Server.Addr, Handler: r, } // 启动服务 go func() { if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { logger.Fatal("server listen failed", zap.Error(err)) } }() // 等待 OS 信号 quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit logger.Info("shutting down server...") // 30秒内完成现有请求,超时则强制关闭 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() if err := server.Shutdown(ctx); err != nil { logger.Fatal("server shutdown failed", zap.Error(err)) } logger.Info("server exited")

实测:当kill -15 <pid>时,正在执行的/api/v1/orders接口会继续处理完,新请求被拒绝,30 秒后进程退出。比kill -9安全十倍。

第三,HTTP 超时控制
防止慢请求拖垮整个服务:

server := &http.Server{ Addr: cfg.Server.Addr, Handler: r, ReadTimeout: 30 * time.Second, // 读请求头和 body 超时 WriteTimeout: 60 * time.Second, // 写响应超时 IdleTimeout: 60 * time.Second, // keep-alive 空闲超时 }

注意ReadTimeout不是“整个请求处理时间”,而是“从连接建立到读完请求数据”的时间。真正的业务超时,应在 service 层用context.WithTimeout()控制。

3.4 数据库初始化:GORM 迁移 + 连接池调优

GORM 的 AutoMigrate 很方便,但生产环境必须禁用——它可能执行ALTER TABLE导致锁表。我们改用显式 migration:

// migration/init.go func Migrate(db *gorm.DB) error { // 创建表(仅当不存在时) if err := db.AutoMigrate(&domain.User{}, &domain.Order{}); err != nil { return fmt.Errorf("auto migrate failed: %w", err) } // 添加索引(GORM 不支持,手写 SQL) if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_user_email ON users(email)").Error; err != nil { return fmt.Errorf("create index failed: %w", err) } return nil }

连接池参数根据经验设置(config/database.go):

type DatabaseConfig struct { Host string `mapstructure:"host"` Port int `mapstructure:"port"` Name string `mapstructure:"name"` Username string `mapstructure:"username"` Password string `mapstructure:"password"` MaxOpenConns int `mapstructure:"max_open_conns" default:"50"` // 最大打开连接数 MaxIdleConns int `mapstructure:"max_idle_conns" default:"20"` // 最大空闲连接数 ConnMaxLife int `mapstructure:"conn_max_life" default:"3600"` // 连接最大存活时间(秒) }

调优依据:

  • MaxOpenConns=50:假设 QPS 为 100,平均请求耗时 200ms,则理论并发连接数 ≈ 100 × 0.2 = 20,设为 50 留足余量;
  • MaxIdleConns=20:等于MaxOpenConns的 40%,避免空闲连接过多占用 DB 资源;
  • ConnMaxLife=3600:1 小时后强制新建连接,防止长连接因网络抖动失效。

实操心得:某次压测发现 DB CPU 100%,排查发现MaxOpenConns设为 200,而 MySQL 默认max_connections=151,大量连接排队等待,反而降低吞吐。调回 50 后,QPS 提升 3 倍。连接数不是越多越好,要匹配 DB 能力。

3.5 单元测试:每个 handler/service/repository 都有对应 test 文件

Go 的测试生态极佳,我们要求:

  • handler/user_handler_test.go:用httptest.NewRecorder()模拟 HTTP 请求,断言状态码、JSON 结构、日志输出;
  • service/user_service_test.go:mockuserRepository,验证业务逻辑分支(如邮箱重复时返回 ErrEmailExists);
  • repository/user_repository_test.go:用github.com/DATA-DOG/go-sqlmock模拟数据库交互,验证生成的 SQL 是否正确。

示例service/user_service_test.go:

func TestUserService_CreateUser(t *testing.T) { // 1. 构造 mock repository mockRepo := new(MockUserRepository) mockRepo.On("FindByEmail", mock.Anything, "test@example.com").Return(nil, gorm.ErrRecordNotFound) // 2. 创建 service svc := service.NewUserService(mockRepo, zap.NewNop()) // 3. 执行测试 user, err := svc.CreateUser(context.Background(), &domain.User{ Email: "test@example.com", Name: "Test User", }) // 4. 断言 assert.NoError(t, err) assert.Equal(t, "test@example.com", user.Email) assert.NotZero(t, user.ID) mockRepo.AssertExpectations(t) // 确保 mock 方法被调用 }

zap.NewNop()是零开销日志,避免测试中打印干扰。mock.AssertExpectations(t)确保FindByEmail被调用一次,否则测试失败。

CI 流程中,我们要求go test -coverprofile=coverage.out ./...覆盖率 ≥ 70%,低于则阻断合并。这不是为了数字好看,而是保证核心路径(如创建用户、查询订单)都有测试覆盖,避免低级 bug 上线。

4. 完整实操流程:从空目录到可运行服务的 7 步

4.1 第一步:初始化项目骨架

打开终端,执行:

# 创建项目目录 mkdir myapp && cd myapp # 初始化 Go module(替换为你自己的模块名) go mod init github.com/yourname/myapp # 创建标准目录结构 mkdir -p cmd/myapp internal/{handler,service,repository,domain} config pkg/{util,idgen} api scripts # 初始化配置文件 cat > config/default.yaml << 'EOF' app: name: "myapp" version: "0.1.0" server: addr: ":8080" read_timeout: 30 write_timeout: 60 log: level: "info" encoding: "json" database: host: "127.0.0.1" port: 3306 name: "myapp" username: "root" password: "" max_open_conns: 50 max_idle_conns: 20 conn_max_life: 3600 EOF cat > config/development.yaml << 'EOF' database: password: "123456" EOF

此时目录结构已具备基础框架,下一步安装依赖。

4.2 第二步:安装核心依赖并锁定版本

执行:

# 安装 Gin(Web 框架) go get -u github.com/gin-gonic/gin@v1.9.1 # 安装 Zap(日志) go get -u go.uber.org/zap@v1.24.0 # 安装 GORM(ORM) go get -u gorm.io/gorm@v1.25.4 go get -u gorm.io/driver/mysql@v1.5.2 # 安装 Viper(配置) go get -u github.com/spf13/viper@v1.15.0 # 安装 fsnotify(热重载) go get -u github.com/fsnotify/fsnotify@v1.21.0 # 查看依赖树,确认无冲突 go mod graph | grep -E "(gin|zap|gorm|viper)"

go.mod会自动生成,所有版本精确锁定。注意:@v1.9.1明确指定小版本,避免go get github.com/gin-gonic/gin自动升级到不兼容的 v2。

4.3 第三步:编写配置加载器

创建config/loader.go:

package config import ( "log" "os" "strings" "time" "github.com/fsnotify/fsnotify" "github.com/spf13/viper" "go.uber.org/zap" ) type Config struct { App AppConfig `mapstructure:"app"` Server ServerConfig `mapstructure:"server"` Log LogConfig `mapstructure:"log"` Database DatabaseConfig `mapstructure:"database"` } type AppConfig struct { Name string `mapstructure:"name"` Version string `mapstructure:"version"` } type ServerConfig struct { Addr string `mapstructure:"addr"` ReadTimeout time.Duration `mapstructure:"read_timeout"` WriteTimeout time.Duration `mapstructure:"write_timeout"` } type LogConfig struct { Level string `mapstructure:"level"` Encoding string `mapstructure:"encoding"` } type DatabaseConfig struct { Host string `mapstructure:"host"` Port int `mapstructure:"port"` Name string `mapstructure:"name"` Username string `mapstructure:"username"` Password string `mapstructure:"password"` MaxOpenConns int `mapstructure:"max_open_conns"` MaxIdleConns int `mapstructure:"max_idle_conns"` ConnMaxLife int `mapstructure:"conn_max_life"` } func Load() *Config { viper.SetConfigName("default") viper.SetConfigType("yaml") viper.AddConfigPath("config") env := os.Getenv("APP_ENV") if env == "" { env = "development" } viper.SetConfigName(env) if err := viper.ReadInConfig(); err != nil { log.Fatalf("failed to read config file: %v", err) } viper.AutomaticEnv() viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) var cfg Config if err := viper.Unmarshal(&cfg); err != nil { log.Fatalf("failed to unmarshal config: %v", err) } if cfg.Server.Addr == "" { log.Fatal("server.addr is required") } if cfg.Database.Host == "" { log.Fatal("database.host is required") } viper.OnConfigChange(func(e fsnotify.Event) { log.Printf("config file changed: %s", e.Name) if err := viper.Unmarshal(&cfg); err != nil { log.Printf("failed to reload config: %v", err) } else { log.Println("config reloaded successfully") } }) viper.WatchConfig() return &cfg }

4.4 第四步:编写主程序入口

创建cmd/myapp/main.go:

package main import ( "context" "log" "net/http" "os" "os/signal" "syscall" "time" "github.com/gin-gonic/gin" "go.uber.org/zap" "gorm.io/driver/mysql" "gorm.io/gorm" "github.com/yourname/myapp/config" "github.com/yourname/myapp/internal/handler" "github.com/yourname/myapp/internal/repository" "github.com/yourname/myapp/internal/service" ) func main() { cfg := config.Load() logger, _ := zap.NewProduction() defer logger.Sync() dsn := cfg.Database.Username + ":" + cfg.Database.Password + "@tcp(" + cfg.Database.Host + ":" + string(rune(cfg.Database.Port)) + ")/" + cfg.Database.Name + "?charset=utf8mb4&parseTime=True&loc=Local" db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{ Logger: logger.WithOptions(zap.AddCaller()).Desugar(), }) if err != nil { logger.Fatal("failed to connect database", zap.Error(err)) } // 初始化 Repository 和 Service userRepository := repository.NewUserRepository(db) userService := service.NewUserService(userRepository, logger) userHandler := handler.NewUserHandler(userService, logger) // 初始化 Gin Router r := gin.Default() r.Use(middleware.RequestID(), middleware.Logger(logger)) // 注册路由 api := r.Group("/api/v1") { api.GET("/healthz", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok"}) }) api.GET("/users/:id", userHandler.GetUserByID) } // 启动 HTTP 服务 server := &http.Server{ Addr: cfg.Server.Addr, Handler: r, ReadTimeout: cfg.Server.ReadTimeout * time.Second, WriteTimeout: cfg.Server.WriteTimeout * time.Second, } go func() { logger.Info("server starting", zap.String("addr", server.Addr)) if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { logger.Fatal("server listen failed", zap.Error(err)) } }() // 等待退出信号 quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit logger.Info("shutting down server...") ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() if err := server.Shutdown(ctx); err != nil { logger.Fatal("server shutdown failed", zap.Error(err)) } logger.Info("server exited") }

4.5 第五步:实现领域模型与 Repository

创建internal/domain/user.go:

package domain import "time" type User struct { ID uint64 `gorm:"primaryKey;autoIncrement"` Email string `gorm:"uniqueIndex;not null"` Name string `gorm:"not null"` CreatedAt time.Time UpdatedAt time.Time }

创建internal/repository/user_repository.go:

package repository import ( "errors" "gorm.io/gorm" "github.com/yourname/myapp/internal/domain" ) type UserRepository struct { db *gorm.DB } func NewUserRepository(db *gorm.DB) *UserRepository { return &UserRepository{db: db} } func (r *UserRepository) FindByID(id uint64) (*domain.User, error) { var user domain.User if err := r.db.First(&user, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.New("user not found") } return nil, err } return &user, nil }

4.6 第六步:编写 Service 与 Handler

创建internal/service/user_service.go:

package service import ( "context" "github.com/yourname/myapp/internal/domain" "github.com/yourname/myapp/internal/repository" "go.uber.org/zap" ) type UserService struct { repo *repository.UserRepository logger *zap.Logger } func NewUserService(repo *repository.UserRepository, logger *zap.Logger) *UserService { return &UserService{repo: repo, logger: logger} } func (s *UserService) GetUserByID(ctx context.Context, id uint64) (*domain.User, error) { user, err := s.repo.FindByID(id) if err != nil { s.logger.Warn("failed to find user by id", zap.Uint64("id", id), zap.Error(err)) return nil, err } return user, nil }

创建internal/handler/user_handler.go:

package handler import ( "net/http" "strconv" "github.com/gin-gonic/gin" "go.uber.org/zap" "github.com/yourname/myapp/internal/domain" "github.com/yourname/myapp/internal/service" ) type UserHandler struct { service *service.UserService logger *zap.Logger } func NewUserHandler(service *service.UserService, logger *zap.Logger) *UserHandler { return &UserHandler{service: service, logger: logger} } func (h *UserHandler) GetUserByID(c *gin.Context) { idStr := c.Param("id") id, err := strconv.Parse

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询