JavaRush /Курсы /Go SELF /Директории данных — os.MkdirAll, права и безопасные дефол...

Директории данных — os.MkdirAll, права и безопасные дефолты

Go SELF
39 уровень, 3 лекция
Открыта

1. Зачем нужна директория данных

Когда вы пишете программу, очень хочется думать, что файлы «просто есть». Но файловая система устроена как реальный мир: если вы пытаетесь положить коробку на полку, а шкафа нет — коробка красиво падает на пол (в Go это выглядит как ошибка). Поэтому почти любая программа, которая хранит данные на диске, должна сначала убедиться, что нужная директория существует.

В учебном приложении мы будем считать, что у нас есть маленький CLI‑таск‑менеджер (условно назовём его todo), который хранит данные в файле внутри директории data/todo. Ровно в этот момент многие новички впервые встречают ошибку "no such file or directory" и задают философский вопрос: «Но я же указал путь! Почему этого мало?»

Путь — это только адрес. Адрес не строит дом.

Мини-пример: «папки нет» как классика жанра

package main

import (
	"fmt"
	"os"
)

func main() {
	f, err := os.Create("data/todo/items.txt")
	if err != nil {
		fmt.Println("create:", err) // create: open data/todo/items.txt: no such file or directory
		return
	}
	defer f.Close()
}

Сама ошибка нормальная: ОС честно говорит «я не могу создать файл в директории, которой не существует».

2. os.MkdirAll: что делает и зачем нужен

В программировании есть полезная лень: не «я ничего не буду делать», а «я не буду делать одно и то же вручную сто раз». Создание директорий как раз из этой серии. Можно создавать директории по одной, проверять «существует ли родитель», потом «существует ли родитель родителя», и где-то на третьем уровне вы начнёте задумчиво смотреть в окно и вспоминать, зачем вообще выбрали разработку.

os.MkdirAll(path, perm) решает проблему радикально: он создаёт всю цепочку директорий, если чего-то не хватает. А если директории уже существуют — это не катастрофа и не повод паниковать: это штатная ситуация.

Важно понимать, что MkdirAll работает именно для директорий. Он не создаёт файл, он не проверяет, что внутри лежит «правильный формат данных». Он всего лишь гарантирует: «каталог есть».

Мини-пример: готовим директорию для данных

package main

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

func main() {
	dir := filepath.Join("data", "todo")
	if err := os.MkdirAll(dir, 0755); err != nil {
		fmt.Println("mkdirall:", err)
		return
	}
	fmt.Println("dir ready:", dir) // dir ready: data/todo
}

Обратите внимание: мы используем filepath.Join, чтобы не склеивать строки руками. А потом — MkdirAll, чтобы не писать велосипед с квадратными колёсами.

3. Права доступа и os.FileMode

Права доступа — тема, где новичок обычно говорит: «Я понял… кажется… нет, не понял». Это нормально. Давайте сделаем это максимально приземлённо.

В Unix‑подобных системах (Linux, macOS) права на файлы и директории обычно задают тремя группами: для владельца, для группы, для остальных. Каждая группа получает набор прав: чтение, запись, выполнение. Для директорий «выполнение» означает право входить внутрь (да, звучит странно, но так исторически сложилось).

В Go права задаются типом os.FileMode. Часто вы пишете числа вроде 0644 и 0755. И главный сюрприз: это восьмеричные числа. Ведущий ноль — не «для красоты», он говорит компилятору: «считай это в base‑8».

Небольшая таблица: что обычно означают режимы

Режим Кому Что примерно означает на практике
0755
директория владелец может всё, остальные могут читать/заходить
0700
директория только владелец может читать/заходить
0644
файл владелец читает/пишет, остальные читают
0600
файл только владелец читает/пишет

В рамках курса нам важны не все тонкости, а устойчивый минимум: выбирать права осознанно и не делать «на всякий случай 0777».

Мини-пример: печатаем режим как восьмеричное число

package main

import (
	"fmt"
	"os"
)

func main() {
	var dm os.FileMode = 0755
	var fm os.FileMode = 0644

	fmt.Printf("dir=%#o file=%#o\n", dm, fm) // dir=0755 file=0644
}

Формат %#o удобен тем, что он показывает число как восьмеричное и сохраняет «вид» 0755, а не просто 493 (что тоже верно математически, но раздражает людей).

4. Безопасные дефолты и режимы

Есть особый тип программистской магии: «я поставлю максимальные права, чтобы точно работало». Работать действительно будет… но вы открываете данные всем вокруг. Это как хранить ключи от квартиры под ковриком и потом удивляться, что в холодильнике кто-то ест вашу пиццу.

В учебных задачах это может пройти. В реальной жизни — это источник проблем: случайная утечка данных, неожиданный доступ другого пользователя, странные баги на CI, и внезапное «почему у меня на сервере появился файл, который я не создавал».

В качестве здоровых дефолтов мы будем придерживаться простого правила.

  • Если данные обычные и не секретные, то директория 0755, файл 0644.
  • Если данные приватные (токены, ключи, персональные записи, что-то чувствительное), то директория 0700, файл 0600.

И даже если вы пока пишете учебный todo, полезно формировать привычку: права — это часть контракта вашего приложения.

Мини-пример: «публичная» и «приватная» директории

package main

import (
	"fmt"
	"os"
)

func main() {
	if err := os.MkdirAll("data/public", 0755); err != nil {
		fmt.Println("mkdir public:", err)
		return
	}
	if err := os.MkdirAll("data/private", 0700); err != nil {
		fmt.Println("mkdir private:", err)
		return
	}
}

Да, это две строки. Но по смыслу — это «мы решили, какие данные кому можно видеть».

Директория ≠ файл: про execute-бит

У новичков часто возникает мысль: «Ну, файл 0644 — норм. Значит и директория пусть будет 0644». И тут ловушка: директория — не файл.

Файл читают и пишут. Директория — это контейнер. Чтобы «пользоваться директорией», нужно иметь право войти внутрь и просматривать/создавать элементы. Поэтому для директорий почти всегда используются режимы с «выполнением», например 0755 или 0700. Если вы поставите директории 0644, вы получите странные эффекты: директория может как бы «видеться», но «зайти в неё» нельзя.

Это похоже на ситуацию: дверь в подъезд прозрачная (вы видите, что внутри), но ручка снята (войти нельзя). С архитектурной точки зрения — очень творчески, но жить так тяжело.

Мини-пример: правильный «набор режимов» как константы

package main

import "os"

const (
	dataDirMode  os.FileMode = 0755
	privateMode  os.FileMode = 0700
	dataFileMode os.FileMode = 0644
)

Мы просто фиксируем «стандарт проекта». Это делает код читабельнее: вы не гадаете, почему тут 0755, а там 0700.

5. Протокол: сначала директория, потом файл

Когда код растёт, важно, чтобы он был предсказуемым. Поэтому мы заведём в нашем приложении маленькую привычку: любая операция с файлом данных начинается с подготовки директории.

Это не «бюрократия». Это способ сделать поведение стабильным: вы сможете запускать программу на чистой машине, на CI, в новом каталоге проекта — и она сама подготовит структуру.

Удобно мыслить так:

собрали путь → убедились, что директория существует → открыли/создали файл

Можно представить это мини‑схемой:

flowchart TD
    A[Есть путь к файлу данных] --> B[Вычислить директорию файла]
    B --> C{Директория существует?}
    C -->|да| D[Открыть/создать файл]
    C -->|нет| E["os.MkdirAll(dir, mode)"]
    E --> D

Главная мысль: мы не надеемся на окружение. Мы сами создаём минимально нужные условия.

6. Реализация: ensureDataDir и ошибки

Сейчас мы напишем несколько маленьких функций. Это важный стиль: вместо того чтобы размазывать MkdirAll по всему main, мы делаем «порог входа» — одну функцию, которая отвечает за подготовку директории данных.

И здесь мы снова пользуемся привычкой Go: ошибки — это значения, их удобно возвращать наверх и добавлять контекст по пути. Именно поэтому wrapping (fmt.Errorf("...: %w", err)) так ценен: он делает ошибку понятнее, не разрушая её причину.

Функция, которая знает где живут данные

package main

import "path/filepath"

func dataDir() string {
	return filepath.Join("data", "todo")
}

Это маленькая функция, но она важна: если позже вы решите хранить данные в другом месте, вы меняете одно место, а не двадцать.

ensureDataDir: создаём директорию, если нужно

package main

import (
	"fmt"
	"os"
)

func ensureDataDir(dir string) error {
	if err := os.MkdirAll(dir, 0755); err != nil {
		return fmt.Errorf("prepare data dir %q: %w", dir, err)
	}
	return nil
}

Обратите внимание на три вещи.

  • Во‑первых, режим 0755 здесь — осознанный дефолт для «обычных данных».
  • Во‑вторых, мы возвращаем ошибку, а не печатаем её внутри. Так код становится переиспользуемым.
  • В‑третьих, мы добавляем контекст ("prepare data dir ..."), но сохраняем причину через %w.

Собираем путь к файлу с задачами

package main

import "path/filepath"

func itemsFilePath() string {
	return filepath.Join(dataDir(), "items.txt")
}

Здесь логика та же: путь строится через filepath.Join, без ручной склейки.

Используем в main: сначала директория, потом файл

package main

import (
	"fmt"
	"os"
)

func main() {
	if err := ensureDataDir(dataDir()); err != nil {
		fmt.Println("error:", err)
		return
	}

	f, err := os.OpenFile(itemsFilePath(), os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
	if err != nil {
		fmt.Println("openfile:", err)
		return
	}
	defer f.Close()

	_, _ = f.WriteString("buy milk\n")
}

Тут мы уже видим целостный протокол: директория готова — значит, файл можно создавать/открывать. Файл мы открыли в режиме дописывания (O_APPEND), а режим 0644 выбрали как безопасный дефолт для обычного текстового файла.

7. Типичные ошибки при работе с директориями и правами

Ошибка №1: создавать файл, не создавая директорию.
Это самый частый случай. Код выглядит логично, но ОС не обязана создавать директории «по догадке». Решение простое: перед любым Create/OpenFile/WriteFile убедитесь, что директория подготовлена через os.MkdirAll.

Ошибка №2: писать 755 вместо 0755 и получать «магические» числа.
Без ведущего нуля число становится десятичным. В итоге вы задаёте не те права, которые думаете. Это одна из тех ошибок, которые особенно неприятны тем, что код компилируется и «почти работает», а затем начинает чудить на чужой машине.

Ошибка №3: ставить директории режимом 0644 и удивляться, что «внутрь не заходит».
Директория требует права «входа» (execute). Поэтому для директорий обычно используются 0755 или 0700. Если перепутать, вы получите загадочные ошибки доступа даже у владельца.

Ошибка №4: лечить всё 0777, потому что «так точно не будет permission denied».
Это действительно может убрать одну ошибку, но создаёт другую: вы делаете данные доступными всем. Даже для учебного приложения полезно тренировать дисциплину и выбирать минимально достаточные права.

Ошибка №5: печатать ошибку без контекста и потом не понимать, где она случилась.
Если вы возвращаете наружу просто err, то при большой программе лог будет выглядеть как «permission denied» без указания, что именно вы делали и с каким путём. Wrapping через fmt.Errorf("...: %w", err) помогает сделать ошибки инженерно пригодными, сохраняя первопричину ошибки для анализа.

1
Задача
Go SELF, 39 уровень, 3 лекция
Недоступна
Лабораторная папка
Лабораторная папка
1
Задача
Go SELF, 39 уровень, 3 лекция
Недоступна
Публичная кладовка
Публичная кладовка
1
Задача
Go SELF, 39 уровень, 3 лекция
Недоступна
Порядок действий
Порядок действий
1
Задача
Go SELF, 39 уровень, 3 лекция
Недоступна
Стартовый набор
Стартовый набор
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ