前言
上个月开发一个配置同步工具,需要从多种数据源读取配置(本地文件、HTTP接口、Git仓库),然后执行一系列处理(验证格式、解密敏感信息、过滤无效配置),最后推送到服务端。
我一开始用if-else判断数据源类型:
if source.Type == "file" {
// 读取文件...
} else if source.Type == "http" {
// 请求HTTP...
} else if source.Type == "git" {
// 拉取Git...
}
代码很快变成了难以维护的"意大利面条"。更痛苦的是,每次新增一种处理方式,都要修改好几个地方的代码。
后来看了kubectl的源码,发现它用Visitor模式处理这个问题——无论是文件、URL还是stdin,都用统一的Visit()方法遍历处理。新增数据源或处理逻辑时,完全不需要改动原有代码。
今天我就结合kubectl的真实实现,聊聊这个在数据处理场景中超实用的设计模式。
什么是Visitor模式?
经典定义
Visitor模式(访问者模式)是一种行为型设计模式,它的核心思想是:
将数据结构的遍历与对元素的操作分离,使得可以在不改变数据结构的前提下定义作用于这些元素的新操作。
用大白话说就是:数据是数据,操作是操作,两者解耦。
为什么需要Visitor模式?
想象这样一个场景:你有一个异构数据集合,包含多种类型的元素,需要对这些元素执行各种操作。
不用Visitor模式的痛点:
// 反例:把操作逻辑耦合在数据结构中
type FileData struct {
Path string
}
func (f *FileData) Process() {
// 文件处理逻辑
}
func (f *FileData) Validate() {
// 文件验证逻辑
}
func (f *FileData) Encrypt() {
// 文件加密逻辑
}
// 每新增一种操作,都要修改FileData结构!
问题:
- 新增操作需要修改数据结构,违反开闭原则
- 数据结构越来越臃肿
- 不同操作混在一起,难以维护
Visitor模式的结构
┌─────────────────────────────────────┐
│ Visitor Interface │
│ Visit(Element) error │
└─────────────┬───────────────────────┘
│
┌─────────┼─────────┐
↓ ↓ ↓
┌───────┐ ┌───────┐ ┌───────┐
│FileVisitor│ │URLVisitor│ │StdinVisitor│
└───────┘ └───────┘ └───────┘
│
↓
┌─────────────────────────────────────┐
│ Element (Data) │
└─────────────────────────────────────┘
核心组件:
| 角色 | 职责 | kubectl中的对应 |
|---|---|---|
| Visitor | 声明访问接口 | Visitor接口 |
| ConcreteVisitor | 实现具体访问逻辑 | FileVisitor, StreamVisitor等 |
| Element | 被访问的数据元素 | Info结构体 |
| VisitorFunc | 对元素的操作函数 | VisitorFunc回调 |
kubectl的Visitor模式实战
kubectl需要处理来自不同数据源的资源(本地文件、HTTP、stdin、Kustomize等),这正是Visitor模式的典型应用场景。
Visitor接口定义
在pkg/resource/interfaces.go中,kubectl定义了简洁的Visitor接口:
// Visitor lets clients walk a list of resources.
// Visitor让客户端能够遍历资源列表
type Visitor interface {
Visit(VisitorFunc) error
}
// VisitorFunc implements the Visitor interface for a matching function.
// 对单个资源元素的处理函数
// 如果遍历过程中出错,传入的error会描述问题,函数可以决定如何处理
// 返回nil表示接受错误并继续遍历,返回error则停止遍历
type VisitorFunc func(*Info, error) error
// Info contains temporary info to execute a REST call
// Info包含了执行REST调用所需的临时信息
type Info struct {
Source string // 数据来源(文件名/URL)
Namespace string // 命名空间
Name string // 资源名称
ResourceVersion string // 资源版本(用于乐观锁)
Object runtime.Object // 解码后的K8s对象
Mapping *meta.RESTMapping // GVK到GVR的映射
Client RESTClient // REST客户端
// ... 其他字段
}
设计要点:
Visitor只定义一个方法Visit,接收一个处理函数VisitorFunc是实际的业务逻辑,操作*Info对象Info是统一的数据结构,屏蔽了底层数据源差异
各种Visitor实现
kubectl实现了多种Visitor来处理不同的数据源:
1. FileVisitor:文件数据源
// FileVisitor处理本地文件
type FileVisitor struct {
Path string // 文件路径
*StreamVisitor // 嵌入StreamVisitor处理文件内容
}
func (v *FileVisitor) Visit(fn VisitorFunc) error {
var f *os.File
// 支持stdin(路径为"-")
if v.Path == constSTDINstr {
f = os.Stdin
} else {
// 打开本地文件
var err error
f, err = os.Open(v.Path)
if err != nil {
return err
}
defer f.Close()
}
// 处理UTF-16 BOM(Windows兼容性)
utf16bom := unicode.BOMOverride(unicode.UTF8.NewDecoder())
v.StreamVisitor.Reader = transform.NewReader(f, utf16bom)
// 委托给StreamVisitor处理内容
return v.StreamVisitor.Visit(fn)
}
特点:
- 负责文件的打开和关闭(资源管理)
- 处理编码问题(UTF-16 BOM)
- 实际内容解析委托给
StreamVisitor
2. URLVisitor:HTTP数据源
// URLVisitor处理HTTP/HTTPS URL
type URLVisitor struct {
URL *url.URL
*StreamVisitor
HTTPAttemptCount int // 重试次数
}
func (v *URLVisitor) Visit(fn VisitorFunc) error {
// HTTP GET请求获取内容
body, err := readHttpWithRetries(
v.HTTPAttemptCount,
"GET",
v.URL.String(),
)
if err != nil {
return err
}
defer body.Close()
// 设置Reader并委托给StreamVisitor
v.StreamVisitor.Reader = body
return v.StreamVisitor.Visit(fn)
}
特点:
- 支持HTTP/HTTPS协议
- 内置重试机制
- 统一的StreamVisitor处理流式数据
3. StreamVisitor:统一的内容处理
// StreamVisitor处理任意io.Reader
type StreamVisitor struct {
Reader io.Reader
Mapper *mapper
Source string
Schema ContentValidator
}
func (v *StreamVisitor) Visit(fn VisitorFunc) error {
// 使用YAML/JSON解码器
d := yaml.NewYAMLOrJSONDecoder(v.Reader, 4096)
// 循环解码多个资源对象(一个文件可能有多个YAML文档)
for {
ext := runtime.RawExtension{}
// 解码一个对象
if err := d.Decode(&ext); err != nil {
if err == io.EOF {
return nil // 正常结束
}
return fmt.Errorf("error parsing %s: %v", v.Source, err)
}
// 跳过空文档
ext.Raw = bytes.TrimSpace(ext.Raw)
if len(ext.Raw) == 0 || bytes.Equal(ext.Raw, []byte("null")) {
continue
}
// Schema验证(可选)
if err := ValidateSchema(ext.Raw, v.Schema); err != nil {
return fmt.Errorf("error validating %q: %v", v.Source, err)
}
// 转换为Info对象
info, err := v.infoForData(ext.Raw, v.Source)
if err != nil {
// 调用VisitorFunc处理错误(可以选择继续或中断)
if fnErr := fn(info, err); fnErr != nil {
return fnErr
}
continue
}
// 调用VisitorFunc处理正常数据
if err := fn(info, nil); err != nil {
return err
}
}
}
关键点:
- 一个文件可能包含多个YAML文档(用
---分隔),所以需要循环解码 - 支持YAML和JSON格式自动识别
- 错误处理灵活:可以通过VisitorFunc决定遇到错误是继续还是中断
Info对象:统一的数据表示
StreamVisitor通过infoForData将原始数据转换为统一的Info对象:
func (m *mapper) infoForData(data []byte, source string) (*Info, error) {
// 1. 解码YAML/JSON为runtime.Object
obj, gvk, err := m.decoder.Decode(data, nil, nil)
if err != nil {
return nil, fmt.Errorf("unable to decode %q: %v", source, err)
}
// 2. 提取元数据
name, _ := metadataAccessor.Name(obj)
namespace, _ := metadataAccessor.Namespace(obj)
resourceVersion, _ := metadataAccessor.ResourceVersion(obj)
// 3. 构建Info对象
info := &Info{
Source: source,
Namespace: namespace,
Name: name,
ResourceVersion: resourceVersion,
Object: obj,
}
// 4. 如果不是本地模式,获取RESTMapping和Client
if m.localFn == nil || !m.localFn() {
restMapper, err := m.restMapperFn()
if err != nil {
return nil, err
}
// GVK -> GVR的映射(如Pod -> pods)
mapping, err := restMapper.RESTMapping(gvk.GroupKind(), gvk.Version)
if err != nil {
return nil, fmt.Errorf("unable to recognize %q: %v", source, err)
}
info.Mapping = mapping
// 创建REST客户端
client, err := m.clientFn(gvk.GroupVersion())
if err != nil {
return nil, fmt.Errorf("unable to connect to a server: %v", err)
}
info.Client = client
}
return info, nil
}
Info对象的设计意义:
- 屏蔽数据源差异:无论来自文件、URL还是stdin,最终都是
*Info - 包含完整上下文:从原始数据到API调用的所有信息
- 延迟加载:Client和Mapping按需创建
DecoratedVisitor:装饰器模式增强
kubectl还使用了装饰器模式来增强Visitor功能,这是Visitor模式的高级应用。
场景:给Visitor添加通用处理
在真正执行业务逻辑前,kubectl需要对每个资源执行一些通用操作:
- 设置默认namespace
- 校验namespace一致性
- 确保对象非空
如果用继承或修改原有Visitor,会违反开闭原则。kubectl使用装饰器模式优雅解决这个问题:
// DecoratedVisitor在原有Visitor基础上添加装饰函数
type DecoratedVisitor struct {
visitor Visitor // 被装饰的Visitor
decorators []VisitorFunc // 装饰函数列表
}
func NewDecoratedVisitor(v Visitor, fn ...VisitorFunc) Visitor {
return &DecoratedVisitor{visitor: v, decorators: fn}
}
func (v DecoratedVisitor) Visit(fn VisitorFunc) error {
return v.visitor.Visit(func(info *Info, err error) error {
if err != nil {
return err
}
// 先执行所有装饰函数
for i := range v.decorators {
if err := v.decorators[i](info, nil); err != nil {
return err
}
}
// 再执行真正的业务逻辑
return fn(info, nil)
})
}
装饰函数示例
// SetNamespace 设置默认namespace
func SetNamespace(namespace string) VisitorFunc {
return func(info *Info, err error) error {
if err != nil {
return err
}
if info.Namespaced() && len(info.Namespace) == 0 {
info.Namespace = namespace
UpdateObjectNamespace(info, nil)
}
return nil
}
}
// RequireNamespace 要求必须匹配指定namespace
func RequireNamespace(namespace string) VisitorFunc {
return func(info *Info, err error) error {
if err != nil {
return err
}
if !info.Namespaced() {
return nil
}
if len(info.Namespace) == 0 {
info.Namespace = namespace
return nil
}
if info.Namespace != namespace {
return fmt.Errorf(
"the namespace from the provided object %q does not match the namespace %q",
info.Namespace, namespace,
)
}
return nil
}
}
// FilterNamespace 过滤namespace相关信息
func FilterNamespace(info *Info, err error) error {
if err != nil {
return err
}
// 清理内部namespace标记
if info.Object != nil {
acc, _ := meta.Accessor(info.Object)
if acc != nil {
acc.SetNamespace("")
}
}
return nil
}
// RetrieveLazy 确保对象已加载
func RetrieveLazy(info *Info, err error) error {
if err != nil {
return err
}
if info.Object != nil {
return nil
}
// 延迟加载对象
return info.Get()
}
组合使用装饰器
// 在Builder.Do()中组合装饰器
func (b *Builder) Do() *Result {
r := b.visitorResult()
// 收集装饰函数
helpers := []VisitorFunc{}
if b.defaultNamespace {
helpers = append(helpers, SetNamespace(b.namespace))
}
if b.requireNamespace {
helpers = append(helpers, RequireNamespace(b.namespace))
}
helpers = append(helpers, FilterNamespace)
if b.requireObject {
helpers = append(helpers, RetrieveLazy)
}
// 创建装饰后的Visitor
if b.continueOnError {
// ContinueOnErrorVisitor是另一个装饰器
r.visitor = NewDecoratedVisitor(
ContinueOnErrorVisitor{r.visitor},
helpers...,
)
} else {
r.visitor = NewDecoratedVisitor(r.visitor, helpers...)
}
return r
}
装饰器模式的优势:
- 开闭原则:新增处理逻辑只需添加装饰函数,不改原有代码
- 灵活组合:根据需要组合不同的装饰函数
- 单一职责:每个装饰函数只做一件事
ContinueOnErrorVisitor:错误处理装饰器
// ContinueOnErrorVisitor收集所有错误,最后一起返回
type ContinueOnErrorVisitor struct {
Visitor
}
func (v ContinueOnErrorVisitor) Visit(fn VisitorFunc) error {
errs := []error{}
err := v.Visitor.Visit(func(info *Info, err error) error {
if err != nil {
errs = append(errs, err)
return nil // 继续遍历
}
if err := fn(info, nil); err != nil {
errs = append(errs, err)
return nil // 继续遍历
}
return nil
})
if err != nil {
errs = append(errs, err)
}
// 返回所有错误的聚合
return utilerrors.NewAggregate(errs)
}
使用场景: 批量处理资源时,一个失败不应该影响其他资源的处理。
kubectl create中的完整调用链
让我们完整走一遍kubectl create的Visitor调用链:
用户执行:kubectl create -f app.yaml
1. FilenameParam解析-f参数
└─ 创建FileVisitor(如果是文件)
└─ 嵌入StreamVisitor
2. Builder.Do()构建Visitor链
└─ 创建DecoratedVisitor
├─ 包装:FlattenListVisitor(拍平列表)
└─ 添加装饰函数:
├─ SetNamespace(设置默认namespace)
├─ RequireNamespace(校验namespace)
└─ FilterNamespace(过滤namespace)
3. Result.Visit()开始遍历
└─ DecoratedVisitor.Visit()
├─ 执行装饰函数
└─ FileVisitor.Visit()
├─ 打开文件
└─ StreamVisitor.Visit()
├─ YAML/JSON解码
├─ Schema验证
├─ 创建Info对象
└─ 调用业务VisitorFunc
├─ CreateOrUpdateAnnotation
├─ Record
└─ Create(发送HTTP请求)
业务VisitorFunc:真正干活的地方
在kubectl create中,业务逻辑通过VisitorFunc注入:
err = r.Visit(func(info *resource.Info, err error) error {
// 1. 错误处理
if err != nil {
return err
}
// 2. 添加kubectl管理的注解(用于apply识别)
if err := util.CreateOrUpdateAnnotation(
info.Object,
scheme.DefaultJSONEncoder(),
); err != nil {
return cmdutil.AddSourceToErr("creating", info.Source, err)
}
// 3. 录制变更历史(审计)
if err := o.Recorder.Record(info.Object); err != nil {
klog.V(4).Infof("error recording: %v", err)
}
// 4. 执行真正的创建(非dry-run模式)
if o.DryRunStrategy != cmdutil.DryRunClient {
if o.DryRunStrategy == cmdutil.DryRunServer {
// 检查server是否支持dry-run
if err := o.DryRunVerifier.HasSupport(info.Mapping.GroupVersionKind); err != nil {
return cmdutil.AddSourceToErr("creating", info.Source, err)
}
}
// 发送HTTP POST创建资源
obj, err := resource.
NewHelper(info.Client, info.Mapping).
DryRun(o.DryRunStrategy == cmdutil.DryRunServer).
WithFieldManager(o.fieldManager).
Create(info.Namespace, true, info.Object)
if err != nil {
return cmdutil.AddSourceToErr("creating", info.Source, err)
}
// 更新Info对象
info.Refresh(obj, true)
}
// 5. 打印结果
return o.PrintObj(info.Object, o.IOStreams.Out)
})
VisitorFunc的职责:
- 操作单个资源(
*Info) - 决定遇到错误时的处理方式(返回error中断,或返回nil继续)
- 可以嵌套调用其他VisitorFunc(装饰器模式)
给你的项目设计Visitor模式的建议
如果你的项目需要处理异构数据集合,可以参考kubectl的设计:
标准实现模板
package main
import (
"fmt"
"io"
)
// ========== 1. 定义Visitor接口 ==========
type DataInfo struct {
Source string
Content []byte
Type string
// ... 其他元数据
}
type VisitorFunc func(*DataInfo, error) error
type Visitor interface {
Visit(VisitorFunc) error
}
// ========== 2. 实现具体Visitor ==========
// FileVisitor 处理文件
type FileVisitor struct {
Path string
}
func (v *FileVisitor) Visit(fn VisitorFunc) error {
content, err := os.ReadFile(v.Path)
if err != nil {
return fn(nil, err)
}
return fn(&DataInfo{
Source: v.Path,
Content: content,
Type: "file",
}, nil)
}
// HTTPVisitor 处理HTTP
type HTTPVisitor struct {
URL string
}
func (v *HTTPVisitor) Visit(fn VisitorFunc) error {
resp, err := http.Get(v.URL)
if err != nil {
return fn(nil, err)
}
defer resp.Body.Close()
content, err := io.ReadAll(resp.Body)
if err != nil {
return fn(nil, err)
}
return fn(&DataInfo{
Source: v.URL,
Content: content,
Type: "http",
}, nil)
}
// MultiVisitor 组合多个Visitor
type MultiVisitor []Visitor
func (v MultiVisitor) Visit(fn VisitorFunc) error {
for _, visitor := range v {
if err := visitor.Visit(fn); err != nil {
return err
}
}
return nil
}
// ========== 3. 装饰器增强 ==========
// ValidatedVisitor 添加验证功能
type ValidatedVisitor struct {
Visitor
Validator func([]byte) error
}
func (v *ValidatedVisitor) Visit(fn VisitorFunc) error {
return v.Visitor.Visit(func(info *DataInfo, err error) error {
if err != nil {
return fn(info, err)
}
if err := v.Validator(info.Content); err != nil {
return fn(info, fmt.Errorf("validation failed: %v", err))
}
return fn(info, nil)
})
}
// ContinueOnErrorVisitor 收集所有错误
type ContinueOnErrorVisitor struct {
Visitor
}
func (v *ContinueOnErrorVisitor) Visit(fn VisitorFunc) error {
var errs []error
v.Visitor.Visit(func(info *DataInfo, err error) error {
if err != nil {
errs = append(errs, err)
return nil // 继续
}
if err := fn(info, nil); err != nil {
errs = append(errs, err)
}
return nil
})
if len(errs) > 0 {
return fmt.Errorf("encountered %d errors: %v", len(errs), errs)
}
return nil
}
// ========== 4. 使用示例 ==========
func main() {
// 创建多个Visitor
visitors := MultiVisitor{
&FileVisitor{Path: "config1.yaml"},
&FileVisitor{Path: "config2.yaml"},
&HTTPVisitor{URL: "http://example.com/config.yaml"},
}
// 添加验证装饰器
validatedVisitor := &ValidatedVisitor{
Visitor: visitors,
Validator: func(data []byte) error {
// 简单的YAML格式校验
if !bytes.HasPrefix(data, []byte("---")) {
return fmt.Errorf("not a valid YAML")
}
return nil
},
}
// 添加错误处理装饰器
continueVisitor := &ContinueOnErrorVisitor{Visitor: validatedVisitor}
// 执行遍历
err := continueVisitor.Visit(func(info *DataInfo, err error) error {
if err != nil {
fmt.Printf("Error processing %s: %v\n", info.Source, err)
return nil
}
fmt.Printf("Processing %s (%d bytes)\n", info.Source, len(info.Content))
// 处理数据...
return nil
})
if err != nil {
fmt.Printf("Final error: %v\n", err)
}
}
Visitor模式 vs 其他模式
| 模式 | 适用场景 | 与Visitor的区别 |
|---|---|---|
| Iterator | 遍历集合 | Iterator关注"如何遍历",Visitor关注"遍历后做什么" |
| Strategy | 算法替换 | Strategy替换整个算法,Visitor替换对元素的操作 |
| Command | 请求封装 | Command封装请求,Visitor封装对数据的操作 |
| Chain of Responsibility | 责任链 | 责任链是线性传递,Visitor是遍历+操作分离 |
踩坑实录:Visitor模式的坑
坑1:错误处理不当
现象:Visitor遇到错误就返回,后面的数据没有机会处理。
// 错误的实现
func (v *BadVisitor) Visit(fn VisitorFunc) error {
for _, item := range v.items {
info := convert(item)
if err := doSomething(info); err != nil {
return err // 直接返回,中断遍历
}
if err := fn(info, nil); err != nil {
return err // 直接返回,中断遍历
}
}
return nil
}
解决方案:提供ContinueOnError机制,把错误处理权交给VisitorFunc。
func (v *GoodVisitor) Visit(fn VisitorFunc) error {
var errs []error
for _, item := range v.items {
info, err := convert(item)
if err != nil {
// 交给VisitorFunc决定
if fnErr := fn(nil, err); fnErr != nil {
return fnErr // VisitorFunc要求中断
}
continue // VisitorFunc要求继续
}
if err := fn(info, nil); err != nil {
errs = append(errs, err)
// 或者根据策略决定是否继续
}
}
return aggregateErrors(errs)
}
坑2:资源泄漏
现象:FileVisitor打开文件后忘记关闭。
// 错误的实现
func (v *FileVisitor) Visit(fn VisitorFunc) error {
f, err := os.Open(v.Path)
if err != nil {
return err
}
// 哎呀,忘记defer f.Close()了!
return fn(&DataInfo{...}, nil)
}
解决方案:使用defer确保资源释放。
func (v *FileVisitor) Visit(fn VisitorFunc) error {
f, err := os.Open(v.Path)
if err != nil {
return err
}
defer f.Close() // 确保关闭
return fn(&DataInfo{...}, nil)
}
坑3:VisitorFunc递归调用死循环
现象:VisitorFunc中调用了会再次触发Visit的方法,导致死循环。
err := visitor.Visit(func(info *Info, err error) error {
// 错误:再次调用Visit!
return visitor.Visit(func(info2 *Info, err error) error {
// ...
})
})
解决方案:明确Visitor和VisitorFunc的职责边界,VisitorFunc不应该再调用Visitor。
坑4:并发安全问题
现象:多个goroutine共享同一个Visitor,导致数据竞争。
visitor := &FileVisitor{Path: "config.yaml"}
// 并发使用
for i := 0; i < 10; i++ {
go func() {
visitor.Visit(fn) // 竞态条件!
}()
}
解决方案:Visitor通常不是线程安全的,每个goroutine应该有自己的Visitor实例,或者加锁保护。
坑5:装饰器顺序问题
现象:装饰器顺序不当导致逻辑错误。
// 错误的顺序:先加密再验证,验证会失败
visitor := &ValidatedVisitor{
Visitor: &EncryptedVisitor{...}, // 先加密
Validator: validatePlainText, // 再验证明文
}
解决方案:装饰器顺序很重要,要根据业务逻辑合理安排。
Visitor模式的优缺点总结
优点
- 开闭原则:新增操作只需添加VisitorFunc,不改数据结构
- 单一职责:数据结构只管存储,操作逻辑分离
- 灵活组合:通过装饰器灵活组合各种操作
- 统一接口:不同类型的数据源用统一方式处理
缺点
- 元素类型受限:新增元素类型需要修改Visitor接口
- 破坏封装:Visitor需要访问元素的内部状态
- 理解成本:相比直接的if-else,Visitor模式更抽象
- 过度设计:简单场景使用会增加复杂度
什么时候不该用Visitor模式?
| 场景 | 建议方案 | 原因 |
|---|---|---|
| 数据类型单一 | 直接处理 | 没有多态需求 |
| 操作类型固定 | 方法调用 | 不需要扩展 |
| 简单脚本 | 顺序执行 | 过度设计 |
| 性能敏感 | 内联处理 | 减少抽象层开销 |
总结
从kubectl的源码,我们学到了Visitor模式的精髓:
- 核心思想:数据结构与操作分离,通过Visitor统一遍历
- kubectl实现:FileVisitor、StreamVisitor处理不同数据源,统一输出
*Info - 装饰器增强:DecoratedVisitor灵活添加通用处理逻辑
- 错误处理:通过VisitorFunc返回error控制遍历流程
- 实战技巧:资源管理、错误累积、装饰器顺序等
Visitor模式特别适合异构数据集合+多种处理操作的场景。如果你的项目有类似需求,不妨参考kubectl的设计。

3365

被折叠的 条评论
为什么被折叠?



