范塔西彬 - Go
type: channel
link: https://fantasticbin.com/tag/Go/
items: Go语言构建用户行为分析系统：优雅架构设计与实战
items: 责任链模式：解耦与灵活流程处理的利器
items: 无锁队列与延时无锁队列：原理、实现与应用
items: 软件测试的重要性：Go语言中的单元测试与Mock实践
items: PHP、Go与MySQL中JSON数据处理与索引实践
items: Go并发编排之扇入模式
items: Go并发编排之扇出模式
items: Go项目中应用简易协程编排
items: Go中使用策略模式的示例
items: Go奇淫巧计（不定时更新）

Go语言构建用户行为分析系统：优雅架构设计与实战
type: item
date: 2025-12-24T23:29:00+08:00
description: 构建高性能用户行为分析系统：Go语言实现优雅架构设计用户行为分析，是产品迭代的"透视镜"在互联网产品"唯快不破"的今天，无数团队陷入同一个怪圈：埋头开发功能、满怀期待发布，最终却对用户的真实反馈一无所知。本文将带你从后端视角，拆解如何构建一套既稳健又灵活的用户行为分析处理流程。为什么要深度投入用户行为分析？从硅谷科技巨头到国内独角兽企业，再到每个追求精细化运营的创业团队，用户行为分析早已不再是锦上添花的"奢侈品"，而是决定产品生死的"标配能力"。行业数据清晰表明：采用深度用户行为分析的产品，其迭代效率与商业转化率通常能实现数倍提升。我们不再依赖主观猜测，而是让数据说话——每一次点击、每一次滑动、每一秒的停留，都在无声地告诉我们：用户是谁？他们真正想要什么？又是在哪个环节默默离开？今天，我们就从后端架构出发，探讨如何优雅地打造这套"产品听诊器"。架构全景：一个优雅的幕后指挥官假设前端已预埋用户事件，并通过消息队列（如Kafka、RabbitMQ等）投递事件消息。后端的核心挑战在于：如何高效、稳健地处理这些如洪峰般涌来的数据？我们需要一个强大的"指挥官"来调度全局，我们将其命名为 UserBehaviorAnalyzer。它必须具备四大核心能力：流量控制：防止瞬时流量冲垮数据库数据清洗：将原始JSON转化为业务可用的结构化数据策略分发：根据业务需求（如点击热力图、用户路径分析、转化率统计）分发至不同处理模块并发执行：利用Go协程优势并行处理，提升吞吐量下面让我们从零开始，一步步构建这个系统。1. 策略规范接口：定义通用语言首先，我们定义策略处理的通用接口，这是整个系统的"契约"：package mq import ( "context" "fmt" ) // StrategyContext 定义策略处理数据的通用接口类型 // 具体策略在实现时，需要将此类型断言为具体的策略上下文结构 type StrategyContext any // ProcessingStrategy Kafka上报消息处理策略接口 type ProcessingStrategy interface { // GetName 返回该策略处理的name值 GetName() string // Process 处理具体的业务逻辑 Process(ctx context.Context, data StrategyContext) error // IsApplicable 判断该策略是否适用于当前上下文 IsApplicable(ctx context.Context, name string, data StrategyContext) bool }2. 策略注册表：集中管理所有"处理器"接下来实现策略的注册与发现机制，通过读写锁保护策略字典，确保并发安全：package mq import ( "sync" ) // StrategyRegistry 策略注册表 type StrategyRegistry struct { strategies map[string]ProcessingStrategy mu sync.RWMutex } // NewStrategyRegistry 创建新的策略注册表 func NewStrategyRegistry() *StrategyRegistry { return &StrategyRegistry{ strategies: make(map[string]ProcessingStrategy), } } // Register 注册策略 func (r *StrategyRegistry) Register(strategy ProcessingStrategy) { r.mu.Lock() defer r.mu.Unlock() r.strategies[strategy.GetName()] = strategy } // GetStrategy 获取指定name对应的策略 func (r *StrategyRegistry) GetStrategy(name string) ProcessingStrategy { r.mu.RLock() defer r.mu.RUnlock() if strategy, exists := r.strategies[name]; exists { return strategy } return nil } // GetAllStrategies 获取所有策略 func (r *StrategyRegistry) GetAllStrategies() []ProcessingStrategy { r.mu.RLock() defer r.mu.RUnlock() …

责任链模式：解耦与灵活流程处理的利器
type: item
date: 2025-07-05T18:41:00+08:00
description: 在日常工作中，我们常常会遇到产品增加需求的情况，或者需要设计一个工作流引擎。如果采用硬编码的方式，不仅不利于代码的解耦，后期的维护工作也会让人头疼不已。这时，责任链（Chain of responsibility）模式就能派上用场。责任链模式概述责任链模式是一种行为型设计模式。通过使用该模式，我们可以为请求创建一条由多个处理器组成的链路。每个处理器专注于自己的职责，彼此之间不存在耦合关系。当一个处理器完成自己的任务后，请求对象会被传递到链路中的下一个处理器继续处理。责任链模式在许多流行框架中都有广泛应用，例如中间件和拦截器等框架组件就采用了这种设计模式。在进行 Web 接口开发时，像记录访问日志、解析 Token、格式化接口响应的统一结构等项目公共逻辑，通常都在中间件和拦截器中完成。这样做可以将这些基础操作与接口的业务逻辑解耦。责任链模式的实现示例：医院患者就诊流程PHP 实现责任链模式使用面向对象语言实现非常简单，因为可以通过抽象类复用公共逻辑，链路的执行逻辑也可以由各个处理器自行实现。下面我们以医院患者就诊流程为例，展示如何使用 PHP 实现责任链模式。declare(strict_types=1); // 就诊状态枚举 enum PatientStatus: int { case Start = 1; // 状态：开始看病 case Reception = 2; // 状态：已挂号 case Clinic = 3; // 状态：已看诊 case Cashier = 4; // 状态：已缴费 case Pharmacy = 5; // 状态：已拿药 } // 患者类 class Patient { public ?PatientStatus $status = null; public function __construct( public string $name ) {} } // 就诊流程责任链接口定义 interface PatientHandler { public function execute(Patient $patient): void; public function setNext(PatientHandler $handler): PatientHandler; public function do(Patient $patient): void; } // 抽象类复用公共定义 abstract class BaseHandler implements PatientHandler { private ?PatientHandler $nextHandler = null; public function execute(Patient $patient): void { $this->do($patient); $this->nextHandler?->execute($patient); } public function setNext(PatientHandler $handler): PatientHandler { $this->nextHandler = $handler; return $handler; } abstract public function do(Patient $patient): void; } class Reception extends BaseHandler { public function do(Patient $patient): void { if ($patient->status > PatientStatus::Start) { echo "患者已经挂号\n"; return; } echo "挂号处为患者 {$patient->name} 挂号\n"; $patient->status = PatientStatus::Reception; } } class Clinic extends BaseHandler { public function do(Patient $patient): void { if ($patient->status === PatientStatus::Start) { echo "患者还未挂号\n"; return; } if ($patient->status > PatientStatus::Reception) { echo "患者已经完成医生诊断\n"; return; } echo "医生为患者 {$patient->name} 进行诊断\n"; $patient->status = PatientStatus::Clinic; } } class Cashier extends BaseH…

无锁队列与延时无锁队列：原理、实现与应用
type: item
date: 2024-12-03T16:29:00+08:00
description: 在之前的文章使用atomic包实现无锁并发统计中，我们了解到原子操作在保证并发安全的同时，能避免锁带来的性能损耗，其应用场景广泛，如并发计数器、自旋锁等。这次，我们将深入探讨原子操作的另一个经典应用场景 —— lock-free queue（无锁队列），并基于此实现一个延时无锁队列。队列基础队列是一种遵循先进先出（FIFO，First In First Out）原则的数据结构，最先进入队列的元素最先被移除，元素顺序得以保持。队列结构包含队首和队尾，支持入队和出队操作。常见的队列实现方式有数组和链表。数组实现的队列在入队和出队时可能需要移动元素，而链表实现的队列则因只需更新指针，操作更为高效。无锁队列实现无锁队列利用原子操作，在保证并发安全的前提下，允许多个线程同时进行入队和出队操作，无需传统锁机制，显著提升并发性能。我们采用链表结构来进一步优化效率，其定义如下：import ( "sync/atomic" "unsafe" ) type LkQueue[T any] struct { head unsafe.Pointer tail unsafe.Pointer } type node[T any] struct { value T next unsafe.Pointer }基础操作实现首先，我们完善队列的创建、读取节点及修改节点的操作：func NewLkQueue[T any]() *LkQueue[T] { n := unsafe.Pointer(&node[T]{}) return &LkQueue[T]{head: n, tail: n} } // load读取节点的值 func load[T any](p *unsafe.Pointer) *node[T] { return (*node[T])(atomic.LoadPointer(p)) } // cas原子地修改节点的值 func cas[T any](p *unsafe.Pointer, old, new *node[T]) bool { return atomic.CompareAndSwapPointer(p, unsafe.Pointer(old), unsafe.Pointer(new)) }这里的读取节点（load）和修改节点（cas）操作使用了原子读及 CAS 操作，确保在并发访问时数据安全。入队操作入队操作流程如下：先接收值并创建新节点，然后循环尝试将新节点插入队列尾部。每次循环中，通过 load 函数读取当前队列的尾部节点 tail 和其下一个节点 next，检查 tail 是否仍为队列尾部（确保状态未变）。若 tail 是队列尾部，再检查 next 是否为 nil。若 next 为 nil，尝试将新节点 n 插入 tail 的 next 位置；若插入成功，进一步更新队列的 tail 指针指向新节点 n（均通过原子操作 CAS 完成）；若插入失败，说明队列状态被其他线程修改，继续循环尝试插入。若 next 不为 nil，说明 tail 不是队列最后一个节点，尝试将 tail 指针移动到 next 节点，以便下次循环继续尝试插入。// Enqueue入队 func (q *LkQueue[T]) Enqueue(value T) { n := &node[T]{value: value} for { tail := load[T](&q.tail) next := load[T](&tail.next) if tail == load[T](&q.tail) { // tail和next是否一致 if next == nil { if cas(&tail.next, next, n) { cas(&q.tail, tail, n) // 入队完成。设置tail return } } else { cas(&q.tail, tail, next) } } } }出队操作出队操作同样在操作前进行检查。代码循环尝试从队列头部移除节点。每次循环中，通过 load 函数读取当前队列的头部节点 head、尾部节点 tail 和头部节点的下一个节点 next，检查 head 是否仍为队列头部（确保状态未变）。若 head 仍为队列头部，进一步检查 head 和 tail 是否相等。若 head 和 tail 相等，队列可能为空或 tail 未到队尾，此时检查 next 是否为 nil ；若 next 为 nil，说明队列为空，返回 false 表示出队失败；若 next 不为 nil，说明 tail 未到队尾，尝试将 tail 指针移动到 next 节点，以便下次循环继续出队操作。若 head 和 tail 不相等，说明队列中有元素可出队，将 next 节点…

软件测试的重要性：Go语言中的单元测试与Mock实践
type: item
date: 2024-05-26T10:40:00+08:00
description: 是的，我又离职了。这一次离开的主要原因是与上家公司团队理念上的不合，可能还包括一些其他因素。真正让我决定离开的核心原因是这个团队乃至整个公司都不注重测试。这或许与公司产品定位有关，因为他们是一家游戏公司，需要快速抓住用户市场并推出爆品来盈利，导致产品周期迭代过快，根本没有多余时间进行测试。此外，缺乏一个好的发布流程和 Review 机制，从而导致生产事故频发。测试的重要性一般软件开发公司会专门设立 QA（Quality Assurance）工程师职位，专门负责把控产品质量。部分职责包括从业务角度（特别是边缘场景）进行测试，确保用户在使用产品过程中不会遇到问题及 bug，然后及时反馈并参与到迭代周期中，这是产品质量控制中不可或缺的一环。单元测试的重要性从开发工程师的角度把控产品质量的方式之一就是单元测试。开发者写的每个应用程序、业务流程、功能模块甚至每个独立的小单元，都必须通过单元测试确定是否达到预期。一些规范化的敏捷开发团队会将单元测试纳入 CI/CD 的持续集成中，实现了自动化构建部署一套体系。而在单元测试中，Mock 对象的使用非常普遍，尤其在需要隔离被测试代码与外部依赖（如数据库、网络服务、文件系统等）时。Mock 对象可以模拟这些外部依赖的行为，从而使测试更加独立和可控。今天我们就来了解一下 Go 语言单元测试中对于 Mock 所做的支持。使用 Gomock 进行单元测试Go 语言官方提供了一款 Mock 框架：Gomock 以及 Mock 文件生成工具：mockgen。它可以方便地生成模拟接口，支持与Go的内置 testing 包整合，也可以在其他上下文中使用。该项目源自 Google 的 golang/mock 仓库，但 Google 已不再维护。鉴于 Uber 内部对 Gomock 的大量使用，Uber 决定接手并继续维护该项目。我们将基于 uber-go/mock 做示例。使用步骤Gomock 的使用通常遵循以下四个基本步骤：生成 Mock 文件：使用 mockgen 为你想要 mock 的接口生成一个 mock 文件。创建 Mock 对象：在测试代码中，创建一个 gomock.Controller 实例，并将其作为参数传递给 mock 对象的构造函数来创建 mock 对象。设置期望：调用 EXPECT() 为 mock 对象设置期望和返回值。验证期望：调用 mock 控制器的 Finish() 方法验证 mock 的期望行为（通常在 defer 中完成）。示例通过一个简单的例子演示 Gomock 的使用流程。假设你有一个简单的服务接口 UserService，它有一个方法 GetUser，通过用户ID获取用户信息。我们将为这个接口创建一个 mock 对象并编写测试代码。首先，在项目中安装 mockgen，并加入到环境变量中：go install go.uber.org/mock/mockgen@latest # 配置环境变量 ... go get -u go.uber.org/mock/gomock go get -u github.com/stretchr/testify/assert接着，在 service/user.go 中定义一个接口，里面有一个 GetUser() string 方法，模拟获取信息。package service type User struct { ID int Name string } type UserService interface { GetUser(id int) (*User, error) }使用 Mockgen 生成 UserService 的 mock 对象：mockgen -source=service/user.go -destination=service/mock_user.go -package=service这里的 destination 参数通常设置成与 source 参数文件同级目录，否则运行 go test 会出现 import cycle not allowed in test，也就是循环导入错误；package 包名一般跟目录名相同即可。假设我们有一个函数 GetUserName，它使用 UserService 接口实例中的方法来获取用户并返回用户的名字：package service func GetUserName(us UserService, id int) (string, error) { // 使用依赖注入进来的具体结构体方法，通常为需要 mock 的方法 user, err := us.GetUser(id) if err != nil { return "", err } return user.Name, nil }在…

PHP、Go与MySQL中JSON数据处理与索引实践
type: item
date: 2024-04-06T15:34:00+08:00
description: PHP 在 8.3 的版本中增加了 json_validate() 函数可用于检查一个字符串是否为语法正确的 JSON，同时官方号称比 json_decode() 更有效。在 8.3 版本之前，如果需要实现 JSON 验证，则需要使用 json_decode() 来辅助实现，并且通过验证最近是否存在 JSON_ERROR_NONE 错误来达到效果：// PHP < 8.3 function json_validate(string $string): bool { json_decode($string); return json_last_error() === JSON_ERROR_NONE; } var_dump(json_validate('{ "test": { "foo": "bar" } }')); // true而在 8.3 之后，只需要通过 json_validate() 函数就能直接验证字符串了：var_dump(json_validate('{ "test": { "foo": "bar" } }')); // true如果在 Go 语言中需要用到 JSON 验证，官方 encoding/json 包中已经定义了 json.Valid 来实现：import ( "encoding/json" "fmt" ) func main() { fmt.Println(json.Valid([]byte(`{ "test": { "foo": "bar" } }`))) // true }同时我们知道 MySQL 5.7 及更高版本中可以为 JSON 字段的某个 key 设置索引来高效检索，可支持两种 Generated Column，即 Virtual Generated Column 和 Stored Generated Column，前者只将 Generated Column 保存在数据字典中（表的元数据），并不会将这一列数据持久化到磁盘上；后者会将 Generated Column 持久化到磁盘上，而不是每次读取的时候计算所得。很明显，后者存放了可以通过已有数据计算而得的数据，需要更多的磁盘空间，与 Virtual Column 相比并没有优势，因此，MySQL 5.7 中，不指定 Generated Column 的类型，默认是 Virtual Column。--- 表结构 CREATE TABLE `users` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, `name` VARCHAR(255) NOT NULL DEFAULT '', `information` JSON, `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `deleted_at` DATETIME DEFAULT NULL ); --- 创建JSON字段的虚拟列，user 是包含 JSON 字段的表名，information 是 JSON 字段的列名，label 是要索引的键名，label_index 是新添加的虚拟列名。 ALTER TABLE users ADD COLUMN label_index VARCHAR(255) AS (information->>'$.label') STORED; --- 为虚拟列创建索引 CREATE INDEX idx_label_index ON users(label_index);然后可以使用市面上成熟的 ORM 来便捷操作 JSON 字段，在 Laravel 中我们可以先使用 make:model Artisan 命令来生成模型类：php artisan make:model User我们再来看看在 Eloquent ORM 中使用 JSON 字段的示例：// 新增 $user = new User; $user->name = 'fantasticbin'; $user->information = ['label' => 'cool', 'language' => ['php', 'go']]; $user->save(); // 修改 $user = User::find(1); $information = $user->information; $information['label'] = 'nice'; $user->information = $information; $user->save(); …

Go并发编排之扇入模式
type: item
date: 2024-03-04T19:11:00+08:00
description: 在以往的博文中我们知道 Go 可以通过 chan 可以实现很多种方式的并发编排，包括 Or-Done 模式、扇入模式、扇出模式、Stream 和 map-reduce 等，并且已经基于扇出模式实现了设计模式之观察者模式，这次我们就来探讨一下扇入模式。扇入模式的定义跟扇出模式是反着来的，即通过多个输入 chan 将结果合并到一个输出 chan 中并返回。举个实际的栗子，假设我们有两个上游，需要将上游数据合并成一个统一的下游进行输出，代码定义如下：func mergeTwo[T any](ch1, ch2 <-chan T) <-chan T { out := make(chan T) go func() { defer close(out) for ch1 != nil || ch2 != nil { // 至少有一个 chan 不是 nil 则继续 select { case v, status := <-ch1: if !status { ch1 = nil // ch1 关闭时，将其赋值为 nil break } out <- v case v, status := <-ch2: if !status { ch2 = nil // ch2 关闭时，将其赋值为 nil break } out <- v } } }() return out }这里有一个 select 动态监听的小技巧，也就是将监听的 chan 设置为 nil 后，就不会再走入到该分支中了，通过使用这种方式可以做到 select 动态去除某个 chan 的监听动作。如果是存在多个上游的情况，那么如何实现扇入模式的合并逻辑？一种方式是通过递归来实现，思路就是将输入 chan 进行两两合并：func mergeRecursion[T any](chs ...<-chan T) <-chan T { switch len(chs) { case 0: // 输入 chan 的数量为0，返回一个已关闭的 chan out := make(chan T) close(out) return out case 1: // 输入 chan 的数量为1，直接返回 return chs[0] case 2: // 输入 chan 的数量为2，合并这两个 chan return mergeTwo(chs[0], chs[1]) default: m := len(chs) / 2 return mergeTwo( mergeRecursion(chs[:m]...), // 递归调用前半部分 mergeRecursion(chs[m:]...)) // 递归调用后半部分 } }这里使用前面的 mergeTwo 方法来处理两个输入 chan 的合并，最终可将所有的输入 chan 进行合并输出。另一种方式则可以用到 reflect.SelectCase 来处理输入 chan 的接收，把结果直接丢入输出 chan 中即可。import "reflect" func fanIn[T any](chs ...<-chan T) <-chan T { out := make(chan T) go func() { defer close(out) var cases []reflect.SelectCase for _, ch := range chs { cases = append(cases, reflect.SelectCase{ Dir: reflect.SelectRecv, Chan: reflect.ValueOf(ch), }) } for len(cases) > 0 { i, value, ok := reflect.Select(cases) // 从输入 chan 中读取数据 if !ok { cases = append(cases[:i], cases[i+1:]...) // 将已关闭的输入 chan 从 select 列表中移除 continue } out <- value.Interface().(T) // 这里需要从 interface 中转换成具体的类型 } }() return out }通过动态去除 reflect.SelectCase 切片中的元素也能解决 chan 动态监听的场景，同时能够优雅处理多个输入上游的监听逻辑。本文代码部分引用《 100个Go语言典型错误 》之 #66：没有使用 nil channel，及晁岳攀（@鸟窝）大佬的《 深入理解Go并发编程 》之第 12 章 - channel 的内部实现和陷阱。

Go并发编排之扇出模式
type: item
date: 2023-12-21T19:36:00+08:00
description: 我们知道 Go 语言中的 channel (以下简称 chan ) 是该语言并发编程模型的一个核心部分。通过 chan , Go 允许在不同的 Goroutine 之间安全地传递数据。并且通过 chan 可以实现很多种方式的并发编排，包括 Or-Done 模式、扇入模式、扇出模式、Stream 和 map-reduce 等，今天我们就来看看扇出模式的典型用例 —— 观察者模式。在鸟窝大佬的新书 《 深入理解Go并发编程 》 之第 12 章 - channel 的内部实现和陷阱 中，给出了扇出模式的具体实现：func fanOut(ch <-chan interface{}, out []chan interface{}, async bool) { go func() { defer func() { //退出时关闭所有的输出chan for i := 0; i < len(out); i++ { close(out[i]) } }() for v := range ch { // 从输入chan中读取数据 v := v for i := 0; i < len(out); i++ { i := i if async { //异步 go func() { out[i] <- v // 放入到输出chan中,异步方式 }() } else { out[i] <- v // 放入到输出chan中，同步方式 } } } }() }这里其实还可以使用反射的方式来实现，具体就是通过 reflect.SelectCase 来处理输入管道的接收，再通过一个循环向输出管道分发数据：func fanOutReflect(ch <-chan interface{}, out []chan interface{}, async bool) { var cases []reflect.SelectCase // 添加输入 chan 的 reflect.SelectCase cases = append(cases, reflect.SelectCase{ Dir: reflect.SelectRecv, Chan: reflect.ValueOf(ch), }) go func() { defer func() { // 退出时关闭所有的输出 chan for _, o := range out { close(o) } }() for { _, value, ok := reflect.Select(cases) // 从输入 chan 中读取数据 if !ok { // 输入 channel 被关闭 return } // 输入 channel 接收到数据 for _, o := range out { if async { // 异步 go func(o chan interface{}, value reflect.Value) { o <- value.Interface() // 放入到输出 chan 中,异步方式 }(o, value) } else { o <- value.Interface() // 放入到输出 chan 中，同步方式 } } } }() }扇出模式最典型的一个使用场景就是观察者模式，下面我们就来手撸一个基于 chan 扇出模式实现的观察者模式。import "reflect" // Subject 被观察者 type Subject struct { observers []Observer in chan any } // Observer 观察者 chan type Observer chan any // NewSubject 获取被观察者实例 func NewSubject() Subject { return Subject{in: make(chan any)} } // Attach 观察者绑定 func (s *Subject) Attach(observer ...Observer) { s.observers = append(s.observers, observer...) } // Detach 观察者解绑 func (s *Subject) Detach(observer Observer) { for i, obs := range s.observers { if obs == observer { s.observers = append(s.observers[:i], s.observers[i+1:]...) close(observer) break } } } // Notify 通知观察者 func (s *Subject) Notify(data any) { go …

Go项目中应用简易协程编排
type: item
date: 2023-12-17T14:16:00+08:00
description: 众所周知 Go 最大的特点是协程相对于其他语言的线程来说是很轻量的，在 Linux 上线程的栈大小默认固定为 8MB，且不能扩展，也不能缩小。而普通的 goroutine 栈大小默认是 2KB （Go 1.19 改成了根据历史使用自动调整的方式），并且在需要的时候可以扩展到 1GB （不同的架构最大的大小会不同）。我们在项目中日常开发会经常碰到需要并发使用协程处理业务逻辑的时候，如果是比较少量的处理并发任务，所耗费的系统资源也不会很多。不过一般来说我们大部分情况都无法判断这种并发任务具体需要执行的数量是多少，而大量频繁的起协程对于调度和垃圾回收的耗时还是会有影响，并且也极度耗费系统资源。因此，协程并不是越多越好，有的时候，我们会创建一个协程编排逻辑来专门处理这种场景，最理想的方式就是使用 Go 内置的 chan 去控制任务的执行，并充分利用到空闲的协程去并发完成任务。目前市面上有很多协程处理第三方库都很优秀，不过这次我们试着自己实现一个简易的协程编排结构，也可以称之为协程池。下面就是一个通过内置 chan 和 wg 来控制并发任务处理的示例：import ( "fmt" "runtime/debug" "sync" ) type Pool[T any] struct { taskQueue chan T // 任务队列 taskFn func(T) // 任务执行函数 workers int // 任务数量 panicHandler func(any) // panic处理函数 wg sync.WaitGroup } // NewPool 创建一个新的协程池 func NewPool[T any](workers, capacity int, taskFn func(T), panicHandler func(any)) *Pool[T] { pool := &Pool[T]{ taskQueue: make(chan T, capacity), panicHandler: panicHandler, workers: workers, } pool.taskFn = func(t T) { defer func() { // 处理协程运行中出现panic的情况 if r := recover(); r != nil { if pool.panicHandler != nil { pool.panicHandler(r) } } }() taskFn(t) } pool.wg.Add(workers) return pool } // Start 启动任务 func (p *Pool[T]) Start() { for i := 0; i < p.workers; i++ { go func() { defer p.wg.Done() for { task, ok := <-p.taskQueue if !ok { return } p.taskFn(task) } }() } } // Push 提交任务 func (p *Pool[T]) Push(task T) { p.taskQueue <- task } // Wait 挂起当前协程 func (p *Pool[T]) Wait() { close(p.taskQueue) p.wg.Wait() }这里的NewPool工厂函数，其实参数定义可以使用 options 函数式选项模式来方便调用，详情可参考： 编程范式之函数式编程的应用 接下来我们写个测试用例来试试看结果是否如我们所预期：import ( "runtime" "runtime/debug" "sync/atomic" "testing" "github.com/brildum/testify/assert" ) func TestNewPool(t *testing.T) { num := runtime.NumCPU() var sum atomic.Int32 task := func(num int32) { if num < 0 { panic("unable to handle negative numbers") } sum.Add(num) } handler := func(r any) { fmt.Printf("Panic: %v\n %s", r, string(debug.Stack())) } pool := NewPool(num, num, task, handler) pool.Start() for i := int32(1000); i >= -1; i-- { pool.Push(i) } pool.Wait() if assert.Equal(t, int32(5…

Go中使用策略模式的示例
type: item
date: 2023-11-20T19:46:00+08:00
description: 两年前写过一篇PHP策略模式的文章，最近在做公司Go项目时也用到了策略模式，那就写一篇文章记录下来吧。首先还是编写一个基类接口用于定义策略规范及实现逻辑方法// MyBaseStrategy 策略公共接口 type MyBaseStrategy interface { MakeImplementation() (string, error) }接着声明好策略执行者，再编写一个策略生成的工厂类用于返回策略执行者的实现逻辑// MyBaseOperator 策略执行者 type MyBaseOperator struct { // 嵌入策略接口，就不需要再声明实现方法了 MyBaseStrategy } // NewMyOperator 策略执行者工厂方法 func NewMyOperator(strategy MyBaseStrategy) *MyBaseOperator { // 返回策略执行者，并将具体策略嵌入 return &MyBaseOperator{strategy} } // GetOperatorByPlanName 通过计划名称获取策略执行者 func GetOperatorByPlanName(planName string) *MyBaseOperator { operator := NewMyOperator(map[string]MyBaseStrategy{ "A": NewMyPlanAImplementationStrategy(), }[planName]) return operator }最后编写具体策略实现// MyPlanAImplementationStrategy A计划实现策略类 type MyPlanAImplementationStrategy struct { } // MakeImplementation A计划策略执行具体方法 func (plan *MyPlanAImplementationStrategy) MakeImplementation () (string, error) { return "This is my a plan class of implementation function", nil } // NewMyPlanAImplementationStrategy A计划实现策略类工厂方法 func NewMyPlanAImplementationStrategy() *MyPlanAImplementationStrategy { return &MyPlanAImplementationStrategy{} }调用策略的时候，就可以配合工厂类去动态生成策略实现类来执行逻辑// 根据名称创建A计划执行者 thePlan := GetOperatorByPlanName("A") // 执行A计划 result, err := thePlan.MakeImplementation() // 有错误返回则抛出错误 if err != nil { panic(err) } fmt.Println(result)这样就可以动态扩展多个策略，动态去调用某个具体策略去执行，起到最终程序解耦的效果，应用场景包括支付对接、计划执行等等。

Go奇淫巧计（不定时更新）
type: item
date: 2023-10-27T14:28:00+08:00
description: // 使用一个int控制N个开关，参考 sync.Mutex state 字段状态记录方式 type IntSwitch struct { state int32 } // 枚举定义 const ( switchFirst = 1 << iota // 十进制跟二进制都为1 switchSecond // 十进制：2 二进制：10 switchThird // 十进制：4 二进制：100 ) // 条件判断 data := IntSwitch{state: 7} if data.state & switchThird == switchThird { // 比对最高一位 fmt.Println("第三个开关为打开状态") } // 数据获取 fmt.Printf("第三个开关的状态为：%d", (data.state & switchThird) >> 2)name := "" switch platform { case "Android": name = "西红柿" case "iOS": name = "番茄" default: return fmt.Errorf("该不会是%s吧", "圣女果") } // 条件赋值巧用 map 来声明 name, ok := map[string]string{ "Android": "西红柿", "iOS": "番茄", }[platform] if !ok { return fmt.Errorf("该不会是%s吧", "圣女果") }// 使用泛型实现类似PHP的in_array // comparable 为可比较的类型 func In[T comparable](needle T, haystack []T) bool { for _, item := range haystack { if item == needle { return true } } return false } name := "猕猴桃" if In(name, []string{"猕猴桃", "奇异果"}) { // code ... }// 两阶段延迟执行 // 可用于创建对象、释放对象，或者执行两阶段的逻辑 func TrackTime() func() { pre := time.Now() return func() { elapsed := time.Since(pre) fmt.Println("elapsed:", elapsed) } } func main() { defer TrackTime()() time.Sleep(500 * time.Millisecond) }