Go语言常用设计模式基础和实践案例介绍

目录

Functional Options Pattern

基础案例

go 复制代码
package main

import (
	"fmt"
	"time"
)

// Server 目标对象
type Server struct {
	addr    string
	port    int
	timeout time.Duration // 可选参数
	maxConn int           // 可选参数
}

// Option 定义一个修改 Server 的函数类型
type Option func(*Server)

// 各个可选配置的构造函数(闭包)
func WithTimeout(timeout time.Duration) Option {
	return func(s *Server) {
		s.timeout = timeout
	}
}

func WithMaxConn(maxConn int) Option {
	return func(s *Server) {
		s.maxConn = maxConn
	}
}

// NewServer 构造函数:必填参数放前面,可选参数用不定参数 ...Option 接收
func NewServer(addr string, port int, opts ...Option) *Server {
	// 1. 设置默认值
	srv := &Server{
		addr:    addr,
		port:    port,
		timeout: 30 * time.Second, // 默认 30s
		maxConn: 100,              // 默认 100
	}

	// 2. 按需应用外部传入的 Option
	for _, opt := range opts {
		opt(srv)
	}

	return srv
}

func main() {
	// 场景 1:使用全默认配置
	s1 := NewServer("127.0.0.1", 8080)

	// 场景 2:只覆盖 timeout
	s2 := NewServer("127.0.0.1", 8080, WithTimeout(5*time.Second))

	// 场景 3:自由组合配置
	s3 := NewServer("127.0.0.1", 8080,
		WithTimeout(10*time.Second),
		WithMaxConn(500),
	)

	fmt.Printf("s1: %+v\ns2: %+v\ns3: %+v\n", s1, s2, s3)
}

实践案例

Docker 的 Go 客户端初始化非常复杂,涉及 API 版本协商、TLS 证书、自定义 HTTP Client、Host 环境变量等。Docker 团队完全抛弃了传统的配置结构体,采用了标准的 Functional Options。

真实出处:Docker 官方 Go SDK (moby/moby/client)

代码路径:client/client.go

go 复制代码
// 1. 定义修改 Client 的函数类型
type Opt func(*Client) error

// 2. 各种 Option 的构造器
// 从环境变量读取 DOCKER_HOST / DOCKER_CERT_PATH
func FromEnv(c *Client) error {
    if dockerHost := os.Getenv("DOCKER_HOST"); dockerHost != "" {
        return WithHost(dockerHost)(c)
    }
    return nil
}

// 自动协商服务端支持的最高 API 版本
func WithAPIVersionNegotiation() Opt {
    return func(c *Client) error {
        c.manualOverride = false
        c.negotiateVersion = true
        return nil
    }
}

// 3. NewClientWithOpts 统一构造器
func NewClientWithOpts(ops ...Opt) (*Client, error) {
    c := &Client{
        host:    DefaultDockerHost,
        version: DefaultVersion,
        client:  defaultHTTPClient(),
    }
    // 遍历执行传入的 Option 闭包
    for _, op := range ops {
        if err := op(c); err != nil {
            return nil, err
        }
    }
    return c, nil
}

Singleflight 模式

基础案例

go 复制代码
package main

import (
	"fmt"
	"sync"
	"time"
	"golang.org/x/sync/singleflight"
)

var (
	g           singleflight.Group
	mockDBCalls int
	mu          sync.Mutex
)

// 模拟慢速数据库查询
func queryDB(key string) (string, error) {
	time.Sleep(100 * time.Millisecond) // 模拟 DB 耗时
	mu.Lock()
	mockDBCalls++
	mu.Unlock()
	return fmt.Sprintf("value-for-%s", key), nil
}

// 获取数据:使用 singleflight 防击穿
func getData(key string) (string, error) {
	// 无论瞬间来了多少个并发请求,相同的 key 对应的 queryDB 只会被执行 1 次!
	v, err, shared := g.Do(key, func() (interface{}, error) {
		return queryDB(key)
	})

	if shared {
		// 表示当前请求复用了其他协程的结果
		fmt.Printf("[共享结果] key: %s\n", key)
	}

	return v.(string), err
}

func main() {
	var wg sync.WaitGroup

	// 瞬间并发发起 10 个相同 key 的读取
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			val, _ := getData("user:1001")
			_ = val
		}(i)
	}

	wg.Wait()
	fmt.Printf("10 个并发请求完成,实际落到 DB 的查询次数: %d 次\n", mockDBCalls)
}

实践案例

K8s 集群中可能有几百个 Controller 同时向 API Server 请求相同的 Pod/Node 资源元数据。如果本地缓存穿透,不能让这几百个请求同时打垮后端的 etcd 数据库。K8s 直接用 singleflight.Group 来合并对 etcd 的底层读取。

真实出处:Kubernetes API Server 缓存层 (kubernetes/kubernetes)

代码路径:staging/src/k8s.io/apiserver/pkg/storage/cacher/cacher.go

go 复制代码
package cacher

import (
    "golang.org/x/sync/singleflight"
    "k8s.io/apiserver/pkg/storage"
)

type Cacher struct {
    storage        storage.Interface
    group          singleflight.Group // 声明 singleflight 控制组
    // ...
}

func (c *Cacher) Get(ctx context.Context, key string, opts storage.GetOptions, obj runtime.Object) error {
    // 1. 先查本地内存缓存...
    if found := c.getFromCache(key, obj); found {
        return nil
    }

    // 2. 本地缓存未命中,使用 singleflight 合并查底层的 etcd
    // 瞬间 100 个相同 key 的读取,只会发起 1 次真实的 storage.Get 请求
    res, err, _ := c.group.Do(key, func() (interface{}, error) {
        return c.storage.Get(ctx, key, opts, obj)
    })

    return err
}

Fan-out / Fan-in模式

基础案例

go 复制代码
package main

import (
	"fmt"
	"sync"
)

// 1. 数据源生成器
func generator(nums ...int) <-chan int {
	out := make(chan int)
	go func() {
		defer close(out)
		for _, n := range nums {
			out <- n
		}
	}()
	return out
}

// 2. Worker:并发计算平方(扇出)
func squareWorker(in <-chan int) <-chan int {
	out := make(chan int)
	go func() {
		defer close(out)
		for n := range in {
			out <- n * n // 模拟耗时计算
		}
	}()
	return out
}

// 3. 扇入:将多个 channel 合并为一个 channel
func merge(channels ...<-chan int) <-chan int {
	var wg sync.WaitGroup
	out := make(chan int)

	// 为每个输入 channel 启动一个收集协程
	output := func(c <-chan int) {
		defer wg.Done()
		for n := range c {
			out <- n
		}
	}

	wg.Add(len(channels))
	for _, c := range channels {
		go output(c)
	}

	// 启动一个守护协程,等所有 worker 结束后关闭输出通道
	go func() {
		wg.Wait()
		close(out)
	}()

	return out
}

func main() {
	in := generator(1, 2, 3, 4, 5, 6, 7, 8)

	// 扇出:启动 3 个 worker 并行消费同一个 in
	w1 := squareWorker(in)
	w2 := squareWorker(in)
	w3 := squareWorker(in)

	// 扇入:合并 3 个 worker 的产物
	for result := range merge(w1, w2, w3) {
		fmt.Printf("%d ", result)
	}
	fmt.Println()
}

实践案例

拉取镜像(docker pull)时,一个镜像包含多层(Layers,如 10 个 rootfs diff 包)。Docker 会扇出多个 Goroutine 并发从镜像仓库下载各个 Layer,然后通过扇入汇总下载进度更新 CLI 进度条,全部完成后合并校验。

真实出处:Docker / Containerd 镜像分层并行下载 (containerd/containerd)

代码路径:pkg/image/pull.go / distribution/

go 复制代码
func PullLayers(ctx context.Context, layers []LayerDescriptor) error {
    layerChan := make(chan LayerDescriptor, len(layers))
    resultChan := make(chan DownloadResult, len(layers))

    // 1. 将所有待拉取的 Layer 扔进通道
    for _, l := range layers {
        layerChan <- l
    }
    close(layerChan)

    // 2. 【扇出】:启动固定数量 (如 5 个) 的 Worker 协程并发下载
    var wg sync.WaitGroup
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for layer := range layerChan {
                // 并发下载单个 Layer
                res := downloadSingleLayer(ctx, layer)
                resultChan <- res
            }
        }()
    }

    // 守护协程:等所有 Worker 结束后关闭结果通道
    go func() {
        wg.Wait()
        close(resultChan)
    }()

    // 3. 【扇入】:汇总所有分层的下载结果与进度
    for res := range resultChan {
        if res.Err != nil {
            return fmt.Errorf("layer %s 下载失败: %w", res.LayerID, res.Err)
        }
        updateProgressBar(res.Progress)
    }

    return nil
}

中间件洋葱模型(Middleware / Decorator Pattern)

基础案例

go 复制代码
package main

import (
	"fmt"
	"net/http"
	"time"
)

// Middleware 签名:接收一个 Handler,返回包裹后的 Handler
type Middleware func(http.Handler) http.Handler

// 1. 日志与耗时中间件
func LoggingMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()
		fmt.Println("[Start] 请求进入:", r.URL.Path)

		next.ServeHTTP(w, r) // 执行下游业务

		fmt.Printf("[End] 请求完成: %s, 耗时: %v\n", r.URL.Path, time.Since(start))
	})
}

// 2. 鉴权中间件
func AuthMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Header.Get("X-Token") == "" {
			fmt.Println("[Auth] 未携带 Token,拦截请求")
			http.Error(w, "Unauthorized", http.StatusUnauthorized)
			return // 阻断洋葱执行链
		}
		next.ServeHTTP(w, r)
	})
}

// 业务核心 Handler
func UserHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Println("  --> 执行核心业务逻辑: 查询用户信息")
	w.Write([]byte("Hello, User"))
}

// 链式组装工具
func Chain(h http.Handler, middlewares ...Middleware) http.Handler {
	for i := len(middlewares) - 1; i >= 0; i-- {
		h = middlewares[i](h)
	}
	return h
}

func main() {
	// 将中间件层层嵌套包裹
	handler := Chain(
		http.HandlerFunc(UserHandler),
		LoggingMiddleware,
		AuthMiddleware,
	)

	// 注册到路由
	http.Handle("/user", handler)
}

实践案例

K8s API Server 处理任何一个 kubectl 请求前,必须经过严密的:审计日志 (Audit) 认证 (AuthN) 鉴权 (AuthZ) 速率限制 (RateLimit) 故障恢复 (Panic Recovery)。K8s 没有使用任何框架,纯靠 Go 的高阶函数组合出洋葱调用链。

真实出处:Kubernetes API Server 全局过滤器链 (kubernetes/kubernetes)

代码路径:staging/src/k8s.io/apiserver/pkg/server/config.go

go 复制代码
// DefaultBuildHandlerChain 构建 API Server 的洋葱模型中间件链
func DefaultBuildHandlerChain(apiHandler http.Handler, c *Config) http.Handler {
    handler := apiHandler

    // 从内向外层层包裹 Handler(洋葱结构)
    handler = filters.WithAuthorization(handler, c.Authorization.Authorizer, c.Serializer)
    handler = filters.WithAuthentication(handler, c.Authentication.Authenticator, ...)
    handler = filters.WithAudit(handler, c.AuditBackend, ...)
    handler = filters.WithRateLimit(handler, c.MaxRequestsInFlight)
    handler = filters.WithPanicRecovery(handler, ...)

    // 最终返回最外层的 Handler
    return handler
}

Context 级联取消与超时模型(Context Propagation)

基础案例

go 复制代码
package main

import (
	"context"
	"fmt"
	"time"
)

func queryServiceA(ctx context.Context) {
	select {
	case <-time.After(500 * time.Millisecond): // 模拟耗时 500ms
		fmt.Println("Service A 成功返回")
	case <-ctx.Done(): // 收到上游取消/超时信号
		fmt.Println("Service A 被取消:", ctx.Err())
	}
}

func queryServiceB(ctx context.Context) {
	select {
	case <-time.After(100 * time.Millisecond): // 模拟耗时 100ms
		fmt.Println("Service B 成功返回")
	case <-ctx.Done():
		fmt.Println("Service B 被取消:", ctx.Err())
	}
}

func main() {
	// 设置总超时时间为 200ms
	ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
	defer cancel()

	// 并发调用两个下游服务,透传同一个 ctx
	go queryServiceA(ctx)
	go queryServiceB(ctx)

	// 主协程等待 1 秒观察结果
	time.Sleep(1 * time.Second)
}

实践案例

go 复制代码
// Run 是所有 K8s Controller 的标准入口
func (dsc *DaemonSetsController) Run(ctx context.Context, workers int) {
    defer utilruntime.HandleCrash()
    defer dsc.queue.ShutDown()

    klog.Info("Starting daemon sets controller")
    defer klog.Info("Shutting down daemon sets controller")

    // 启动多个 Worker 协程处理事件
    for i := 0; i < workers; i++ {
        go wait.UntilWithContext(ctx, dsc.runWorker, time.Second)
    }

    // 【核心控制点】:阻塞等待上游的停止信号 (context.Done)
    <-ctx.Done()
    
    // 一旦收到信号,后续代码执行优雅停机
}

func (dsc *DaemonSetsController) runWorker(ctx context.Context) {
    for dsc.processNextWorkItem(ctx) {
        select {
        // 任何一个 Worker 都在感知 Context 取消事件
        case <-ctx.Done():
            return
        default:
        }
    }
}
相关推荐
MayZork1 小时前
std::filesystem 文件操作详解
开发语言·c++·qt
Nebula_g2 小时前
JavaSE加强:IO流
java·开发语言·windows·stream·javase
Ada's10 小时前
【计算机基础系列】003:Python数据结构
开发语言·数据结构·python
2601_9628857211 小时前
如何用 Python 计算 TRIX 三重指数平滑均线指标?
开发语言·python
还卿一钵无情泪11 小时前
Unsloth 微调 构建自己的大模型 没有GPU也能微调
linux·开发语言·人工智能·python·大模型·nlp·unsloth
倔强的石头10613 小时前
应用账号最小权限实践:读写账号、报表账号、运维账号分层
java·运维·开发语言
传人once13 小时前
页面中心圆圈放大效果如何写
开发语言·javascript·ecmascript
NeilYuen13 小时前
【C++】STL源码仿写(二):vector
开发语言·c++
蜗牛互联网13 小时前
Python消费Responses SSE事件:增量文本、超时与取消
java·开发语言·人工智能·后端·python