一千条短信,800 条规则,原本要跑近 5 秒------这比我那什么的时间还长!
但经过三次"魔法优化",最终只要 0.25 秒,提速 19 倍!
今天,我就手把手带你复刻这场性能逆袭,代码、测试、结果全公开,包教包会!
🚨 背景:短信规则匹配的"性能灾难"
想象一下:你有个短信风控系统,里面有 800 条正则规则,比如:
text
.*【阿里云】.*验证码.*\d{6}
.*【招商银行】.*登录.*\d{6}
...
每当收到一条短信(比如 【阿里云】您的验证码是123456,请勿泄露。),你就要检查它是否命中任意一条规则。
最朴素的做法?暴力 for 循环 + 每次都编译正则。
听起来没问题?直到你跑起来------
💥 4.8 秒!处理 1000 条短信居然要近 5 秒!
这哪是风控系统,这是"风冷系统"------CPU 都快烧干了!
🧪 我们先写个"慢到哭"的 V1 版本
go
// ========== 版本一:暴力匹配(每次编译) ==========
func MatchV1(content string, patterns []string) []string {
var hits []string
for _, pattern := range patterns {
matched, _ := regexp.MatchString(pattern, content)
if matched {
hits = append(hits, pattern)
}
}
return hits
}
问题在哪?
regexp.MatchString 内部会 每次重新编译正则表达式!
而正则编译,是出了名的"重量级操作"。
📈 性能测试:用数据说话!
我们生成:
- 800 条规则(均匀分布在 5 个签名:阿里云、腾讯云、招行、京东、美团)
- 1000 条测试短信
然后跑 benchmark:
go
func BenchmarkV1(b *testing.B) {
for i := 0; i < b.N; i++ {
for _, msg := range testMessages {
_ = MatchV1(msg, testPatterns)
}
}
}
结果震惊了:
text
BenchmarkV1-16 1 4877707000 ns/op 4002663840 B/op 28499560 allocs/op
- 4.87 秒 处理 1000 条短信
- 4GB 内存分配!
- 2850 万次内存分配!
这哪是程序,这是内存粉碎机!
✨ 优化第一步:预编译正则(V2)
核心思想:正则表达式只编译一次,复用!
go
type MatcherV2 struct {
compiled []*regexp.Regexp
patterns []string
}
func NewMatcherV2(patterns []string) *MatcherV2 {
compiled := make([]*regexp.Regexp, len(patterns))
for i, p := range patterns {
compiled[i] = regexp.MustCompile(p) // 👈 只编译一次!
}
return &MatcherV2{compiled: compiled, patterns: patterns}
}
func (m *MatcherV2) Match(content string) []string {
var hits []string
for i, r := range m.compiled {
if r.MatchString(content) {
hits = append(hits, m.patterns[i])
}
}
return hits
}
效果如何?
text
BenchmarkV2-16 2 768627000 ns/op 9372024 B/op 9009 allocs/op
- 0.77 秒 → 提速 6.3 倍!
- 内存从 4GB → 9MB!
- 分配次数从 2850 万 → 9009 次!
🎉 结论:永远不要在循环里编译正则!
🧠 优化第二步:按签名分桶(V3)
观察业务:每条短信都有签名,比如 【阿里云】。
而 800 条规则,其实只属于 5 个签名,每个签名约 160 条规则。
那干嘛要匹配全部 800 条?只匹配对应签名的规则不就行了?
V3 实现:
go
type RuleV3 struct {
Pattern string
Sign string
Regexp *regexp.Regexp
}
type MatcherV3 struct {
buckets map[string][]RuleV3
}
func NewMatcherV3(rules []RuleV3) *MatcherV3 {
buckets := make(map[string][]RuleV3)
for _, rule := range rules {
rule.Regexp = regexp.MustCompile(rule.Pattern)
buckets[rule.Sign] = append(buckets[rule.Sign], rule)
}
return &MatcherV3{buckets: buckets}
}
// 从短信中提取【】内的签名
func extractSign(content string) string {
start := strings.Index(content, "【")
if start == -1 {
return "unknown"
}
rest := content[start+3:] // "【" 长度为3(UTF-8)
end := strings.Index(rest, "】")
if end == -1 {
return "unknown"
}
return content[start+3 : start+3+end]
}
func (m *MatcherV3) Match(content string) []string {
sign := extractSign(content)
rules := m.buckets[sign] // 只取这个签名的规则!
var hits []string
for _, rule := range rules {
if rule.Regexp.MatchString(content) {
hits = append(hits, rule.Pattern)
}
}
return hits
}
测试用例(确保逻辑正确):
go
func TestMatcherV3_Match(t *testing.T) {
msg := "【阿里云】您的验证码是123456,请勿泄露。"
hits := matcherV3.Match(msg)
if len(hits) == 0 {
t.Errorf("期望命中至少1条,实际命中0条")
}
}
Benchmark 结果:
text
BenchmarkV3-16 4 256013800 ns/op 9343618 B/op 9006 allocs/op
- 0.256 秒 处理 1000 条短信
- 单条短信仅需 256 微秒!
- 比 V1 快 19 倍!
- 内存和分配次数几乎和 V2 一样(因为正则已预编译)
🎯 性能提升总结
| 优化步骤 | 技术手段 | 提速效果 | 关键收获 |
|---|---|---|---|
| V1 → V2 | 预编译正则 | 6.3 倍 | 避免重复编译,内存暴跌 |
| V2 → V3 | 按签名分桶 | 3.0 倍 | 利用业务特征,减少 80% 无效匹配 |
| 总计 | 双重优化 | 19 倍 | 4.8 秒 → 0.25 秒! |
💡 性能优化的黄金法则:
- 消除重复计算(预编译)
- 减少无效计算(分桶剪枝)
🧩 附:完整测试数据生成器
go
func GenerateTestData() (messages []string, patterns []string, rulesV3 []RuleV3) {
rand := newRandSource()
signs := []string{"阿里云", "腾讯云", "招商银行", "京东", "美团"}
patterns = make([]string, 0, 800)
rulesV3 = make([]RuleV3, 0, 800)
for i := 0; i < 800; i++ {
sign := signs[rand.Intn(len(signs))]
pattern := `.*` + regexp.QuoteMeta(sign) + `.*验证码.*\d{6}`
patterns = append(patterns, pattern)
rulesV3 = append(rulesV3, RuleV3{Pattern: pattern, Sign: sign})
}
messages = make([]string, 1000)
for i := 0; i < 1000; i++ {
sign := signs[rand.Intn(len(signs))]
code := 100000 + rand.Intn(900000)
messages[i] = fmt.Sprintf("【%s】您的验证码是%d,请勿泄露。", sign, code)
}
return
}
// 简单伪随机(确保可复现)
type randSource struct{ seed int64 }
func newRandSource() *randSource { return &randSource{seed: 123456789} }
func (r *randSource) Intn(n int) int {
r.seed = (r.seed*1664525 + 1013904223) % (1 << 32)
return int(r.seed % int64(n))
}
✅ 结语:性能优化,没那么玄
很多人觉得"性能优化"很高深,其实核心就两点:
- 别干重复的活(比如重复编译正则);
- 别干没用的活(比如匹配不相关的规则)。
V3 的代码不到 50 行,却带来了 19 倍性能提升。
下次你的程序变慢了,不妨问问自己:
"我是不是在重复造轮子?是不是在干无用功?"
答案找到了,性能就回来了!
✅ 源码:
main.go
go
package main
import (
"fmt"
"regexp"
"strings"
)
// ========== 公共数据结构 ==========
// RuleV3 带签名的规则(用于 V3)
type RuleV3 struct {
Pattern string
Sign string
Regexp *regexp.Regexp
}
// ========== 版本一:暴力匹配(每次编译) ==========
func MatchV1(content string, patterns []string) []string {
var hits []string
for _, pattern := range patterns {
matched, _ := regexp.MatchString(pattern, content)
if matched {
hits = append(hits, pattern)
}
}
return hits
}
// ========== 版本二:预编译正则 ==========
type MatcherV2 struct {
compiled []*regexp.Regexp
patterns []string
}
func NewMatcherV2(patterns []string) *MatcherV2 {
compiled := make([]*regexp.Regexp, len(patterns))
for i, p := range patterns {
compiled[i] = regexp.MustCompile(p)
}
return &MatcherV2{
compiled: compiled,
patterns: patterns,
}
}
func (m *MatcherV2) Match(content string) []string {
var hits []string
for i, r := range m.compiled {
if r.MatchString(content) {
hits = append(hits, m.patterns[i])
}
}
return hits
}
// ========== 版本三:按签名分桶 ==========
// 性能测试结果精确分析:
// - V2比V1快约6.3倍:预编译正则避免了重复编译开销
// - V3比V2快约3.0倍:通过签名分桶,每个短信只匹配约1/5的规则(800规则分布在5个签名)
// - V3比V1快约18.8倍:结合了预编译和分桶的双重优化
type MatcherV3 struct {
buckets map[string][]RuleV3
}
func NewMatcherV3(rules []RuleV3) *MatcherV3 {
buckets := make(map[string][]RuleV3)
for _, rule := range rules {
rule.Regexp = regexp.MustCompile(rule.Pattern)
buckets[rule.Sign] = append(buckets[rule.Sign], rule)
}
// 注意:无需显式初始化 "unknown",Go map 会返回 nil slice,range 安全
return &MatcherV3{buckets: buckets}
}
// extractSign 从短信中提取【】内的签名
func extractSign(content string) string {
left := "【"
right := "】"
start := strings.Index(content, left)
if start == -1 {
return "unknown"
}
// 在 start 之后查找 】
rest := content[start+len(left):]
end := strings.Index(rest, right)
if end == -1 {
return "unknown"
}
return content[start+len(left) : start+len(left)+end]
}
func (m *MatcherV3) Match(content string) []string {
sign := extractSign(content)
rules := m.buckets[sign] // 若 sign 不存在,rules 为 nil,range 安全
var hits []string
for _, rule := range rules {
if rule.Regexp.MatchString(content) {
hits = append(hits, rule.Pattern)
}
}
return hits
}
// ========== 测试数据生成器 ==========
func GenerateTestData() (messages []string, patterns []string, rulesV3 []RuleV3) {
rand := newRandSource()
signs := []string{"阿里云", "腾讯云", "招商银行", "京东", "美团"} // 5 个签名
// 生成 800 条正则规则(均匀分布)
patterns = make([]string, 0, 800)
rulesV3 = make([]RuleV3, 0, 800)
for i := 0; i < 800; i++ {
sign := signs[rand.Intn(len(signs))] // ✅ 修复:使用 len(signs),包含全部 5 个
pattern := `.*` + regexp.QuoteMeta(sign) + `.*验证码.*\d{6}`
patterns = append(patterns, pattern)
rulesV3 = append(rulesV3, RuleV3{Pattern: pattern, Sign: sign})
}
// 生成 1000 条测试短信
messages = make([]string, 1000)
for i := 0; i < 1000; i++ {
sign := signs[rand.Intn(len(signs))] // ✅ 同样使用全部 5 个
code := 100000 + rand.Intn(900000)
messages[i] = fmt.Sprintf("【%s】您的验证码是%d,请勿泄露。", sign, code)
}
return messages, patterns, rulesV3
}
// 简单的伪随机,确保测试可复现
type randSource struct {
seed int64
}
func newRandSource() *randSource {
return &randSource{seed: 123456789}
}
func (r *randSource) Intn(n int) int {
r.seed = (r.seed*1664525 + 1013904223) % (1 << 32)
return int(r.seed % int64(n))
}
main_test.go
go
package main
import (
"testing"
)
var (
testMessages, testPatterns, testRulesV3 = GenerateTestData()
matcherV2 = NewMatcherV2(testPatterns)
matcherV3 = NewMatcherV3(testRulesV3)
)
func TestMatcherV3_Match(t *testing.T) {
msg := "【阿里云】您的验证码是123456,请勿泄露。"
hits := matcherV3.Match(msg)
if len(hits) == 0 {
t.Errorf("期望命中至少1条,实际命中0条")
}
}
func BenchmarkV1(b *testing.B) {
for i := 0; i < b.N; i++ {
for _, msg := range testMessages {
_ = MatchV1(msg, testPatterns)
}
}
}
func BenchmarkV2(b *testing.B) {
for i := 0; i < b.N; i++ {
for _, msg := range testMessages {
_ = matcherV2.Match(msg)
}
}
}
func BenchmarkV3(b *testing.B) {
for i := 0; i < b.N; i++ {
for _, msg := range testMessages {
_ = matcherV3.Match(msg)
}
}
}
往期部分文章列表
- 用 Go 手搓一个内网 DNS 服务器:从此告别 IP 地址,用域名畅游家庭网络!
- 我用Go写了个华容道游戏,曹操终于不用再求关羽了!
- 用 Go 接口把 Excel 变成数据库:一个疯狂但可行的想法
- 穿墙术大揭秘:用 Go 手搓一个"内网穿透"神器!
- 布隆过滤器(go):一个可能犯错但从不撒谎的内存大师
- 自由通讯的魔法:Go从零实现UDP/P2P 聊天工具
- Go语言实现的简易远程传屏工具:让你的屏幕「飞」起来
- 当你的程序学会了"诈尸":Go 实现 Windows 进程守护术
- 验证码识别API:告别收费接口,迎接免费午餐
- 用 Go 给 Windows 装个"顺风耳":两分钟写个录音小工具
- 无奈!我用go写了个MySQL服务
- 使用 Go + govcl 实现 Windows 资源管理器快捷方式管理器
- 用 Go 手搓一个 NTP 服务:从"时间混乱"到"精准同步"的奇幻之旅
- 用 Go 手搓一个 Java 构建工具:当 IDE 不在身边时的自救指南
- 深入理解 Windows 全局键盘钩子(Hook):拦截 Win 键的 Go 实现
- 用 Go 语言实现《周易》大衍筮法起卦程序
- Go 语言400行代码实现 INI 配置文件解析器:支持注释、转义与类型推断
- 高性能 Go 语言带 TTL 的内存缓存实现:精确过期、自动刷新、并发安全
- Golang + OpenSSL 实现 TLS 安全通信:从私有 CA 到动态证书加载