How to defer resource cleanup when that resource outlives the scope of the surrounding function?

675 Views Asked by At

Let's take the example of this piece of code that makes the logger write to a local file instead of the standard output:

f, err := os.OpenFile("filename", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
        log.Fatal(err)
}   

defer f.Close()

log.SetOutput(f)

The author is putting this code straight into the main() function, which makes it work as intended. But if I wanted to put this code into a a dedicated function which main() may then call, then it would no longer work, because the f.Close() would be called before the logger ever gets used.

E.g. (if the code above is now in a function called logToFile()):

main() {
   logToFile()
   log.Print("I'm going to end up in stdout\n")
}

Can this be moved into its own function, and still have it work as intended?

I've had the same situation with opening/closing of a database connection. And it seems like the only way is to do both of these things inside the main(), but I think the code would look cleaner and more SoC if we could divide it into functions. Is this a no-no in Go?

2

There are 2 best solutions below

4
colm.anseo On BEST ANSWER

You looking for something like this?

type closerFunc func() error

func logToFile(path string) closerFunc {
    f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
    if err != nil {
        log.Fatal(err)
    }   

    log.SetOutput(f)

    return func() error {
        return f.Close()
    }
}

To use:

func main() {
    closerFn := logToFile("filename")
    defer closerFn()

    log.Print("logs to file\n")
}
2
bcmills On

One option is to use continuation-passing style, passing the code to be executed within the defer block as an explicit argument:

func withLogToFile(filename string, body func()) {
    f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
    if err != nil {
        log.Fatal(err)
    }
    prevOut := log.Writer()
    log.SetOutput(f)
    defer func() {
        log.SetOutput(prevOut)
        if err := f.Close(); err != nil {
            log.Fatal(err)
        }
    }()

    body()
}

Then the call site becomes:

func main() {
    withLogToFile(filename, func() {
        log.Print("I'm going to end up in ", filename)
    })
}

(https://play.golang.org/p/ebCvtzufU5U)