Golang中embed.File.ModTime返回空time.Time而非文件嵌入时的修改时间问题

Golang中embed.File.ModTime返回空time.Time而非文件嵌入时的修改时间问题 我像这样嵌入了一个文件:

package main

import (
	"fmt"
	"embed"
	"net/http"
)

//go:embed test.txt
var fs embed.FS

func main() {
	f, _ := fs.Open("test.txt")
	fi, _ := f.Stat()
	fmt.Println(fi.ModTime()) // 0001-01-01 00:00:00 +0000 UTC

	// 使用 Last-Modified 头
	http.Handle("/dir/", http.StripPrefix("/dir/", http.FileServer(http.Dir("."))))
	
	// 没有 Last-Modified 头 -> 容易规避,但不适合作为直接替代品
	http.Handle("/embed/", http.StripPrefix("/embed/", http.FileServer(http.FS(fs))))

	http.ListenAndServe(":8080", nil)
}

我期望 fi.ModTime 能返回文件在嵌入时的修改时间。

但返回的却是 time.Time{}https://golang.org/src/embed/embed.go#229)。上面的程序包含了一个这种情况会变得相关的例子。请帮我理解这个设计决策。


更多关于Golang中embed.File.ModTime返回空time.Time而非文件嵌入时的修改时间问题的实战教程也可以访问 https://www.itying.com/category-94-b0.html

4 回复

优秀的发现,感谢。我现在理解了这个设计背后的原理,但这使得文档(https://golang.org/pkg/embed/#hdr-File_Systems)产生了误导。这至少值得一个警告。你知道如何正确地报告这个问题吗?

更多关于Golang中embed.File.ModTime返回空time.Time而非文件嵌入时的修改时间问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


如果我查看git blame的结果没错,看起来在最初的实现中它就是以这种方式提交的。基于这一点,我怀疑当时认为包含它并不重要。你可以开一个issue来请求这个功能,也许可以提交一个PR,附带一个实现此功能的补丁。

我很好奇,你希望获取嵌入文件的原始修改时间是出于什么目的呢?

编辑:你或许可以包装嵌入的fs.FS,通过一个go:generate命令自己来实现这个功能。

哦,我找到了:

github.com/golang/go

提案:cmd/go:支持在二进制文件中嵌入静态资源(文件)

有许多工具可以将静态资源文件嵌入到二进制文件中: https://godoc.org/perkeep.org/pkg/fileembed / perkeep.org/pkg/fileembed/genfileembed https://godoc.org/github.com/gobuffalo/packr https://godoc.org/github.com/knadh/stuffbin https://github.com/rakyll/statik Bazel go_embed_data 实际上,https://tech.townsourced.com/post/embedding-static-files-in-go/ 列出了更多: vfsgen - https://github.com/shurcooL/vfsgen go.rice - https://github.com/GeertJohan/go.rice statik…

标签: Proposal, Proposal-Hold

这是关于构建可重现性的。具有不同修改时间但内容相同的嵌入文件不应导致不同的构建结果。

在Go的embed包中,File.ModTime()返回空time.Time是设计上的有意行为,主要出于以下原因:

  1. 可重现构建:嵌入文件的修改时间被固定为编译时间戳,但为了确保每次构建的二进制文件完全一致(特别是对于容器镜像和部署),Go团队选择不暴露时间信息。

  2. 安全性:避免泄露文件系统元数据,防止攻击者通过修改时间推断构建环境信息。

  3. 确定性输出embed.FS旨在提供确定性的文件访问,不依赖外部文件系统状态。

查看embed/embed.go源码可以看到:

func (f *file) ModTime() time.Time {
    // 返回零值time.Time,而不是编译时间
    return time.Time{}
}

如果你需要获取嵌入时的修改时间,可以通过以下方式解决:

方案1:使用构建标签注入时间戳

//go:embed test.txt
var fs embed.FS

// 在构建时通过-ldflags注入
var buildTime string

func getModTime() time.Time {
    if t, err := time.Parse(time.RFC3339, buildTime); err == nil {
        return t
    }
    return time.Time{}
}

// 构建命令:go build -ldflags="-X main.buildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)"

方案2:自定义文件系统包装器

type customFS struct {
    embed.FS
    modTime time.Time
}

func (c *customFS) Open(name string) (http.File, error) {
    f, err := c.FS.Open(name)
    if err != nil {
        return nil, err
    }
    return &customFile{File: f, modTime: c.modTime}, nil
}

type customFile struct {
    fs.File
    modTime time.Time
}

func (cf *customFile) Stat() (fs.FileInfo, error) {
    fi, err := cf.File.Stat()
    if err != nil {
        return nil, err
    }
    return &customFileInfo{FileInfo: fi, modTime: cf.modTime}, nil
}

type customFileInfo struct {
    fs.FileInfo
    modTime time.Time
}

func (cfi *customFileInfo) ModTime() time.Time {
    return cfi.modTime
}

// 使用示例
var (
    //go:embed test.txt
    embeddedFS embed.FS
    myFS = &customFS{FS: embeddedFS, modTime: time.Now()} // 或使用编译时时间
)

方案3:使用http.FileServer的缓存控制替代

func embedFileServer(fs embed.FS) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 设置缓存头替代Last-Modified
        w.Header().Set("Cache-Control", "public, max-age=31536000")
        http.FileServer(http.FS(fs)).ServeHTTP(w, r)
    })
}

对于你的HTTP服务器场景,建议使用方案3的缓存控制头,或方案2的自定义文件系统来提供修改时间。

回到顶部