Где находится мой файл? Учим Go находить свой путь

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Вступление: "А где я?" — спрашивает программа

Представьте: вы пишете на Go утилиту, которая должна читать конфиг из папки рядом со своим исполняемым файлом. Или, например, вы создаёте лог-файл в той же директории, где лежит программа. Казалось бы, что может быть проще? Но тут начинается самое интересное: Go — это компилируемый язык, и у него есть свои "секреты", когда речь заходит о том, чтобы узнать собственное местоположение.

Вопрос "How to get the directory of the currently running file?" — один из самых популярных на Stack Overflow среди новичков в Go. И это не случайно. В отличие от скриптовых языков, где __file__ (Python) или __dirname (Node.js) решают всё на раз-два, в Go приходится думать: а какой именно файл нас интересует? Исходный код (во время компиляции) или исполняемый бинарник (во время выполнения)?

В этой статье мы разберём оба подхода, научимся избегать типичных граблей и напишем несколько примеров кода на Go, которые можно использовать в реальных проектах. Поехали!

Два мира, два детства: исходник vs бинарник

Прежде чем писать код, давайте поймём главную дилемму. В Go программа сначала компилируется в статический бинарник. Когда вы запускаете go run main.go, Go создаёт временный исполняемый файл и запускает его. Когда вы делаете go build — получаете готовый файл, который можно перенести на другой компьютер.

Поэтому вопрос "где я нахожусь?" имеет два ответа:

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

Большинство ответов на Stack Overflow касаются второго варианта, но мы рассмотрим оба, чтобы вы были во всеоружии.

Способ №1: Узнаём директорию запущенного бинарника

Это самый частый сценарий. Ваша программа уже скомпилирована и запущена, и ей нужно понять, из какой папки её вызвали. Go предоставляет для этого элегантный способ — пакет os и функция os.Executable().

Пример кода: базовый вариант

package main

import (
    "fmt"
    "os"
    "path/filepath"
)

func main() {
    // Получаем полный путь к исполняемому файлу
    exePath, err := os.Executable()
    if err != nil {
        fmt.Println("Ошибка получения пути:", err)
        return
    }
    fmt.Println("Полный путь к бинарнику:", exePath)

    // Извлекаем директорию
    exeDir := filepath.Dir(exePath)
    fmt.Println("Директория исполняемого файла:", exeDir)
}

Запустите этот код через go run main.go — вы увидите путь вроде /tmp/go-build123456/b001/exe/main. Это временная директория. А если скомпилировать (go build -o myapp) и запустить ./myapp — получите текущую папку.

Подводные камни: симлинки и chdir

Функция os.Executable() возвращает путь, по которому программа была запущена. Но есть нюансы:

  • Символические ссылки. Если вы запустили программу через симлинк (/usr/local/bin/myapp -> /opt/myapp_v2/myapp), os.Executable() вернёт путь к самой ссылке, а не к реальному файлу. Чтобы получить "настоящий" путь, используйте filepath.EvalSymlinks().
  • Изменение рабочей директории. Если программа внутри себя вызвала os.Chdir(), это никак не влияет на os.Executable() — она всё равно вернёт путь к бинарнику.

Улучшенная версия с обработкой симлинков

package main

import (
    "fmt"
    "os"
    "path/filepath"
)

func main() {
    exePath, err := os.Executable()
    if err != nil {
        panic(err)
    }

    // Разрешаем симлинки
    realPath, err := filepath.EvalSymlinks(exePath)
    if err != nil {
        panic(err)
    }

    exeDir := filepath.Dir(realPath)
    fmt.Println("Реальная директория:", exeDir)
}

Способ №2: Узнаём директорию исходного файла (для разработчиков)

Иногда нужно узнать, где находится исходный код, который сейчас компилируется. Это полезно для тестов, которые читают файлы из папки testdata, или для утилит, генерирующих код. Здесь на помощь приходит пакет runtime.

Функция runtime.Caller

runtime.Caller(skip) возвращает информацию о том, кто вызвал текущую функцию. Параметр skip — сколько уровней стека пропустить. Если передать 0, получим информацию о самой функции, если 1 — о том, кто её вызвал, и т.д.

package main

import (
    "fmt"
    "path/filepath"
    "runtime"
)

func getSourceDir() string {
    _, filename, _, ok := runtime.Caller(1)
    if !ok {
        return "не удалось определить"
    }
    return filepath.Dir(filename)
}

func main() {
    dir := getSourceDir()
    fmt.Println("Директория исходного файла:", dir)
}

Этот код покажет папку, где лежит main.go. Обратите внимание: runtime.Caller(1) берёт информацию о вызывающей функции (то есть main), поэтому мы получаем путь к main.go, а не к файлу с getSourceDir.

Когда это критично: тесты и конфиги

Представьте, что у вас есть тест, который читает файл testdata/config.yaml. Если тест запускается из корня проекта, всё хорошо. Но если вы запускаете тест из другой папки (cd some/package && go test), относительный путь сломается. Использование runtime.Caller в тестовом файле решает эту проблему.

// helper_test.go
package mypackage

import (
    "path/filepath"
    "runtime"
    "testing"
)

func TestConfig(t *testing.T) {
    _, filename, _, _ := runtime.Caller(0)
    testDir := filepath.Dir(filename)
    configPath := filepath.Join(testDir, "testdata", "config.yaml")
    // ... читаем configPath
}

Сравнение подходов: что выбрать?

Чтобы не запутаться, вот простая таблица-шпаргалка:

  • os.Executable() — для продакшена. Даёт путь к бинарнику. Подходит для загрузки конфигов, логов, плагинов рядом с программой.
  • runtime.Caller() — для разработки и тестов. Даёт путь к исходному фай

Блоги

Рекомендаций по книгам