Avatar ·

Как получить директорию текущего исполняемого файла в Go?

Всем привет! Я начинающий Go-разработчик и столкнулся с проблемой: мне нужно определить директорию, в которой находится текущий исполняемый файл моей программы. Я пишу небольшое CLI-приложение, которое читает конфигурационный файл, лежащий в той же папке, что и бинарник. Но когда я запускаю программу из другой директории (например, через `go run` из корня проекта или из системного PATH), относительные пути ломаются. Вот что я пробовал: ```go package main import ( "fmt" "os" "path/filepath" ) func main() { // Попытка 1: просто os.Getwd() — даёт рабочую директорию, а не папку файла wd, _ := os.Getwd() fmt.Println("Working dir:", wd) // Попытка 2: os.Args[0] — показывает путь, которым вызвали программу fmt.Println("Args[0]:", os.Args[0]) // Попытка 3: filepath.Abs(os.Args[0]) — всё ещё проблема с символическими ссылками abs, _ := filepath.Abs(os.Args[0]) fmt.Println("Abs:", abs) } ``` Проблема в том, что `os.Args[0]` может быть просто именем файла (если программа в PATH), или относительным путём, или даже символической ссылкой. Мне нужно надёжное решение, которое работает и при `go run`, и для скомпилированного бинарника, и если программа вызвана через символическую ссылку. Как правильно получить путь к папке, где физически лежит исполняемый файл? Буду благодарен за пример кода! <<>>

Надёжное получение директории исполняемого файла

Проблема, которую вы описали, действительно распространена в 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 может вернуть некорректный путь или пустую строку. Используйте этот метод только для разработки и отладки.