在Golang开发中,文件缓存是优化性能的常见方案,通过内存缓存可以减少磁盘IO次数,结合HTTP缓存策略能进一步降低客户端重复请求的开销,两种方案结合能覆盖更多性能优化场景。

内存缓存实现文件缓存
内存缓存的核心是将文件内容加载到内存的键值对中,后续请求直接返回内存中的数据,避免重复读取磁盘。我们可以实现一个简单的带过期时间的内存缓存结构。
缓存结构设计
首先需要定义缓存的条目结构,包含文件内容、过期时间和文件路径等信息:
package main
import (
"io/ioutil"
"sync"
"time"
)
// 缓存条目结构
type cacheItem struct {
content []byte // 文件内容
expireTime time.Time // 过期时间
filePath string // 文件路径
}
// 内存缓存结构体
type memoryCache struct {
items map[string]*cacheItem // 键值对存储,key为文件路径
mu sync.RWMutex // 读写锁,保证并发安全
}
缓存操作方法
接下来实现缓存的初始化、获取和设置方法,设置时支持自定义过期时间:
// 初始化内存缓存
func newMemoryCache() *memoryCache {
return &memoryCache{
items: make(map[string]*cacheItem),
}
}
// 从缓存获取文件内容
func (c *memoryCache) get(filePath string) ([]byte, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
item, exists := c.items[filePath]
if !exists {
return nil, false
}
// 检查是否过期
if time.Now().After(item.expireTime) {
return nil, false
}
return item.content, true
}
// 设置文件缓存
func (c *memoryCache) set(filePath string, content []byte, expire time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()
c.items[filePath] = &cacheItem{
content: content,
expireTime: time.Now().Add(expire),
filePath: filePath,
}
}
读取文件并缓存示例
下面是读取文件并写入缓存的完整示例,缓存有效期设置为10分钟:
func readFileWithCache(cache *memoryCache, filePath string) ([]byte, error) {
// 先尝试从缓存获取
content, exists := cache.get(filePath)
if exists {
return content, nil
}
// 缓存未命中,读取文件
content, err := ioutil.ReadFile(filePath)
if err != nil {
return nil, err
}
// 写入缓存,有效期10分钟
cache.set(filePath, content, 10*time.Minute)
return content, nil
}
HTTP缓存策略设置
HTTP缓存通过响应头控制客户端和代理服务器的缓存行为,常用的响应头包括Cache-Control、Expires、ETag和Last-Modified。
常用HTTP缓存头说明
- Cache-Control:设置缓存的最大有效时间,比如
max-age=3600表示缓存1小时 - ETag:文件内容的哈希值,客户端下次请求时携带
If-None-Match头,服务端对比判断是否返回304 - Last-Modified:文件最后修改时间,客户端下次请求携带
If-Modified-Since头,服务端判断文件是否修改
HTTP缓存响应设置示例
下面是Golang中设置HTTP缓存头的示例代码:
package main
import (
"crypto/md5"
"encoding/hex"
"net/http"
"os"
"time"
)
// 计算文件ETag,使用文件内容的MD5值
func getFileETag(filePath string) (string, error) {
content, err := ioutil.ReadFile(filePath)
if err != nil {
return "", err
}
hash := md5.Sum(content)
return hex.EncodeToString(hash[:]), nil
}
// 处理文件请求,设置HTTP缓存头
func fileHandler(w http.ResponseWriter, r *http.Request) {
filePath := r.URL.Path[1:] // 简化处理,实际需要根据路径做安全校验
// 获取文件信息
fileInfo, err := os.Stat(filePath)
if err != nil {
http.NotFound(w, r)
return
}
// 设置Cache-Control,缓存1小时
w.Header().Set("Cache-Control", "public, max-age=3600")
// 设置Last-Modified
w.Header().Set("Last-Modified", fileInfo.ModTime().Format(time.RFC1123))
// 设置ETag
etag, err := getFileETag(filePath)
if err == nil {
w.Header().Set("ETag", etag)
// 检查客户端携带的If-None-Match头
if r.Header.Get("If-None-Match") == etag {
w.WriteHeader(http.StatusNotModified)
return
}
}
// 返回文件内容
http.ServeFile(w, r, filePath)
}
结合内存缓存与HTTP缓存策略
将两种缓存结合后,流程为:客户端第一次请求时,服务端先检查内存缓存,未命中则读取磁盘文件,写入内存缓存并设置HTTP缓存头返回;客户端后续请求时,先验证HTTP缓存有效性,若缓存有效直接返回304,否则再检查内存缓存,减少磁盘读取。
完整结合示例
func combinedFileHandler(cache *memoryCache, w http.ResponseWriter, r *http.Request) {
filePath := r.URL.Path[1:]
// 先处理HTTP缓存验证
fileInfo, err := os.Stat(filePath)
if err != nil {
http.NotFound(w, r)
return
}
// 设置HTTP缓存头
w.Header().Set("Cache-Control", "public, max-age=3600")
w.Header().Set("Last-Modified", fileInfo.ModTime().Format(time.RFC1123))
etag, _ := getFileETag(filePath)
if etag != "" {
w.Header().Set("ETag", etag)
if r.Header.Get("If-None-Match") == etag {
w.WriteHeader(http.StatusNotModified)
return
}
}
// 检查内存缓存
content, exists := cache.get(filePath)
if !exists {
// 内存缓存未命中,读取文件
content, err = ioutil.ReadFile(filePath)
if err != nil {
http.NotFound(w, r)
return
}
// 写入内存缓存,有效期和HTTP缓存一致
cache.set(filePath, content, 3600*time.Second)
}
w.Write(content)
}
func main() {
cache := newMemoryCache()
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
combinedFileHandler(cache, w, r)
})
http.ListenAndServe(":8080", nil)
}
注意事项
- 内存缓存需要注意内存占用,对于大文件不建议全部存入内存,可以设置缓存大小上限,淘汰不常用缓存
- 文件修改后需要及时清理对应的缓存条目,避免返回旧内容
- HTTP缓存的
Cache-Control设置需要根据文件类型调整,比如静态JS、CSS可以设置更长的缓存时间,动态生成的文件可以设置更短或者不缓存 - 并发场景下内存缓存的读写需要加锁,避免数据竞争问题