We are early adopters of io/fs: we'd been wanting to move to a VFS implementation and the original proposal gave us the push to do so. So far, it has been a good decision. It's allowed us to stub out the file system implementation for tests (SlowFS, CorruptedFS, etc.), to more easily test bugs by replaying application state from ZIP files, and to transparently encrypt files.
One pain point, however, is knowing what constitutes a valid file path for a particular FS. With the os package, we could make a reasonable set of assumptions. For example, rejecting or not creating paths with problematic or illegal characters like null bytes or rejecting or not creating excessively long paths. But with io/fs, we can't. It's impossible to know whether the FS is os.DirFS, a remote object store, or an in-memory file system.
There seem to be two possibly correct answers:
A FS must accept any path for which ValidPath returns true and therefore translate it accordingly
It's impossible to know and therefore the API using io/fs must document what it expects out of the FS
The implementation of os.DirFS seems to assume (2), but it's not clearly documented. ValidPath should more clearly document its assumptions.
Personally, I worry that (2) will cause too much friction.
I've found that io/fs causes me to document a multitude of supported auxiliary interfaces (see snippet below). Adding path element conventions on top of that increases the wall of text, which makes it that much more frustrating to use the API.
Further, it means users can't just drop in their preferred FS like you can with other common standard library interfaces (io.Reader, etc.). Instead, they'll have to add a glue layer that translates my path element scheme—whatever it may be—to their desired FS. For example, if I document that my API uses colons in path elements then a user using os.DirFS will have to strip those out on Windows.
One of Go's strengths is making small, composable interfaces that you can rely on. The classic example is io.Reader: its documentation is exacting, and if you see Read(byte) (int, error) you can reasonably assume what the method is going to do. But, if the documentation were vague—perhaps not documenting that (0, nil) is valid but discouraged or that it can return n !=0 if err == io.EOF—then it would be much more difficult to use: what does a particular result mean on Unix vs Windows? Does it matter that I'm reading from a memory buffer or socket? Etc.
// NewT creates a T that uses the FS to store its
// FS provides the minimum functionality required
// for a read-only T.
// To allow the T to modify its state, the FS must
// implement one or more of the following interfaces:
// Allows the T to create additional files
// Allows the T to remove files that are no longer
func NewT(fsys fs.FS) T