Description
An os.File
provides two ways to read a directory: Readdirnames
returns a list of the names of the directory entries, and Readdir
returns the names along with stat information.
On Plan 9 and Windows, Readdir
can be implemented with only a directory read - the directory read operation provides the full stat information.
But many Go users use Unix systems.
On most Unix systems, the directory read does not provide full stat information. So the implementation of Readdir reads the names from the directory and then calls Lstat for each file. This is fairly expensive.
Much of the time, such as in the implementation of filepath.Glob and other file system walking, the only information the caller of Readdir
really needs is the name and whether the name denotes a directory. On most Unix systems, that single bit of information—is this name a directory?—is available from the plain directory read, without an additional stat. If the caller is only using that bit, the extra Lstat calls are unnecessary and slow. (Goimports, for example, has its own directory walker to avoid this cost.)
Various people have proposed adding a third directory reading option of one form or another, to get names and IsDir bits. This would certainly address the slow directory walk issue on Unix systems, but it seems like overfitting to Unix.
Note that os.FileInfo
is an interface. What if we make Readdir
return a slice of lazily-filled os.FileInfo
? That is, on Unix, Readdir
would stop calling Lstat
. Each returned FileInfo
would already know the answer for its Name
and IsDir
methods. The first call to any of the other methods would incur an Lstat
at that moment to find out the rest of the information. A directory walk that uses Readdir
and then only calls Name
and IsDir
would have all its Lstat
calls optimized away with no code changes in the caller.
The downside of this is that the laziness would be visible when you do the Readdir
and wait a while before looking at the results. For example if you did Readdir
, then touched one of the files in the list, then called the ModTime
method on the os.FileInfo
that Readdir
retruned, you'd see the updated modification time. And then if you touched the file again and called ModTime
again, you wouldn't see the further-updated modification time. That could be confusing. But I expect that the vast majority of uses of Readdir
use the results immediately or at least before making changes to files listed in the results. I suspect the vast majority of users would not notice this change.
I propose we make this change—make Readdir
return lazy os.FileInfo
—soon, intending it to land in Go 1.16, but ready to roll back the change if the remainder of the Go 1.16 dev cycle or beta/rc testing turns up important problems with it.
Activity
bradfitz commentedon Sep 2, 2020
Last time this was proposed there was debate about what the behavior for
ModTime
andMode
andSize
should be if the lazyLstat
fails later, as they don't return errors. Panic is bad. Logging is weird. Zero values I guess?bcmills commentedon Sep 2, 2020
I think this is likely to introduce subtle changes in behavior. Perhaps more importantly, I don't think this is the sort of change that we can reliably verify during a development cycle.
In my experience, very few users who are not either Go contributors or Googlers test Beta or RC releases of the Go toolchain, and changes in the
os
package are less likely to turn up during Google testing because a significant fraction of Google programs do most of their I/O without using theos
package directly.tv42 commentedon Sep 2, 2020
os.FileInfo can't be lazy as-is, because it can't return an error. Returning a 0 size on transient errors is not acceptable.
ianlancetaylor commentedon Sep 2, 2020
See also #40352, which is about different approaches to efficiently uncover similar information.
tv42 commentedon Sep 2, 2020
(Ignoring the POSIX API for a moment) NFS, and likely many other network filesystems, can do completely separate operations depending on whether the stat info is going to be needed (NFSv3 readdir vs readdirplus, NFSv4 "bulk LOOKUP", FUSE_READDIRPLUS).
There's also been a lot of talk about a Linux syscall that would fetch getdents+lstat info, for example https://lwn.net/Articles/606995/ -- they all seem to revolve around the idea of the client knowing beforehand whether it will be doing the lstat calls or not, and communicating that to the kernel.
These combined make me think the right path forward would be a Readdir method that takes arguments that inform it which os.FileInfo fields will be wanted; the rest could be zero values.
(That extended Readdir could also take a flag for whether to sort the results or not, removing one common cause of forks for performance reasons.)
networkimprov commentedon Sep 3, 2020
EDIT: This has a detailed proposal in #41265
I believe we need a new dirent abstraction.
After reviewing suggestions from the FS API discussion...
ReadDir()
argument gives fields to populateDirEntry
gets native dirent fields and lazy-loads others; .Has() indicates is-loadedLet's consider a hybrid:
ReadDir(path string, n int, opt uint64) ([]DirItem, error)
- opt is fields to load and sorting (0 is OS defaults)(may return more fields than requested)
(d *DirEntry) Load(fields uint64) error
- (re-)loads the fields(returns error if inode doesn't match)
(d *DirEntry) Has(fields uint64) bool
- indicates whether the fields are loaded(d *DirEntry) Id() FileId
- gives unix inode or winapi fileId; could take an argument re device info(d *DirEntry) Xyz() T
- panics for any field not loaded (a programmer mistake)That solves the
.ModTime()
etc issue with lazy-loading, and avoids an error check after every field access.EDIT: The interface which
DirEntry
implements andReadDir()
returns:Rationale:
a) If you need certain fields for every item, request them in
ReadDir()
.b) If you need certain fields for some items, request them in
DirEntry.Load()
.c) If you need certain fields only when they're the OS default, check for them with
DirEntry.Has()
.d) If you need the latest data for an item, request it with
DirEntry.Load()
.tv42 commentedon Sep 3, 2020
Wasn't the Windows FileId 128-bit? (Seen somewhere on go issues around the greater topic in the last few days.)
Either way, the unix inode number isn't very useful without the device major:minor. For example, you can't
filepath.Walk
and expect inode alone to identify hardlinked files, because you may have crossed a mountpoint.Also, inode number and such belong in a
.Sys()
style, platform-specific, extension point.networkimprov commentedon Sep 3, 2020
I gave the .Id() type as
FileId
, which can be whatever (e.g. opaque array), and store major:minor. The fact that aFileId
can't be compared across platforms isn't a reason to hide it. It's needed to replicate a tree containing multiple hard links for a file -- which I do.Today on Windows, FileInfo.Sys() can't even provide the fileId! Adding it was debated and discarded.
Is there any practical value in .Sys() besides providing .Ino on unix?
Winapi fileId is 64-bit on NTFS, 128-bit on ReFS (an alternative for Windows Server):
https://docs.microsoft.com/en-us/windows/win32/api/fileapi/ns-fileapi-by_handle_file_information
https://docs.microsoft.com/en-us/windows-server/storage/refs/refs-overview
benhoyt commentedon Sep 3, 2020
I really like the intent, but I agree with other commenters that the API just doesn't quite fit as is, because of potential errors returned by the lazy methods. We actually debated a very similar issue when designing the os.scandir / os.DirEntry API in Python. At first we wanted to make the DirEntry methods properties, like
entry.stat
without the function call parentheses. That works in Python, but it looks like a plain attribute access, and people aren't expecting to have to catch exceptions (errors) when accessing an attribute, so we made it a function call. Per the docs:I believe this decision was based on theoretical concerns, not from actual testing, but still, the logic seems sound. Especially in Go, where all error handling is super-explicit (we always want "find grained control over errors"). Panic-ing is not going to work, and silently returning a zero value is arguably worse.
32 remaining items
mpx commentedon Sep 17, 2020
I think this proposal trades correctness & simplicity for performance, as well as breaking the Go1 compatiblity promise.
In the past I've really appreciated that Go hasn't made this tradeoff and has found other ways of improving performance. This kind of behaviour is better suited to
unsafe
or other similarly out of the way places where people are expected to understand the risks.All existing programs have (implicitly or explicitly) been written with the assumption that
FileInfo
methods cannot fail, and thatFileInfo
contains data that was accurate during the Readdir call. All failures are explicitly handled via Readdir.Is this assuming that
Stat
cannot fail afterReaddir
? If so, it's very likely programs will misbehave. Eg, race with unlink, race with file replacement, corruption, network/fuse failures,.. these failures are uncommon but they will definitely occur and they should be handled gracefully. As a trivial example, a "du" implementation might display a bogus size, or subtract 1 from the total size.In future, programs would need to check the
Readdir
error and the sentinel values from some of theFileInfo
methods to be correct.In practice, many developers will not check the results from
Size
,Mode
,ModTime
since it mostly works, is easier, and they don't expect failures (mismatch with mental model). When the deferred stat fails the resulting misbehaviour may be hard to recognise or understand - especially since there is no concrete error. This would be a poor API prone to incorrect use and bugs.Using it correctly would be extra hassle:
APIs that guarantee correctness without needing explicit error handling are extremely useful (eg,
FileInfo
). It would be disappointing to lose this property.I want to use
io/fs
andembed
as soon as is practical - but I wouldn't want to compromise correctness or ease of use. Ifos.File
cannot be changed, then I'd strongly prefer we accept the current performance over deferred stat.If adding a method to
os.File
was acceptible we could achieve the same performance objective by:Dirent
interface as a subset ofFileInfo
(Name
,IsDir
methods).os.File.ReadDirent(n int) ([]Dirent, error)
methodReaddir
andReaddirnames
in theio/fs
proposal in favour ofReadDirent
This might be less controversial than deferring stat?
diamondburned commentedon Sep 17, 2020
> This might be less controversial than deferring stat?
👀
tv42 commentedon Sep 17, 2020
@mpx I like what you said, but: Dirent.IsDir is impossible. Linux dirent d_type generally communicates more than just IsDir (DT_LNK etc), but the direntry type can be unknown. There's no way to write an
func (Dirent) IsDir() bool
that isn't forced to lie; the API needs to have a slightly different shape.https://www.man7.org/linux/man-pages/man3/readdir.3.html
diamondburned commentedon Sep 17, 2020
For the sake of completeness, couldn't there be a fallback to
lstat
whenDT_UNKNOWN
is seen? Given my API, one could implement like so:Although this no longer completely satisfies the goals of this issue (i.e. having a
ReadDir
API that would not calllstat
many times), I would argue that this is a simpler API than some of the other verbose ones while still covering most of the use-cases.I can see another problem with this API though:
Lstat()
now may or may not return a newerFileInfo
, which is the same issue as the lazy-loadedFileInfo
API.jimmyfrasche commentedon Sep 17, 2020
While I am generally in favor, discussing the specifics of a new api is premature when it hasn't been decided if it's necessary yet.
rsc commentedon Sep 18, 2020
I certainly hear you all about the change being strange. I agree it's a bit odd.
For what it's worth, I don't even think this is my idea. Brad says this has been proposed before.
The reason I'm trying hard to find a path forward here is that I'm trying to balance a few different concerns:
Allowing lazy Readdir elegantly solves almost all of this, at the cost of the lazy behavior that seems from direct code inspection not to matter as much as you'd initially think. If we don't fix this problem now, we will be stuck with programs like goimports having their own custom filepath.Walk, and worse there will be no way to write a custom filepath.Walk for the general FS implementations.
If there's not consensus on the lazy Readdir - as there does not seem to be - then it still seems worth trying to fix the problem another way. Whatever we do, it needs to be a limited change: a simple, Go-like API. I expanded @mpx's suggestion above into a separate proposal, #41467. Please see the description and comment over there. Thanks.
rsc commentedon Sep 23, 2020
Retracting per discussion above; see #41467.