- Collect baseline benchmark summary across all core modules - Save key results to benchmarks/v0.4.0/summary.txt - Update .gitignore to track benchmark summaries/reports - Include performance optimization design docs and plan
23 KiB
消除代码冗余实施计划
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 消除 lolly 项目中的代码冗余:删除 8 处死代码、重构 2 处源文件重复模式、提取测试辅助函数减少 184 处配置字面量重复。
Architecture: 分三阶段实施:阶段 1 删除未使用的死代码(零风险);阶段 2 提取路由注册和 DEBUG 日志辅助函数(低风险重构);阶段 3 创建测试辅助函数包并迁移重复代码(逐步替换)。
Tech Stack: Go 1.22+, golangci-lint, dupl/unused linters
文件结构
创建:
internal/testutil/proxy.go- 测试辅助函数(ProxyConfig、Target 创建)
修改:
internal/config/validate.go- 删除validateStatic()函数internal/config/validate_test.go- 删除TestValidateStatic测试internal/http2/server.go- 删除connectionPool.get()和connectionPool.count()internal/middleware/bodylimit/bodylimit.go- 删除formatSize()函数internal/middleware/bodylimit/bodylimit_test.go- 删除TestFormatSize测试internal/middleware/security/headers.go- 删除 3 个 security headers 函数internal/middleware/security/headers_test.go- 删除 3 个对应测试internal/ssl/ocsp.go- 删除extractCertificates()函数internal/ssl/ocsp_test.go- 删除 2 个对应测试internal/server/router.go- 提取registerRoute辅助函数internal/proxy/proxy.go- 提取proxyDebugLog辅助函数
阶段 1:死代码删除
Task 1: 删除 validateStatic 函数及其测试
Files:
-
Modify:
internal/config/validate.go:475-484 -
Modify:
internal/config/validate_test.go:752-809 -
Step 1: 删除
validateStatic函数
删除 internal/config/validate.go 第 475-484 行:
// validateStatic 验证静态文件配置。
//
// 参数:
// - s: 静态文件配置对象
//
// 返回值:
// - error: 验证失败时返回错误信息,成功返回 nil
func validateStatic(s *StaticConfig) error {
// 静态文件根目录非空时验证路径有效性
if s.Root != "" {
// 路径安全检查:不允许包含 ".."
if err := ValidatePathTraversal(s.Root, "根目录路径"); err != nil {
return err
}
}
return nil
}
- Step 2: 删除对应的单元测试
删除 internal/config/validate_test.go 第 752-809 行的 TestValidateStatic 函数:
func TestValidateStatic(t *testing.T) {
t.Parallel()
// TestValidateStatic 测试静态文件配置验证。
tests := []struct {
name string
errMsg string
config StaticConfig
wantErr bool
}{
{
name: "空配置有效",
config: StaticConfig{},
wantErr: false,
},
{
name: "有效根目录",
config: StaticConfig{
Root: "/var/www/html",
},
wantErr: false,
},
{
name: "根目录含..路径遍历",
config: StaticConfig{
Root: "/var/www/../etc",
},
wantErr: true,
errMsg: "根目录路径不能包含 '..'",
},
{
name: "根目录含多个..",
config: StaticConfig{
Root: "/var/../www/../html",
},
wantErr: true,
errMsg: "根目录路径不能包含 '..'",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateStatic(&tt.config)
if tt.wantErr {
if err == nil {
t.Errorf("validateStatic() 期望返回错误,但返回 nil")
return
}
if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
t.Errorf("validateStatic() 错误消息不匹配,期望包含 %q,实际 %q", tt.errMsg, err.Error())
}
} else {
if err != nil {
t.Errorf("validateStatic() 期望返回 nil,但返回错误: %v", err)
}
}
})
}
}
- Step 3: 运行测试确认通过
Run: go test ./internal/config/... -run TestValidateStatic -v
Expected: 无此测试(因为已删除)
Run: go test ./internal/config/... -v
Expected: PASS
- Step 4: Commit
git add internal/config/validate.go internal/config/validate_test.go
git commit -m "refactor: remove unused validateStatic function and its test"
Task 2: 删除 connectionPool 未使用的方法
Files:
-
Modify:
internal/http2/server.go:575-587 -
Step 1: 删除
get和count方法
删除 internal/http2/server.go 第 575-587 行:
// get 获取连接。
func (p *connectionPool) get(key string) []net.Conn {
p.mu.RLock()
defer p.mu.RUnlock()
return p.conns[key]
}
// count 获取连接数。
func (p *connectionPool) count(key string) int {
p.mu.RLock()
defer p.mu.RUnlock()
return len(p.conns[key])
}
- Step 2: 运行测试确认通过
Run: go test ./internal/http2/... -v
Expected: PASS
- Step 3: Commit
git add internal/http2/server.go
git commit -m "refactor: remove unused connectionPool.get and connectionPool.count methods"
Task 3: 删除 bodylimit.formatSize 函数及其测试
Files:
-
Modify:
internal/middleware/bodylimit/bodylimit.go:279-305 -
Modify:
internal/middleware/bodylimit/bodylimit_test.go:36-72 -
Step 1: 删除
formatSize函数
删除 internal/middleware/bodylimit/bodylimit.go 第 279-305 行:
// formatSize 将字节数格式化为人类可读的字符串。
//
// 根据大小自动选择合适的单位(b、kb、mb、gb)。
//
// 参数:
// - size: 字节数
//
// 返回值:
// - string: 格式化后的字符串,如 "1.00mb"、"10.00kb"
func formatSize(size int64) string {
const (
KB = 1024
MB = 1024 * KB
GB = 1024 * MB
)
switch {
case size >= GB:
return fmt.Sprintf("%.2fgb", float64(size)/GB)
case size >= MB:
return fmt.Sprintf("%.2fmb", float64(size)/MB)
case size >= KB:
return fmt.Sprintf("%.2fkb", float64(size)/KB)
default:
return fmt.Sprintf("%db", size)
}
}
- Step 2: 删除对应的单元测试
删除 internal/middleware/bodylimit/bodylimit_test.go 第 36-72 行的 TestFormatSize 函数:
func TestFormatSize(t *testing.T) {
tests := []struct {
input int64
expected string
}{
{512, "512b"},
{1024, "1.00kb"},
{1024 * 1024, "1.00mb"},
{1024 * 1024 * 1024, "1.00gb"},
{1536, "1.50kb"},
}
for _, tt := range tests {
t.Run(formatSize(tt.input), func(t *testing.T) {
got := formatSize(tt.input)
if got != tt.expected {
t.Errorf("formatSize(%d) = %s, want %s", tt.input, got, tt.expected)
}
})
}
}
- Step 3: 运行测试确认通过
Run: go test ./internal/middleware/bodylimit/... -v
Expected: PASS
- Step 4: Commit
git add internal/middleware/bodylimit/bodylimit.go internal/middleware/bodylimit/bodylimit_test.go
git commit -m "refactor: remove unused bodylimit.formatSize function and test"
Task 4: 删除 security headers 未使用的函数及其测试
Files:
-
Modify:
internal/middleware/security/headers.go:291-331 -
Modify:
internal/middleware/security/headers_test.go:184-215 -
Step 1: 删除 3 个 security headers 函数
删除 internal/middleware/security/headers.go 第 291-331 行:
// defaultSecurityHeaders 返回安全的安全头默认配置。
//
// 返回值:
// - *config.SecurityHeaders: 包含安全默认值的配置对象
func defaultSecurityHeaders() *config.SecurityHeaders {
return &config.SecurityHeaders{
XFrameOptions: "DENY",
XContentTypeOptions: "nosniff",
ReferrerPolicy: "strict-origin-when-cross-origin",
}
}
// strictSecurityHeaders 返回严格模式的安全头配置。
//
// 适用于高安全要求的应用场景,包含严格的 CSP 和权限策略。
//
// 返回值:
// - *config.SecurityHeaders: 包含严格安全值的配置对象
func strictSecurityHeaders() *config.SecurityHeaders {
return &config.SecurityHeaders{
XFrameOptions: "DENY",
XContentTypeOptions: "nosniff",
ContentSecurityPolicy: "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; connect-src 'self'; frame-ancestors 'none'",
ReferrerPolicy: "no-referrer",
PermissionsPolicy: "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()",
}
}
// developmentSecurityHeaders 返回开发环境使用的宽松安全头配置。
//
// 警告:请勿在生产环境使用此配置,安全性较低。
//
// 返回值:
// - *config.SecurityHeaders: 包含宽松安全值的配置对象
func developmentSecurityHeaders() *config.SecurityHeaders {
return &config.SecurityHeaders{
XFrameOptions: "SAMEORIGIN",
XContentTypeOptions: "nosniff",
ReferrerPolicy: "strict-origin-when-cross-origin",
}
}
- Step 2: 删除对应的单元测试
删除 internal/middleware/security/headers_test.go 第 184-215 行:
func TestDefaultSecurityHeaders(t *testing.T) {
cfg := defaultSecurityHeaders()
if cfg.XFrameOptions != "DENY" {
t.Errorf("Expected default X-Frame-Options 'DENY', got %s", cfg.XFrameOptions)
}
if cfg.XContentTypeOptions != "nosniff" {
t.Errorf("Expected default X-Content-Type-Options 'nosniff', got %s", cfg.XContentTypeOptions)
}
}
func TestStrictSecurityHeaders(t *testing.T) {
cfg := strictSecurityHeaders()
if cfg.XFrameOptions != "DENY" {
t.Errorf("Expected X-Frame-Options 'DENY', got %s", cfg.XFrameOptions)
}
if cfg.ReferrerPolicy != "no-referrer" {
t.Errorf("Expected Referrer-Policy 'no-referrer', got %s", cfg.ReferrerPolicy)
}
if cfg.ContentSecurityPolicy == "" {
t.Error("Expected non-empty CSP for strict config")
}
}
func TestDevelopmentSecurityHeaders(t *testing.T) {
cfg := developmentSecurityHeaders()
if cfg.XFrameOptions != "SAMEORIGIN" {
t.Errorf("Expected X-Frame-Options 'SAMEORIGIN' for dev, got %s", cfg.XFrameOptions)
}
}
- Step 3: 运行测试确认通过
Run: go test ./internal/middleware/security/... -v
Expected: PASS
- Step 4: Commit
git add internal/middleware/security/headers.go internal/middleware/security/headers_test.go
git commit -m "refactor: remove unused security header preset functions and tests"
Task 5: 删除 extractCertificates 函数及其测试
Files:
-
Modify:
internal/ssl/ocsp.go:482-514 -
Modify:
internal/ssl/ocsp_test.go:311-335 -
Step 1: 删除
extractCertificates函数
删除 internal/ssl/ocsp.go 第 482-514 行:
// extractCertificates 解析 PEM 数据并返回证书列表。
//
// 参数:
// - pemData: PEM 编码的证书数据
//
// 返回值:
// - []*x509.Certificate: 解析后的证书列表
// - error: 解析失败时返回错误
func extractCertificates(pemData []byte) ([]*x509.Certificate, error) {
var certs []*x509.Certificate
rest := pemData
for {
block, remaining := pem.Decode(rest)
if block == nil {
break
}
if block.Type == "CERTIFICATE" {
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse certificate: %w", err)
}
certs = append(certs, cert)
}
rest = remaining
}
if len(certs) == 0 {
return nil, errors.New("no certificates found in PEM data")
}
return certs, nil
}
- Step 2: 删除对应的单元测试
删除 internal/ssl/ocsp_test.go 第 311-335 行:
func TestExtractCertificates(t *testing.T) {
// Create valid PEM data
certPEM, _ := generateTestCertWithOCSP(t, nil)
certs, err := extractCertificates(certPEM)
if err != nil {
t.Fatalf("extractCertificates() failed: %v", err)
}
if len(certs) == 0 {
t.Error("Expected at least one certificate")
}
}
func TestExtractCertificatesInvalidPEM(t *testing.T) {
invalidPEM := []byte("not valid pem data")
certs, err := extractCertificates(invalidPEM)
if err == nil {
t.Error("Expected error for invalid PEM data")
}
if certs != nil {
t.Error("Expected nil certs for invalid PEM data")
}
}
- Step 3: 运行测试确认通过
Run: go test ./internal/ssl/... -v
Expected: PASS
- Step 4: Commit
git add internal/ssl/ocsp.go internal/ssl/ocsp_test.go
git commit -m "refactor: remove unused extractCertificates function and tests"
阶段 2:源文件重复模式重构
Task 6: 提取路由注册辅助函数
Files:
-
Modify:
internal/server/router.go:84-124和internal/server/router.go:190-220和internal/server/router.go:390-420 -
Step 1: 添加
registerRoute辅助函数
在 internal/server/router.go 的 configureProxyRoutes 函数之前添加:
// registerRoute 根据位置类型注册路由
func (s *Server) registerRoute(
locType string,
path string,
handler fasthttp.RequestHandler,
internal bool,
source string,
) error {
var err error
switch locType {
case matcher.LocationTypeExact:
err = s.locationEngine.AddExact(path, handler, internal)
case matcher.LocationTypePrefixPriority:
err = s.locationEngine.AddPrefixPriority(path, handler, internal)
case matcher.LocationTypeRegex:
err = s.locationEngine.AddRegex(path, handler, false, internal)
case matcher.LocationTypeRegexCaseless:
err = s.locationEngine.AddRegex(path, handler, true, internal)
case matcher.LocationTypeNamed:
err = s.locationEngine.AddNamed(path, handler)
default:
err = s.locationEngine.AddPrefix(path, handler, internal)
}
if err != nil {
return s.handleRegistrationError(source, path, err)
}
return nil
}
- Step 2: 重构 proxy 路由注册
将 internal/server/router.go 第 84-124 行的 switch 语句替换为:
switch locType {
case matcher.LocationTypeExact:
if err := s.registerRoute(locType, proxyCfg.Path, p.ServeHTTP, proxyCfg.Internal, "proxy"); err != nil {
return err
}
case matcher.LocationTypePrefixPriority:
if err := s.registerRoute(locType, proxyCfg.Path, p.ServeHTTP, proxyCfg.Internal, "proxy"); err != nil {
return err
}
case matcher.LocationTypeRegex, matcher.LocationTypeRegexCaseless:
caseInsensitive := locType == matcher.LocationTypeRegexCaseless
if err := s.registerRoute(locType, proxyCfg.Path, p.ServeHTTP, proxyCfg.Internal, "proxy"); err != nil {
return err
}
case matcher.LocationTypeNamed:
if proxyCfg.LocationName != "" {
if err := s.registerRoute(locType, "@"+proxyCfg.LocationName, p.ServeHTTP, false, "proxy"); err != nil {
return err
}
}
case matcher.LocationTypePrefix:
if err := s.registerRoute(locType, proxyCfg.Path, p.ServeHTTP, proxyCfg.Internal, "proxy"); err != nil {
return err
}
default:
if err := s.registerRoute(locType, proxyCfg.Path, p.ServeHTTP, proxyCfg.Internal, "proxy"); err != nil {
return err
}
}
- Step 3: 重构 static 路由注册
将 internal/server/router.go 第 190-220 行的类似代码替换为 registerRoute 调用。
- Step 4: 重构 lua 路由注册
将 internal/server/router.go 第 390-420 行的类似代码替换为 registerRoute 调用。
- Step 5: 运行测试确认通过
Run: go test ./internal/server/... -v
Expected: PASS
- Step 6: Commit
git add internal/server/router.go
git commit -m "refactor: extract registerRoute helper to reduce repetition"
Task 7: 提取 DEBUG 日志辅助函数
Files:
-
Modify:
internal/proxy/proxy.go:470-476和类似位置 -
Step 1: 添加
proxyDebugLog辅助函数
在 internal/proxy/proxy.go 的 ServeHTTP 方法之前添加:
// proxyDebugLog 在 DEBUG 级别记录代理日志
func proxyDebugLog(msg string, kv ...interface{}) {
if !logging.Debug().Enabled() {
return
}
event := logging.Debug()
for i := 0; i < len(kv)-1; i += 2 {
key, ok := kv[i].(string)
if !ok {
continue
}
switch v := kv[i+1].(type) {
case string:
event = event.Str(key, v)
case int:
event = event.Int(key, v)
case bool:
event = event.Bool(key, v)
}
}
event.Msg(msg)
}
- Step 2: 替换第一个 DEBUG 日志
将第 470-476 行:
if logging.Debug().Enabled() {
logging.Debug().
Str("path", b2s(ctx.Path())).
Str("host", b2s(ctx.Host())).
Str("method", b2s(ctx.Method())).
Msg("[PROXY] 收到请求")
}
替换为:
proxyDebugLog("[PROXY] 收到请求",
"path", b2s(ctx.Path()),
"host", b2s(ctx.Host()),
"method", b2s(ctx.Method()),
)
- Step 3: 替换其余 4 个 DEBUG 日志
重复 Step 2 的模式,替换第 536-540、555-559、627-631、715-719 行的 DEBUG 日志。
- Step 4: 运行测试确认通过
Run: go test ./internal/proxy/... -v
Expected: PASS
- Step 5: Commit
git add internal/proxy/proxy.go
git commit -m "refactor: extract proxyDebugLog helper for repeated debug logging"
阶段 3:测试辅助函数
Task 8: 创建测试辅助函数包
Files:
-
Create:
internal/testutil/proxy.go -
Step 1: 创建测试辅助函数文件
创建 internal/testutil/proxy.go:
package testutil
import (
"time"
"rua.plus/lolly/internal/config"
"rua.plus/lolly/internal/loadbalance"
)
// NewTestProxyConfig 创建测试用的代理配置
//
// 参数:
// - path: 代理路径
// - targetURLs: 后端目标 URL 列表
//
// 返回值:
// - *config.ProxyConfig: 配置好的代理配置
func NewTestProxyConfig(path string, targetURLs ...string) *config.ProxyConfig {
cfg := &config.ProxyConfig{
Path: path,
LoadBalance: "round_robin",
Timeout: config.ProxyTimeout{
Connect: 5 * time.Second,
Read: 30 * time.Second,
Write: 30 * time.Second,
},
}
if len(targetURLs) > 0 {
cfg.Targets = make([]config.ProxyTargetConfig, len(targetURLs))
for i, url := range targetURLs {
cfg.Targets[i] = config.ProxyTargetConfig{URL: url}
}
}
return cfg
}
// NewTestProxyConfigWithCache 创建带缓存的测试代理配置
func NewTestProxyConfigWithCache(path string, maxAge time.Duration, targetURLs ...string) *config.ProxyConfig {
cfg := NewTestProxyConfig(path, targetURLs...)
cfg.Cache = config.ProxyCacheConfig{
Enabled: true,
MaxAge: maxAge,
}
return cfg
}
// NewTestTarget 创建测试用的代理目标
//
// 参数:
// - url: 目标 URL
//
// 返回值:
// - *loadbalance.Target: 测试目标
func NewTestTarget(url string) *loadbalance.Target {
return &loadbalance.Target{URL: url}
}
// NewTestTargets 批量创建测试目标
func NewTestTargets(urls ...string) []*loadbalance.Target {
targets := make([]*loadbalance.Target, len(urls))
for i, url := range urls {
targets[i] = NewTestTarget(url)
}
return targets
}
// NewTestHealthyTarget 创建已标记为健康的测试目标
//
// 参数:
// - url: 目标 URL
//
// 返回值:
// - *loadbalance.Target: 已标记为健康的测试目标
func NewTestHealthyTarget(url string) *loadbalance.Target {
t := NewTestTarget(url)
t.Healthy.Store(true)
return t
}
// NewTestHealthyTargets 批量创建健康测试目标
func NewTestHealthyTargets(urls ...string) []*loadbalance.Target {
targets := make([]*loadbalance.Target, len(urls))
for i, url := range urls {
targets[i] = NewTestHealthyTarget(url)
}
return targets
}
- Step 2: 编写辅助函数测试
创建 internal/testutil/proxy_test.go:
package testutil
import (
"testing"
"time"
)
func TestNewTestProxyConfig(t *testing.T) {
cfg := NewTestProxyConfig("/api", "http://localhost:8080")
if cfg.Path != "/api" {
t.Errorf("expected path /api, got %s", cfg.Path)
}
if len(cfg.Targets) != 1 {
t.Errorf("expected 1 target, got %d", len(cfg.Targets))
}
if cfg.Timeout.Connect != 5*time.Second {
t.Errorf("expected 5s connect timeout, got %v", cfg.Timeout.Connect)
}
}
func TestNewTestHealthyTarget(t *testing.T) {
target := NewTestHealthyTarget("http://localhost:8080")
if target.URL != "http://localhost:8080" {
t.Errorf("expected URL http://localhost:8080, got %s", target.URL)
}
if !target.Healthy.Load() {
t.Error("expected target to be healthy")
}
}
func TestNewTestHealthyTargets(t *testing.T) {
targets := NewTestHealthyTargets("http://localhost:8080", "http://localhost:8081")
if len(targets) != 2 {
t.Errorf("expected 2 targets, got %d", len(targets))
}
for i, target := range targets {
if !target.Healthy.Load() {
t.Errorf("expected target %d to be healthy", i)
}
}
}
- Step 3: 运行测试确认通过
Run: go test ./internal/testutil/... -v
Expected: PASS
- Step 4: Commit
git add internal/testutil/
git commit -m "feat: add testutil package for proxy config helpers"
Task 9: 迁移 proxy 测试使用辅助函数
Files:
-
Modify:
internal/proxy/proxy_test.go -
Modify:
internal/integration/proxy_integration_test.go -
Step 1: 修改
internal/proxy/proxy_test.go导入
添加导入:
import (
"rua.plus/lolly/internal/testutil"
)
- Step 2: 替换重复的 ProxyConfig 创建
将测试中的重复模式替换为:
// 替换前:
cfg := &config.ProxyConfig{
Path: "/api",
LoadBalance: "round_robin",
Timeout: config.ProxyTimeout{
Connect: 5 * time.Second,
Read: 30 * time.Second,
Write: 30 * time.Second,
},
}
// 替换后:
cfg := testutil.NewTestProxyConfig("/api")
- Step 3: 替换重复的 Target 创建
将:
targets := []*loadbalance.Target{{URL: "http://localhost:8080"}}
targets[0].Healthy.Store(true)
替换为:
targets := testutil.NewTestHealthyTargets("http://localhost:8080")
- Step 4: 运行测试确认通过
Run: go test ./internal/proxy/... -v
Expected: PASS
- Step 5: Commit
git add internal/proxy/proxy_test.go internal/integration/proxy_integration_test.go
git commit -m "refactor: use testutil helpers in proxy tests"
Task 10: 迁移 server 测试使用辅助函数
Files:
-
Modify:
internal/server/*_test.go -
Step 1: 批量替换 server 测试中的重复代码
使用与 Task 9 相同的模式,替换 internal/server/ 下所有测试文件中的重复 ProxyConfig 和 Target 创建。
- Step 2: 运行测试确认通过
Run: go test ./internal/server/... -v
Expected: PASS
- Step 3: Commit
git add internal/server/
git commit -m "refactor: use testutil helpers in server tests"
验收检查
Task 11: 最终验证
- Step 1: 运行 unused linter
Run: golangci-lint run --enable=unused ./...
Expected: 无 unused 错误
- Step 2: 运行 dupl linter
Run: golangci-lint run --enable=dupl ./...
Expected: 源文件无 dupl 错误(测试文件允许)
- Step 3: 运行完整测试套件
Run: go test ./...
Expected: 全部 PASS
- Step 4: 统计代码行数变化
Run: git diff --stat
Expected: 总行数净减少 >200 行
- Step 5: 最终 Commit
git commit -m "chore: eliminate code redundancy - dead code removal, pattern extraction, test helpers"
Self-Review Checklist
- Spec coverage: 所有 3 个阶段都有详细任务 ✓
- Placeholder scan: 无 TBD、TODO 或模糊描述 ✓
- Type consistency:
registerRoute和proxyDebugLog签名与使用处一致 ✓ - File paths: 所有路径均为绝对路径,与代码库匹配 ✓
- Commands: 每个测试步骤都有明确的运行命令和预期输出 ✓