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 (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
}
go run .
# 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:

var mu sync.Mutex

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

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. memfs keeps the tutorial to one dependency; work with a go-git worktree is the case the module exists for.
  • Permissions. memfs records 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