Файли та шляхи¶
Два пакети покривають майже все: os для файлової системи та
path/filepath для роботи з іменами. Виклики "весь файл за раз" —
ті, до яких ви звертаєтесь найчастіше.
if err := os.WriteFile("notes.txt", []byte("hello\n"), 0o644); err != nil {
return err
}
b, err := os.ReadFile("notes.txt")
if err != nil {
return err
}
fmt.Printf("%q\n", string(b)) // output: "hello\n"
ReadFile повертає []byte, а не рядок, і сам відкриває, читає та
закриває файл. Використовуйте її, коли файл спокійно вміщається в
пам'ять; інакше — потоково, про що розповідає наступна
стаття.
Права доступу — вісімкові¶
Це 0o644 — маска Unix-прав доступу: власник читає й пише, всі інші
лише читають. Префікс 0o — це синтаксис вісімкового літерала в Go.
Він застосовується лише тоді, коли виклик створює файл; наявний файл
зберігає власний режим.
0o644 для даних, 0o755 для каталогів і виконуваних файлів, 0o600
коли вміст чутливий.
Перевірка того, що є¶
fi, err := os.Stat("notes.txt")
if err != nil {
return err
}
fmt.Println(fi.Name(), fi.Size(), fi.IsDir(), fi.Mode().Perm())
// output: notes.txt 6 false -rw-r--r--
Perm() виводить символьну форму тих самих бітів: три групи rwx для
власника, групи та інших, з - там, де права відсутні. -rw-r--r-- —
це 0o644, записаний у зворотному порядку.
Відсутні файли: перевіряйте помилку, а не шлях¶
Кожен виклик до файлової системи повертає *fs.PathError, що обгортає
конкретну причину, тож errors.Is відповідає на питання "чи існує":
_, err := os.ReadFile("nope.txt")
fmt.Println(errors.Is(err, fs.ErrNotExist)) // output: true
var pe *fs.PathError
fmt.Println(errors.As(err, &pe), pe.Op) // output: true open
os.ErrNotExist і fs.ErrNotExist — те саме значення, тож підходить
будь-яке з них. У старому коді є хелпер os.IsNotExist(err); надавайте
перевагу errors.Is, яка бачить крізь обгортання.
Опирайтесь спокусі спочатку викликати os.Stat, а потім відкривати. Між
двома викликами відповідь може змінитися, а обробляти невдачу все одно
доведеться — просто відкрийте файл і перевірте помилку.
Відкриття з контролем¶
os.Create обрізає або створює. os.OpenFile — загальна форма, і
дописування — найпоширеніша причина до неї звертатись:
f, _ := os.Create("log.txt")
fmt.Fprintln(f, "line1")
f.Close()
af, _ := os.OpenFile("log.txt", os.O_APPEND|os.O_WRONLY, 0o644)
fmt.Fprintln(af, "line2")
af.Close()
*os.File — це io.Writer, тому fmt.Fprintln працює на ньому. У
реальному коді закривайте через defer:
Для файлу, у який ви писали, відкладений Close, чию помилку ви
відкидаєте, — реальний ризик. Виклики запису можуть завершитись успішно,
а файлова система повідомить про збій — переповнений диск, відпалий
мережевий диск — лише тоді, коли дескриптор закривається. Закривайте
явно й перевіряйте помилку, перш ніж повідомляти про успіх.
Каталоги¶
os.MkdirAll("a/b/c", 0o755) // створює батьківські каталоги, без помилки, якщо вже існує
os.Remove(path) // один файл або один порожній каталог
os.RemoveAll(dir) // рекурсивно; без помилки, якщо відсутній
MkdirAll — те, до чого варто звертатись; звичайний Mkdir падає, якщо
батьківський каталог відсутній, і дає помилку, якщо каталог уже існує.
Видалення файлу, якого немає, — це справді помилка:
fmt.Println(os.Remove("notes.txt")) // output: <nil>
fmt.Println(os.Remove("notes.txt") != nil) // output: true
Для тимчасового простору дозвольте бібліотеці обрати місце й приберіть за собою:
Побудова шляхів через filepath¶
Ніколи не з'єднуйте шляхи через + чи /. filepath.Join використовує
правильний роздільник для платформи й очищає результат:
fmt.Println(filepath.Join("a", "b", "..", "c")) // output: a/c
fmt.Println(filepath.Join("a", "", "b")) // output: a/b
Порожні сегменти зникають, тож безпечно з'єднувати змінну, яка може бути порожньою.
fmt.Println(filepath.Base("/x/y/z.txt")) // output: z.txt
fmt.Println(filepath.Dir("/x/y/z.txt")) // output: /x/y
fmt.Println(filepath.Ext("/x/y/z.txt")) // output: .txt
Ext включає крапку. Прибрати її — робота для strings:
filepath.Rel дає шлях з одного місця до іншого — так ви перетворюєте
абсолютні результати обходу назад у зручні для читання імена:
Є також пакет path. Він для розділених слешем речей, які не є
шляхами файлової системи — URL, імена у вбудованій (embedded) FS. Для
файлів використовуйте filepath.
Перелічення та обхід¶
os.ReadDir перелічує один каталог. Вона повертає значення
fs.DirEntry, які знають ім'я й те, чи це каталог, без додаткового
системного виклику:
ents, err := os.ReadDir(dir)
if err != nil {
return err
}
for _, e := range ents {
fmt.Println(e.Name(), e.IsDir())
}
filepath.WalkDir рекурсивно обходить. Колбек отримує аргумент-помилку,
і перше, що варто зробити, — перевірити її, інакше нечитабельний
підкаталог мовчки обрізає ваш обхід:
var found []string
walkErr := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() && filepath.Ext(path) == ".go" {
found = append(found, path)
}
return nil
})
if walkErr != nil {
return walkErr
}
Повернення ненульової помилки зупиняє обхід, і WalkDir повертає її.
Повернення fs.SkipDir з каталогу пропускає натомість усе його
піддерево:
filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if d.IsDir() && d.Name() == "vendor" {
return fs.SkipDir
}
// ...
return nil
})
Це ідіома для ігнорування vendor, node_modules чи .git.
Перейменування та копіювання¶
os.Rename переміщує файл, атомарно, коли обидва шляхи на одній
файловій системі, — саме так ви безпечно записуєте файл: пишете під
тимчасовим іменем, а потім перейменовуєте поверх цілі, тож читач ніколи
не побачить наполовину записаний файл.
Функції os.Copy немає. Відкрийте обидва і використайте io.Copy, у
наступній статті.
З досвіду Python:
os.ReadFile/WriteFile— цеpathlib.read_bytes/write_bytes,filepath.Join— цеos.path.join,WalkDir— цеos.walkз колбеком замість генератора. Звичку, яку варто відкинути, —if os.path.exists(...): тут ви пробуєте виконати операцію й перевіряєте помилку черезerrors.Is.
Швидка довідка¶
| Завдання | Виклик |
|---|---|
| прочитати весь файл | os.ReadFile(p) → []byte |
| записати весь файл | os.WriteFile(p, b, 0o644) |
| відкрити для читання / запису | os.Open / os.Create / os.OpenFile |
| дописати | os.OpenFile(p, os.O_APPEND\|os.O_WRONLY, 0o644) |
| метадані | os.Stat(p) → fs.FileInfo |
| чи існує | errors.Is(err, fs.ErrNotExist) |
| створити каталоги | os.MkdirAll(p, 0o755) |
| видалити | os.Remove / os.RemoveAll |
| тимчасовий каталог | os.MkdirTemp("", "prefix") + defer os.RemoveAll |
| побудувати шлях | filepath.Join(...) |
| розбити шлях | Base, Dir, Ext, Rel |
| перелічити один каталог | os.ReadDir(p) |
| обійти дерево | filepath.WalkDir(root, fn), fs.SkipDir для обрізання |
| перемістити / замінити атомарно | os.Rename(tmp, target) |