大家好,这里是每周都陪你进步的网管~
在搭建项目中一般都会有确定项目根目录的绝对路径的需求,一旦有了根目录的绝对路径,就能以这个根目录为基准,设置静态文件、配置文件所在的目录,这样做的好处是无论把项目部署到哪个目录下,执行程序时都不会出现No such file or directory
这样的错误。
今天就总结一下在 Go 程序里边怎么获取项目的根目录绝对路径。在网上搜索怎么获取 Go 项目的根目录,一般有三种,分别依赖 Go 的以下三个底层函数实现:
os.Getwd()
os.Args[0]
runtime.Caller
虽然这三种方式都能获取到Go项目的根目录,但前两种方式在某些情况下拿到的结果并不是我们想要的,只有使用第三种才是在所有执行环境下都能正确拿到Go项目的根目录路径。
我们接下来对这三种方法一一进行解释。首先我们来探讨一下为什么我们要在程序里拿到项目的根目录路径。
为什么需要项目根目录路径
这个问题其实开头已经提过了,假如一个项目有如下这样的目录结构
代码语言:javascript复制.
|-- config
| `-- config.go
| `-- config_dev.yaml
|-- main.go
|-- go.mod
|-- go.sum
假设我们要在config.go
中使用 viper
库把config_dev.yaml
中的配置项加载到内存中,这里看到config.go
和config_dev.yaml
在同一个目录里,一般人都会这么设置配置文件的路径:
vp.AddConfigPath("./")
vp.SetConfigType("yaml")
乍一看起来,没啥毛病,不过假如运行下程序你就会发现完全不行,.
虽然代表当前目录,但在Go语言里边它不代表是当前代码文件所在的目录,而是代表执行程序可执行文件的目录。
比如执行下面一套操作后
代码语言:javascript复制cd /Code/demo
go build -o demo.app
./demo.app
刚才我们在代码里写的".",它代表的是/Code/demo
这个目录
PS,并不是所有语言都是这样,如果是Java程序,"."就是代表当前代码文件的文件目录。
好,搞清楚了我们为什么要费劲获取Go项目的根目录后,我们来说下三种获取他们的方法,以及为什么前两种不够通用。
os.Getwd
os.Getwd()
方法,Go 语言os
包的Getwd
函数能够获取进程的当前工作目录,其实从函数名字中的wd
—working directory (工作目录)的缩写,差不多就能猜出来它的功能。
用os.Getwd
改进我们上面的程序,能保证拿到的项目根目录是正确的吗?我们先用它改一下程序试试。
wd, _ := os.Getwd()
// 输出目录,看看路径对不对
fmt.Println("工作目录: " wd)
// 用工作目录拼接出正确的配置文件目录
vp.AddConfigPath(wd "/config")
vp.SetConfigType("yaml")
打包执行一下程序,会输出:
代码语言:javascript复制cd /Code/demo
go build -o demo.app
./demo.app
=== 以下是输出内容 ===
工作目录: /Code/demo
看起来没什么问题,不过刚才我们是在项目的根目录下编译并执行的程序,假如我们切换到其他目录执行呢?
代码语言:javascript复制cd /Users/xxx
/Code/demo/demo.app
=== 以下是执行后的输出内容 ===
工作目录:/Users/xxx
切换目录后再执行程序,发现程序中输出的工作目录变成了/Users/xxx
,此时后面用它拼接出项目的配置文件目录的代码自然是不对的。
所以os.Getwd()
这个方法获取的是进程在OS
系统所在的目录,仅当在可执行文件所在的目录下启动程序的情况下才能正确拿到 Go 项目的根目录,这种情况还是不够通用的,需要与运维约定项目的启动命令才行。
os.Args[0]
接下来我们看第二种方式,os.Args
这个列表里保存的是程序的启动参数,而参数0按照约定是程序的可执行文件名。
下面我们用它改进一下我们的程序
代码语言:javascript复制filePath, _ := exec.LookPath(os.Args[0])
absFilePath, _ := filepath.Abs(filePath)
rootDir := path.Dir(absFilePath)
// 输出目录,看看路径对不对
fmt.Println("程序根目录: " rootDir)
// 用程序根目录拼接出正确的配置文件目录
vp.AddConfigPath(rootDir "/config")
vp.SetConfigType("yaml")
打包编译程序后,试着在程序所在目录和其他的目录下都执行一下程序,看看程序能不能拿到正确的路径
代码语言:javascript复制cd /Code/demo
go build -o demo.app
./demo.app
=== 以下是执行后的输出内容 ===
程序根目录:/Code/demo
cd /Users/xxx
/Code/demo/demo.app
=== 以下是执行后的输出内容 ===
程序根目录:/Code/demo
通过上面的输出我们能看到,这两种情况都能够正确拿到程序的目录路径,os.Args[0]
在这两种情况下的值分别是./demo.app
和/Code/demo/demo.app
。示例程序里之后的两个方法调用会帮我们找到可执行文件所在目录的绝对路径。
这种方式看起来挺完美,不过有一种情况它是满足不了的,就是,如果我们在研发阶段用go run
启动程序的时候是不行的,此时程序会输出一个临时目录。
/var/folders/3g/f2sh8sgs5ls_z62npf80v69w0000gn/T/go-build1053443992/b001/exe
接下来我们再看第三种方法,它能适配各种情况。
runtime.Caller
想获取到程序的根目录,如果能拿到当前正在执行的代码的文件路径,我们也就能推断出程序的根目录了。怎么能拿到当前正在执行的代码的文件路径呢?
之前我们在介绍日志库Zap
的时候说过,好的日志是需要记录下来当时的现场信息的--比如记录日志的代码所在的文件路径、行号、函数名等。这些信息怎么获取到的呢?当时看了源码后我们发现用的是 runtime.Caller()
func Caller(skip int) (pc uintptr, file string, line int, ok bool)
所以我们在config.go
中,能这样获取当前文件的路径:
// 获取当前文件的路径
_, filename, _, _ := runtime.Caller(0)
runtime.Caller 这里不在详细介绍,感兴趣的请看这两篇旧文
- 如何在 Go 函数中获取调用者的函数名、文件名、行号...
- 从Go log库到Zap,怎么打造出好用又实用的Logger
用它再推断出项目的根目录即可
代码语言:javascript复制root := path.Dir(path.Dir(filename))
用这种方法改造代码后,我们再用上面的几种启动方式,会看到都能正确获取到程序的根目录。
应该用哪种方式?
应该用哪种方式呢?其实没有固定答案,第三种方式更通用,不论是在开发阶段使用 go run
,还是我们在项目里写单元测试,用InteliJ IDEA 这样的IDE帮我们执行单元测试的时候,都是把程序放到一个临时目录执行的,所以第三种方式更通用一些。
如果是在生产环境启动项目,要是能跟运维约定好启动命令,用前两种方式也是没有问题的,甚至我们可以让运维在系统里设置ROOTDIR
之类的环境变量,把根目录放在环境变量里,在程序里用os.Getenv("ROOTDIR")
取也行。
如果让你架构项目,你会用哪种方式呢?评论区里说说吧,喜欢今天的文章欢迎转发和点赞,我们下期再见。
- END -