前面七篇把功能写完了,这一篇让它能在服务器上稳稳跑。三件事:配置别写死、进程被杀时不丢正在处理的请求、用容器和反向代理接公网。
1. 用 viper 管配置
建 config.yaml:
server: port: "8080" mysql: dsn: "root:pass@tcp(127.0.0.1:3306)/demo?charset=utf8mb4&parseTime=True&loc=Local" jwt: secret: "change-me"
读配置统一走一个包:
import "github.com/spf13/viper" type Config struct { Server struct{ Port string } MySQL struct{ DSN string } JWT struct{ Secret string } } func LoadConfig() *Config { v := viper.New() v.SetConfigName("config") v.SetConfigType("yaml") v.AddConfigPath(".") v.AutomaticEnv() // 环境变量覆盖文件,部署时注入密钥 v.ReadInConfig() var c Config v.Unmarshal(&c) return &c }
AutomaticEnv 让 JWT_SECRET 环境变量直接覆盖 jwt.secret,密钥就不进配置文件、不进镜像。本地用 yaml,生产用环境变量注入,一套代码两处生效。
2. 优雅停机
r.Run() 是阻塞的,且不会处理退出信号。用 http.Server 包一层,Shutdown 先停收新请求、等存量请求跑完再退出:
srv := &http.Server{ Addr: ":" + cfg.Server.Port, Handler: r, } go func() { if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("服务启动失败: %v", err) } }() quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit // 阻塞,等 Ctrl+C 或 docker stop ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { log.Printf("强制退出: %v", err) } log.Println("服务已优雅关闭")
docker stop 发的是 SIGTERM,这串代码保证滚动发布、重启时不丢请求。
3. 多阶段 Dockerfile
编译放第一阶段,运行阶段只留二进制,镜像能压到 10MB 出头:
FROM golang:1.22 AS build WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -o server . FROM gcr.io/distroless/static-debian12 WORKDIR /app COPY --from=build /app/server . COPY config.yaml . EXPOSE 8080 CMD ["./server"]
CGO_ENABLED=0 编出静态二进制,才能跑在 distroless 这种无 libc 的镜像里。go.sum 单独 COPY 利用层缓存,改业务代码不用重下依赖。
4. docker-compose 编排
version: "3.8" services: gin-demo: build: . environment: - JWT_SECRET=${JWT_SECRET} ports: - "8080:8080" restart: unless-stopped
restart: unless-stopped 让进程意外退出自动拉起。密钥走宿主环境变量,compose 文件里不写明文。
5. nginx 反代
Gin 不直接暴露公网,前面挂 nginx 做 SSL 和转发:
server {
listen 443 ssl;
server_name api.demo.com;
ssl_certificate /etc/letsencrypt/live/api.demo.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.demo.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
proxy_set_header 把真实客户端 IP 透给 Gin,否则 c.ClientIP() 拿到的是 nginx 的地址。证书用 Let's Encrypt 免费签发,90 天续期一次。
6. 健康检查
给编排和负载均衡一个探活 endpoint:
r.GET("/healthz", func(c *gin.Context) { c.JSON(200, gin.H{"status": "ok"}) })
k8s 的 readinessProbe、docker-compose 的 healthcheck 都打这个路径。服务依赖 MySQL 时,健康检查里顺手 db.Raw("SELECT 1") 探一下库,库挂了探针失败,流量不再打进来。
7. 上线检查清单
密钥全部走环境变量,配置文件不提交密钥。编译开 CGO_ENABLED=0。进程接 SIGTERM 做优雅停机。nginx 只暴露 443,Gin 端口不对外。健康检查 /healthz 已接。日志用结构化输出,方便后面接采集。
八篇走完,你已经有了一条从 go mod init 到生产部署的完整 Gin 链路。下一步可以补单元测试、接 Redis 缓存、做链路追踪,按业务需要往这套骨架上加就行。