如何相对于我的GOPATH打开文件?

89
我正在使用 io/ioutil 读取一个小文本文件:
fileBytes, err := ioutil.ReadFile("/absolute/path/to/file.txt")

这很好用,但并不是非常便携。在我的情况下,我想要打开的文件都在我的GOPATH中,例如:

/Users/matt/Dev/go/src/github.com/mholt/mypackage/data/file.txt

由于 data 文件夹与源代码处于同一级目录,所以我希望只需指定相对路径:

data/file.txt

但是我得到了这个错误:

panic:打开数据/文件.txt:没有那个文件或目录

如何使用相对路径打开文件,特别是如果它们与我的Go代码并存?

请注意,我的问题特别涉及相对于GOPATH打开文件。在Go中使用任何相对路径打开文件都很容易,只需给出相对路径即可代替绝对路径;文件相对于编译二进制文件的工作目录打开。在我的情况下,我想相对于编译二进制文件的位置打开文件。事后看来,这是一个不好的设计决策。)


10
一旦编译程序,GOPATH 就没有太大的意义,甚至在分发程序时也不重要。 - Denys Séguret
2
有点像...但我希望数据文件与源代码分开。数据文件对程序的功能至关重要。因此,当有人下载我的源代码(与数据文件并排)并编译和运行它时,数据文件将使用相对路径加载,因为它们存在于源代码附近或程序执行的位置附近。 - Matt
1
编译后的二进制文件不应依赖于源文件的位置,但如果有一种方法可以创建一个可执行捆绑包,其中包含外部资源的副本,以供包可能依赖,那将是很好的。 - user2437417
@CrazyTrain 这就是 fileembed-go 的作用吗?(https://bitbucket.org/rj/fileembed-go/) - Rick-777
1
这里有一个关于捆绑资源的相关问题,可能足够解决问题,尽管在我的情况下这不是我首选的方法:https://dev59.com/YmYr5IYBdhLWcg3wKHIp -- 或者这个:https://dev59.com/q2DVa4cB1Zd3GeqPhNrG - Matt
显示剩余3条评论
5个回答

107

嗯... path/filepath 包有一个 Abs() 函数可以满足我的需求(目前为止),虽然它有一点不方便:

absPath, _ := filepath.Abs("../mypackage/data/file.txt")

然后我使用 absPath 加载文件,它能正常工作。

请注意,在我的情况下,数据文件位于一个与我运行程序的main包不同的包中。如果它们都在同一个包中,我将删除前导的../mypackage/。由于这个路径显然是相对的,不同的程序将有不同的结构,并需要做出相应的调整。

如果有更好的方法来使用Go程序的外部资源并保持其可移植性,请随时提供另一个答案。


12
我学到了什么? :) 我们不应该在软件包之间共享文件吗? - srt32
8
@马特,分享一下爱吧。如果你有更好的方法,请不要使用这个“小技巧”来帮我们。你是谷歌搜索结果中排名最高的。 - OGHaza
4
这个问题是我刚开始学习Go语言时遇到的。更好的问题应该是如何将所需数据与程序捆绑在一起。我的答案是针对我非常具体的情况,希望大家不要随意使用。 - Matt
1
@Davos 我认为相对路径是相对于当前工作目录解析的。如果您从不同的目录运行程序会发生什么?例如,使用'go run ./some/directory/main.go'而不是'go run main.go'。 - Aruna Herath
1
@Edwinner 这整个问题和答案都不应再被视为相关。GOPATH早已被弃用,应该使用go:embed将随二进制文件一起提供的资源嵌入其中。(但对于Caddy而言并非如此,因为我直到4个月后才开始开发它。) - Matt
显示剩余6条评论

63

这似乎相当有效:

import "os"
import "io/ioutil"

pwd, _ := os.Getwd()
txt, _ := ioutil.ReadFile(pwd+"/path/to/file.txt")

23
使用 / 连接路径是不好的做法,因为它太具体化了。更好的做法是使用 filepath.join() 来连接路径,因为它会使用 os.PathSeperator。 - Ace.C
11
让我为那些愿意遵循之前评论中(好的)建议节省时间:使用import "path/filepath"并调用filepath.Join(...)(大写字母J)。 :-) - Bojan Komazec
2
os.Open(pwd + "../mocks/product/default.json") 你如何处理这种情况?它会将其视为字面量。 - kamal
os.Open 对我来说很好用: csvfile, err := os.Open("/mnt/c/Users/dmotta/Downloads/archivos_csv/tb_desp_new_201904081633.csv") - dmotta

13
我使用gobundle解决了这个问题。它可以从数据文件生成Go源代码,你可以将其编译到二进制文件中。然后,通过类似VFS的层访问文件数据。它是完全可移植的,支持添加整个文件树、压缩等功能。
缺点在于需要一个中间步骤来从源数据构建Go文件。我通常使用make来完成这个步骤。
以下是如何迭代包中所有文件并读取字节的方法:
for _, name := range bundle.Files() {
    r, _ := bundle.Open(name)
    b, _ := ioutil.ReadAll(r)
    fmt.Printf("file %s has length %d\n", name, len(b))
}

你可以在我的GeoIP包中看到它的实际使用示例。 Makefile生成代码,geoip.go使用虚拟文件系统。


@BurntSushi5说得很对;我的数据文件大小达到了数百MB。这很酷,但我认为将纯文本文件编译成二进制文件并不可行。 - Matt

8
从 Go 1.16 开始,你可以使用 embed 包。它可以让你在运行的 Go 程序中嵌入文件。
假设有以下文件结构:
-- main.go
-- data
  \- file.txt

您可以使用Go指令引用该文件。
package main

import (
  "embed"
  "fmt"
)

//go:embed data/file.txt
var content embed.FS

func main() {
  text, _ := content.ReadFile("data/file.txt")
  fmt.Println(string(text))
}

无论程序在哪里执行,此程序都能成功运行。这对于文件可能从多个不同位置调用的情况非常有用,例如从测试目录中调用。

依我之见,这个答案应该被提名为正确答案。对于我的使用情况来说非常完美:https://github.com/VOU-folks/HoGo/blob/main/apps/hogo-manager/main.go#L74 - num8er

4
我认为Alec Thomas提供了答案,但根据我的经验,它并不是万无一失的。将资源编译成二进制文件的一个问题是,根据资产的大小,编译可能需要大量内存。如果它们很小,则可能没什么可担心的。在我特定的情况下,一个1MB的字体文件导致编译需要约1GB的内存来编译。这是个问题,因为我希望在树莓派上实现Go获取功能。这是使用Go 1.0版本进行的; 在Go 1.1中可能已经有所改进。
因此,在这种特殊情况下,我选择只使用go/build包来基于导入路径查找程序的源目录。当然,这需要您的目标设置了GOPATH并且源可用。因此,并非所有情况下都是理想的解决方案。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接