项目部署与优化
第28课:项目部署与优化
生活类比
想象你开了一家餐厅。菜谱写好了(代码写完了),但要真正营业,你需要:
- 选址装修 — 交叉编译让同一份菜谱能在不同城市开店(不同平台运行)
- 厨房设计 — Docker 多阶段构建就像只把做菜需要的工具搬进厨房,不搬多余的家具
- 员工手册 — 环境变量配置就像不同分店用不同的营业时间,但手册格式一样
- 监控摄像 — 日志管理记录每一道菜的制作过程,出了问题可以追溯
- 体检报告 — 性能分析就像定期体检,找出哪个环节最慢
- 打烊流程 — 优雅关闭就像打烊时不赶走正在吃饭的客人,等他们吃完再关门
核心概念
| 概念 | 说明 |
|---|---|
| 交叉编译 | 在一个平台上编译出另一个平台的可执行文件 |
| 多阶段构建 | Docker 构建时分离编译环境和运行环境,减小镜像体积 |
| 环境变量 | 在不修改代码的情况下配置程序行为 |
| 结构化日志 | 使用 log/slog 输出机器可解析的日志格式 |
| 性能分析 | 使用 pprof 找出 CPU 和内存瓶颈 |
| 优雅关闭 | 收到终止信号后,等待正在处理的请求完成再退出 |
基本语法与用法
1. 交叉编译
Go 原生支持交叉编译,只需设置 GOOS 和 GOARCH 环境变量:
# 编译 Linux amd64 版本(在 Windows/Mac 上执行)
GOOS=linux GOARCH=amd64 go build -o myapp-linux .
# 编译 Windows 版本(在 Linux/Mac 上执行)
GOOS=windows GOARCH=amd64 go build -o myapp.exe .
# 编译 macOS ARM (M1/M2) 版本
GOOS=darwin GOARCH=arm64 go build -o myapp-darwin .
CGO 默认被禁用。如果依赖 C 库,需要安装交叉编译工具链。
2. Docker 多阶段构建
# ---- 构建阶段 ----
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o server .
# ---- 运行阶段 ----
FROM alpine:3.19
RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=builder /app/server .
EXPOSE 8080
CMD ["./server"]
3. 环境变量配置
package main
import (
"fmt"
"os"
"strconv"
)
func main() {
// 读取环境变量,提供默认值
port := getEnv("APP_PORT", "8080")
dbHost := getEnv("DB_HOST", "localhost")
debug := getEnv("DEBUG", "false")
fmt.Printf("启动配置: port=%s, db=%s, debug=%s\n", port, dbHost, debug)
}
// getEnv 获取环境变量,若不存在则返回默认值
func getEnv(key, defaultVal string) string {
if val := os.Getenv(key); val != "" {
return val
}
return defaultVal
}
.env 文件配合 godotenv 库,但不要把 .env 文件提交到版本控制。
4. 结构化日志(log/slog)
Go 1.21 引入了标准库 log/slog,支持 JSON 格式输出:
package main
import (
"log/slog"
"os"
)
func main() {
// 创建 JSON 格式的 handler
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
// 使用结构化日志
logger.Info("服务启动",
"port", 8080,
"env", "production",
)
logger.Error("数据库连接失败",
"host", "db.example.com",
"error", "connection refused",
)
}
slog 支持通过 slog.SetDefault() 设置全局默认 logger,整个项目统一使用即可。
5. 性能分析(pprof)
package main
import (
"fmt"
"net/http"
_ "net/http/pprof" // 导入即注册 pprof 路由
"time"
)
func main() {
// 在单独的端口启动 pprof 服务
go func() {
fmt.Println("pprof 监听 :6060")
http.ListenAndServe(":6060", nil)
}()
// 你的主业务逻辑
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(10 * time.Millisecond)
fmt.Fprintln(w, "Hello, World!")
})
http.ListenAndServe(":8080", nil)
}
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30 可以采集 30 秒的 CPU 数据。
6. 优雅关闭
package main
import (
"context"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(2 * time.Second) // 模拟耗时请求
fmt.Fprintln(w, "处理完成")
})
server := &http.Server{
Addr: ":8080",
Handler: mux,
}
// 在 goroutine 中启动服务
go func() {
fmt.Println("服务监听 :8080")
if err := server.ListenAndServe(); err != http.ErrServerClosed {
fmt.Printf("服务异常退出: %v\n", err)
}
}()
// 等待中断信号
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
fmt.Println("\n收到关闭信号,正在优雅退出...")
// 给正在处理的请求最多 10 秒时间完成
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
fmt.Printf("强制关闭: %v\n", err)
} else {
fmt.Println("服务已优雅关闭")
}
}
server.Shutdown() 会停止接受新连接,等待已有连接处理完毕后返回。
示例:基础交叉编译脚本(难度⭐)
创建一个脚本,一键编译多个平台的二进制文件:
package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
)
// 目标平台列表
type Target struct {
OS string
Arch string
}
func main() {
targets := []Target{
{"linux", "amd64"},
{"linux", "arm64"},
{"darwin", "amd64"},
{"darwin", "arm64"},
{"windows", "amd64"},
}
outputDir := "dist"
os.MkdirAll(outputDir, 0755)
for _, t := range targets {
outputName := fmt.Sprintf("myapp-%s-%s", t.OS, t.Arch)
if t.OS == "windows" {
outputName += ".exe"
}
outputPath := filepath.Join(outputDir, outputName)
fmt.Printf("正在编译: %s/%s -> %s\n", t.OS, t.Arch, outputPath)
cmd := exec.Command("go", "build", "-o", outputPath, ".")
cmd.Env = append(os.Environ(),
"GOOS="+t.OS,
"GOARCH="+t.Arch,
"CGO_ENABLED=0",
)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fmt.Printf("编译失败 %s/%s: %v\n", t.OS, t.Arch, err)
}
}
fmt.Println("全部编译完成!")
}
运行方式:
go run build.go
输出目录结构:
dist/
├── myapp-darwin-amd64
├── myapp-darwin-arm64
├── myapp-linux-amd64
├── myapp-linux-arm64
└── myapp-windows-amd64.exe
示例:带环境变量配置的 HTTP 服务(难度⭐⭐)
演示完整的环境变量配置方案:
package main
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"strconv"
"syscall"
"time"
)
// Config 应用配置结构体
type Config struct {
Port int `json:"port"`
Env string `json:"env"`
LogLevel string `json:"log_level"`
DBHost string `json:"db_host"`
DBPort int `json:"db_port"`
IdleTimeout int `json:"idle_timeout"` // 秒
}
// LoadConfig 从环境变量加载配置
func LoadConfig() Config {
return Config{
Port: getEnvInt("APP_PORT", 8080),
Env: getEnvStr("APP_ENV", "development"),
LogLevel: getEnvStr("LOG_LEVEL", "info"),
DBHost: getEnvStr("DB_HOST", "localhost"),
DBPort: getEnvInt("DB_PORT", 5432),
IdleTimeout: getEnvInt("IDLE_TIMEOUT", 30),
}
}
func getEnvStr(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}
func getEnvInt(key string, fallback int) int {
if v := os.Getenv(key); v != "" {
if n, err := strconv.Atoi(v); err == nil {
return n
}
}
return fallback
}
func (c Config) String() string {
data, _ := json.MarshalIndent(c, "", " ")
return string(data)
}
func main() {
cfg := LoadConfig()
// 根据环境配置日志级别
var level slog.Level
switch cfg.LogLevel {
case "debug":
level = slog.LevelDebug
case "warn":
level = slog.LevelWarn
case "error":
level = slog.LevelError
default:
level = slog.LevelInfo
}
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: level,
}))
slog.SetDefault(logger)
logger.Info("应用配置加载完成", "config", cfg.String())
mux := http.NewServeMux()
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"status": "ok",
"env": cfg.Env,
})
})
mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
logger.Info("收到请求", "method", r.Method, "path", r.URL.Path)
fmt.Fprintf(w, "Hello from %s environment!", cfg.Env)
})
server := &http.Server{
Addr: fmt.Sprintf(":%d", cfg.Port),
Handler: mux,
IdleTimeout: time.Duration(cfg.IdleTimeout) * time.Second,
}
go func() {
logger.Info("服务启动", "port", cfg.Port, "env", cfg.Env)
if err := server.ListenAndServe(); err != http.ErrServerClosed {
logger.Error("服务异常", "error", err)
os.Exit(1)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
logger.Info("正在关闭服务...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
server.Shutdown(ctx)
logger.Info("服务已关闭")
}
启动方式:
# 使用默认配置
go run main.go
# 自定义配置
APP_PORT=3000 APP_ENV=production LOG_LEVEL=debug go run main.go
测试请求:
# 健康检查
curl http://localhost:8080/health
# 主页
curl http://localhost:8080/
示例:Docker 部署完整方案(难度⭐⭐⭐)
包含 Dockerfile、docker-compose 和 Makefile 的完整部署方案:
项目结构:
myproject/
├── main.go
├── go.mod
├── go.sum
├── Dockerfile
├── docker-compose.yml
├── Makefile
└── .env.example
main.go:
package main
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"net/http"
_ "net/http/pprof"
"os"
"os/signal"
"runtime"
"syscall"
"time"
)
type AppConfig struct {
Port string
Env string
LogLevel string
PprofPort string
}
func loadConfig() AppConfig {
return AppConfig{
Port: getEnv("APP_PORT", "8080"),
Env: getEnv("APP_ENV", "production"),
LogLevel: getEnv("LOG_LEVEL", "info"),
PprofPort: getEnv("PPROF_PORT", "6060"),
}
}
func getEnv(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}
func setupLogger(level string) *slog.Logger {
var lvl slog.Level
switch level {
case "debug":
lvl = slog.LevelDebug
case "warn":
lvl = slog.LevelWarn
case "error":
lvl = slog.LevelError
default:
lvl = slog.LevelInfo
}
return slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: lvl}))
}
func main() {
cfg := loadConfig()
logger := setupLogger(cfg.LogLevel)
slog.SetDefault(logger)
// 启动 pprof(仅非生产环境或显式启用)
if cfg.Env != "production" || os.Getenv("ENABLE_PPROF") == "true" {
go func() {
logger.Info("pprof 服务启动", "port", cfg.PprofPort)
http.ListenAndServe(":"+cfg.PprofPort, nil)
}()
}
mux := http.NewServeMux()
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "ok",
"env": cfg.Env,
"goroutine": runtime.NumGoroutine(),
"uptime": time.Since(startTime).String(),
})
})
mux.HandleFunc("GET /api/info", func(w http.ResponseWriter, r *http.Request) {
logger.Info("info 请求", "remote", r.RemoteAddr)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"app": "myproject",
"version": version,
"go": runtime.Version(),
"env": cfg.Env,
})
})
mux.HandleFunc("POST /api/data", func(w http.ResponseWriter, r *http.Request) {
// 模拟数据处理
time.Sleep(100 * time.Millisecond)
logger.Info("数据处理完成", "content_length", r.ContentLength)
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]string{"result": "created"})
})
server := &http.Server{
Addr: ":" + cfg.Port,
Handler: mux,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
// 优雅关闭逻辑
done := make(chan struct{})
go func() {
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
sig := <-quit
logger.Info("收到关闭信号", "signal", sig)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// 停止 pprof
// 注意:标准库 pprof 没有 Shutdown,依赖进程退出即可
if err := server.Shutdown(ctx); err != nil {
logger.Error("服务关闭失败", "error", err)
}
close(done)
}()
logger.Info("服务启动",
"port", cfg.Port,
"env", cfg.Env,
"version", version,
)
if err := server.ListenAndServe(); err != http.ErrServerClosed {
logger.Error("服务异常退出", "error", err)
os.Exit(1)
}
<-done
logger.Info("服务已完全关闭")
}
var (
startTime = time.Now()
version = "1.0.0"
)
Dockerfile:
# ========== 构建阶段 ==========
FROM golang:1.22-alpine AS builder
# 安装 git(go mod 可能需要)
RUN apk add --no-cache git
WORKDIR /build
# 先复制依赖文件,利用 Docker 缓存层
COPY go.mod go.sum ./
RUN go mod download
# 复制源码并编译
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -ldflags="-s -w" -o /app/server .
# ========== 运行阶段 ==========
FROM alpine:3.19
# 安装 CA 证书(HTTPS 请求需要)和时区数据
RUN apk --no-cache add ca-certificates tzdata
# 创建非 root 用户
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
# 从构建阶段复制二进制文件
COPY --from=builder /app/server .
COPY --from=builder /build/.env.example .env.example
# 使用非 root 用户运行
USER appuser
EXPOSE 8080
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget -qO- http://localhost:8080/health || exit 1
CMD ["./server"]
docker-compose.yml:
version: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
environment:
- APP_ENV=production
- LOG_LEVEL=info
- APP_PORT=8080
- ENABLE_PPROF=false
restart: unless-stopped
deploy:
resources:
limits:
memory: 256M
cpus: '0.5'
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
Makefile:
APP_NAME := myproject
VERSION := 1.0.0
.PHONY: build run clean docker docker-run docker-stop
# 本地编译
build:
CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/$(APP_NAME) .
# 本地运行
run: build
./bin/$(APP_NAME)
# 清理
clean:
rm -rf bin/
# 构建 Docker 镜像
docker:
docker build -t $(APP_NAME):$(VERSION) .
# 启动 Docker 容器
docker-run:
docker-compose up -d
# 停止 Docker 容器
docker-stop:
docker-compose down
# 交叉编译所有平台
cross-build:
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/$(APP_NAME)-linux-amd64 .
GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/$(APP_NAME)-darwin-amd64 .
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/$(APP_NAME)-windows-amd64.exe .
.env.example:
APP_ENV=production
APP_PORT=8080
LOG_LEVEL=info
ENABLE_PPROF=false
PPROF_PORT=6060
构建和部署流程:
# 1. 本地测试
make run
# 2. 构建 Docker 镜像
make docker
# 3. 启动容器
make docker-run
# 4. 测试服务
curl http://localhost:8080/health
curl http://localhost:8080/api/info
# 5. 查看日志
docker-compose logs -f
# 6. 停止服务
make docker-stop
场景一:生产环境日志与监控
在一个实际的 Web 服务中,需要将日志收集到集中式日志系统:
package main
import (
"context"
"log/slog"
"net/http"
"os"
"time"
)
// RequestLogger 中间件:记录每个请求的详细信息
func RequestLogger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// 包装 ResponseWriter 以捕获状态码
rw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
next.ServeHTTP(rw, r)
slog.Info("HTTP 请求",
"method", r.Method,
"path", r.URL.Path,
"status", rw.statusCode,
"duration_ms", time.Since(start).Milliseconds(),
"remote_addr", r.RemoteAddr,
"user_agent", r.UserAgent(),
)
})
}
type responseWriter struct {
http.ResponseWriter
statusCode int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
// RecoveryMiddleware 捕获 panic 并记录日志
func RecoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
slog.Error("请求处理 panic",
"error", err,
"path", r.URL.Path,
"method", r.Method,
)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
func main() {
// 生产环境使用 JSON 格式,方便日志收集系统解析
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
slog.SetDefault(logger)
mux := http.NewServeMux()
mux.HandleFunc("GET /api/users", func(w http.ResponseWriter, r *http.Request) {
// 模拟业务逻辑
ctx := r.Context()
slog.DebugContext(ctx, "查询用户列表", "page", 1)
// ...
})
// 中间件链:Recovery -> Logger -> Router
handler := RecoveryMiddleware(RequestLogger(mux))
server := &http.Server{Addr: ":8080", Handler: handler}
slog.Info("服务启动", "addr", server.Addr)
server.ListenAndServe()
}
运行后,每个请求都会输出结构化日志:
{"time":"2024-01-15T10:30:00Z","level":"INFO","msg":"HTTP 请求","method":"GET","path":"/api/users","status":200,"duration_ms":5,"remote_addr":"127.0.0.1","user_agent":"curl/7.68.0"}
场景二:使用 pprof 定位性能瓶颈
当服务响应变慢时,使用 pprof 进行诊断:
package main
import (
"fmt"
"net/http"
_ "net/http/pprof"
"runtime"
"strings"
"time"
)
func main() {
// 主服务
mux := http.NewServeMux()
// 模拟一个有性能问题的接口
mux.HandleFunc("GET /api/search", func(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("q")
result := slowSearch(query) // 故意写慢的函数
fmt.Fprint(w, result)
})
// pprof 在默认 http.DefaultServeMux 上注册,需要单独端口
go func() {
fmt.Println("pprof: http://localhost:6060/debug/pprof/")
http.ListenAndServe(":6060", nil)
}()
fmt.Println("主服务: http://localhost:8080")
http.ListenAndServe(":8080", mux)
}
// slowSearch 模拟一个 CPU 密集且有内存分配问题的函数
func slowSearch(query string) string {
var results []string
// 故意在循环中反复分配内存
for i := 0; i < 100000; i++ {
data := fmt.Sprintf("item_%d_%s", i, query) // 每次循环都分配新字符串
if strings.Contains(data, query) {
results = append(results, data)
}
}
// 模拟额外耗时
time.Sleep(50 * time.Millisecond)
return fmt.Sprintf("找到 %d 条结果", len(results))
}
使用 pprof 进行分析:
# 1. 安装 pprof 工具(如果还没有)
go install github.com/google/pprof@latest
# 2. 采集 30 秒 CPU profile
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
# 3. 在 pprof 交互界面中查看
(pprof) top # 查看最耗 CPU 的函数
(pprof) list slowSearch # 查看具体代码行的耗时
(pprof) web # 生成可视化图(需要安装 graphviz)
# 4. 查看内存分配
go tool pprof http://localhost:6060/debug/pprof/heap
# 5. 查看 goroutine 数量(检测泄漏)
go tool pprof http://localhost:6060/debug/pprof/goroutine
# 6. 使用浏览器查看所有指标
# 打开 http://localhost:6060/debug/pprof/
❓ 常见问题
Q1:交叉编译时遇到 cgo: exec gcc: exec: "gcc": not found 怎么办?
这是因为代码中使用了 CGO。如果不需要 C 库依赖,设置 CGO_ENABLED=0 即可:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o myapp .
如果确实需要 CGO,需要安装目标平台的交叉编译工具链(如 gcc-aarch64-linux-gnu)。
Q2:Docker 镜像太大,如何进一步减小体积?
使用 scratch 作为基础镜像(无任何系统工具):
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app/server /server
CMD ["/server"]
配合编译时去除调试信息:go build -ldflags="-s -w"。-s 去除符号表,-w 去除 DWARF 调试信息,通常能减小 20-30% 的体积。
Q3:log/slog 和第三方日志库(如 zap、zerolog)相比如何?
log/slog 的优势是标准库,无需额外依赖,性能对大多数场景足够。如果你的应用日志量极大(每秒数万条),zap 的零分配设计会更快。建议新项目先用 slog,遇到性能瓶颈再考虑切换。
Q4:优雅关闭时,如何确保正在处理的 WebSocket 连接也被正确处理?
http.Server.Shutdown() 只处理 HTTP 请求。对于长连接(WebSocket),需要自己管理连接池:
var connections sync.Map // 存储活跃连接
// 新连接时注册
connections.Store(conn, true)
// 关闭时遍历所有连接,发送关闭帧
connections.Range(func(key, value any) bool {
wsConn := key.(*websocket.Conn)
wsConn.WriteMessage(websocket.CloseMessage, closeMsg)
wsConn.Close()
return true
})
📖 小节
本课学习了 Go 项目从开发到部署的完整流程:
- 交叉编译 让你在任何平台上编译目标平台的二进制文件,Go 原生支持,设置
GOOS和GOARCH即可 - Docker 多阶段构建 将编译环境与运行环境分离,大幅减小镜像体积,同时提升安全性
- 环境变量配置 是 12-Factor App 的最佳实践,让同一份代码适配不同环境
log/slog提供标准库级别的结构化日志,JSON 格式便于日志收集系统解析pprof是 Go 内置的性能分析工具,可以定位 CPU、内存、goroutine 等问题- 优雅关闭 确保服务收到终止信号后,等待正在处理的请求完成再退出,避免数据丢失
掌握这些技能后,你就能将 Go 项目安全、高效地部署到生产环境了。
📝 作业
练习 1:编写交叉编译脚本
编写一个 Go 程序,自动编译当前项目到 linux/amd64、darwin/arm64、windows/amd64 三个平台,输出文件名包含版本号和平台信息。
练习 2:添加请求日志中间件
为一个 HTTP 服务添加日志中间件,记录每个请求的:方法、路径、状态码、响应时间、客户端 IP。使用 log/slog 输出 JSON 格式。
练习 3:Docker 部署你的项目
为你之前课程中写过的任意一个项目(如 Todo API)编写完整的 Dockerfile 和 docker-compose.yml,要求:
- 使用多阶段构建
- 最终镜像基于
alpine或scratch - 包含健康检查
- 使用非 root 用户运行
下一课
完成本课后,你已经掌握了 Go 项目部署的核心技能。接下来我们将进入实战项目阶段:第29课:实战项目(上)



