目录
- [Functional Options Pattern](#Functional Options Pattern)
- [Singleflight 模式](#Singleflight 模式)
- [Fan-out / Fan-in模式](#Fan-out / Fan-in模式)
- [中间件洋葱模型(Middleware / Decorator Pattern)](#中间件洋葱模型(Middleware / Decorator Pattern))
- [Context 级联取消与超时模型(Context Propagation)](#Context 级联取消与超时模型(Context Propagation))
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:
}
}
}