Как получить директорию текущего исполняемого файла в Go?
Надёжное получение директории исполняемого файла
Проблема, которую вы описали, действительно распространена в Go. К счастью, есть несколько проверенных способов, и лучший из них — использовать os.Executable() в сочетании с filepath.EvalSymlinks() и filepath.Dir(). Этот подход решает все перечисленные проблемы: и относительные пути, и символические ссылки, и работу с go run.
Вот полное решение:
package main
import (
"fmt"
"os"
"path/filepath"
)
func getExecutableDir() (string, error) {
// 1. Получаем путь к исполняемому файлу
exePath, err := os.Executable()
if err != nil {
return "", fmt.Errorf("не удалось получить путь к исполняемому файлу: %w", err)
}
// 2. Разрешаем символические ссылки (если программа запущена через symlink)
realPath, err := filepath.EvalSymlinks(exePath)
if err != nil {
return "", fmt.Errorf("не удалось разрешить символическую ссылку: %w", err)
}
// 3. Получаем директорию файла
dir := filepath.Dir(realPath)
return dir, nil
}
func main() {
dir, err := getExecutableDir()
if err != nil {
fmt.Println("Ошибка:", err)
return
}
fmt.Println("Директория исполняемого файла:", dir)
// Пример использования: читаем конфиг из той же папки
configPath := filepath.Join(dir, "config.json")
fmt.Println("Путь к конфигу:", configPath)
}
Почему это работает
os.Executable()— возвращает абсолютный путь к исполняемому файлу, даже если программа была запущена черезgo run(в этом случае он вернёт путь к временному бинарнику, который создаётgo run).filepath.EvalSymlinks()— разрешает все символические ссылки в цепочке, давая реальный физический путь к файлу.filepath.Dir()— извлекает родительскую директорию из полного пути.
Что делать с go run?
При использовании go run программа компилируется во временную директорию (обычно в /tmp/go-build...). os.Executable() вернёт именно этот временный путь. Если вам нужно получить исходную директорию проекта (где лежит main.go), это уже другая задача — для неё лучше использовать runtime.Caller(0) или переменные окружения.
Важные замечания
- Не используйте
os.Args[0]— он ненадёжен при запуске из PATH или через символические ссылки. - Не используйте
os.Getwd()— он возвращает рабочую директорию, а не папку бинарника. - Всегда обрабатывайте ошибки —
os.Executable()может вернуть ошибку в редких случаях (например, если файловая система недоступна).
Этот код будет работать на Linux, macOS и Windows. На Windows символические ссылки поддерживаются начиная с Windows 10, но filepath.EvalSymlinks корректно обрабатывает и обычные ярлыки.
Альтернативный подход: runtime.Caller для исходного кода
Если ваша цель — получить директорию исходного файла .go (например, во время разработки или тестирования), а не скомпилированного бинарника, используйте runtime.Caller(0). Это полезно для логирования, загрузки ресурсов из папки проекта или при работе с go test.
package main
import (
"fmt"
"path/filepath"
"runtime"
"strings"
)
func getSourceDir() string {
_, filename, _, ok := runtime.Caller(0)
if !ok {
return ""
}
// filename содержит полный путь к файлу .go
dir := filepath.Dir(filename)
return dir
}
func main() {
// Получаем папку, где лежит main.go
sourceDir := getSourceDir()
fmt.Println("Исходная директория:", sourceDir)
// Чистим путь от временных папок go run (если нужно)
// runtime.Caller может вернуть путь во временной директории при go run
if strings.Contains(sourceDir, "go-build") {
fmt.Println("Внимание: вы используете go run, путь временный")
}
}
Плюсы: работает даже если бинарник перемещён, показывает исходную структуру проекта.
Минусы: не подходит для скомпилированного бинарника в production — там нет исходников, и runtime.Caller может вернуть некорректный путь или пустую строку. Используйте этот метод только для разработки и отладки.