Getting started¶
By the end of this you'll have a small Go program that prints the root of whatever project it is run inside, tells you which marker file gave that answer, and fails with a useful message when it is run somewhere that isn't a project at all.
Allow about ten minutes.
What you need first¶
- Go 1.26.5 or newer — that is the
godirective in this module'sgo.mod. On an older toolchain with the defaultGOTOOLCHAIN=auto, Go downloads a matching one and switches to it; withGOTOOLCHAIN=localit stops with an error instead. - A directory to work in. Any Go module will do — you'll run the finished program from inside one.
Install the module¶
This also pulls in afero and
cockroachdb/errors. That is the whole dependency graph — there is no framework
underneath.
Print the root of the project you're standing in¶
The common case is a tool run from somewhere inside a project that wants the project
root. DetectFromCWD starts the walk at the process working directory and climbs.
package main
import (
"fmt"
"github.com/spf13/afero"
"gitlab.com/phpboyscout/go/workspace"
)
func main() {
ws, err := workspace.DetectFromCWD(afero.NewOsFs(), workspace.DefaultMarkers)
if err != nil {
fmt.Println("not inside a project:", err)
return
}
fmt.Println("Project root:", ws.Root)
fmt.Println("Detected via:", ws.Marker)
}
afero.NewOsFs() is the real filesystem. The package never builds one for you — you
pass the filesystem in, which is what makes the walk testable later.
Build it and run it from a subdirectory of a Go module:
$ cd ~/code/myproject/internal/store
$ myprogram
Project root: /home/you/code/myproject
Detected via: go.mod
Two things to notice in that output. Root is absolute, always — the start path is
resolved with filepath.Abs before the walk begins. And Marker is the entry from
your marker list that matched, not a full path, so you can branch on which kind of
boundary you found.
Start the walk somewhere other than the working directory¶
When you already have a path — a flag value, a positional argument — use Detect and
pass the start directory yourself. Same markers, same options.
A relative startDir works, but be clear about what it is relative to: it is resolved
against the process working directory, not against the filesystem you passed in. That
distinction does not matter here, where both are the real machine. It matters a lot in
tests — see test filesystem walks in memory.
Handle "this isn't a project"¶
Run the program from somewhere with no marker anywhere above it — /tmp on most
machines — and it prints:
That is the package's single sentinel error, workspace.ErrNotFound. The only other
error either function can return comes from os.Getwd failing, which in practice
means the working directory was deleted out from under the process. Everything else
arrives as this one value, so errors.Is is all you need:
ws, err := workspace.DetectFromCWD(afero.NewOsFs(), workspace.DefaultMarkers)
switch {
case errors.Is(err, workspace.ErrNotFound):
return errors.New("run this inside a project directory")
case err != nil:
return err
default:
// use ws.Root
}
Worth knowing before you rely on that branch: ErrNotFound does not tell you
why nothing was found. Reaching the filesystem root without a match and exhausting
the depth budget produce the same error, and so does a start directory that does not
exist. If you need to tell those apart, check before you call — the package will not
do it for you.
Detect a project that isn't a Go module¶
DefaultMarkers is {".gtb/manifest.yaml", "go.mod", ".git"}, which only knows about
Go. For anything else, hand Detect your own list:
The list is checked in order at each directory level and the first entry that exists wins, so put the most specific marker first.
What to read next¶
- Detect other project types — Node, Rust, Python, monorepos.
- Test filesystem walks in memory — build a project tree in memory and assert on the walk.
- Reference — every exported symbol, every default, and what happens when an argument is wrong.
- What workspace does not do — the limits, before you build on an assumption the package does not support.