Skip to content

Getting started

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

Prerequisites

  • Go, at least the version in the go directive of the module's go.mod. It moves with the toolchain, so it is not repeated here.

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. Add os to the imports for this snippet:

_ = 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 (this snippet needs sync imported):

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