静态文件服务 #
一、静态文件概述 #
静态文件包括CSS、JavaScript、图片、字体等资源文件。
1.1 静态文件目录结构 #
text
project/
├── static/
│ ├── css/
│ │ ├── style.css
│ │ └── bootstrap.min.css
│ ├── js/
│ │ ├── app.js
│ │ └── jquery.min.js
│ ├── images/
│ │ ├── logo.png
│ │ └── banner.jpg
│ └── fonts/
│ └── iconfont.ttf
├── uploads/
│ └── files/
└── views/
二、基本配置 #
2.1 Static方法 #
go
e := echo.New()
e.Static("/static", "static")
访问:
text
/static/css/style.css → static/css/style.css
/static/js/app.js → static/js/app.js
/static/images/logo.png → static/images/logo.png
2.2 多个静态目录 #
go
e := echo.New()
e.Static("/static", "static")
e.Static("/uploads", "uploads")
e.Static("/assets", "public/assets")
2.3 静态文件中间件 #
go
e := echo.New()
e.Use(middleware.Static("static"))
2.4 自定义配置 #
go
e := echo.New()
e.Use(middleware.StaticWithConfig(middleware.StaticConfig{
Root: "static",
Browse: true,
}))
三、静态文件配置选项 #
3.1 配置选项 #
| 选项 | 说明 |
|---|---|
| Root | 静态文件根目录 |
| Skipper | 跳过函数 |
| Index | 默认索引文件 |
| Browse | 是否启用目录浏览 |
| HTML5 | HTML5模式 |
| IgnoreBase | 忽略基础路径 |
3.2 目录浏览 #
go
e.Use(middleware.StaticWithConfig(middleware.StaticConfig{
Root: "static",
Browse: true,
}))
访问 /static/ 会显示目录列表。
3.3 默认索引文件 #
go
e.Use(middleware.StaticWithConfig(middleware.StaticConfig{
Root: "static",
Index: "index.html",
}))
3.4 HTML5模式 #
go
e.Use(middleware.StaticWithConfig(middleware.StaticConfig{
Root: "static",
HTML5: true,
}))
所有未匹配的路由返回index.html,适用于SPA应用。
3.5 跳过特定路径 #
go
e.Use(middleware.StaticWithConfig(middleware.StaticConfig{
Root: "static",
Skipper: func(c echo.Context) bool {
return strings.HasPrefix(c.Path(), "/api")
},
}))
四、文件服务 #
4.1 单个文件 #
go
e.GET("/robots.txt", func(c echo.Context) error {
return c.File("static/robots.txt")
})
4.2 文件下载 #
go
e.GET("/download/:filename", func(c echo.Context) error {
filename := c.Param("filename")
return c.Attachment("files/"+filename, filename)
})
4.3 内联显示 #
go
e.GET("/view/:filename", func(c echo.Context) error {
filename := c.Param("filename")
return c.Inline("files/"+filename, filename)
})
4.4 动态生成文件 #
go
e.GET("/report.csv", func(c echo.Context) error {
var buf bytes.Buffer
writer := csv.NewWriter(&buf)
writer.Write([]string{"ID", "Name", "Email"})
writer.Write([]string{"1", "张三", "zhangsan@example.com"})
writer.Flush()
c.Response().Header().Set("Content-Disposition", "attachment; filename=report.csv")
return c.Blob(http.StatusOK, "text/csv", buf.Bytes())
})
五、缓存控制 #
5.1 缓存中间件 #
go
func cacheMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
c.Response().Header().Set("Cache-Control", "public, max-age=31536000")
return next(c)
}
}
e.Use(cacheMiddleware)
5.2 静态文件缓存 #
go
staticGroup := e.Group("/static")
staticGroup.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
c.Response().Header().Set("Cache-Control", "public, max-age=31536000")
c.Response().Header().Set("Expires", time.Now().AddDate(1, 0, 0).Format(http.TimeFormat))
return next(c)
}
})
staticGroup.Static("/", "static")
5.3 ETag支持 #
go
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
file := "static" + c.Request().URL.Path
info, err := os.Stat(file)
if err != nil {
return next(c)
}
etag := fmt.Sprintf(`"%x-%x"`, info.ModTime().Unix(), info.Size())
c.Response().Header().Set("ETag", etag)
if c.Request().Header.Get("If-None-Match") == etag {
return c.NoContent(http.StatusNotModified)
}
return next(c)
}
})
六、文件上传目录 #
6.1 上传目录配置 #
go
e.Static("/uploads", "uploads")
6.2 安全限制 #
go
e.Use(middleware.StaticWithConfig(middleware.StaticConfig{
Root: "uploads",
Skipper: func(c echo.Context) bool {
path := c.Request().URL.Path
forbidden := []string{".php", ".asp", ".jsp", ".exe"}
for _, ext := range forbidden {
if strings.HasSuffix(path, ext) {
return true
}
}
return false
},
}))
七、CDN配置 #
7.1 CDN URL #
go
func cdnURL(path string) string {
cdnBase := os.Getenv("CDN_BASE_URL")
if cdnBase == "" {
cdnBase = "/static"
}
return cdnBase + path
}
func main() {
e := echo.New()
e.Renderer = &Template{
templates: template.Must(template.New("").Funcs(template.FuncMap{
"cdn": cdnURL,
}).ParseGlob("views/**/*.html")),
}
e.Start(":8080")
}
7.2 模板中使用 #
html
<link rel="stylesheet" href="{{cdn "/css/style.css"}}">
<script src="{{cdn "/js/app.js"}}"></script>
<img src="{{cdn "/images/logo.png"}}" alt="Logo">
八、完整示例 #
go
package main
import (
"bytes"
"encoding/csv"
"fmt"
"html/template"
"io"
"net/http"
"os"
"strings"
"time"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
type Template struct {
templates *template.Template
}
func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
return t.templates.ExecuteTemplate(w, name, data)
}
func main() {
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Static("/static", "static")
e.Static("/uploads", "uploads")
e.Use(staticCacheMiddleware)
funcMap := template.FuncMap{
"cdn": func(path string) string {
return "/static" + path
},
"now": time.Now,
}
t := &Template{
templates: template.Must(template.New("").Funcs(funcMap).ParseGlob("views/**/*.html")),
}
e.Renderer = t
e.GET("/", home)
e.GET("/download/:filename", downloadFile)
e.GET("/report.csv", downloadReport)
e.POST("/upload", uploadFile)
e.Logger.Fatal(e.Start(":8080"))
}
func staticCacheMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if strings.HasPrefix(c.Path(), "/static") || strings.HasPrefix(c.Path(), "/uploads") {
c.Response().Header().Set("Cache-Control", "public, max-age=31536000")
c.Response().Header().Set("Expires", time.Now().AddDate(1, 0, 0).Format(http.TimeFormat))
}
return next(c)
}
}
func home(c echo.Context) error {
return c.Render(http.StatusOK, "pages/home.html", map[string]interface{}{
"Title": "首页",
})
}
func downloadFile(c echo.Context) error {
filename := c.Param("filename")
ext := strings.ToLower(filename[strings.LastIndex(filename, "."):])
forbidden := []string{".php", ".asp", ".jsp", ".exe", ".sh"}
for _, f := range forbidden {
if ext == f {
return echo.NewHTTPError(http.StatusForbidden, "不允许下载此类型文件")
}
}
filepath := "uploads/" + filename
if _, err := os.Stat(filepath); os.IsNotExist(err) {
return echo.NewHTTPError(http.StatusNotFound, "文件不存在")
}
return c.Attachment(filepath, filename)
}
func downloadReport(c echo.Context) error {
var buf bytes.Buffer
writer := csv.NewWriter(&buf)
writer.Write([]string{"ID", "Name", "Email", "Created"})
writer.Write([]string{"1", "张三", "zhangsan@example.com", "2024-01-01"})
writer.Write([]string{"2", "李四", "lisi@example.com", "2024-01-02"})
writer.Flush()
c.Response().Header().Set("Content-Disposition", "attachment; filename=report.csv")
c.Response().Header().Set("Content-Type", "text/csv; charset=utf-8")
return c.Blob(http.StatusOK, "text/csv", buf.Bytes())
}
func uploadFile(c echo.Context) error {
file, err := c.FormFile("file")
if err != nil {
return err
}
src, err := file.Open()
if err != nil {
return err
}
defer src.Close()
os.MkdirAll("uploads", 0755)
dst, err := os.Create("uploads/" + file.Filename)
if err != nil {
return err
}
defer dst.Close()
if _, err = io.Copy(dst, src); err != nil {
return err
}
return c.JSON(http.StatusOK, map[string]string{
"filename": file.Filename,
"url": "/uploads/" + file.Filename,
"message": "上传成功",
})
}
views/pages/home.html
html
<!DOCTYPE html>
<html>
<head>
<title>{{.Title}}</title>
<link rel="stylesheet" href="{{cdn "/css/style.css"}}">
</head>
<body>
<h1>Welcome!</h1>
<img src="{{cdn "/images/logo.png"}}" alt="Logo">
<form action="/upload" method="POST" enctype="multipart/form-data">
<input type="file" name="file">
<button type="submit">Upload</button>
</form>
<a href="/download/report.pdf">Download Report</a>
<a href="/report.csv">Download CSV</a>
<script src="{{cdn "/js/app.js"}}"></script>
</body>
</html>
九、总结 #
静态文件服务要点:
| 方法 | 说明 |
|---|---|
Static() |
静态目录 |
File() |
单个文件 |
Attachment() |
文件下载 |
Inline() |
内联显示 |
Blob() |
二进制响应 |
配置选项:
| 选项 | 说明 |
|---|---|
| Root | 根目录 |
| Browse | 目录浏览 |
| Index | 索引文件 |
| HTML5 | SPA模式 |
准备好学习数据库集成了吗?让我们进入下一章!
最后更新:2026-03-28