主要修复: - 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>
325 lines
7.2 KiB
Go
325 lines
7.2 KiB
Go
// 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
|
||
}
|