Unlocking Go’s io/fs: Unified File System Access and Real‑World Examples

This article explains Go 1.16’s io/fs package, its purpose as a unified file‑system abstraction, the core and extended interfaces it defines, practical use‑cases such as testing, embedded resources, memory and cloud storage, and provides multiple runnable code examples.

BirdNest Tech Talk
BirdNest Tech Talk
BirdNest Tech Talk
Unlocking Go’s io/fs: Unified File System Access and Real‑World Examples

Purpose of the io/fs package

The io/fs package, introduced in Go 1.16, defines a set of interfaces that abstract file‑system operations. By programming against these interfaces the same code can work with local disks, in‑memory files, zip archives, or any custom implementation.

Core interfaces

fs.FS : represents a file system and provides Open(name string) (fs.File, error).

fs.File : an opened file exposing Read, Write, Close, etc.

fs.FileInfo : metadata such as name, size, modification time.

fs.DirEntry : a directory entry that may be a file or sub‑directory.

fs.FileMode : a bitmask describing permissions and type.

Extended interfaces

fs.GlobFS

adds Glob(pattern string) ([]string, error) for wildcard matching. fs.ReadDirFS adds ReadDir(name string) ([]fs.DirEntry, error) to list directory contents. fs.ReadDirFile extends fs.File with ReadDir(n int) ([]fs.DirEntry, error). fs.ReadFileFS adds ReadFile(name string) ([]byte, error) for one‑shot reads. fs.StatFS adds Stat(name string) (fs.FileInfo, error). fs.SubFS adds Sub(dir string) (fs.FS, error) to create a view limited to a sub‑directory. fs.WalkDirFunc defines the callback signature used by fs.WalkDir.

Typical application scenarios

Accessing different file‑system types : the same code can read from the local disk, an in‑memory FS, or a zip archive because all implement fs.FS.

Testing : swapping a real FS with a mock (e.g., testing/fstest.MapFS) makes tests deterministic and fast.

Embedded resources : the standard embed package builds an embed.FS that can be accessed through io/fs APIs.

Code examples

Example 1 – Using fs.FS with os.DirFS

package main

import (
    "fmt"
    "io/fs"
    "log"
    "os"
)

func main() {
    // Create a file system representing the current directory
    fsys := os.DirFS(".")

    // Open a file
    f, err := fsys.Open("README.md")
    if err != nil { log.Fatal(err) }
    defer f.Close()

    // Read its content
    data := make([]byte, 100)
    n, err := f.Read(data)
    if err != nil { log.Fatal(err) }
    fmt.Println(string(data[:n]))
}

Example 2 – Getting file info with fs.File

package main

import (
    "fmt"
    "io/fs"
    "log"
    "os"
)

func main() {
    fsys := os.DirFS(".")
    f, err := fsys.Open("README.md")
    if err != nil { log.Fatal(err) }
    defer f.Close()

    info, err := f.Stat()
    if err != nil { log.Fatal(err) }
    fmt.Println("File size:", info.Size())
}

Example 3 – Listing directory entries with fs.ReadDir

package main

import (
    "fmt"
    "io/fs"
    "log"
    "os"
)

func main() {
    fsys := os.DirFS(".")
    entries, err := fs.ReadDir(fsys, ".")
    if err != nil { log.Fatal(err) }
    for _, entry := range entries {
        fmt.Println("Name:", entry.Name())
        fmt.Println("Is directory:", entry.IsDir())
    }
}

Example 4 – Glob pattern matching with fs.GlobFS

package main

import (
    "fmt"
    "io/fs"
    "log"
    "os"
)

func main() {
    fsys := os.DirFS(".")
    if globFS, ok := fsys.(fs.GlobFS); ok {
        matches, err := globFS.Glob("*.go")
        if err != nil { log.Fatal(err) }
        fmt.Println("Go files:", matches)
    }
}

Example 5 – Reading a directory with fs.ReadDirFS

package main

import (
    "fmt"
    "io/fs"
    "log"
    "os"
)

func main() {
    fsys := os.DirFS(".")
    if readDirFS, ok := fsys.(fs.ReadDirFS); ok {
        entries, err := readDirFS.ReadDir(".")
        if err != nil { log.Fatal(err) }
        fmt.Println("Directory contents:")
        for _, entry := range entries { fmt.Println(entry.Name()) }
    }
}

Example 6 – Creating a sub‑file‑system with fs.SubFS

package main

import (
    "fmt"
    "io/fs"
    "log"
    "os"
)

func main() {
    fsys := os.DirFS(".")
    if subFS, ok := fsys.(fs.SubFS); ok {
        sub, err := subFS.Sub("subdir")
        if err != nil { log.Fatal(err) }
        entries, err := fs.ReadDir(sub, ".")
        if err != nil { log.Fatal(err) }
        fmt.Println("Sub directory contents:")
        for _, entry := range entries { fmt.Println(entry.Name()) }
    }
}

Example 7 – Walking a tree with fs.WalkDir

package main

import (
    "fmt"
    "io/fs"
    "log"
    "os"
)

func main() {
    fsys := os.DirFS(".")
    err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
        if err != nil { return err }
        fmt.Println("Walking:", path)
        return nil
    })
    if err != nil { log.Fatal(err) }
}

Interesting file‑system implementations

In‑memory file system

The testing/fstest package offers MapFS, which stores files in a Go map for fast, temporary use.

package main

import (
    "fmt"
    "io/fs"
    "log"
    "os"
    "testing/fstest"
)

func main() {
    fsys := fstest.MapFS{
        "file1.txt": {Data: []byte("Hello, world!")},
        "dir1/file2.txt": {Data: []byte("This is file2.")},
    }
    f, err := fsys.Open("file1.txt")
    if err != nil { log.Fatal(err) }
    defer f.Close()
    data := make([]byte, 100)
    n, err := f.Read(data)
    if err != nil { log.Fatal(err) }
    fmt.Println(string(data[:n]))
    // Walk the in‑memory FS
    err = fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
        if err != nil { return err }
        fmt.Println("Walking:", path)
        return nil
    })
    if err != nil { log.Fatal(err) }
}

Third‑party library psanford/memfs at https://github.com/psanford/memfs provides similar functionality.

Embedded file system

The standard embed package builds an embed.FS that can be accessed via the same io/fs APIs.

package main

import (
    "embed"
    "fmt"
    "io/fs"
    "log"
)

//go:embed static
var staticFiles embed.FS

func main() {
    f, err := staticFiles.Open("static/file1.txt")
    if err != nil { log.Fatal(err) }
    defer f.Close()
    data := make([]byte, 100)
    n, err := f.Read(data)
    if err != nil { log.Fatal(err) }
    fmt.Println(string(data[:n]))
    // Walk embedded FS
    err = fs.WalkDir(staticFiles, "static", func(path string, d fs.DirEntry, err error) error {
        if err != nil { return err }
        fmt.Println("Walking:", path)
        return nil
    })
    if err != nil { log.Fatal(err) }
}

Cloud‑storage file system

Libraries like gocloud.dev/blob expose cloud buckets (e.g., S3, GCS) as an fs.FS, enabling the same traversal and read operations.

package main

import (
    "context"
    "fmt"
    "io/fs"
    "log"
    "gocloud.dev/blob"
    _ "gocloud.dev/blob/gcs"
)

func main() {
    bucketURL := "gs://my-bucket"
    bucket, err := blob.OpenBucket(context.Background(), bucketURL)
    if err != nil { log.Fatal(err) }
    defer bucket.Close()
    fsys := bucket.NewFS()
    err = fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
        if err != nil { return err }
        fmt.Println("Walking:", path)
        return nil
    })
    if err != nil { log.Fatal(err) }
}

References

psanford/memfs – https://github.com/psanford/memfs

Testingcloud storageFilesystemabstractionembedio/fsmemory-fs
BirdNest Tech Talk
Written by

BirdNest Tech Talk

Author of the rpcx microservice framework, original book author, and chair of Baidu's Go CMC committee.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.