Golang中如何正确将io.Reader返回到主函数

Golang中如何正确将io.Reader返回到主函数 我正在构建自己的Docker客户端,遇到了一个我认为非常基础的问题。这可能源于我对Go如何处理IO流的理解不够扎实,或者别的什么……我猜我遗漏了一些非常基础的东西,希望你们能帮我指出来:

我在这里编写的是相当于docker load的功能,需要处理多种格式的压缩包(tar.gz、tar、tar.bz2、tar.xz)。我在处理这些多种格式时遇到了问题。

目前,主要的“加载器”函数如下所示:

func LoadImage(filepaths []string) {
	ctx := context.Background()
	cli := hosts.Connect2daemon(true)

	for _, filepath := range filepaths {
		tarballFile, err := os.Open(filepath)
		if err != nil {
			fmt.Println("Opening tarball file error: ", err)
			os.Exit(-9)
		}
		defer tarballFile.Close()

		tarballFormatReader := getTarballCompressedFormatReader(filepath, tarballFile)
		if tarballFormatReader == nil {
			fmt.Println("Unknown file format, exiting")
			os.Exit(-10)
		}
		defer tarballFormatReader.Close()

		tarReader := tar.NewReader(tarballFormatReader)
		
	}
}

如你所见,它调用了getTarballCompressedFormatReader函数,该函数如下:

func getTarballCompressedFormatReader(tbname string, tbfile *os.File) io.Reader {
	if strings.HasSuffix(tbname, ".bz2") {
		return bzip2.NewReader(tbfile)
	}
	if strings.HasSuffix(tbname, ".xz") {
		reader, _ := xz.NewReader(tbfile)
		return reader
	}
	if strings.HasSuffix(tbname, ".gz") {
		reader, _ := gzip.NewReader(tbfile)
		return reader
	}
	if strings.HasSuffix(tbname, ".tar") {
		return tar.NewReader(tbfile)
	}
	return nil
}

问题出在defer tarballFormatReader.Close()这一行,编译器抱怨它没有Close()方法。我不明白,它应该有这个方法,不是吗?

这是我用Go编写的第一个完整规模的软件,所以我可能遗漏了一些基础的理解……


更多关于Golang中如何正确将io.Reader返回到主函数的实战教程也可以访问 https://www.itying.com/category-94-b0.html

7 回复

是的,我今天早些时候也查看了那个文件。

更多关于Golang中如何正确将io.Reader返回到主函数的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


不确定你具体指什么,但常规的 Docker 也必须关闭所有读取器,至少是基于文件的那些。

因此,它确实可能会打开并关闭从打开文件获得的任何内容,但只传递读取器部分,并在打开文件的地方将其关闭。

这实际上是一个很好的通用实践:在打开的地方关闭资源,并且关闭的正是那个资源,而不是在处理后获得的某个东西。

我同意,这也是我非常喜欢GO的一个部分:defer语句允许你在打开某物后立即在其下方关闭它。

我想说的是,我还没有找到一种与压缩格式无关的方法来执行docker load。我想我现在有点不自量力了,因为我显然还没有掌握io流。

func main() {
    fmt.Println("hello world")
}

感谢,使用 io.ReadCloser() 我在应用的其他地方也用过,不知道为什么之前没想到。

看来我可能在其他地方卡住了。我打算尝试逆向分析 Docker 的 ImageLoad() 代码,因为我不知道如何无缝地从普通的 tarball 切换到 tar.gz、tar.bz2、tar.xz 等格式,也就是在运行时选择合适的 io.Reader

根据我有限的理解,我似乎只能选择正确的读取器,或者使用实现了 Close() 的读取器,无法同时兼顾两者。

jfgratton:

问题出在 defer tarballFormatReader.Close() 这一行

你的代码中缺少这一行…

jfgratton:

(tbname string, tbfile *os.File) io.Reader {

你真的应该也返回一个 error 值…

jfgratton:

io.Reader

我猜你是想对返回的值调用 Close() 方法?它没有 Close 方法,这可以解释这个错误。

如果我没记错的话,io.Reader 只有 Read() 方法。要关闭的话,你需要 io.ReadCloser 类型。

它只是将输入原样转发给守护进程。

// ImageLoad loads an image in the docker host from the client host.
// It's up to the caller to close the io.ReadCloser in the
// ImageLoadResponse returned by this function.
func (cli *Client) ImageLoad(ctx context.Context, input io.Reader, quiet bool) (types.ImageLoadResponse, error) {
	v := url.Values{}
	v.Set("quiet", "0")
	if quiet {
		v.Set("quiet", "1")
	}
	headers := map[string][]string{"Content-Type": {"application/x-tar"}}
	resp, err := cli.postRaw(ctx, "/images/load", v, input, headers)
	if err != nil {
		return types.ImageLoadResponse{}, err
	}
	return types.ImageLoadResponse{
		Body: resp.body,
		JSON: resp.header.Get("Content-Type") == "application/json",
	}, nil
}

在Go中,io.Reader接口只定义了Read(p []byte) (n int, err error)方法,不包含Close()方法。你需要使用io.ReadCloser接口,它组合了io.Readerio.Closer接口。

以下是修改后的代码:

func getTarballCompressedFormatReader(tbname string, tbfile *os.File) io.ReadCloser {
    if strings.HasSuffix(tbname, ".bz2") {
        return struct {
            io.Reader
            io.Closer
        }{
            Reader: bzip2.NewReader(tbfile),
            Closer: tbfile,
        }
    }
    if strings.HasSuffix(tbname, ".xz") {
        reader, err := xz.NewReader(tbfile)
        if err != nil {
            return nil
        }
        return struct {
            io.Reader
            io.Closer
        }{
            Reader: reader,
            Closer: tbfile,
        }
    }
    if strings.HasSuffix(tbname, ".gz") {
        reader, err := gzip.NewReader(tbfile)
        if err != nil {
            return nil
        }
        return reader // gzip.Reader 实现了 io.ReadCloser
    }
    if strings.HasSuffix(tbname, ".tar") {
        return struct {
            io.Reader
            io.Closer
        }{
            Reader: tar.NewReader(tbfile),
            Closer: tbfile,
        }
    }
    return nil
}

对于.tar.xz格式,需要手动创建组合类型。对于.gz格式,gzip.Reader已经实现了io.ReadCloser。另外,需要处理压缩库返回的错误。

主函数也需要相应调整:

func LoadImage(filepaths []string) {
    ctx := context.Background()
    cli := hosts.Connect2daemon(true)

    for _, filepath := range filepaths {
        tarballFile, err := os.Open(filepath)
        if err != nil {
            fmt.Println("Opening tarball file error: ", err)
            os.Exit(-9)
        }

        tarballFormatReader := getTarballCompressedFormatReader(filepath, tarballFile)
        if tarballFormatReader == nil {
            fmt.Println("Unknown file format, exiting")
            tarballFile.Close()
            os.Exit(-10)
        }
        defer tarballFormatReader.Close()

        tarReader := tar.NewReader(tarballFormatReader)
        // 处理tarReader...
    }
}

这样修改后,tarballFormatReader就具有了Close()方法,可以正确关闭底层文件。

回到顶部