为了账号安全,请及时绑定邮箱和手机立即绑定

如果没有显式关闭,Go 会自动关闭资源吗?

如果没有显式关闭,Go 会自动关闭资源吗?

Go
泛舟湖上清波郎朗 2023-07-31 10:37:40
以下内容Open后跟 deferredClose在 Go 中是惯用的:func example() {    f, err := os.Open(path)    if err != nil {        return    }    defer f.Close()}如果我没有会怎样defer f.Close()?当我调用此函数并f超出范围时,它会自动关闭文件还是我有僵尸文件句柄?如果它自动关闭,具体什么时候关闭?
查看完整描述

4 回答

?
白板的微信

TA贡献1883条经验 获得超3个赞

确实,垃圾收集时文件会关闭,但是...正如Alexander Morozov的“ Go 中终结器之谜”中提到的 - LK4D4math :

在 Go 中,我们同时拥有 GC 和专业用户:)
因此,在我看来,显式调用Close总是比魔术终结器更好

亚历山大补充道:

终结器的问题在于您无法控制它们,而且更重要的是,您并不期望它们。

看这段代码:

func getFd(path string) (int, error) {

    f, err := os.Open(path)

    if err != nil {

        return -1, err

    }

    return f.Fd(), nil

}

当你为 Linux 编写一些东西时,从路径获取文件描述符是非常常见的操作。
但该代码是不可靠的,因为当您从 , 返回时getFd()f会丢失其最后一个引用,因此您的文件注定迟早会被关闭(当下一个 GC 周期到来时)。

在这里,问题不在于文件将被关闭,而在于它没有记录并且根本不是预期的。


有人提议扩展终结器并检测泄漏(例如文件描述符泄漏)

但是……拉斯·考克斯令人信服地推翻了这一点:

任何对此主题感兴趣的人都应该阅读 Hans Boehm 的论文“析构函数、终结器和同步”。
它极大地影响了我们尽可能限制 Go 中终结器范围的决定。
它们是允许与关联堆内存同时回收非(堆内存)资源的必要之恶,但它们本质上比大多数人最初认为的更有限。

我们不会扩展终结器的范围,无论是在实现中还是在标准库中,或者在 x 存储库中,我们也不会鼓励其他人扩展该范围。

如果您想跟踪手动管理的对象,最好使用runtime/pprof.NewProfile.

例如,在 Google 的源代码树中,我们有一个 Google 范围内的“文件”抽象,并且 Go 包装器包声明了一个全局的:

var profiles = pprof.NewProfile("file")

创建新文件的函数末尾显示:

profiles.Add(f, 2)
return f

然后f.Close_

profiles.Remove(f)

然后我们可以获得所有正在使用的文件的配置文件,无论是“泄露”还是其他方式,来自/debug/pprof/file或来自pprof.Lookup("file").WriteTo(w, 0).
该配置文件包括堆栈跟踪。


查看完整回答
反对 回复 2023-07-31
?
慕村9548890

TA贡献1884条经验 获得超4个赞

Go 应用程序应该显式释放资源。当变量超出范围时,没有语言功能会隐式释放资源。该语言确实提供了延迟功能,以便更轻松地编写显式代码。

正如 bserdar 和 VonC 所指出的,垃圾收集器有一个用于释放外部资源的钩子。该钩子由 os.File 类型使用。应用程序不应依赖此挂钩,因为在收集对象时未指定它(如果有的话)。


查看完整回答
反对 回复 2023-07-31
?
烙印99

TA贡献1829条经验 获得超13个赞

当 os.File 被垃圾收集时,文件将自动关闭。这似乎是通过 SetFinalizer 调用完成的,因此文件最终将被关闭,而不是在它变得无法访问后立即关闭。



查看完整回答
反对 回复 2023-07-31
?
慕雪6442864

TA贡献1812条经验 获得超5个赞

在您的简单示例中,defer不需要该语句。该文件在退出函数作用域时关闭。

但是,如果您返回该变量f或将其保存在全局变量中,则其引用计数器会增加,因此不会被删除。

下面是一个示例,证明终结器以与初始化相反的顺序立即被调用(由于引用计数器达到零,因此立即被垃圾收集):

package main


import (

        "fmt"

        "runtime"

        "time"

)


type Foo struct {

        name    string

        num     int

}


func  finalizer(f *Foo) {

        fmt.Println("a finalizer has run for ", f.name, f.num)

}


var counter int

func MakeFoo(name string) (a_foo *Foo) {

        a_foo = &Foo{name, counter}

        counter++

        runtime.SetFinalizer(a_foo, finalizer)

        return

}


func Bar() {

        f1 := MakeFoo("one")

        f2 := MakeFoo("two")


        fmt.Println("f1 is: ", f1.name)

        fmt.Println("f2 is: ", f2.name)

}


func main() {

        for i := 0; i < 3; i++ {

                Bar()

                time.Sleep(time.Second)

                runtime.GC()

        }

        fmt.Println("done.")

}

事实上,您无法真正控制终结器是否被调用。但是,如果您确保不将局部变量复制到任何地方,那么它会立即发生。


实际上,一个有趣的事实是,如果您使用 defer,那么您将创建对该对象的引用(您需要有一个引用才能Close()在退出函数时调用该对象)。


这是非常重要的一点,因为如果有循环,实际上会使情况变得更糟:


    ...

    for i:=0; i<10; i++ {

        f := os.Open(...)

        defer f.Close()

        ...

    } // without the defer, the file is closed here, as expected


} // <- defer is called here, so you will open 10 files

  // and keep all 10 open until this line

所以你必须非常小心defer。有些情况下它不起作用。


查看完整回答
反对 回复 2023-07-31
  • 4 回答
  • 0 关注
  • 149 浏览
慕课专栏
更多

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信