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».
Невелика таблиця: що зазвичай означають режими
| Режим | Для чого | Що приблизно означає на практиці |
|---|---|---|
|
директорія | власник може все, інші можуть читати й заходити |
|
директорія | лише власник може читати й заходити |
|
файл | власник читає й пише, інші читають |
|
файл | лише власник читає й пише |
У межах курсу нам важливі не всі тонкощі, а надійний мінімум: обирати права усвідомлено й не ставити про всяк випадок 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: помилки — це значення, їх зручно повертати вгору й додавати контекст по дорозі. Саме тому обгортання (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("не вдалося підготувати директорію даних %q: %w", dir, err)
}
return nil
}
Зверніть увагу на три речі.
- По-перше, режим 0755 тут — усвідомлене значення за замовчуванням для «звичайних даних».
- По-друге, ми повертаємо помилку, а не друкуємо її всередині. Так код стає придатним до повторного використання.
- По-третє, ми додаємо контекст ("не вдалося підготувати директорію даних ..."), але зберігаємо причину через %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» без указання, що саме ви робили і з яким шляхом. Обгортання через fmt.Errorf("...: %w", err) допомагає зробити помилки придатними для інженерного аналізу, зберігаючи першопричину помилки.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ