What does os.ReadDir return, and how do you tell whether an entry is a subdirectory?
answer
- one directory, not the whole tree
- the slice comes back in a fixed order
- three cheap questions, one expensive one
- Name is a base name, not a path
- size and mod time cost extra
basics
~10 sos.ReadDir returns a sorted slice of fs.DirEntry values for one directory plus an error. Each entry gives Name, IsDir and Type; IsDir reports a subdirectory. It reads a single level and never recurses.
solid answer
~40 s`os.ReadDir(name)` returns `([]os.DirEntry, error)`, where `os.DirEntry` is an alias for `fs.DirEntry`. It reads the named directory once, in full, and hands back its immediate children sorted by filename — it does not descend, so recursion is your job or `filepath.WalkDir`'s. Each `fs.DirEntry` answers four things: `Name()` (the base name only, so join it with the directory to get a usable path), `IsDir()`, `Type()` (the file-mode type bits, with no permission bits), and `Info()`, which returns the full `fs.FileInfo` with size and modification time. The first three are answered from data the directory read already produced; `Info()` is the one that may cost a system call. So test `e.IsDir()` rather than calling `os.Stat` on every name.
code
go · 15 linesentries, err := os.ReadDir(dir)
if err != nil {
return err
}
for _, e := range entries {
if e.IsDir() {
continue // os.ReadDir does not descend into it
}
full := filepath.Join(dir, e.Name())
info, err := e.Info() // this is the call that may hit the filesystem
if err != nil {
return err
}
fmt.Println(full, info.Size())
}go deeper
Be ready to write the ten-line listing loop from memory: call os.ReadDir, range over the entries, skip the ones where IsDir() is true, and join Name() onto the directory before opening anything.
Explain which DirEntry methods are answered from the directory read and which one reaches the filesystem, and why os.ReadDir replaced an older call that returned FileInfo for every entry.
Show judgment about scale and staleness: when the eager sorted slice is wrong and streaming is right, and how you handle an entry that disappears between the listing and the Info() call.
Frame it as a cost model for a shared helper: a listing API that returns full metadata makes every caller pay for it, so decide what your internal file-scanning helpers hand back before other teams build on the shape.
## What the call is ```go func os.ReadDir(name string) ([]os.DirEntry, error) ``` It opens the named directory, reads **all** of its entries, sorts them by filename, closes it, and returns the slice. `os.DirEntry` is simply an alias for the `fs.DirEntry` interface declared in `io/fs`, so the same values flow through `filepath.WalkDir` callbacks and through filesystem abstractions unchanged. Two properties surprise newcomers: * **It is one level deep.** `os.ReadDir("assets")` tells you what is directly inside `assets`. Subdirectories appear as entries whose `IsDir()` is true, but their contents are not in the slice. Walking a tree means recursing yourself or calling `filepath.WalkDir`. * **It is eager.** The whole directory is read into memory before you see anything. For a directory with a few thousand entries that is fine; for a pathological one with millions it is not, and `os.Open` plus `(*os.File).ReadDir(n)` lets you page through `n` entries at a time until `io.EOF`. That streaming form returns entries in **directory order**, not sorted — the sort is a service `os.ReadDir` performs for you so that output is reproducible across runs and machines. ## The fs.DirEntry contract `fs.DirEntry` has exactly four methods: | method | returns | cost | |---|---|---| | `Name() string` | the **base name** of the entry | free | | `IsDir() bool` | whether it is a directory | free | | `Type() fs.FileMode` | only the **type bits** (`fs.ModeDir`, `fs.ModeSymlink`, …) | free | | `Info() (fs.FileInfo, error)` | full metadata: `Size()`, `Mode()`, `ModTime()`, `Sys()` | may cost a syscall | "Free" means the answer came out of the directory read itself. On Unix the kernel returns a type hint alongside each name, so `IsDir()` and `Type()` are already known; on filesystems that do not supply the hint, Go fills it in when it builds the entry. `Info()` is the expensive one: it fetches full metadata for that specific entry, and because the file may have been renamed or deleted since the directory was read, it can fail — an error satisfying `errors.Is(err, fs.ErrNotExist)` is normal on a tree somebody else is writing to. That cost split is the whole design point. An indexer that only needs to know "is this a directory, and does the name end in `.png`" never touches metadata at all. One that needs sizes pays per file, and pays it explicitly. `Type()` is not `Mode()`. `Type()` gives you the mode's type portion only — enough to say "directory", "symlink", "regular file" — and deliberately omits permission bits. If you need permissions you must go through `Info().Mode()`. ## Name is a base name The most common beginner bug is treating `e.Name()` as a path: ```go f, err := os.Open(e.Name()) // wrong unless the directory happens to be the working directory ``` The entry knows nothing about where it came from. Join it back onto the directory you passed in: ```go full := filepath.Join(dir, e.Name()) ``` ## Why not just os.Stat every name? A loop that calls `os.Stat(filepath.Join(dir, name))` for each entry works, and it is how the same job is written in languages whose directory listing returns bare strings. In Go it doubles the system calls for information you were already handed. On a checked-out repository with tens of thousands of small files, that difference is visible in a CPU profile as a wall of stat calls under your walk function — which is exactly the smell that sends people from a hand-rolled recursion to `filepath.WalkDir`. ## The ioutil ancestor Before Go 1.16 the idiomatic call was `ioutil.ReadDir`, which returned `[]fs.FileInfo` — that is, it stat'ed every entry for you whether you wanted the metadata or not. `os.ReadDir` replaced it precisely to make that cost opt-in. Code ported from the old API often keeps the old shape, calling `Info()` on every entry inside the loop and reintroducing the very cost the newer call avoids. ## Errors A failure from `os.ReadDir` is normally a `*fs.PathError`; classify it with `errors.Is(err, fs.ErrNotExist)` or `errors.Is(err, fs.ErrPermission)` rather than comparing strings. A partial read is not a thing here: you either get the whole listing or an error.
- In what order does os.ReadDir return entries, and what would you use for a directory with millions of files?os.ReadDir sorts by filename, which makes output reproducible, but it reads the entire directory into memory first. For a huge directory, use os.Open and then (*os.File).ReadDir(n) to pull n entries at a time until io.EOF. That streaming form returns entries in directory order, unsorted, so sort or process them yourself if order matters.
- What does DirEntry.Type() report that fs.FileInfo.Mode() does not?Type() returns only the type bits of the mode — directory, symlink, device, regular file — and no permission bits. Mode(), reached through Info(), returns the full fs.FileMode including permissions. Type() is answered from the directory read, while Info() may need to touch the filesystem, so filtering on Type() is the cheap path.
- When can DirEntry.Info() fail even though the entry was just listed?The listing is a snapshot. If the file is renamed or deleted between the directory read and the Info() call, Info() returns an error satisfying errors.Is(err, fs.ErrNotExist). On a tree another process is writing to, that is expected rather than exceptional, so an indexer should skip the entry and carry on rather than abort.
It is like being handed a table of contents: you get every chapter title in order for free, but finding out how long a chapter is means turning to it.
saying these in an interview costs you the question
- Believes os.ReadDir recurses into subdirectories
- Expects entries in creation or on-disk order
- Uses e.Name() as if it were a full path
- Assumes DirEntry already carries size and modification time
- Calls os.Stat on every entry just to test IsDir