xfy d21e27fbac fix(lint): 修复 golangci-lint 错误 (119 -> 0 issues)
主要修复:
- errcheck: defer Close 使用 //nolint:errcheck,类型断言改为 ok 检查
- govet fieldalignment: 调整结构体字段顺序优化内存布局
- revive unused-parameter: 将未使用参数改为 _
- exhaustive: 添加缺失的 switch case 或 default
- goconst: 提取重复字符串为常量 (accessAllow, accessDeny 等)
- staticcheck SA9003: 修复空分支逻辑
- gofmt: 运行 gofmt -w 格式化
- nolintlint: 修复 nolint 注释格式

其他改进:
- 更新 .golangci.yml 配置,启用更严格的检查
- 移除未使用的代码和导入
- 简化测试辅助函数调用

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 16:15:31 +08:00

325 lines
7.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package compression 提供 HTTP 响应压缩中间件,支持 gzip 和 brotli 算法。
//
// 该文件包含压缩相关的核心逻辑,包括:
// - gzip 压缩(兼容性好,所有浏览器支持)
// - brotli 压缩(压缩率更高,适合现代浏览器)
// - MIME 类型过滤
// - 最小压缩大小控制
//
// 主要用途:
//
// 用于压缩 HTTP 响应内容,减少传输数据量,提升页面加载速度。
//
// 注意事项:
// - 使用缓冲池复用压缩对象,减少内存分配
// - 小于 MinSize 的响应不压缩
//
// 作者xfy
package compression
import (
"bytes"
"strings"
"sync"
"github.com/andybalholm/brotli"
"github.com/klauspost/compress/gzip"
"github.com/valyala/fasthttp"
"rua.plus/lolly/internal/config"
)
// Algorithm 压缩算法类型。
type Algorithm int
const (
// AlgorithmGzip 使用 gzip 压缩。
AlgorithmGzip Algorithm = iota
// AlgorithmBrotli 使用 brotli 压缩。
AlgorithmBrotli
compressionGZIP = "gzip"
)
// Middleware 响应压缩中间件。
type Middleware struct {
// gzipPool gzip.Writer 缓冲池
gzipPool sync.Pool
// brotliPool brotli.Writer 缓冲池
brotliPool sync.Pool
// types 可压缩的 MIME 类型列表
types []string
// level 压缩级别1-9
level int
// minSize 最小压缩大小(字节)
minSize int
// algorithm 压缩算法
algorithm Algorithm
}
// New 创建压缩中间件。
//
// 参数:
// - cfg: 压缩配置,包含算法类型、压缩级别、最小压缩大小等
//
// 返回值:
// - *Middleware: 压缩中间件实例
// - error: 配置无效时返回错误
func New(cfg *config.CompressionConfig) (*Middleware, error) {
if cfg == nil {
cfg = &config.CompressionConfig{
Type: "gzip",
Level: 6,
MinSize: 1024,
Types: defaultCompressibleTypes(),
}
}
// 设置默认值
if cfg.Level == 0 {
cfg.Level = 6
}
if cfg.MinSize == 0 {
cfg.MinSize = 1024
}
if len(cfg.Types) == 0 {
cfg.Types = defaultCompressibleTypes()
}
// 解析算法类型
var algo Algorithm
switch strings.ToLower(cfg.Type) {
case "brotli":
algo = AlgorithmBrotli
case compressionGZIP:
algo = AlgorithmGzip
case "both":
// both 模式优先使用 brotli如果客户端支持
algo = AlgorithmBrotli
default:
algo = AlgorithmGzip
}
m := &Middleware{
types: cfg.Types,
level: cfg.Level,
minSize: cfg.MinSize,
algorithm: algo,
}
// 初始化缓冲池
m.gzipPool = sync.Pool{
New: func() interface{} {
w, err := gzip.NewWriterLevel(nil, cfg.Level)
if err != nil {
// 使用默认压缩级别作为回退
w, _ = gzip.NewWriterLevel(nil, gzip.DefaultCompression)
}
return w
},
}
// 初始化 brotli 缓冲池
m.brotliPool = sync.Pool{
New: func() interface{} {
return brotli.NewWriterOptions(nil, brotli.WriterOptions{
Quality: cfg.Level,
})
},
}
return m, nil
}
// defaultCompressibleTypes 返回默认可压缩的 MIME 类型。
func defaultCompressibleTypes() []string {
return []string{
"text/html",
"text/css",
"text/javascript",
"text/plain",
"text/xml",
"application/json",
"application/javascript",
"application/xml",
"application/xhtml+xml",
}
}
// Name 返回中间件名称。
func (m *Middleware) Name() string {
return "compression"
}
// Process 应用压缩中间件。
//
// 参数:
// - next: 下一个请求处理器
//
// 返回值:
// - fasthttp.RequestHandler: 包装后的请求处理器
func (m *Middleware) Process(next fasthttp.RequestHandler) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
// 检查客户端是否支持压缩
acceptEncoding := string(ctx.Request.Header.Peek("Accept-Encoding"))
// 根据算法和客户端支持选择压缩方式
var useGzip, useBrotli bool
switch m.algorithm {
case AlgorithmGzip:
useGzip = strings.Contains(acceptEncoding, "gzip")
case AlgorithmBrotli:
// brotli 或 both 模式
if strings.Contains(acceptEncoding, "br") {
useBrotli = true
} else if strings.Contains(acceptEncoding, "gzip") {
useGzip = true
}
}
// 如果不需要压缩,直接执行
if !useGzip && !useBrotli {
next(ctx)
return
}
// 执行处理器
next(ctx)
// 获取响应体
body := ctx.Response.Body()
bodyLen := len(body)
// 检查是否满足压缩条件
if bodyLen < m.minSize {
return // 不压缩
}
// 检查 MIME 类型
contentType := string(ctx.Response.Header.ContentType())
if !m.isCompressible(contentType) {
return // 不压缩此类型
}
// 执行压缩
var compressed []byte
var encoding string
if useBrotli {
compressed = m.compressBrotli(body)
encoding = "br"
} else if useGzip {
compressed = m.compressGzip(body)
encoding = compressionGZIP
}
if len(compressed) > 0 && len(compressed) < bodyLen {
ctx.Response.SetBody(compressed)
ctx.Response.Header.Set("Content-Encoding", encoding)
ctx.Response.Header.Del("Content-Length") // 让 fasthttp 自动计算
}
}
}
// isCompressible 检查 MIME 类型是否可压缩。
//
// 参数:
// - contentType: 内容类型MIME 类型)
//
// 返回值:
// - bool: 是否可压缩
func (m *Middleware) isCompressible(contentType string) bool {
// 移除 charset 等参数
ct := contentType
if idx := strings.Index(ct, ";"); idx >= 0 {
ct = ct[:idx]
}
ct = strings.TrimSpace(strings.ToLower(ct))
for _, t := range m.types {
if strings.ToLower(t) == ct {
return true
}
// 支持通配符匹配
if strings.HasSuffix(t, "/*") {
base := strings.TrimSuffix(t, "/*")
if strings.HasPrefix(ct, base) {
return true
}
}
}
return false
}
// compressGzip 使用 gzip 压缩数据。
//
// 参数:
// - data: 待压缩的原始数据
//
// 返回值:
// - []byte: 压缩后的数据
func (m *Middleware) compressGzip(data []byte) []byte {
w, ok := m.gzipPool.Get().(*gzip.Writer)
if !ok {
return data // fallback to uncompressed
}
defer m.gzipPool.Put(w)
var buf bytes.Buffer
w.Reset(&buf)
if _, err := w.Write(data); err != nil { //nolint:staticcheck // intentionally empty branch
// 忽略写入错误,缓冲到 bytes.Buffer 时不太可能失败
}
_ = w.Close()
return buf.Bytes()
}
// compressBrotli 使用 brotli 压缩数据。
//
// 参数:
// - data: 待压缩的原始数据
//
// 返回值:
// - []byte: 压缩后的数据
func (m *Middleware) compressBrotli(data []byte) []byte {
w, ok := m.brotliPool.Get().(*brotli.Writer)
if !ok {
return data // fallback to uncompressed
}
defer m.brotliPool.Put(w)
var buf bytes.Buffer
w.Reset(&buf)
if _, err := w.Write(data); err != nil { //nolint:staticcheck // intentionally empty branch
// 忽略写入错误,缓冲到 bytes.Buffer 时不太可能失败
}
_ = w.Close()
return buf.Bytes()
}
// Types 返回可压缩的 MIME 类型列表。
//
// 返回值:
// - []string: 可压缩的 MIME 类型列表
func (m *Middleware) Types() []string {
return m.types
}
// Level 返回压缩级别。
//
// 返回值:
// - int: 压缩级别1-9
func (m *Middleware) Level() int {
return m.level
}
// MinSize 返回最小压缩大小。
//
// 返回值:
// - int: 最小压缩大小(字节)
func (m *Middleware) MinSize() int {
return m.minSize
}