Golang实现静态文件服务的完整示例

最简可行方案是用 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.Absos.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/ 下,所有请求都会返回同一个文件,毫无灵活性。

典型误用场景:

  • 想让 /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.FSos.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 错误,得自己实现文件存在性判断。