詳解Go語言中獲取文件路徑的不同方法與應(yīng)用場景
在使用 Go 開發(fā)項目時,估計有不少人遇到過無法正確處理文件路徑的問題,特別是剛從如 PHP、python 這類動態(tài)語言轉(zhuǎn)向 Go 的朋友,已經(jīng)習(xí)慣了通過相對源碼文件找到其他文件。這個問題能否合理解決,不僅關(guān)系到程序的可移植性,還直接影響到程序的穩(wěn)定性和安全性。
本文將嘗試從簡單到復(fù)雜,詳細介紹 Go 中獲取路徑的不同方法及應(yīng)用場景。
引言
首先,為什么要獲取文件路徑?
一般來說,程序在運行時必須準確地讀取相關(guān)的配置和資源以順利啟動。確定這些信息的存儲位置,即獲取文件路徑,成為了正確訪問這些信息的首要步驟,對于構(gòu)建穩(wěn)定可靠的應(yīng)用程序而言至關(guān)重要。
其次,為什么從動態(tài)語言轉(zhuǎn)到 Go,容易被這個問題困擾?
與 Go(一種靜態(tài)語言)相比,動態(tài)語言通過直接解釋腳本文件而執(zhí)行的。這一機制使得動態(tài)語言在路徑獲取方面更為直觀和易懂。然而,Go語言將源代碼編譯成獨立的二進制可執(zhí)行文件,這導(dǎo)致可執(zhí)行文件與源代碼間缺乏直接的聯(lián)系。
為了簡化調(diào)試過程,Go 通過 go run
命令提供了一種類似動態(tài)語言直接執(zhí)行源代碼的便捷方式,實質(zhì)上是將構(gòu)建和運行步驟合二為一。這個過程中,會生成一個臨時可執(zhí)行文件,但這個文件不是存在當(dāng)前工作目錄中,這又為理解上帶來額外的挑戰(zhàn)。
如果想找到這個文件,可通過 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
可執(zhí)行文件就是位于 $WORK/b001/exe/
的 main
文件。
若你習(xí)慣于動態(tài)語言中獲取路徑的做法,在 Go 中通過相對于可執(zhí)行文件的路徑來定位其他文件,使用 go run
調(diào)試的時候,就可能會引起一定的困惑。
下面開始進入正題,詳細 Go 中的文件路徑的不同獲取方式吧。
相對于執(zhí)行文件獲取路徑
之前提到了那么多在 Go 中獲取可執(zhí)行文件路徑時可能導(dǎo)致的問題,我們就先從如何獲取當(dāng)前執(zhí)行文件的路徑開始吧。
我將介紹實現(xiàn)這個目標的兩種方式。
命令行參數(shù) os.Args[0]
第一種方式是通過命令行參數(shù) os.Args[0]
。os.Args
是一個字符串切片,包含啟動程序時傳遞給它的命令行參數(shù)。os.Args[0]
是這個切片的第一個元素,通常表示程序的執(zhí)行文件路徑。引言部分的演示示例,我就是通過這種方式獲取執(zhí)行文件的路徑的。
這個方式缺點是,依賴于可執(zhí)行文件是被調(diào)用的方式,它可能是一個相對路徑、一個絕對路徑,或者僅僅是程序名。
于是,為了保險起見,我們可通過 exec.LookPath
對 os.Args[0]
做一個處理。
fmt.Println(exec.LookPath(os.Args[0]))
這個函數(shù)的作用是,輸入?yún)?shù) filename
中如果包含如 /
字符,直接返回 filename
,否則會從 PATH
環(huán)境變量中尋找名為 filename
的可執(zhí)行文件。這就解決了僅僅通過程序名調(diào)用無法獲取文件路徑的問題。
我是在 MacOS 上測試的,這段邏輯是在 lp_unix.go 文件中,window 應(yīng)該是不同的邏輯,windows 的文件路徑分隔符和類 unix 不同,或者也有其他復(fù)雜邏輯。
另外,它獲取到的可能是相對路徑也可能是絕對路徑。如果希望得到絕對路徑,要通過 filepath.Abs
處理下。
exePath, _ := exec.LookPath(os.Args[0]) fmt.Println(filepath.Abs(exePath))
但這種不是最優(yōu)的方式,明顯是繞的遠了。我提這個方法是為了順便介紹下 exec.LookPath
和 filepath.Abs
這兩個函數(shù)。
使用 os.Executable
獲取當(dāng)前 Go 程序的執(zhí)行文件路徑最優(yōu)的解法是,使用 os.Executable
函數(shù)。這個方法會返回可執(zhí)行文件的絕對路徑。
fmt.Println(os.Executable()) //
輸出:
$ 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
這個值在 go 啟動時,運行時自動解析到內(nèi)存的值,而調(diào)用 os.Executable
實際就是直接從這個變量中獲取,沒有額外的處理。
它的性能相對于前面的通過幾個函數(shù)組合實現(xiàn)的方式,肯定是吊打前者。
但,這兩種方式都沒有解決一個問題:如果執(zhí)行文件是符號鏈接,不會返回真正的可執(zhí)行文件。
符號鏈接
我們可通過使用 filepath.EvalSymlinks
來獲取符號鏈接實際指向的路徑。
realPath, _:= filepath.EvalSymlinks(exePath) fmt.Println("Real path of executable:", realPath)
兼容 go run 與 go build
講了那么多關(guān)于獲取當(dāng)前執(zhí)行文件路徑的方案,但如何解決由 go run
臨時文件產(chǎn)生的問題呢?
我的建議是,換個思路,不要把拘泥在相對于可執(zhí)行文件定位其他文件路徑這一個方向上。我在網(wǎng)上看到過通過判斷是否是 go run
運行實現(xiàn)的適配方案。
大概意思是,通過判斷執(zhí)行文件的運行目錄或手動添加環(huán)境變量標識當(dāng)前位于 go run
運行模式。如果處理 go run
模式下,我們再通過相對于源碼文件位置定位其他文件。
嘗試實現(xiàn)下吧。
// isGoRun 檢查當(dāng)前是否處于 go run 模式 func isGoRun() bool { // 檢查環(huán)境變量(如果你選擇設(shè)置一個特定的環(huán)境變量來標識) 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 } // 示例中僅僅檢查路徑是否包含臨時目錄特征,實際情況可能需要更復(fù)雜的邏輯 return exePath[:5] == "/var/" { }
而在入口函數(shù) 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) }
除了那個獲取源碼文件位置的函數(shù) runtime.Caller
,這個代碼并不復(fù)雜。runtime.Caller
函數(shù)用于獲取當(dāng)前函數(shù)的調(diào)用棧信息。
它的函數(shù)簽名,如下所示:
func Caller(skip int) (pc uintptr, file string, line int, ok bool)
返回信息有調(diào)用者(main 函數(shù))的程序計數(shù)器(PC)、文件名、代碼行號、一個布爾值,布爾值表示獲取信息是否成功。我們關(guān)心的是源碼文件路徑,runtime.Caller
返回的文件名可以用來確定當(dāng)前執(zhí)行代碼的位置。
看到這里,不知道是不是有人發(fā)出疑問,竟然通過能定位源碼文件位置,為什么還要另外一種方式。這是源碼文件的位置不會因執(zhí)行文件的移動而變動。舉例來說,如果 main.go
文件在 /Users/poloxue/
下構(gòu)建出 main
執(zhí)行文件。我將其移動到其他目錄,甚至是服務(wù)器上,它的路徑依然是 /Users/poloxue/main.go
。
現(xiàn)在,即使在 go run
模式下,依然能正確定位其他文件的路徑了。
這種方式看起來挺不錯的,但我不推薦。我的建議是,為項目定義清晰明確的規(guī)則來管理配置和資源文件的路徑。
定義配置和資源的路徑規(guī)則
常見的是用絕對路徑規(guī)則指定配置和資源文件路徑,如 Linux 或其他類 Unix 系統(tǒng)有一套 XDG 基準規(guī)則(XDG Base Directory Specification),有興趣可了解下。
或者是另一套更常見被用于日常項目中的方案,通過環(huán)境變量或其他方式設(shè)置固定的項目根目錄或工作目錄,而其他文件路徑皆相對于這個固定不變目錄的位置。
$RootDir/config.yaml
$RootDir/logs/
$RootDir/resources/
$RootDir/static
實際上,這種方式更常見于平時的項目中。無論可執(zhí)行文件被放在什么路徑下,都不會對其他文件的路徑位置產(chǎn)生影響。
如果希望文件路徑支持自定義,可在配置中提供路徑配置項,或通過命令行選項的方式傳遞。
log_path = "/var/log/"
或
$ go run main.go --config-path "./config.toml"
如果覺得每次 go run
都要帶上環(huán)境變量麻煩,可提前設(shè)置環(huán)境變量
export ROOTDIR=`pwd`
我們也可以在 IDE 中設(shè)置項目級別的環(huán)境變量。
亦或是提供默認值,如果 ROOTDIR 為空,默認項目根目錄為 ./
,即當(dāng)前路徑,
# ROOTDIR=./ go run main.go $ go run main.go
如果是運行在 Docker 中,可通過 WORKDIR
指定工作目錄,問題也變得簡單很多,程序相對當(dāng)前的當(dāng)前目錄就是這個特定的工作目錄。
總結(jié)
在 Go 項目中正確處理文件路徑是確保程序可移植性、穩(wěn)定性和安全性的關(guān)鍵。與動態(tài)語言不同,Go編譯成二進制可執(zhí)行文件,使得直接關(guān)聯(lián)源碼和運行時文件變得復(fù)雜。
本文介紹了多種獲取文件路徑的方法,包括 os.Args[0]
、exec.LookPath
、filepath.Abs
和 os.Executable
,并討論了如何通過判斷是否是 go run
運行來兼容 go run
和go build
的路徑問題。
最后,建議定義清晰的規(guī)則管理配置和資源文件路徑,使用環(huán)境變量或配置項指定路徑,避免依賴于可執(zhí)行文件位置,以求提高 Go 項目的健壯性。
到此這篇關(guān)于詳解Go語言中獲取文件路徑的不同方法與應(yīng)用場景的文章就介紹到這了,更多相關(guān)Go獲取文件路徑內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
go?zero微服務(wù)實戰(zhàn)處理每秒上萬次的下單請求
這篇文章主要為大家介紹了go?zero微服務(wù)實戰(zhàn)處理每秒上萬次的下單請求示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-07-07使用gRPC實現(xiàn)獲取數(shù)據(jù)庫版本
這篇文章主要為大家詳細介紹了如何使用gRPC實現(xiàn)獲取數(shù)據(jù)庫版本,文中的示例代碼講解詳細,具有一定的借鑒價值,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-12-12