Go Embedding Series — 04. Advanced Usage of embed.FS
File System
Go Embedding Series — 04. Advanced Usage of embed.FS File System
In the previous posts of this series, we explored how to embed files in Go using the //go:embed
directive. But let’s pause and ask a natural next question:
“I’ve embedded some files. Now what? How can I access them as if they were real files?”
That’s where embed.FS
comes in.
1. What Exactly Is embed.FS?
Before diving into code, let’s start with a thought experiment:
If you were to simulate a read-only file system inside your Go program, what capabilities would you need?
Likely, at minimum:
- Read file contents
- List directory contents
- Open a file stream (e.g., for HTTP)
Now consider what embed.FS
implements:
type FS interface {
Open(name string) (fs.File, error)
}
Yes — embed.FS
implements Go’s standard fs.FS
interface, introduced in Go 1.16 to provide a unified abstraction over various file systems (disk, embedded, zip, etc.).
So we can say:
embed.FS
is simply a read-only virtual file system that conforms tofs.FS
.
2. The Three Essential Methods of embed.FS
2.1 ReadFile
: Read Embedded File Content
data, err := fs.ReadFile(myFS, "static/config.json")
- Returns the content as
[]byte
- Errors often stem from path mismatches — always verify the path is relative to the
.go
file
Think of it as ioutil.ReadFile
, but for embedded files.
2.2 ReadDir
: List Files in an Embedded Directory
entries, err := fs.ReadDir(myFS, "templates")
for _, entry := range entries {
fmt.Println(entry.Name(), entry.IsDir())
}
Perfect for:
- Enumerating template files
- Running batch SQL migrations or loading multiple configs
Analogy: Like opening a folder inside a zip file and seeing what’s inside.
2.3 Open
: Stream Access to Embedded Files
f, err := myFS.Open("static/logo.png")
defer f.Close()
io.Copy(w, f) // Stream directly into HTTP response
Returns an fs.File
, which supports methods like Read
, Seek
, and Stat
. Use cases include:
- Streaming large images/videos
- Integration with HTTP servers
- Preprocessing or caching resources
3. Real-World Use: Template System
In web servers, it’s common to embed HTML templates:
//go:embed templates/*
var tmplFS embed.FS
tmpl := template.Must(template.ParseFS(tmplFS, "templates/*.html"))
You can go further:
- Hot-reload from disk in dev, use
embed.FS
in production - Handle multilingual pages:
index.zh.html
,index.en.html
- Load partials on demand: combine
ReadFile
+Parse
Challenge: Try writing a
LoadLocalizedTemplate(lang string)
function.
4. Design Philosophy: Why Abstract fs.FS?
You might wonder:
“Why make all file systems — disk, zip, embedded — look the same?”
Because abstraction brings uniformity.
Imagine building a plugin system:
- Plugin A is on disk
- Plugin B is in a zip
- Plugin C is embedded
If they all implement fs.FS
, you can write one unified loader:
func LoadPlugin(fs fs.FS, path string) {
data, _ := fs.ReadFile(fs, path)
// ...
}
This is one of Go’s core philosophies:
Favor composition via interfaces over inheritance or complex structs.
5. Quick Q\&A
❓ Why can’t I read the embedded file?
- Paths must be relative to the .go file that uses
//go:embed
- No variable paths or runtime composition
- On Windows: case-sensitive, and always use
/
❓ Why isn’t my updated file showing?
Embedded content is frozen at compile time. Rebuild your binary after any file change.
❓ How do I support multilingual templates?
Use ReadDir
to scan the directory and filter by file suffix, e.g., index.zh.html
, index.en.html
.
❓ Can I write to embed.FS
?
No. It’s strictly read-only and compiled into the binary.
6. Summary and Takeaways
embed.FS
is a clean and powerful abstraction in Go- It works seamlessly with the standard
fs.FS
interface - It’s not just about embedding files — it teaches us how to design abstract interfaces
If you can use
embed.FS
as naturally as your local file system, then you truly understand it.
Further Reading & Challenges
- embed — Go Standard Library
- ✏️ Challenge: Use
embed.FS
to build a zip-based virtual file explorer