JavaRush/Courses/Go SELF/os.ReadDir — listing ...

os.ReadDir — listing a directory via DirEntry

41 level, 1 lecture
Open

1. os.ReadDir and DirEntry: how to read a directory

Once you know how to read a file, a natural question comes up: “OK, but how do you know which file to read?” In real life, files rarely live in one single place with a name like data.txt the way they do in beginner exercises. More often you have a folder containing lots of files: logs, reports, notes, “final_version_3_definitely_final.txt”. And before you process anything, you need to learn how to confidently obtain a list of items in a directory.

You can think of a directory as a “box of objects”: you don’t have to open every object and read its contents—often it’s enough to know the names and types (file/folder). And this is where we need a function that can quickly and neatly produce a listing (an enumeration) of the contents.

What os.ReadDir does and what it returns

os.ReadDir is the standard way to “peek inside a folder” with a single call. According to the docs, it reads the named directory and returns all entries sorted by filename; if an error occurs while reading, it may return both the entries already read and the error itself.

The signature looks like this:

func ReadDir(name string) ([]os.DirEntry, error)

So you get a slice []os.DirEntry and an error. And that’s very Go-ish: “here’s your result, here’s your problem—deal with it like an adult.”

A minimal example of “just print the names”:

package main

import (
	"fmt"
	"os"
)

func main() {
	entries, err := os.ReadDir(".")
	if err != nil {
		fmt.Println("readdir:", err)
		return
	}

	for _, e := range entries {
		fmt.Println(e.Name())
	}
}

The key idea to catch here: ReadDir is not recursive. It reads only one level—what lies directly inside the specified folder. Recursion and walking a directory tree are a separate story (and a separate lecture); today we’re learning to confidently “walk one floor at a time.”

What DirEntry is and why it’s “lightweight”

At the type level, os.DirEntry is not “a special entity from package os”, but an alias to an interface from io/fs: type DirEntry = fs.DirEntry. This is convenient: the same interface is used in different parts of the standard library, and you gradually get used to a single contract of “what a directory entry is.”

The interface fs.DirEntry (and therefore os.DirEntry) gives us a minimal set of methods: Name(), IsDir(), Type(), Info(). The important idea: the first methods can be used quickly, while Info() is “more expensive”, because it may require additional calls into the OS.

To keep your head from overheating, it helps to keep a small table like this in mind:

DirEntry method What it gives you When it’s usually enough
Name()
the entry name inside the directory almost always, for output/filtering
IsDir()
whether it’s a directory to decide “descend” vs. “process as a file”
Type()
quick type bits (simplified) when you need a type “tag” without details
Info()
a full FileInfo (size, time, mode) when you really need metadata

Let’s make an example that prints [dir] or [file] (without unnecessary heroics and without Info()):

package main

import (
	"fmt"
	"os"
)

func main() {
	entries, err := os.ReadDir(".")
	if err != nil {
		fmt.Println("readdir:", err)
		return
	}

	for _, e := range entries {
		if e.IsDir() {
			fmt.Println("[dir]", e.Name())
		} else {
			fmt.Println("[file]", e.Name())
		}
	}
}

At this point, beginners often think: “Oh, nice, e has everything—where’s the path?” And this is where the most common confusion starts.

Why DirEntry doesn’t have a full path

DirEntry is an “entry inside a specific directory”. It only knows the entry name relative to the folder you read. And that’s fine: if you read a folder notes, the entry says todo.txt, but it doesn’t have to store /home/user/project/notes/todo.txt.

So you build the full path yourself, and you do it not by concatenating strings (please don’t do "notes/" + name), but with filepath.Join. This protects you from path separator quirks on different OSes and from small mistakes like “double slashes” or “forgot the slash”.

Example:

package main

import (
	"fmt"
	"os"
	"path/filepath"
)

func main() {
	dir := "notes"

	entries, err := os.ReadDir(dir)
	if err != nil {
		fmt.Println("readdir:", err)
		return
	}

	for _, e := range entries {
		fullPath := filepath.Join(dir, e.Name())
		fmt.Println(fullPath)
	}
}

Notice the calm idea: e.Name() is the name, filepath.Join(dir, e.Name()) is the path. It’s easier to read the code, especially when you come back a week later and no longer remember why you have three slashes and one space in a string.

When you need “full metadata”: e.Info() → FileInfo

Sometimes “name + type” is not enough. For example, you want to show file size, or sort by modification time, or filter “files larger than 10 MB”. For that you need FileInfo—the same metadata interface you’ve already seen via os.Stat.

DirEntry has a method Info(), which returns (FileInfo, error)—and it’s important not to lose the error. By the DirEntry contract, Info() may fail. For example, you read the directory, and then the file gets deleted/renamed/permissions changed—yes, the world contains someone other than your program, and that someone sometimes does strange things.

Example: print the size, but handle the Info() error carefully:

package main

import (
	"fmt"
	"os"
)

func main() {
	entries, err := os.ReadDir(".")
	if err != nil {
		fmt.Println("readdir:", err)
		return
	}

	for _, e := range entries {
		info, err := e.Info()
		if err != nil {
			fmt.Println("info failed:", e.Name(), err)
			continue
		}
		fmt.Printf("%s size=%d\n", info.Name(), info.Size())
	}
}

Here we did two “adult” things. First, we didn’t crash the whole program because of one problematic file—we simply skipped it. Second, we printed the name along with the error so we can understand what exactly broke (because an error without context is like saying “it hurts” without saying where).

Practical mini-step: a “notes list”

So it doesn’t feel like “we just read directories for the sake of reading directories”, let’s put this into a small scenario. Imagine our training app stores notes in a folder notes/, and each note is a separate .txt file. We’re not building a nice CLI with flags yet (we’ll get there), but we can write a program that:

  1. reads the folder name from stdin,
  2. enumerates entries,
  3. shows only .txt files.

Let’s start with a function that filters by extension. Here strings.HasSuffix is enough, because we’re building a “simple version without magic” for now.

package main

import (
	"strings"
)

func isNoteFile(name string) bool {
	return strings.HasSuffix(name, ".txt")
}

Now the main code:

package main

import (
	"bufio"
	"fmt"
	"os"
	"strings"
)

func main() {
	in := bufio.NewReader(os.Stdin)

	fmt.Print("Directory with notes: ")
	dirLine, _ := in.ReadString('\n')
	dir := strings.TrimSpace(dirLine)

	entries, err := os.ReadDir(dir)
	if err != nil {
		fmt.Println("readdir:", err)
		return
	}

	for _, e := range entries {
		if e.IsDir() {
			continue
		}
		if isNoteFile(e.Name()) {
			fmt.Println(e.Name())
		}
	}
}

This example is intentionally “down to earth”. We’re not building architecture, not creating packages and layers, not discussing UX. We’re just reinforcing the key points: ReadDir gives a list, DirEntry provides signals, Name() is the name inside the folder, and filtering is done with a normal if.

If you want to make the output a bit clearer to a human, you can add a prefix:

fmt.Println("note:", e.Name()) // note: todo.txt

ReadDir errors and “partial results”

At this stage it’s easy to fall into an extreme: either “errors don’t happen, everything is fine”, or “an error is the end of the world”. In Go, most people pick the middle: errors are a normal part of the contract, and you should handle them in a way that keeps the code readable.

The documentation says that ReadDir returns directory entries sorted by filename, and if an error occurs while reading, it can return both the entries already read and the error. In beginner practice, this means one simple thing: you can’t pretend err is “maybe not important”. It is important. But sometimes you still have a choice: “I need either a full result or nothing”, or “I want at least what we managed to read”.

For a training app, it’s reasonable to start with a simple rule: if err != nil, we treat the list as unreliable and exit. This is the most honest option for early versions of the code:

entries, err := os.ReadDir(dir)
if err != nil {
	return fmt.Errorf("read dir %q: %w", dir, err)
}

Here we also added context and wrapped the error with %w. In Go, that’s the convention: add context while keeping the original cause inside so it can be analyzed later.

3. Common mistakes when working with os.ReadDir and DirEntry

Mistake #1: assuming e.Name() is a full path.
This happens all the time: you get a DirEntry, print the name, it looks like a path, and it feels like “well, it works”. Then you try to open a file using e.Name() and suddenly get “no such file or directory”, because the program’s current working directory is different. The fix is simple: a name is a name, and you build the path with filepath.Join(baseDir, e.Name()).

Mistake #2: gluing paths together as strings.
A path like "dir/" + name feels like a quick solution until you meet Windows, or until dir already ends with a trailing slash. You end up with “double separators”, broken paths, and very creative bugs. filepath.Join does exactly what you need, and the code becomes calmer.

Mistake #3: calling Info() everywhere “just in case”.
DirEntry is designed to be “lightweight” so you can quickly filter the list by name/type first, and only then request metadata selectively. If you call Info() for every entry without a need, you turn a fast listing into a “pseudo-Stat for everything”, and in large directories this will be noticeable.

Mistake #4: ignoring the Info() error inside the loop.
A very tricky situation: ReadDir successfully returned a list, you iterate over it, and then Info() fails for one file (the file disappeared, you don’t have permissions, the filesystem hiccuped). If you don’t check err, you either crash later in an unexpected place or get zero values and start making decisions based on garbage. The right path is boring but reliable: info, err := e.Info(); if err != nil { ... }.

Mistake #5: not realizing that ReadDir reads one level, not the whole tree.
Sometimes people expect ReadDir("root") to return all files in all subfolders. It won’t. This is “listing a floor”, not “walking the entire building”. And that’s actually good: fewer surprises, more control.

1
Task
Go SELF,  41 level,  1 lecture
Locked
Directory showcase
Directory showcase
1
Task
Go SELF,  41 level,  1 lecture
Locked
Extension Filter
Extension Filter
1
Task
Go SELF,  41 level,  1 lecture
Locked
Basket Weight
Basket Weight
1
Task
Go SELF,  41 level,  1 lecture
Locked
Folder report
Folder report
Комментарии
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны sign in
This page has no comments yet