最简可行方案是用 http.FileServer + http.Dir 组合,传入绝对路径并配合 http.StripPrefix;禁用路径穿越需自定义 safeFileSystem 或用 Go 1.16+ 的 http.FS;SPA 需自定义 handler 实现 index.html fallback。

用 http.FileServer 提供静态文件服务最简可行
Go 标准库的 http.FileServer 就是为这事设计的,不需要额外依赖。它本质是一个 http.Handler,接收请求路径后按相对关系映射到本地文件系统。
常见错误是直接传入相对路径如 "./public",结果在非项目根目录运行时 404 —— 因为 Go 不会自动解析相对路径到绝对路径。
实操建议:
- 始终用
filepath.Abs或os.Executable+filepath.Dir构造绝对路径 - 用
http.StripPrefix去掉 URL 前缀,否则访问/static/logo.png会去查./static/static/logo.png - 注意权限:确保运行进程对目标目录有读取权限,否则返回 403
package main
import (
"net/http"
"os"
"path/filepath"
)
func main() {
dir, _ := filepath.Abs("./public")
fs := http.FileServer(http.Dir(dir))
http.Handle("/static/", http.StripPrefix("/static/", fs))
http.ListenAndServe(":8080", nil)
}
http.ServeFile 只适合单文件,别误用作目录服务
http.ServeFile 是快捷函数,但只响应一个固定路径(比如 /favicon.ico),不能处理子路径或目录遍历。如果把它注册到 /assets/ 下,所有请求都会返回同一个文件,毫无灵活性。
立即学习“go语言免费学习笔记(深入)”;
典型误用场景:
- 想让
/assets/css/app.css和/assets/js/main.js都走同一 handler,却写了http.HandleFunc("/assets/", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./assets/index.html") }) - 没意识到
http.ServeFile不做路径解析,也不检查 MIME 类型,连 404 都要自己写逻辑
正确做法:坚持用 http.FileServer + http.Dir 组合,它内置了安全路径校验、MIME 推断和 404 处理。
生产环境必须加 http.FileSystem 包装层防路径穿越
默认 http.Dir 允许 ../ 路径,攻击者可构造 /static/../../etc/passwd 读取任意文件。这不是理论风险 —— Go 官方文档明确警告过。
解决方法不是禁用 ../,而是用自定义 http.FileSystem 拦截非法路径。标准做法是用 http.FS(Go 1.16+)配合 embed.FS 或 os.DirFS,它们默认拒绝越界访问。
兼容旧版本(Go
type safeFileSystem struct {
root http.FileSystem
}
func (s safeFileSystem) Open(name string) (http.File, error) {
cleaned := path.Clean(name)
if strings.HasPrefix(cleaned, ".."+string(os.PathSeparator)) || cleaned == ".." {
return nil, os.ErrNotExist
}
return s.root.Open(cleaned)
}
fs := safeFileSystem{http.Dir("./public")}
注意:path.Clean 必须在 Open 中调用,不能只在 http.StripPrefix 后做 —— 攻击者可在前缀后插入 ../ 绕过。
前端路由(如 Vue Router history 模式)需要 fallback 到 index.html
单页应用(SPA)启用 history 模式后,/user/profile 这类前端路由实际由 JS 处理,但服务器收到请求时并不知道该返回 index.html,而是直接 404。
标准解法是自定义 handler,在文件不存在时兜底返回 index.html:
func spaHandler(root http.FileSystem) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
file, err := root.Open(r.URL.Path)
if os.IsNotExist(err) {
r.URL.Path = "/index.html"
file, err = root.Open("/index.html")
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
} else if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer file.Close()
fi, _ := file.Stat()
http.ServeContent(w, r, fi.Name(), fi.ModTime(), file)
})
}
http.Handle("/", spaHandler(http.Dir("./public")))
关键点:必须用 http.ServeContent 而非 http.ServeFile,前者支持范围请求(Range)、ETag、缓存头等,对大文件和现代前端至关重要。
容易忽略的是,这种 fallback 逻辑不能套在 http.FileServer 外面直接用 —— 它不暴露底层 Open 错误,得自己实现文件存在性判断。










