导读

由于Golang是编译型语言(非脚本型语言),如果你想在Golang程序中获取当前执行目录将是一件非常蛋疼的事情。

以前大家最折中的解决方案就是通过启动传参或是环境变量将路径手动传递到程序,而今天我在看日志库的时候发现了一种新的解决方案。

Go 程序两种不同的执行方式

用 Go 编写的程序有两种执行方式,go rungo build

  • 通常的做法是 go run 用于本地开发,用一个命令中快速测试代码确实非常方便;
  • 在部署生产环境时,我们会通过 go build 构建出二进制文件然后上传到服务器再去执行。

两种启动方式会产生什么问题?

那么两种启动方式下,获取到当前执行路径会产生什么问题?

话不多说,我们直接上代码

我们编写获取当前可执行文件路径的方法

  1. // 获取当前执行程序所在的绝对路径
  2. func getCurrentAbPathByExecutable() string {
  3. exePath, err := os.Executable()
  4. if err != nil {
  5. log.Fatal(err)
  6. }
  7. res, _ := filepath.EvalSymlinks(filepath.Dir(exePath))
  8. return res
  9. }

首先通过 go run 启动

  1. PS D:\Projects\Github\NoobWu\go-samples\qpaint\paintweb> go run .\main.go

image.png

再尝试 go build 执行

  1. PS D:\Projects\Github\NoobWu\go-samples\qpaint\paintweb> go build | .\paintweb.exe

image.png

  1. source /etc/profile
  2. root@MiHome:/mnt/d/Projects/Github/NoobWu/go-samples/qpaint/paintweb# go build & paintweb

image.png

通过对比执行结果,我们发现两种执行方式,我们获取到了不同的路径。而且很明显,go run获取到的路径是错误的。

原因: 这是由于 go run 会将源代码编译到系统 TEMPTMP 环境变量目录中并启动执行;而 go build 只会在当前目录编译出可执行文件,并不会自动执行。

我们可以简单理解为,go run main.go 等价于 go build & ./main
虽然两种执行方式最终都是一样的过程:源码->编译->可执行文件->执行输出,但他们的执行目录却完全不一样了。

新的方案诞生

这是在我今天查看服务日志(zap 库)的时候,突然反应过来一件事情。

比如下面是一条简单的日志,而服务是通过go run启动的,但日志库却把我正确的程序路径D:/Projects/te-server/modules/es/es.go:139 给打印出来了

  1. 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()这个调用。

我开心的以为找到了最终答案,然后写代码试了下:

  1. package main
  2. import (
  3. "fmt"
  4. "path"
  5. "runtime"
  6. )
  7. func main() {
  8. fmt.Println("getCurrentAbPathByCaller = ", getCurrentAbPathByCaller())
  9. }
  10. // 获取当前执行文件绝对路径(go run
  11. func getCurrentAbPathByCaller() string {
  12. var abPath string
  13. _, filename, _, ok := runtime.Caller(0)
  14. if ok {
  15. abPath = path.Dir(filename)
  16. }
  17. return abPath
  18. }

首先在windows下面go rungo build试一下

  1. D:\Projects\demo>go run main.go getCurrentAbPathByCaller = D:/Projects/demo D:\Projects\demo>go build & demo.exe getCurrentAbPathByCaller = D:/Projects/demo

嗯~~ 结果完全正确!

然后我再把构建好的程序扔到linux再运行后,它把我windows的路径给打印出来了 —!

  1. [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 的执行原理,我们得知它会源代码编译到系统 TEMPTMP 环境变量目录中并启动执行;

那我们可以直接在程序中对比 os.Executable() 获取到的路径是否与环境变量 TEMP 设置的路径相同,
如果相同,说明是通过 go run 启动的,因为当前执行路径是在 TEMP 目录;不同的话自然是 go build
的启动方式。

下面是完整代码:

  1. package main
  2. import (
  3. "fmt"
  4. "log"
  5. "os"
  6. "path"
  7. "path/filepath"
  8. "runtime"
  9. "strings"
  10. )
  11. func main() {
  12. fmt.Println("getTmpDir(当前系统临时目录) = ", os.TempDir())
  13. fmt.Println("getCurrentAbPathByExecutable(仅支持go build) = ", getCurrentAbPathByExecutable())
  14. fmt.Println("getCurrentAbPathByCaller(仅支持go run) = ", getCurrentAbPathByCaller())
  15. fmt.Println("getCurrentAbPath(最终方案-全兼容) = ", getCurrentAbPath())
  16. }
  17. // 最终方案-全兼容
  18. func getCurrentAbPath() string {
  19. dir := getCurrentAbPathByExecutable()
  20. tmpDir, _ := filepath.EvalSymlinks(os.TempDir())
  21. if strings.Contains(dir, tmpDir) {
  22. return getCurrentAbPathByCaller()
  23. }
  24. return dir
  25. }
  26. // 获取当前执行文件绝对路径
  27. func getCurrentAbPathByExecutable() string {
  28. exePath, err := os.Executable()
  29. if err != nil {
  30. log.Fatal(err)
  31. }
  32. res, _ := filepath.EvalSymlinks(filepath.Dir(exePath))
  33. return res
  34. }
  35. // 获取当前执行文件绝对路径(go run
  36. func getCurrentAbPathByCaller() string {
  37. var abPath string
  38. _, filename, _, ok := runtime.Caller(0)
  39. if ok {
  40. abPath = path.Dir(filename)
  41. }
  42. return abPath
  43. }

在 windows 执行

  1. 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 执行

  1. [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! 🎉🎉🎉~~

示例

  • linux

    1. root@MiHome:/mnt/d/Projects/Github/NoobWu/go-samples/qpaint/paintweb# go run main.go

    image.png

  • windows

    1. PS D:\Projects\Github\NoobWu\go-samples\qpaint\paintweb> go run .\main.go

    image.png

    原文链接

    https://tehub.com/a/44BceBfRK0