404 Not Found

404 Not Found


nginx

项目部署与优化

第28课:项目部署与优化

生活类比

想象你开了一家餐厅。菜谱写好了(代码写完了),但要真正营业,你需要:

核心概念

概念 说明
交叉编译 在一个平台上编译出另一个平台的可执行文件
多阶段构建 Docker 构建时分离编译环境和运行环境,减小镜像体积
环境变量 在不修改代码的情况下配置程序行为
结构化日志 使用 log/slog 输出机器可解析的日志格式
性能分析 使用 pprof 找出 CPU 和内存瓶颈
优雅关闭 收到终止信号后,等待正在处理的请求完成再退出

基本语法与用法

1. 交叉编译

Go 原生支持交叉编译,只需设置 GOOSGOARCH 环境变量:

BASH
# 编译 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 多阶段构建

DOCKERFILE
# ---- 构建阶段 ----
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"]
💡 提示:最终镜像只包含编译好的二进制文件和最小运行环境,体积可从 1GB 缩小到 20MB 以内。

3. 环境变量配置

GO
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 格式输出:

GO
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)

GO
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. 优雅关闭

GO
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() 会停止接受新连接,等待已有连接处理完毕后返回。


示例:基础交叉编译脚本(难度⭐)

创建一个脚本,一键编译多个平台的二进制文件:

GO
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("全部编译完成!")
}
▶ 试一试

运行方式:

BASH
go run build.go

输出目录结构:

TEXT
dist/
├── myapp-darwin-amd64
├── myapp-darwin-arm64
├── myapp-linux-amd64
├── myapp-linux-arm64
└── myapp-windows-amd64.exe

示例:带环境变量配置的 HTTP 服务(难度⭐⭐)

演示完整的环境变量配置方案:

GO
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("服务已关闭")
}
▶ 试一试

启动方式:

BASH
# 使用默认配置
go run main.go

# 自定义配置
APP_PORT=3000 APP_ENV=production LOG_LEVEL=debug go run main.go

测试请求:

BASH
# 健康检查
curl http://localhost:8080/health

# 主页
curl http://localhost:8080/

示例:Docker 部署完整方案(难度⭐⭐⭐)

包含 Dockerfile、docker-compose 和 Makefile 的完整部署方案:

项目结构:

TEXT
myproject/
├── main.go
├── go.mod
├── go.sum
├── Dockerfile
├── docker-compose.yml
├── Makefile
└── .env.example
▶ 试一试

main.go:

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:

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:

YAML
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:

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:

TEXT
APP_ENV=production
APP_PORT=8080
LOG_LEVEL=info
ENABLE_PPROF=false
PPROF_PORT=6060

构建和部署流程:

BASH
# 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 服务中,需要将日志收集到集中式日志系统:

GO
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()
}

运行后,每个请求都会输出结构化日志:

JSON
{"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 进行诊断:

GO
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 进行分析:

BASH
# 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 即可:

BASH
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o myapp .

如果确实需要 CGO,需要安装目标平台的交叉编译工具链(如 gcc-aarch64-linux-gnu)。

Q2:Docker 镜像太大,如何进一步减小体积?

使用 scratch 作为基础镜像(无任何系统工具):

DOCKERFILE
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),需要自己管理连接池:

GO
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 项目安全、高效地部署到生产环境了。


📝 作业

练习 1:编写交叉编译脚本

编写一个 Go 程序,自动编译当前项目到 linux/amd64darwin/arm64windows/amd64 三个平台,输出文件名包含版本号和平台信息。

练习 2:添加请求日志中间件

为一个 HTTP 服务添加日志中间件,记录每个请求的:方法、路径、状态码、响应时间、客户端 IP。使用 log/slog 输出 JSON 格式。

练习 3:Docker 部署你的项目

为你之前课程中写过的任意一个项目(如 Todo API)编写完整的 Dockerfile 和 docker-compose.yml,要求:


下一课

完成本课后,你已经掌握了 Go 项目部署的核心技能。接下来我们将进入实战项目阶段:第29课:实战项目(上)

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏