徹底解決Golang獲取當前專案絕對路徑問題
導讀
由於Golang是編譯型語言(非指令碼型語言),如果你想在Golang程式中獲取當前執行目錄將是一件非常蛋疼的事情。以前大家最折中的解決方案就是通過啟動傳參或是環境變數將路徑手動傳遞到程式,而今天我在看日誌庫的時候發現了一種新的解決方案。
Go程式兩種不同的執行方式
用Go編寫的程式有兩種執行方式,go run和go build
通常的做法是go run用於本地開發,用一個命令中快速測試程式碼確實非常方便;在部署生產環境時,我們會通過go build構建出二進位制檔案然後上傳到伺服器再去執行。
兩種啟動方式會產生什麼問題?
那麼兩種啟動方式下,獲取到當前執行路徑會產生什麼問題?
話不多說,我們直接上程式碼
我們編寫獲取當前可執行檔案路徑的方法
package main
import (
"fmt"
"log"
"os"
"path/filepath"
)
func main() {
fmt.Println("getCurrentAbPathByExecutable = ", getCurrentAbPathByExecutable())
}
// 獲取當前執行程式所在的絕對路徑
func getCurrentAbPathByExecutable() string {
exePath, err := os.Executable()
if err != nil {
log.Fatal(err)
}
res, _ := filepath.EvalSymlinks(filepath.Dir(exePath))
return res
}
首先通過go run啟動
D:\Projects\demo>go run main.go
getCurrentAbPathByExecutable = C:\Users\XXX\AppData\Local\Temp\go-build216571510\b001\exe
再嘗試go build執行
D:\Projects\demo>go build & demo.exe
getCurrentAbPathByExecutable = D:\Projects\demo
通過對比執行結果,我們發現兩種執行方式,我們獲取到了不同的路徑。而且很明顯,go run獲取到的路徑是錯誤的。
原因: 這是由於go run會將原始碼編譯到系統TEMP或TMP環境變數目錄中並啟動執行;而go build只會在當前目錄編譯出可執行檔案,並不會自動執行。
我們可以簡單理解為,go run main.go等價於go build & ./main
雖然兩種執行方式最終都是一樣的過程:原始碼->編譯->可執行檔案->執行輸出,但他們的執行目錄卻完全不一樣了。
新的方案誕生
這是在我今天檢視服務日誌(zap庫)的時候,突然反應過來一件事情。比如下面是一條簡單的日誌,而服務是通過go run啟動的,但日誌庫卻把我正確的程式路徑D:/Projects/te-server/modules/es/es.go:139給打印出來了
2021-03-26 17:47:06 D:/Projects/te-server/modules/es/es.go:139 update es index {"index": "tags", "data": "[200 OK] {"acknowledged":true}"}
於是我馬上去翻看zap原始碼,發現是通過runtime.Caller()實現的,其實所有Golang日誌庫都會有runtime.Caller()這個呼叫。
我開心的以為找到了最終答案,然後寫程式碼試了下:
package main
import (
"fmt"
"path"
"runtime"
)
func main() {
fmt.Println("getCurrentAbPathByCaller = ", getCurrentAbPathByCaller())
}
// 獲取當前執行檔案絕對路徑(go run)
func getCurrentAbPathByCaller() string {
var abPath string
_, filename, _, ok := runtime.Caller(0)
if ok {
abPath = path.Dir(filename)
}
return abPath
}
首先在windows下面go run 和go build試一下
D:\Projects\demo>go run main.go
getCurrentAbPathByCaller = D:/Projects/demo
D:\Projects\demo>go build & demo.exe
getCurrentAbPathByCaller = D:/Projects/demo
嗯~~ 結果完全正確!
然後我再把構建好的程式扔到linux再執行後,它把我windows的路徑給打印出來了 --!
[root@server app]# chmod +x demo
[root@server app]# ./demo
getCurrentAbPathByCaller = D:/Projects/demo
沒想到白白高興一場,這個時候我就在想,既然go run時可以通過runtime.Caller()獲取到正確的結果,go build時也可以通過 os.Executable()來獲取到正確的路徑;
那如果我能判定當前程式是通過go run還是go build執行的,選擇不同的路徑獲取方法,所有問題不就迎刃而解了嗎。
區分程式是go run還是go build執行
Go沒有提供介面讓我們區分程式是go run還是go build執行,但我們可以換個思路來實現:
根據go run的執行原理,我們得知它會原始碼編譯到系統TEMP或TMP環境變數目錄中並啟動執行;
那我們可以直接在程式中對比os.Executable()獲取到的路徑是否與環境變數TEMP設定的路徑相同, 如果相同,說明是通過go run啟動的,因為當前執行路徑是在TEMP目錄;不同的話自然是go build的啟動方式。
下面是完整程式碼:
package main
import (
"fmt"
"log"
"os"
"path"
"path/filepath"
"runtime"
"strings"
)
func main() {
fmt.Println("getTmpDir(當前系統臨時目錄) = ", getTmpDir())
fmt.Println("getCurrentAbPathByExecutable(僅支援go build) = ", getCurrentAbPathByExecutable())
fmt.Println("getCurrentAbPathByCaller(僅支援go run) = ", getCurrentAbPathByCaller())
fmt.Println("getCurrentAbPath(最終方案-全相容) = ", getCurrentAbPath())
}
// 最終方案-全相容
func getCurrentAbPath() string {
dir := getCurrentAbPathByExecutable()
if strings.Contains(dir,getTmpDir()) {
return getCurrentAbPathByCaller()
}
return dir
}
// 獲取系統臨時目錄,相容go run
func getTmpDir() string {
dir := os.Getenv("TEMP")
if dir == "" {
dir = os.Getenv("TMP")
}
res, _ := filepath.EvalSymlinks(dir)
return res
}
// 獲取當前執行檔案絕對路徑
func getCurrentAbPathByExecutable() string {
exePath, err := os.Executable()
if err != nil {
log.Fatal(err)
}
res, _ := filepath.EvalSymlinks(filepath.Dir(exePath))
return res
}
// 獲取當前執行檔案絕對路徑(go run)
func getCurrentAbPathByCaller() string {
var abPath string
_, filename, _, ok := runtime.Caller(0)
if ok {
abPath = path.Dir(filename)
}
return abPath
}
在windows執行
D:\Projects\demo>go run main.go
getTmpDir(當前系統臨時目錄) = C:\Users\XXX\AppData\Local\Temp
getCurrentAbPathByExecutable(僅支援go build) = C:\Users\XXX\AppData\Local\Temp\go-build456189690\b001\exe
getCurrentAbPathByCaller(僅支援go run) = D:/Projects/demo
getCurrentAbPath(最終方案-全相容) = D:/Projects/demo
D:\Projects\demo>go build & demo.exe
getTmpDir(當前系統臨時目錄) = C:\Users\XXX\AppData\Local\Temp
getCurrentAbPathByExecutable(僅支援go build) = D:\Projects\demo
getCurrentAbPathByCaller(僅支援go run) = D:/Projects/demo
getCurrentAbPath(最終方案-全相容) = D:\Projects\demo
在windows編譯後上傳到Linux執行
[root@server app]# pwd
/data/app
[root@server app]# ./demo
getTmpDir(當前系統臨時目錄) = .
getCurrentAbPathByExecutable(僅支援go build) = /data/app
getCurrentAbPathByCaller(僅支援go run) = D:/Projects/demo
getCurrentAbPath(最終方案-全相容) = /data/app
對比結果,我們可以看到,在不同的系統中,不同的執行方式,我們封裝的getCurrentAbPath方法最終都輸出的正確的結果,perfect!
來源:Golang社群 | 作者:chris(侵刪)