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 (the module declares
go 1.26.5).
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
}
Note that notes/ was created implicitly. billy creates missing parent directories for
any O_CREATE open, so a write never fails for a missing parent the way it would against
os or afero.OsFs. That is one of a handful of deliberate semantic choices; see
billy vs afero semantics.
3. Walk it like any afero filesystem¶
Because it is 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 cannot Open a directory, so the adapter synthesises a read-only directory handle
whose Readdir and 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:
Every operation now serialises through mu. Two limits come with that, and they matter
before you rely on it: a sequence of operations is not atomic, and the locker is not
reentrant, so using the handle inside a critical section that already holds mu
deadlocks. Make a handle concurrency-safe covers both.
What this tutorial did not cover¶
- A real git worktree.
memfskeeps the tutorial to one dependency; work with a go-git worktree is the case the module exists for. - Permissions.
memfsrecords the mode you pass, but nothing enforces it in memory. On a disk-backed backend the answer differs per backend — see billy backend support. - Anything the adapter refuses to do. It is a filesystem view and nothing more; see what this does not do.
Where next¶
- Work with a go-git worktree — the real reason this module exists.
- Make a handle concurrency-safe
- Filesystem operations — every method, what it maps onto, and what it returns when the call is wrong.