如何正确处理 Go 项目中关于文件路径的问题

嗨,大家好!我是波罗学。本文是系列文章 Go 技巧第十九篇,系列文章查看:Go 语言技巧。

在使用 Go 开发项目时,估计有不少人遇到过无法正确处理文件路径的问题,特别是刚从如 PHP、python 这类动态语言转向 Go 的朋友,已经习惯了通过相对源码文件找到其他文件。这个问题能否合理解决,不仅关系到程序的可移植性,还直接影响到程序的稳定性和安全性。

本文将尝试从简单到复杂,详细介绍 Go 中获取路径的不同方法及应用场景。

引言

首先,为什么要获取文件路径?

一般来说,程序在运行时必须准确地读取相关的配置和资源以顺利启动。确定这些信息的存储位置,即获取文件路径,成为了正确访问这些信息的首要步骤,对于构建稳定可靠的应用程序而言至关重要。

其次,为什么从动态语言转到 Go,容易被这个问题困扰?
请添加图片描述

与 Go(一种静态语言)相比,动态语言通过直接解释脚本文件而执行的。这一机制使得动态语言在路径获取方面更为直观和易懂。然而,Go语言将源代码编译成独立的二进制可执行文件,这导致可执行文件与源代码间缺乏直接的联系。

为了简化调试过程,Go 通过 go run 命令提供了一种类似动态语言直接执行源代码的便捷方式,实质上是将构建和运行步骤合二为一。这个过程中,会生成一个临时可执行文件,但这个文件不是存在当前工作目录中,这又为理解上带来额外的挑战。

如果想找到这个文件,可通过 go run -work 保留文件,通过 os.Args[0] 确认文件路径。

func main() {fmt.Println(os.Args[0])
}

输出:

$ go run -work main.go
WORK=/var/folders/0b/v4r1lzyj0n566qgd8dt_km4c0000gn/T/go-build1458488796
/var/folders/0b/v4r1lzyj0n566qgd8dt_km4c0000gn/T/go-build1458488796/b001/exe/main

可执行文件就是位于 $WORK/b001/exe/main 文件。

若你习惯于动态语言中获取路径的做法,在 Go 中通过相对于可执行文件的路径来定位其他文件,使用 go run 调试的时候,就可能会引起一定的困惑。

下面开始进入正题,详细 Go 中的文件路径的不同获取方式吧。

相对于执行文件获取路径

之前提到了那么多在 Go 中获取可执行文件路径时可能导致的问题,我们就先从如何获取当前执行文件的路径开始吧。

我将介绍实现这个目标的两种方式。

请添加图片描述

命令行参数 os.Args[0]

第一种方式是通过命令行参数 os.Args[0]os.Args 是一个字符串切片,包含启动程序时传递给它的命令行参数。os.Args[0] 是这个切片的第一个元素,通常表示程序的执行文件路径。引言部分的演示示例,我就是通过这种方式获取执行文件的路径的。

这个方式缺点是,依赖于可执行文件是被调用的方式,它可能是一个相对路径、一个绝对路径,或者仅仅是程序名。

于是,为了保险起见,我们可通过 exec.LookPathos.Args[0] 做一个处理。

fmt.Println(exec.LookPath(os.Args[0]))

这个函数的作用是,输入参数 filename 中如果包含如 / 字符,直接返回 filename,否则会从 PATH 环境变量中寻找名为 filename 的可执行文件。这就解决了仅仅通过程序名调用无法获取文件路径的问题。

我是在 MacOS 上测试的,这段逻辑是在 lp_unix.go 文件中,window 应该是不同的逻辑,windows 的文件路径分隔符和类 unix 不同,或者也有其他复杂逻辑。

另外,它获取到的可能是相对路径也可能是绝对路径。如果希望得到绝对路径,要通过 filepath.Abs 处理下。

exePath, _ := exec.LookPath(os.Args[0])
fmt.Println(filepath.Abs(exePath))

但这种不是最优的方式,明显是绕的远了。 我提这个方法是为了顺便介绍下 exec.LookPathfilepath.Abs 这两个函数。

使用 os.Executable

获取当前 Go 程序的执行文件路径最优的解法是,使用 os.Executable 函数。这个方法会返回可执行文件的绝对路径。

fmt.Println(os.Executable()) // 

输出:

$ go run main.go
/var/folders/0b/v4r1lzyj0n566qgd8dt_km4c0000gn/T/go-build305466852/b001/exe/main <nil>

这个值在 go 启动时,运行时自动解析到内存的值,而调用 os.Executable 实际就是直接从这个变量中获取,没有额外的处理。

它的性能相对于前面的通过几个函数组合实现的方式,肯定是吊打前者。

但,这两种方式都没有解决一个问题:如果执行文件是符号链接,不会返回真正的可执行文件。

符号链接

我们可通过使用 filepath.EvalSymlinks 来获取符号链接实际指向的路径。

realPath,  _:= filepath.EvalSymlinks(exePath)
fmt.Println("Real path of executable:", realPath)

兼容 go rungo build

讲了那么多关于获取当前执行文件路径的方案,但如何解决由 go run 临时文件产生的问题呢?

我的建议是,换个思路,不要把拘泥在相对于可执行文件定位其他文件路径这一个方向上。我在网上看到过通过判断是否是 go run 运行实现的适配方案。

大概意思是,通过判断执行文件的运行目录或手动添加环境变量标识当前位于 go run 运行模式。如果处理 go run 模式下,我们再通过相对于源码文件位置定位其他文件。

请添加图片描述

尝试实现下吧。

// isGoRun 检查当前是否处于 go run 模式
func isGoRun() bool {// 检查环境变量(如果你选择设置一个特定的环境变量来标识)if _, ok := os.LookupEnv("GO_RUN_MODE"); ok {return true}
}

或者是

func isGoRun() bool {// 或者通过分析 executable 路径的特征来判断exePath, err := os.Executable()if err != nil {fmt.Println("Error getting executable path:", err)return false}// 示例中仅仅检查路径是否包含临时目录特征,实际情况可能需要更复杂的逻辑return exePath[:5] == "/var/" {
}

而在入口函数 main 中,通过 runtime.Caller(0) 获取源码文件路径。

func EntryPath() string {if IsGoRun() {_, file, _, ok := runtime.Caller(0)if ok {return filepath.Dir(file)}} else {path, _ := os.Executable()return filepath.Dir(path)}return "./"
}func main() {configPath := filepath.Join(EntryPath(), "config.json")fmt.Println("ConfigPath:", configPath)
}

除了那个获取源码文件位置的函数 runtime.Caller,这个代码并不复杂。runtime.Caller 函数用于获取当前函数的调用栈信息。

它的函数签名,如下所示:

func Caller(skip int) (pc uintptr, file string, line int, ok bool)

返回信息有调用者(main 函数)的程序计数器(PC)、文件名、代码行号、一个布尔值,布尔值表示获取信息是否成功。我们关心的是源码文件路径,runtime.Caller 返回的文件名可以用来确定当前执行代码的位置。

看到这里,不知道是不是有人发出疑问,竟然通过能定位源码文件位置,为什么还要另外一种方式。这是源码文件的位置不会因执行文件的移动而变动。举例来说,如果 main.go 文件在 ~/Users/poloxue/ 下构建出 main 执行文件。我将其移动到其他目录,甚至是服务器上,它的路径依然是 /Users/poloxue/main.go

现在,即使在 go run 模式下,依然能正确定位其他文件的路径了。

这种方式看起来挺不错的,但我不推荐。我的建议是,为项目定义清晰明确的规则来管理配置和资源文件的路径。

定义明确的路径规则

常见的是用绝对路径规则指定配置和资源文件路径,如 Linux 或其他类 Unix 系统有一套 XDG 基准规则(XDG Base Directory Specification),有兴趣可了解下。

或者是另一套更常见日常项目中的方案,通过环境变量或其他方式设置固定的项目根目录或工作目录,而其他文件路径皆相对于这个固定不变目录的位置。

$RootDir/config.yaml
$RootDir/logs/
$RootDir/resources/
$RootDir/static

实际上,这种方式更常见于平时的项目中。无论可执行文件被放在什么路径下,都不会对其他文件的路径位置产生影响。

如果希望文件路径支持自定义,可在配置中提供路径配置项,或通过命令行选项的方式传递。

log_path = "/var/log/"

$ go run main.go --config-path "./config.toml"

如果觉得每次 go run 都要带上环境变量麻烦,可提前设置环境变量

export ROOTDIR=`pwd`

我们也可以在 IDE 中设置项目级别的环境变量。

亦或是提供默认值,如果 ROOTDIR 为空,默认项目根目录为 ./,即当前路径,

# ROOTDIR=./ go run main.go
$ go run main.go

如果是运行在 Docker 中,可通过 WORKDIR 指定工作目录,问题也变得简单很多。

总结

在 Go 项目中正确处理文件路径是确保程序可移植性、稳定性和安全性的关键。与动态语言不同,Go编译成二进制可执行文件,使得直接关联源码和运行时文件变得复杂。

本文介绍了多种获取文件路径的方法,包括 os.Args[0]exec.LookPathfilepath.Absos.Executable,并讨论了如何通过判断是否是 go run 运行来兼容 go rungo build 的路径问题。

最后,建议定义清晰的规则管理配置和资源文件路径,使用环境变量或配置项指定路径,避免依赖于可执行文件位置,以求提高 Go 项目的健壮性。

感谢阅读,欢迎关注我的更多文章。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/497106.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

复盘昨天的内容

vue调节css 后端做业务处理 1.分类管理 GetMapping("/queryCtc")public ApiResult queryCtc(){return ctcService.queryCtc();}/*** 修改类目信息* return*/PutMapping("/updateCtc")public ApiResult updateCtc(RequestBody ShopCtc shopCtc){return c…

Python Web开发记录 Day4:JavaScript

名人说&#xff1a;莫道桑榆晚&#xff0c;为霞尚满天。——刘禹锡&#xff08;刘梦得&#xff0c;诗豪&#xff09; 创作者&#xff1a;Code_流苏(CSDN)&#xff08;一个喜欢古诗词和编程的Coder&#x1f60a;&#xff09; 目录 四、JavaScript1、JavaScript-基础①JavaScript…

【AI绘画·24年1月最新】Stable Diffusion整合包安装!解压即用--秋葉aaaki 大佬的作品,试用

前言 Stable Diffusion 之前费老大的劲部署安装&#xff0c;解决报错。搞完之后&#xff0c;突然发现有个现成集成包可以用&#xff0c;真是效率高到不行&#xff0c;今天搞下来试试 我电脑配置&#xff1a; CPU: 12th Gen Intel Core™ i7-12700F 2.10 GHz 内存32G&#xff0…

【MySQL】内置函数 -- 详解

一、日期函数 日期&#xff1a;年月日时间&#xff1a;时分秒 1、获得年月日 2、获得时分秒 3、获得时间戳 4、在日期的基础上加日期 5、在日期的基础上减去时间 6、计算两个日期之间相差多少天 7、获得当前时间 ⚪练习 &#xff08;1&#xff09;记录生日 &#xff08;2&…

蓝桥杯备战刷题three(自用)

1.合法日期 #include <iostream> #include <map> #include <string> using namespace std; int main() {map<string,int>mp;int days[13]{0,31,28,31,30,31,30,31,31,30,31,30,31};for(int i1;i<12;i){for(int j1;j<days[i];j){string sto_strin…

硬件工程师入门基础知识(三)钽电容应用(二)

钽电容应用&#xff08;二&#xff09; 1.钽电容使用容量选择2.非固体电解质钽电容器使用时应注意的问题2.1 容量和损耗2.2 直流漏电流2.3 使用电压2.4 反向电压2.5 纹波电流2.6 失效率的影响因素2.7 补充说明&#xff1a; 1.钽电容使用容量选择 许多情况下&#xff0c;高能混…

Vue 3, TypeScript 和 Element UI Plus:前端开发的高级技巧与最佳实践

Vue 3、TypeScript 和 Element UI Plus 结合使用时&#xff0c;可以提供一个强大且灵活的前端开发环境。以下是一些高级用法和技巧&#xff0c;帮助你更有效地使用这些技术&#xff1a; 1. Vue 3 高级特性 Composition API 使用 setup 函数: Vue 3 引入了 Composition API&am…

算法沉淀——动态规划之子序列问题(下)(leetcode真题剖析)

算法沉淀——动态规划之子序列问题 01.最长定差子序列02.最长的斐波那契子序列的长度03.最长等差数列04.等差数列划分 II - 子序列 01.最长定差子序列 题目链接&#xff1a;https://leetcode.cn/problems/longest-arithmetic-subsequence-of-given-difference/ 给你一个整数数…

Jenkins的存储主目录更改(5)

Jenkins的存储主目录更改 默认路径&#xff1a; /var/lib/jenkins Linux环境更改Jenkins的主目录 Linux环境中&#xff0c;Jenkins主目录默认在/root/.jenkins 1、使用你Web容器的管理工具设置JENKINS_HOME环境参数. 打开tomcat的bin目录&#xff0c;编辑catalina.sh文件。 …

VHDL函数和过程、VHDL预定义包

VHDL函数和过程 对于在设计中多次使用的块&#xff0c;请使用VHDL函数和过程。这个内容类似于组合过程内容在中声明函数和过程&#xff1a; •实体的声明性部分 •架构 •一个包 函数或过程由声明部分和主体组成。声明性部分规定&#xff1a; •输入参数&#xff0c;可以…

一位有着近 10 年 iOS 开发经验的全职爸爸如何高效管理时间?

名字: Mindr 开发者 / 团队: Florian Vates 平台: iOS, iPadOS, Android 正在开发中 请简要介绍下这款产品 有没有发现自己总是不断推迟待办事项的通知&#xff1f; Mindr 以一种全新的方法来解决这个问题&#xff0c;它直观的界面设计将待办事项的进度直接显示在桌面上&#x…

SD-WAN技术:优化国内外服务器访问的关键

在全球化的商业环境中&#xff0c;企业经常需要在国内访问国外的服务器。然而&#xff0c;由于地理位置和网络架构的限制&#xff0c;这种跨国访问往往会遇到速度慢、延迟高等问题。SD-WAN&#xff08;软件定义广域网&#xff09;技术的兴起&#xff0c;为企业提供了一种新的解…