Project Root Is All You Need
编辑前言
你是否也遇到过这样的问题:你的项目中有一个配置文件夹,底下有若干个配置文件。你需要在代码中根据环境读取其中的某一个,然后继续后面的流程。
- myAmazingProject
- config
- config.test.yaml
- config.prod.yaml
- internal
- config
- config.go
- config_test.go
- main.go
能行,好用
一般来说,我们在 main.go
里面通过参数传入环境名称,然后在 config.go
里通过这个路径来找到文件。因为我们一般从 main.go
来启动程序,这个通常没有什么问题。
// main.go
package main
import (
"flag"
"fmt"
"log"
"myAmazingProject/internal/config"
)
func main() {
// 用一个参数来区分环境 (test/prod)
env := flag.String("env", "test", "Environment for configuration (test or prod)")
flag.Parse()
// 根据环境加载配置
data, err := config.LoadConfig(*env)
if err != nil {
log.Fatalf("Error reading config file: %v", err)
}
fmt.Printf("Configuration file content:\n%s\n", data)
}
// internal/config/config.go
package config
import (
"fmt"
"io/ioutil"
)
func LoadConfig(env string) (string, error) {
configFilePath := fmt.Sprintf("config/config.%s.yaml", env)
data, err := ioutil.ReadFile(configFilePath)
if err != nil {
return "", err
}
return string(data), nil
}
这个方法看起来没什么大问题,直到有一天你想起来要写单测(或迫于 KPI 要写单测,x),然后你又点了 IDE 自带的运行测试按钮。
当你使用相对路径时,程序会以当前工作路径(Working Directory, 有时也称 CWD)来判断路径。因此,如果你点击了 IDE 的运行按钮,你的工作路径会变成 /...(省略一部分绝对路径)/internal/config
,这个目录下的 config/config.%s.yaml
并不存在,因此这个测试跑不通。
要解决这个问题也有很简单的方法,例如把 LoadConfig
的参数变成一个路径,然后通过命令参数传入一个路径。这样在跑测试的时候,可以把路径替换成 ../../../config/config.test.yaml
来解决。但这个问题是,这路径也太多 . 了,它就像烦人的临时脚本一样,永远不可能在第一次就成功。
Project Root Is All You Need
这个问题的根本原因是相对路径的问题,如果我们使用绝对路径,这个问题就迎刃而解。绝对路径的缺点是,我们开发的软件可能会在多种环境上运行,这个软件可能会被放在系统的任何一个地方,因此绝对路径不能写死(绝对路径 != 静态路径)。对于一个项目里的文件,有一个最公共的锚点——项目根目录。只要找到项目根目录,我们就能很有信心地找到项目中的任意文件。因此,我们只需要动态地去找项目根目录就可以了。
P.S. 在以前写 PHP 的时候,就有不少框架有这样的设计,例如 ThinkPHP 就有一个 $rootPath
。
这个根目录怎么找呢?我求助了 GPT-4o。它给出的方案是:
- 在项目根目录放置一个
.project_root
的文件。- 写一个函数,一直往上层文件夹找,直到找到这个
.project_root
文件,或者已经走到根目录。
看看代码:
// MarkerFile 项目根目录标识文件
const MarkerFile = ".project_root"
var (
projectRoot string
once sync.Once
)
// GetProjectRoot 获取项目根目录
func GetProjectRoot() string {
once.Do(func() {
// 从当前目录开始
dir, err := os.Getwd()
if err != nil {
projectRoot = getDefaultRoot()
return
}
for {
// 检查标记文件是否在当前目录
if _, err := os.Lstat(filepath.Join(dir, MarkerFile)); err == nil {
projectRoot = dir
return
}
// 移动到上一级目录
parentDir := filepath.Dir(dir)
if parentDir == dir {
// 到达根目录,还是没找到文件
projectRoot = getDefaultRoot()
return
}
dir = parentDir
}
})
return projectRoot
}
func getDefaultRoot() string {
_, b, _, _ := runtime.Caller(0)
return filepath.Dir(b)
}
这个方案有一些好处和坏处,好处是能非常准确地找到项目根目录,代码里不需要写死任何目录;坏处是执行起来并不快,需要逐级往上找。
在这个代码里,我优化了两个地方:
- 缓存+懒加载:用
sync.Once
来保证这个代码只会执行一次,并且缓存结果,只有第一次使用会有性能影响。 - 默认的目录:这个是在 stackoverflow 上找到的方法,在以
main.go
为入口时可以正确找到根目录。
回到开头的问题,要找到配置文件就变得非常简单了。
configFilePath := fmt.Sprintf("%s/config/config.%s.yaml", GetConfigPath(), env)
参考
- 0
- 0
-
赞助
微信赞赏码 -
分享