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
 - 
              
              
  
赞助
                微信赞赏码
               - 
              
              
  
分享