Avatar ·

Как создать папку в Go, если она не существует (mkdir if not exists)

Всем привет! Я только начал изучать Go и столкнулся с такой проблемой. Мне нужно в программе создать директорию для сохранения логов, но только если её ещё нет. Если папка уже существует — ничего делать не надо, просто продолжить работу. Проблема в том, что стандартная функция os.Mkdir() возвращает ошибку, если директория уже существует. Я пробовал сначала проверять через os.Stat(), а потом создавать, но это выглядит как костыль. В других языках (например, Python) есть os.makedirs(exist_ok=True), а в PHP — mkdir($path, 0777, true). Есть ли в Go что-то подобное? Вот мой текущий код, который мне кажется неправильным: ```go package main import ( "fmt" "os" ) func main() { path := "./logs" if _, err := os.Stat(path); os.IsNotExist(err) { err := os.Mkdir(path, 0755) if err != nil { fmt.Println("Ошибка создания папки:", err) } } } ``` Этот код работает, но я слышал, что это не thread-safe, и вообще может быть race condition. Как правильно сделать «mkdir if not exists» в Go? Есть ли встроенная функция или идиоматический способ? Заранее спасибо за помощь! <<>>

Правильный способ: функция os.MkdirAll

В Go существует встроенная функция os.MkdirAll(), которая решает вашу задачу одной строкой. Она создаёт все необходимые родительские директории (аналог mkdir -p в Linux) и не возвращает ошибку, если директория уже существует.

Вот как выглядит ваш исправленный код:

package main

import (
    "fmt"
    "os"
)

func main() {
    path := "./logs"
    err := os.MkdirAll(path, 0755)
    if err != nil {
        fmt.Println("Ошибка создания папки:", err)
        return
    }
    fmt.Println("Директория готова к использованию")
}

Почему это лучше вашего подхода?

  • Thread-safe: os.MkdirAll атомарно выполняет проверку и создание, что исключает race condition.
  • Вложенные директории: если в пути есть несуществующие родительские папки (например, ./logs/2024/01), они тоже будут созданы.
  • Код чище: не нужно писать лишние проверки и обрабатывать ошибки os.IsNotExist.
  • Идиоматично: это стандартный способ в Go, используемый во всех крупных проектах.

Детали работы os.MkdirAll

Функция принимает два аргумента:

  • path — путь к директории (строка)
  • perm — права доступа (тип os.FileMode), обычно 0755 для папок

Важно: os.MkdirAll использует umask процесса, поэтому реальные права могут отличаться от указанных. Если вам нужны строго заданные права, сначала установите umask:

import "syscall"

func main() {
    syscall.Umask(0) // отключаем umask
    err := os.MkdirAll("./logs", 0755)
    // ...
}

Обработка ошибок

Хотя функция не падает на существующей директории, она всё равно может вернуть ошибку в других случаях:

  • Недостаточно прав на создание
  • Файл с таким именем уже существует (не директория)
  • Проблемы с файловой системой (диск переполнен, повреждена ФС)

Пример правильной обработки:

func EnsureDir(path string) error {
    err := os.MkdirAll(path, 0755)
    if err != nil {
        return fmt.Errorf("не удалось создать директорию %s: %w", path, err)
    }
    return nil
}

// Использование:
if err := EnsureDir("./logs"); err != nil {
    log.Fatal(err)
}

Когда всё-таки использовать os.Stat + os.Mkdir?

Единственный случай — если вам нужно создать только конечную директорию, без родительских, и вы хотите явно проверить, что она существует. Но даже тогда лучше использовать os.MkdirAll — он безопаснее.

Таким образом, запомните простое правило: нужно создать папку, если её нет? Используйте os.MkdirAll!

<<>>

Альтернативный подход: проверка существования с обработкой ошибок

Иногда вам может понадобиться более детальный контроль — например, логировать, когда директория создаётся впервые, или выполнять разные действия в зависимости от того, существовала ли папка ранее. В таких случаях можно использовать комбинацию os.Stat и os.Mkdir, но с правильной синхронизацией.

package main

import (
    "fmt"
    "os"
)

func CreateDirIfNotExists(path string) (bool, error) {
    _, err := os.Stat(path)
    if err == nil {
        // Директория уже существует
        return false, nil
    }
    if !os.IsNotExist(err) {
        // Произошла другая ошибка (например, нет прав на чтение)
        return false, err
    }
    
    // Директория не существует — создаём
    err = os.MkdirAll(path, 0755)
    if err != nil {
        return false, err
    }
    return true, nil // true — директория только что создана
}

func main() {
    created, err := CreateDirIfNotExists("./logs")
    if err != nil {
        fmt.Println("Ошибка:", err)
        return
    }
    if created {
        fmt.Println("Директория создана впервые")
    } else {
        fmt.Println("Директория уже существовала")
    }
}

Когда использовать этот подход?

  • Вам нужно знать, была ли директория создана сейчас или уже существовала
  • Вы пишете тесты и хотите проверить поведение для обоих случаев
  • Требуется особая логика при первом создании (например, инициализация конфигурации)

Однако помните: этот код не является полностью потокобезопасным между проверкой os.Stat и созданием os.MkdirAll. В 99% случаев для однопоточных приложений это не проблема, но если у вас конкурентный доступ, лучше использовать os.MkdirAll с последующей проверкой на ошибку.