Skip to content

Getting started

This tutorial bridges an in-memory billy filesystem to afero and writes to it with ordinary afero calls — enough to see the whole API, which is three symbols.

Prerequisites

  • Go 1.26 or newer.

1. Create a module

mkdir bridgedemo && cd bridgedemo
go mod init bridgedemo
go get gitlab.com/phpboyscout/go/aferobilly github.com/go-git/go-billy/v5 github.com/spf13/afero

2. Bridge a billy filesystem

memfs gives you a billy filesystem with no setup. Wrap it and you have an afero.Fs:

package main

import (
    "fmt"

    "github.com/go-git/go-billy/v5/memfs"
    "github.com/spf13/afero"

    "gitlab.com/phpboyscout/go/aferobilly"
)

func main() {
    bfs := memfs.New()              // a billy filesystem
    fs := aferobilly.New(bfs)       // ...now an afero.Fs

    if err := afero.WriteFile(fs, "notes/hello.txt", []byte("hi"), 0o644); err != nil {
        panic(err)
    }

    data, err := afero.ReadFile(fs, "notes/hello.txt")
    if err != nil {
        panic(err)
    }

    fmt.Println(string(data))
    // hi
}
go run .
# hi

Note that notes/ was created implicitly — afero's WriteFile calls Mkdir, which maps onto billy's MkdirAll. That's one of a handful of deliberate semantic choices; see billy vs afero semantics.

3. Walk it like any afero filesystem

Because it's a genuine afero.Fs, the whole afero toolbox works — including helpers that expect real directory handles:

_ = afero.WriteFile(fs, "notes/a.txt", []byte("a"), 0o644)
_ = afero.WriteFile(fs, "notes/b.txt", []byte("b"), 0o644)

entries, _ := afero.ReadDir(fs, "notes")
for _, e := range entries {
    fmt.Println(e.Name(), e.Size())
}

_ = afero.Walk(fs, "/", func(path string, info os.FileInfo, err error) error {
    fmt.Println(path)
    return nil
})

billy can't Open a directory, so the adapter synthesises a read-only directory handle whose Readdir/Stat behave the way os and afero callers expect.

4. Add locking when a handle escapes

If the handle is used from more than one goroutine — or outlives the function that made it — give it the mutex guarding whatever produced the filesystem:

var mu sync.Mutex

fs := aferobilly.New(bfs, aferobilly.WithLocker(&mu))

Now every operation serialises through mu. See make a handle concurrency-safe for what that does and does not guarantee — there are two footguns worth knowing before you rely on it.

Where next