4 ms·
Curious what the overhead of dealing with files in Go would be if finalisers were cheaper / not in use. Sorry there’s no link to the source, AFK right now, but
by javierhonduco 3y ago
Curious what the overhead of dealing with files in Go would be if finalisers were cheaper / not in use.
Sorry there’s no link to the source, AFK right now, but files opened with os.Open will be automatically closed once all its references have been collected.
Found out about this behaviour some months ago while debugging some code at work.
- ithkuil 3y agoThat's not the behaviour you actually want though. You need to know exactly when a file closes.
- cratermoon 3y agoStrong disagree. If a long-running program opens a lot of files, has no references to them and they are not closed, those files will stay open, using system resources. There's no way for the Go code to close them. Yes, it's a bug to open a file and lose all references before closing it, but the runtime is doing the right thing by closing them for the program.
- chowells 3y agoYou could go the other way: the program is incorrect if it doesn't explicitly close files. Allowing incorrect programs to somehow work correctly only encourages programmers to create more advanced incorrect programs, until they exceed the ability of the language/runtime to fix. I tend more towards this line of thought. But I'd still put a finalizer on file handles. I just would have it yell very loudly that there's a bug. Maybe even close the file, but there's no way I wouldn't at the very least be generating a lot of diagnostics saying things were broken.
- jrockway 3y agoI agree with your take on correctness. The problem that you run into is that GC and thus finalization is triggered by memory pressure. But closing a file doesn't relieve memory pressure, it relieves FD pressure. So what you'd want is some sort of native thing not related to GC that closes unused files. This isn't the only resource like this. If you write to a lot of temporary files, you'd want to start cleaning those up before the OS returns "no space left on device". If you listen on a lot of TCP ports, you want to clean up stale listeners before you've listened 65535 times. If you span a subprocess, you want to start reaping the children when PID pressure prevents creating future subprocesses. This all ends up being rather esoteric so many programming languages just say "tell me as soon as you're done" and you pay the cost right then. This isn't optimal (especially "x := Open(thing); defer x.Close(); do something with x; do a bunch of slow stuff without x"). A big downside is that it's possible to have an object in memory that refers to something in an invalid state; you can write "x := Open(thing); x.Close(); x.Read()" just fine in many languages; the code compiles and it runs, but it returns incorrect results. This could be an area of focus for future languages, but I doubt much of this would be general. A lot of nitty gritty specifics depending on what resources you want to track. It would also be weird to not close files just because someone turned GC off, right? If you tie finalizers to memory management, if the memory management isn't under memory pressure, then you start leaking FDs. Weird! All in all, the memory management subsystem is a weird place to manage non-memory resources. So to get this right, you really need something GC-like for everything that's not memory. I think people are looking for something like, or are already very comfortable with, C++'s RAII. "I am certain this variable is allocated on the stack, so the compiler should fail to compile if that's untrue, and a Close method should be called when the function returns." But sometimes you will want the resource to be allocated beyond the current stack frame, and now you need special syntax for that. (Newer languages like to avoid exposing stack vs. heap to the programmer and let The Algorithm pick the best place. C++ never considered that an option so you do get some extra flexibility when you need it.) Anyway this is a long post to say "all options are bad".
- ithkuil 3y agoSure. But if you think that a file will be closed by the runtime "when there are no more references to it" then you're prone to assume that that the file must be closed when you later reopen the file or pass that filename to some other process . And most of the times the file will be indeed closed, until the GC just happens to run the finalized a little bit later than usual. It's very hard to debug such bugs. Imo it's much easier to make the leak more prominent so that people are incentivised to properly close the files
- cratermoon 3y agoIt's possible to use Go's pprof to find leaks. Also, if it's important enough then runtime.SetFinalizer(fd, any) will allow the programmer to do something appropriate.
- yawaramin 3y agoSo will 'defer', and that is much more idiomatic and well-understood in the Go community.
- cratermoon 3y agoGranted, but that only works for a subset of all cases. Remember that FDs can also refer to sockets and other things that aren't "files". A careful programmer will ensure that all FDs get cleaned up, but even careful programmers can fall victim to corner cases.
- yawaramin 3y agoI don't see why 'defer' wouldn't work for FDs or any other resource. There's nothing about 'defer' that says it will work only with 'files'. In fact I've been using it for general-purpose resource management for a while now.
- majewsky 3y ago> AFK right now, but files opened with os.Open will be automatically closed once all its references have been collected. No, they're not. The idiomatic usage is to defer a Close() call after checking that Open succeeded, like so: file, err := os.Open("/etc/hostname") if err != nil { doSomethingWith(err) } defer file.Close() doSomethingWith(file) But you'd be forgiven for not knowing that: To my own surprise, I could not find the need for this idiom explained in the package documentation for os.Open, though you can see it in action throughout the std implementation. For example: https://cs.opensource.google/go/go/+/refs/tags/go1.20.4:src/os/file.go;l=674 https://cs.opensource.google/go/go/+/refs/tags/go1.20.4:src/...
- javierhonduco 3y agoFWIW, I never advocated for not explicitly calling Close(). Brought up the finalizer in File, because it seems to have performance implications and it’s called for every file handle. From the docs (https://pkg.go.dev/os#File.Fd https://pkg.go.dev/os#File.Fd): > Fd returns the integer Unix file descriptor referencing the open file. If f is closed, the file descriptor becomes invalid. If f is garbage collected, a finalizer may close the file descriptor, making it invalid; see runtime.SetFinalizer for more information on when a finalizer might be run. To give more context on how I found this out, we were missing a Close for a couple of files in our codebase. As soon as I realised I added them, but checked in production for file descriptor leaks and there were none. Checking Go’s source code led me to the finalizer and this doc confirmed what I saw in the code.