Go 装饰器模式与洋葱模型

前言 装饰器模式 是实现功能解耦和动态扩展的核心设计模式,而 洋葱模型 作为装饰器模式的链式进阶,是 Web 框架中间件、RPC 拦截器的底层核心。 本文介绍用 Go 实现支持 context.Context 上下文传递的装饰器模式(包括函数式和接口式两种实现),并进阶实现洋葱模型 一、装饰器模式 模式定义 装饰器模式是一种结构型设计模式,核心原则:不修改原有代码逻辑,动态为对象/函数添加额外功能。 它遵循开放封闭原则:对扩展开放,对修改关闭。 核心特性 动态增强:运行期为目标添加前置/后置逻辑 无侵入性:不改动核心业务代码 可组合:多个装饰器链式叠加,自由组合功能 上下文透传:配合 context.Context 实现全链路数据传递(超时、请求ID、用户信息等) 适用场景 日志打印、耗时统计 权限校验、参数校验 缓存、重试、限流熔断 全链路上下文管理 二、Go 实现装饰器模式(带 Context 上下文) 函数式装饰器 Go 没有类继承,通过函数包装实现装饰器是最简洁的方案。 为装饰器加入 context.Context,实现上下文全链路传递(核心需求)。 设计思路 定义统一的业务函数类型(强制携带 Context) 编写核心业务函数(无任何增强逻辑) 编写装饰器函数:接收原函数 → 返回包装后的新函数 装饰器内部通过 Context 传递/读取数据,实现全链路交互 函数式装饰器代码示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 package main import ( "context" "fmt" "time" ) // 定义业务函数类型:携带 context.Context,支持上下文传递 // 这是装饰器的统一接口 type BusinessFunc func(ctx context.Context, name string) string // 原始核心业务函数:仅实现核心逻辑,无增强代码 func SayHello(ctx context.Context, name string) string { // 从上下文读取中间件传递的数据 reqID, _ := ctx.Value("req_id").(string) user, _ := ctx.Value("user").(string) fmt.Printf("[核心业务] 请求ID:%s | 操作用户:%s | 执行核心逻辑\n", reqID, user) return fmt.Sprintf("你好,%s!", name) } // 装饰器1:日志装饰器(携带上下文) func LogDecorator(f BusinessFunc) BusinessFunc { return func(ctx context.Context, name string) string { // 前置增强:打印请求日志 reqID := ctx.Value("req_id").(string) fmt.Printf("[日志装饰器] 请求ID:%s | 开始调用,参数:%s\n", reqID, name) // 调用原函数(透传上下文) result := f(ctx, name) // 后置增强:打印响应日志 fmt.Printf("[日志装饰器] 请求ID:%s | 调用结束,结果:%s\n", reqID, result) return result } } // 装饰器2:计时装饰器(携带上下文) func TimeDecorator(f BusinessFunc) BusinessFunc { return func(ctx context.Context, name string) string { start := time.Now() reqID := ctx.Value("req_id").(string) // 调用原函数(透传上下文) result := f(ctx, name) // 后置增强:打印耗时 fmt.Printf("[计时装饰器] 请求ID:%s | 执行耗时:%s\n", reqID, time.Since(start)) return result } } // 装饰器3:上下文初始化装饰器(往ctx存入数据,供下游使用) func ContextDecorator(f BusinessFunc) BusinessFunc { return func(ctx context.Context, name string) string { // 给上下文添加请求ID、用户信息(全链路透传) ctx = context.WithValue(ctx, "req_id", "REQ_123456") ctx = context.WithValue(ctx, "user", "admin") fmt.Println("[上下文装饰器] 初始化上下文完成") return f(ctx, name) // 无后置增强 } } func main() { // 链式装饰:顺序 = 上下文装饰 → 日志装饰 → 计时装饰 decoratedFunc := ContextDecorator(LogDecorator(TimeDecorator(SayHello))) // 根上下文 ctx := context.Background() // 执行增强后的函数 res := decoratedFunc(ctx, "张三") fmt.Println("\n最终返回结果:", res) } 运行结果 注意每层装饰器中被装饰函数 BusinessFunc 的调用位置,后面的代码是被装饰函数返回后再执行的 ...

August 18, 2022 · 7 min · 1283 words · erpan

golang context包用法理解

同时启很多个goroutine来完成一个任务,在一些必要的情况下如何跟踪或取消这些goroutine?常见的有下面几种方式: WaitGroup,goroutine之间的同步 for select 加上 stop channel来监听消息管理协程 context包 context包就是用来在goroutine之间传递上下文信息的,它提供了超时timeout和取消cancel机制,利用了channel进行信息传递,方便的管理具有复杂层级关系的多个goroutine,这些复杂的层级关系类似于一棵树,可以有多个分叉。Go标准库中的net/http,database/sql等都用到了context包。 接口 context包定义了两个接口 Context 1 2 3 4 5 6 type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key interface{}) interface{} } Deadline 方法,第一个返回值是该上下文执行完的截止时间,第二个返回值是布尔值,表示是否设置了截止日期,没设置返回ok==false,否则为true。该方法幂等 Done,返回一个只读通道,在context被取消或者context到了deadline时,此通道会被关闭 WithCancel 上下文,当调用cancel后此通道关闭关闭 WithDeadline,到达deadline时间点后自动关闭此通道 WithTimeout,指定的时间超时后自动关闭此通道 Err,Done返回的通道未被关闭时,Err的返回值是nil,关闭后,返回 context 取消的原因,被取消时返回Canceled,到了截止时间返回 DeadlineExceeded Value,获取该Context上绑定的值,需要注意的是键和值都是any类型。以上四个方法都是幂等的 canceler ,私有接口,表示一个可以被取消cancel的context对象,包内的*cancelCtx 和 *timerCtx结构体实现了此接口 1 2 3 4 type canceler interface { cancel(removeFromParent bool, err, cause error) Done() <-chan struct{} } 创建 几个实现此接口的结构体关系: ...

February 15, 2022 · 3 min · 503 words · erpan

ArgoCD试用

ArgoCD 遵循GitOPS模式,应用定义、配置和环境等都应该是声明式和版本化的。应用部署和生命周期管理是自动化、可审计和易于理解的 特性 拥有GitOPS的一切特性,如回滚到任意git commit点 自动发布应用到指定环境,支持多集群管理 支持多种配置管理工具、模板 (Kustomize,Helm, Jsonnet, plain-YAML,自定义配置管理插件) 支持单点登录 (OIDC, OAuth2, LDAP, SAML 2.0, GitHub, GitLab, Microsoft, LinkedIn) 支持多租户及RBAC授权 服务健康状态分析 资源版本偏移检查和Web UI实时可视化 可自动或手动同步资源到目标状态 提供命令行工具,自动化集成简单方便 Webhook 集成 (GitHub, BitBucket, GitLab) 支持访问令牌 支持各阶段钩子定义PreSync, Sync, PostSync hooks to support complex application rollouts (e.g.blue/green & canary upgrades) 应用事件和API调用审计追踪 有暴露Prometheus 指标 Parameter overrides for overriding helm parameters in Git 几个核心概念 Application:指定manifest路径下的一组kubernetes资源,是一个CRD AppProject:Application的逻辑分组,可以配置一些约束选项,如限制git源、目标集群、namespace和资源类型等,也可以订阅project roles App of Apps:官网文档中的 cluster bootstrapping,一个Application包含多个子Application,在批量创建Application时比较有用,也可以用来自我管理(官方文档有示例) 下图是定义了一个Application对象,其指定的仓库目录下包含多种资源,其中的子Application对象所指定的仓库目录又可以包含多种资源,我们可以开启递归的选项,在往仓库添加资源对象的时候自动apply到对应集群当中去 注意点: 一个ArgoCD实例中,Application名字是唯一的,且只能放在与argo部署的同一名称空间中 Application中没有指定resources-finalizer.argocd.argoproj.io终结器,在删除Application的时候是不会删除它所管理的资源,App of Apps也是一样 ApplicationSet 跨集群和仓库灵活的管理Applications,补充了以集群管理为中心的场景 ...

January 10, 2022 · 3 min · 485 words · erpan

client-go RingGrowingBuffer 环形缓冲区

在client-go源码中,processorListener对象里面定义了一个RingBuffer用于缓存所有尚未分发的事件通知,在此记录下这个RingBuffer。 RingBuffer一般用于数据的缓存机制,例如tcp协议里面数据包的缓冲就利用到了RingBuffer。 client-go中的这个buffer是非线程安全、可增长、无边界的先进先出环形缓冲区。环是一个逻辑上的概念,有了环,此段内存空间就可以重复利用,不用频繁重新申请内存,本质上数据还是存在数组里面的,这个数组的大小可以按需进行倍数扩容,扩容后需要重新分配内存空间并拷贝未消费的数据到新数组来。因为它是数组,内存是预先分配的,数组是内存上连续的一段空间,它有一个容易预测的访问模式,因此对CPU高速缓存友好,垃圾回收(GC)在这种情况下也不用做什么。 在实现上,可以理解为两个指针:a) 读指针、b)写指针。在一段buffer上,读指针控制下一次该读数据的位置,写指针控制下一次该写数据的位置。在数组里面我们可以直接用数组下标。要遵循FIFO原则,读指针不能超过写指针,两指针重叠了要么buffer写满了,要么buffer为空。 参考这里的一张图 目前我们只需要考虑: 存储啥数据类型 数据如何存放 何时空间满了需要扩容 不能丢失未消费数据 k8s中的源码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 // 源码路径k8s.io/utils/buffer/ring_growing.go package buffer // 非线程安全、可增长的环形缓冲区 type RingGrowing struct { data []interface{} // 存任意数据的数组 n int // 缓冲区大小 beg int // 第一个可用的元素位置索引 readable int // 未消费的元素数量 } // 初始化一个RingBuffer func NewRingGrowing(initialSize int) *RingGrowing { return &RingGrowing{ data: make([]interface{}, initialSize), n: initialSize, } } // 取出未消费元素中的第一个元素 func (r *RingGrowing) ReadOne() (data interface{}, ok bool) { if r.readable == 0 { return nil, false } r.readable-- element := r.data[r.beg] r.data[r.beg] = nil // Remove reference to the object to help GC if r.beg == r.n-1 { // 这种情况就是读到数组最后一个元素了,下次读就得从列表的头部开始,以免越界 r.beg = 0 } else { r.beg++ } return element, true } // 在buffer尾部添加一个元素,buffer满了就扩容 func (r *RingGrowing) WriteOne(data interface{}) { // 满了的情况 if r.readable == r.n { newN := r.n * 2 // 新开辟一个两倍大小的数组 newData := make([]interface{}, newN) to := r.beg + r.readable if to <= r.n { copy(newData, r.data[r.beg:to]) } else { // 未消费的元素在数组两端的情况 copied := copy(newData, r.data[r.beg:]) copy(newData[copied:], r.data[:(to%r.n)]) } r.beg = 0 r.data = newData r.n = newN } r.data[(r.readable+r.beg)%r.n] = data r.readable++ } client-go中的这个buffer是非线程安全、可增长、无边界的先进先出环形缓冲区,因此,在此client-go ringBuffer基础上可以继续考虑下面几个问题: ...

October 11, 2021 · 2 min · 252 words · erpan

信号和容器关闭

前提概要 信号概念 进程间通信 socket 消息队列 管道:类似瀑布开发模式 共享内存 信号量:一般和共享内存一起使用 信号:应急事件、通知 信号,一般是异常情况下的工作模式(其他几种通信方式是常规情况下的),是进程间通信唯一一种异步通信方式,即可以在任何时候发送信号给一个进程。 为了响应各种各样的事件,定义了下面64种信号: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 [root@whatfuck ~]# kill -l 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR 31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8 43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2 63) SIGRTMAX-1 64) SIGRTMAX 对收到的信号有三种处理方式 ...

June 10, 2021 · 6 min · 1093 words · erpan