1. os.Stat и os.FileInfo
Прежде чем читать содержимое (а иногда вместо чтения), удобно получить метаданные: имя, тип (файл/директория), размер, время изменения, «режим» (права и тип). Метаданные помогают принимать решения в коде: можно ли читать файл, стоит ли его парсить, не перепутали ли вы путь к папке и к файлу, и почему программа «вдруг» перестала работать на чужом компьютере.
Ключевая идея: метаданные ≠ содержимое. Метаданные — это «паспорт» объекта файловой системы, а содержимое — данные внутри файла.
os.Stat: базовый запрос к файловой системе
os.Stat — функция стандартной библиотеки, которая берёт путь (строку) и возвращает метаданные объекта по этому пути. Это одна из самых практичных функций, потому что почти любая программа, работающая с файлами, рано или поздно должна ответить на вопросы: «путь существует?» и «это точно файл?».
Сигнатура выглядит так:
info, err := os.Stat(path)
Если всё хорошо, err == nil, а info содержит метаданные. Если что-то пошло не так (пути нет, нет прав, путь «битый»), то err != nil, и использовать info нельзя — это важная привычка.
Минимальный пример «узнать, существует ли путь и что это»:
package main
import (
"fmt"
"os"
)
func main() {
info, err := os.Stat("data/tasks.json")
if err != nil {
fmt.Println("stat:", err)
return
}
fmt.Println("isDir:", info.IsDir()) // isDir: false (если это файл)
}
Важно: os.Stat не читает файл целиком и не парсит его. Он получает информацию о самом объекте.
Интерфейс os.FileInfo и как его читать
os.Stat возвращает не структуру, а интерфейс os.FileInfo. Это сделано специально: разные реализации файловых систем (и даже некоторые вспомогательные пакеты) могут хранить метаданные по‑разному, но наружу отдаётся единый контракт.
Полезные методы FileInfo — это наш «словарик паспорта»:
| Метод FileInfo | Что означает | Практический смысл |
|---|---|---|
|
базовое имя объекта (без пути) | красиво печатать, логировать |
|
директория или нет | основная развилка «папка/файл» |
|
размер в байтах | понять «пустой файл?» / оценить объём |
|
время последнего изменения | сравнивать «свежее/старее», печатать пользователю |
|
режим (тип + права) | диагностика, проверка «обычный файл?» |
Сразу запомните одну «ловушку ожиданий»: Size() для директории не означает “размер всех файлов внутри”. Это просто техническое поле метаданных директории, которое на разных ОС имеет разный смысл. Для «размера папки» нужен обход содержимого, но это уже другая задача.
3. Практика: утилита inspect и ModTime()
Утилита inspect: печатаем метаданные по пути
Когда изучаешь новую функцию, полезно сделать маленькую утилиту‑лупу: «ввёл путь → увидел, что о нём знает система». Это быстро проясняет, что такое метаданные, и помогает отлаживаться, когда проект начинает работать «не так».
Сделаем программу, которая читает путь из stdin и печатает «паспорт»:
package main
import (
"fmt"
"os"
)
func main() {
var path string
fmt.Print("Enter path: ")
fmt.Scan(&path)
info, err := os.Stat(path)
if err != nil {
fmt.Println("stat:", err)
return
}
fmt.Printf("name=%s isDir=%v size=%d\n", info.Name(), info.IsDir(), info.Size())
}
Если ввести путь к файлу, вывод будет примерно таким:
Enter path: data/tasks.json
name=tasks.json isDir=false size=128
В этом месте многие впервые замечают важную вещь: Name() — это не полный путь, а только имя последнего элемента.
Время изменения: ModTime() и форматирование
ModTime() полезен даже в маленьких проектах: можно вывести пользователю «файл обновлялся тогда‑то», а в отладке — увидеть, что файл вообще не менялся (хотя вы были уверены, что менялся).
Так как ModTime() возвращает time.Time, мы используем форматирование времени. Например, RFC3339 — удобный машинно‑читаемый формат:
package main
import (
"fmt"
"os"
"time"
)
func main() {
info, err := os.Stat("data/tasks.json")
if err != nil {
fmt.Println("stat:", err)
return
}
fmt.Println(info.ModTime().Format(time.RFC3339)) // 2026-01-16T12:34:56-05:00
}
Если вы видите время, которое выглядит «странно», это хороший повод вспомнить: на разных машинах может быть разный часовой пояс. Это не баг Go — это реальность времени.
4. Проверяем файл хранилища задач в TaskBox
В учебном приложении (условно назовём его TaskBox) мы храним данные на диске, например в "data/tasks.json". Типичные проблемы новичка: программа пытается читать файл, а по этому пути лежит папка; файла нет; прав нет; путь перепутали (особенно когда проект запускают из другой рабочей директории).
Вместо того чтобы «падать» где‑то в ReadFile, сделаем «проверку паспорта» через os.Stat.
Сначала заведём функцию, которая возвращает путь к хранилищу:
package main
import "path/filepath"
func storagePath() string {
return filepath.Join("data", "tasks.json")
}
Теперь сделаем проверку: существует ли путь, и это именно файл, а не директория.
package main
import (
"fmt"
"os"
)
func mustBeFile(path string) error {
info, err := os.Stat(path)
if err != nil {
return fmt.Errorf("stat %q: %w", path, err)
}
if info.IsDir() {
return fmt.Errorf("%q is a directory, want a file", path)
}
return nil
}
И используем в main перед чтением:
package main
import (
"fmt"
)
func main() {
path := storagePath()
if err := mustBeFile(path); err != nil {
fmt.Println("storage check failed:", err)
return
}
fmt.Println("storage looks OK")
}
Обратите внимание: мы не читаем содержимое, но уже защищаемся от половины «странных ситуаций», которые иначе превращаются в неочевидные ошибки и вопросы уровня «почему оно работало вчера».
5. Обработка ошибок os.Stat
Ошибки файловой системы — это не исключение, а нормальная ветка выполнения. И os.Stat здесь особенно честный: если он не может получить метаданные, он так и скажет через err. Важно научиться различать хотя бы два частых класса: «не существует» и «нет прав».
Для этого у пакета os есть удобные функции‑предикаты: os.IsNotExist(err) и os.IsPermission(err).
Вот пример, как можно сделать понятное сообщение:
package main
import (
"fmt"
"os"
)
func main() {
path := "data/tasks.json"
_, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
fmt.Println("no such file:", path)
return
}
if os.IsPermission(err) {
fmt.Println("no permission:", path)
return
}
fmt.Println("stat error:", err)
return
}
fmt.Println("ok")
}
Психологически это важный апгрейд: вы перестаёте воспринимать ошибки как «что-то ужасное», а начинаете воспринимать как ветвление логики. Особенно это полезно в CLI‑утилитах: пользователю нужно коротко и понятно.
6. Mode() и проверка «обычного файла»
info.Mode() возвращает os.FileMode. Внутри него закодированы и права доступа, и тип (файл/директория/устройство и т.д.). На старте важнее всего другое: Mode() — хороший диагностический «ярлык», который можно напечатать, чтобы понять, что система думает о вашем объекте.
Печать режима часто выглядит довольно понятно:
package main
import (
"fmt"
"os"
)
func main() {
info, err := os.Stat("data/tasks.json")
if err != nil {
fmt.Println("stat:", err)
return
}
fmt.Printf("mode=%v\n", info.Mode()) // mode=-rw-r--r--
}
Если хочется убедиться, что это «обычный файл», можно использовать IsRegular():
package main
import (
"fmt"
"os"
)
func main() {
info, _ := os.Stat("data/tasks.json")
fmt.Println(info.Mode().IsRegular()) // true (для обычного файла)
}
Здесь мы не копаемся в битовых масках режима: сегодня задача проще — читать метаданные и принимать базовые решения.
7. Схема «Stat → решение»
Когда os.Stat становится привычным инструментом, код начинает выглядеть более предсказуемо. Полезно держать в голове простую схему: сначала метаданные, потом действие. Это снижает шанс «прочитать директорию как файл» и получить загадочную ошибку.
flowchart TD
A["Есть путь (string)"] --> B["os.Stat(path)"]
B -->|err != nil| C["Обработать ошибку: not exist / permission / другое"]
B -->|err == nil| D["info.IsDir()?"]
D -->|true| E["Это директория: выбрать другую логику"]
D -->|false| F["Это файл: можно читать/писать/парсить"]
8. Типичные ошибки при работе с os.Stat и FileInfo
Ошибка №1: использовать info, не проверив err.
os.Stat возвращает два значения не из жадности, а потому что файловая система — штука капризная. Если err != nil, то info нельзя воспринимать как валидный объект. В хорошем стиле сначала идёт проверка ошибки, и только потом чтение полей метаданных.
Ошибка №2: путать метаданные и содержимое.
Иногда хочется «проверить, что файл не пустой» и начать «читать первые 10 строк». Но очень часто достаточно Size(). Или хочется узнать «когда файл менялся» и вы начинаете парсить JSON и искать поле updated_at — хотя ModTime() уже даёт системное время изменения файла.
Ошибка №3: трактовать Size() директории как “размер папки”.
Размер директории — не сумма размеров файлов внутри. Если вам нужен «размер папки», это уже другая задача: нужно перечислять содержимое и суммировать размеры. Если использовать Size() директории как «размер папки», вы получите числа, которые иногда выглядят правдоподобно, но на самом деле вводят в заблуждение.
Ошибка №4: печатать ошибку без контекста пути.
Сообщение "stat: permission denied" без пути быстро превращается в квест «угадай, где именно». Лучше оборачивать ошибку с указанием пути: fmt.Errorf("stat %q: %w", path, err). Тогда при отладке у вас будет человеческий след.
Ошибка №5: делать “проверку существования” через Stat, а потом ещё раз вызывать Stat ради метаданных.
Иногда пишут так: «если файл есть — хорошо», а потом второй раз делают Stat, чтобы получить Size() и ModTime(). Это лишняя работа. Если вам нужны метаданные — один вызов os.Stat уже даёт их, просто используйте info.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ