如何正確處理 Go 項目中關於文件路徑的問題
在使用 Go 開發項目時,估計有不少人遇到過無法正確處理文件路徑的問題,特別是剛從如 PHP、python 這類動態語言轉向 Go 的朋友,已經習慣了通過相對源碼文件找到其他文件。這個問題能否合理解決,不僅關係到程序的可移植性,還直接影響到程序的穩定性和安全性。
本文將嘗試從簡單到複雜,詳細介紹 Go 中獲取路徑的不同方法及應用場景。
歡迎關注我的公衆號:
引言
首先,爲什麼要獲取文件路徑?
一般來說,程序在運行時必須準確地讀取相關的配置和資源以順利啓動。確定這些信息的存儲位置,即獲取文件路徑,成爲了正確訪問這些信息的首要步驟,對於構建穩定可靠的應用程序而言至關重要。
其次,爲什麼從動態語言轉到 Go,容易被這個問題困擾?
與 Go(一種靜態語言)相比,動態語言通過直接解釋腳本文件而執行的。這一機制使得動態語言在路徑獲取方面更爲直觀和易懂。然而,Go 語言將源代碼編譯成獨立的二進制可執行文件,這導致可執行文件與源代碼間缺乏直接的聯繫。
爲了簡化調試過程,Go 通過 go run 命令提供了一種類似動態語言直接執行源代碼的便捷方式,實質上是將構建和運行步驟合二爲一。這個過程中,會生成一個臨時可執行文件,但這個文件不是存在當前工作目錄中,這又爲理解上帶來額外的挑戰。
如果想找到這個文件,可通過 go run -work 保留文件,通過 os.Args[0] 確認文件路徑。
func main() {
fmt.Println(os.Args[0])
}
輸出:
$ go run -work main.go
WORK=/var/folders/0b/v4r1lzyj0n566qgd8dt_km4c0000gn/T/go-build1458488796
/var/folders/0b/v4r1lzyj0n566qgd8dt_km4c0000gn/T/go-build1458488796/b001/exe/main
可執行文件就是位於 $WORK/b001/exe/ 的 main 文件。
若你習慣於動態語言中獲取路徑的做法,在 Go 中通過相對於可執行文件的路徑來定位其他文件,使用 go run 調試的時候,就可能會引起一定的困惑。
下面開始進入正題,詳細 Go 中的文件路徑的不同獲取方式吧。
相對於執行文件獲取路徑
之前提到了那麼多在 Go 中獲取可執行文件路徑時可能導致的問題,我們就先從如何獲取當前執行文件的路徑開始吧。
我將介紹實現這個目標的兩種方式。
命令行參數 os.Args[0]
第一種方式是通過命令行參數 os.Args[0]。os.Args 是一個字符串切片,包含啓動程序時傳遞給它的命令行參數。os.Args[0] 是這個切片的第一個元素,通常表示程序的執行文件路徑。引言部分的演示示例,我就是通過這種方式獲取執行文件的路徑的。
這個方式缺點是,依賴於可執行文件是被調用的方式,它可能是一個相對路徑、一個絕對路徑,或者僅僅是程序名。
於是,爲了保險起見,我們可通過 exec.LookPath 對 os.Args[0] 做一個處理。
fmt.Println(exec.LookPath(os.Args[0]))
這個函數的作用是,輸入參數 filename 中如果包含如 / 字符,直接返回 filename,否則會從 PATH 環境變量中尋找名爲 filename 的可執行文件。這就解決了僅僅通過程序名調用無法獲取文件路徑的問題。
我是在 MacOS 上測試的,這段邏輯是在 lp_unix.go 文件中,window 應該是不同的邏輯,windows 的文件路徑分隔符和類 unix 不同,或者也有其他複雜邏輯。
另外,它獲取到的可能是相對路徑也可能是絕對路徑。如果希望得到絕對路徑,要通過 filepath.Abs 處理下。
exePath, _ := exec.LookPath(os.Args[0])
fmt.Println(filepath.Abs(exePath))
但這種不是最優的方式,明顯是繞的遠了。我提這個方法是爲了順便介紹下 exec.LookPath 和 filepath.Abs 這兩個函數。
使用 os.Executable
獲取當前 Go 程序的執行文件路徑最優的解法是,使用 os.Executable 函數。這個方法會返回可執行文件的絕對路徑。
fmt.Println(os.Executable()) //
輸出:
$ go run main.go
/var/folders/0b/v4r1lzyj0n566qgd8dt_km4c0000gn/T/go-build305466852/b001/exe/main <nil>
這個值在 go 啓動時,運行時自動解析到內存的值,而調用 os.Executable 實際就是直接從這個變量中獲取,沒有額外的處理。
它的性能相對於前面的通過幾個函數組合實現的方式,肯定是吊打前者。
但,這兩種方式都沒有解決一個問題:如果執行文件是符號鏈接,不會返回真正的可執行文件。
符號鏈接
我們可通過使用 filepath.EvalSymlinks 來獲取符號鏈接實際指向的路徑。
realPath, _:= filepath.EvalSymlinks(exePath)
fmt.Println("Real path of executable:", realPath)
兼容 go run 與 go build
講了那麼多關於獲取當前執行文件路徑的方案,但如何解決由 go run 臨時文件產生的問題呢?
我的建議是,換個思路,不要把拘泥在相對於可執行文件定位其他文件路徑這一個方向上。我在網上看到過通過判斷是否是 go run 運行實現的適配方案。
大概意思是,通過判斷執行文件的運行目錄或手動添加環境變量標識當前位於 go run 運行模式。如果處理 go run 模式下,我們再通過相對於源碼文件位置定位其他文件。
嘗試實現下吧。
// isGoRun 檢查當前是否處於 go run 模式
func isGoRun() bool {
// 檢查環境變量(如果你選擇設置一個特定的環境變量來標識)
if _, ok := os.LookupEnv("GO_RUN_MODE"); ok {
return true
}
}
或者是
func isGoRun() bool {
// 或者通過分析 executable 路徑的特徵來判斷
exePath, err := os.Executable()
if err != nil {
fmt.Println("Error getting executable path:", err)
return false
}
// 示例中僅僅檢查路徑是否包含臨時目錄特徵,實際情況可能需要更復雜的邏輯
return exePath[:5] == "/var/" {
}
而在入口函數 main 中,通過 runtime.Caller(0) 獲取源碼文件路徑。
func EntryPath() string {
if IsGoRun() {
_, file, _, ok := runtime.Caller(0)
if ok {
return filepath.Dir(file)
}
} else {
path, _ := os.Executable()
return filepath.Dir(path)
}
return "./"
}
func main() {
configPath := filepath.Join(EntryPath(), "config.json")
fmt.Println("ConfigPath:", configPath)
}
除了那個獲取源碼文件位置的函數 runtime.Caller,這個代碼並不複雜。runtime.Caller 函數用於獲取當前函數的調用棧信息。
它的函數簽名,如下所示:
func Caller(skip int) (pc uintptr, file string, line int, ok bool)
返回信息有調用者(main 函數)的程序計數器(PC)、文件名、代碼行號、一個布爾值,布爾值表示獲取信息是否成功。我們關心的是源碼文件路徑,runtime.Caller 返回的文件名可以用來確定當前執行代碼的位置。
看到這裏,不知道是不是有人發出疑問,竟然通過能定位源碼文件位置,爲什麼還要另外一種方式。這是源碼文件的位置不會因執行文件的移動而變動。舉例來說,如果 main.go 文件在 /Users/poloxue/ 下構建出 main 執行文件。我將其移動到其他目錄,甚至是服務器上,它的路徑依然是 /Users/poloxue/main.go。
現在,即使在 go run 模式下,依然能正確定位其他文件的路徑了。
這種方式看起來挺不錯的,但我不推薦。我的建議是,爲項目定義清晰明確的規則來管理配置和資源文件的路徑。
定義配置和資源的路徑規則
常見的是用絕對路徑規則指定配置和資源文件路徑,如 Linux 或其他類 Unix 系統有一套 XDG 基準規則(XDG Base Directory Specification),有興趣可瞭解下。
或者是另一套更常見被用於日常項目中的方案,通過環境變量或其他方式設置固定的項目根目錄或工作目錄,而其他文件路徑皆相對於這個固定不變目錄的位置。
$RootDir/config.yaml
$RootDir/logs/
$RootDir/resources/
$RootDir/static
實際上,這種方式更常見於平時的項目中。無論可執行文件被放在什麼路徑下,都不會對其他文件的路徑位置產生影響。
如果希望文件路徑支持自定義,可在配置中提供路徑配置項,或通過命令行選項的方式傳遞。
log_path = "/var/log/"
或
$ go run main.go --config-path "./config.toml"
如果覺得每次 go run 都要帶上環境變量麻煩,可提前設置環境變量
export ROOTDIR=`pwd`
我們也可以在 IDE 中設置項目級別的環境變量。
亦或是提供默認值,如果 ROOTDIR 爲空,默認項目根目錄爲 ./,即當前路徑,
# ROOTDIR=./ go run main.go
$ go run main.go
如果是運行在 Docker 中,可通過 WORKDIR 指定工作目錄,問題也變得簡單很多,程序相對當前的當前目錄就是這個特定的工作目錄。
總結
在 Go 項目中正確處理文件路徑是確保程序可移植性、穩定性和安全性的關鍵。與動態語言不同,Go 編譯成二進制可執行文件,使得直接關聯源碼和運行時文件變得複雜。
本文介紹了多種獲取文件路徑的方法,包括 os.Args[0]、exec.LookPath、filepath.Abs和 os.Executable,並討論瞭如何通過判斷是否是 go run 運行來兼容 go run 和go build 的路徑問題。
最後,建議定義清晰的規則管理配置和資源文件路徑,使用環境變量或配置項指定路徑,避免依賴於可執行文件位置,以求提高 Go 項目的健壯性。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/iygt43U3EqcQfS7eEtxfOg