Skip to content

proposal: Go 2: allow defer to have its own scope #30130

@vincent-163

Description

@vincent-163

EDIT: Added a solution to the problem. Removed the arguments taken from other issues since the deferblock keyword doesn't change these code.

I know this has been proposed many times but I really do find it extremely inconvenient to use defer in the current way. Even if we can't change the current functionality of defer I would suggest that another keyword be introduced with similar functionality.

The nature of defer where all functions are forced to run just before the function returns seems counter-intuitive to me since there are some cases where I would expect the deferred function to run when the resource goes out of to scope instead of when the function returns.

Here are some cases where I find it hard to write code:

  1. Using multiple resources one by one.
    Pseudo code:
lock1.Lock()
if err := operation1(); err != nil {
  lock1.Unlock()
  return err
}
if err := operation2(); err != nil {
  lock1.Unlock()
  return err
}
lock1.Unlock()

lock2.Lock()
if err := operation3(); err != nil {
  lock2.Unlock()
  return err
}
if err := operation4(); err != nil {
  lock2.Unlock()
  return err
}
lock2.Unlock()

The lock1 gets used first, then lock2. If I used defer in this case, I would have to lock lock2 without unlocking lock1, causing potential deadlock and performance issues. In some cases, I want to perform some expensive operations after obtaining the results from operation2(), but if the operation is performed with the lock held it can hurt performance.

  1. Using defer in a for block.
    This is similar to the first issue, except that the resources are wrapped in a for loop. Pseudocode:
for i := 0; i < 10; i++ {
  locks[i].Lock()
  performOperation(i)
  locks[i].Unlock()
}

Without defer Unlock() will not be called in case of a panic(), causing deadlocks. Also all the locks from 0 to 8 would be held while performing operation 9, which will certainly cause deadlocks.

A workaround for the issue is to wrap the defer statement in a closure, like this:

for i := 0; i < 10; i++ {
  func() {
    locks[i].Lock()
    defer locks[i].Unlock()
    performOperation(i)
  }()
}

The problem with this approach is that the closure creates a completely different scope. If we want to break out of the for loop from the closure or return from the function, it would not be possible without some extra code that checks for breaks or returns in the closure. This hurts simplicity.

Intuitively the defer is just like any other blocks, such as for and switch. The deferred function gets called as soon as the program leaves the scope.

I propose a special keyword, called deferblock or something else, that expects to be followed by a block of code, like:

deferblock {
  lock.Lock()
  defer lock.Unlock()
  // perform operations with lock
}

defer always appends a deferred function to the last deferblock rather than an arbitrarily chosen deferblock, since defer functions have to be arranged like a stack and it's unnatural to "insert" to the stack.

deferblock can be break-ed out, just like for, switch and select. When program leaves the deferblock in any way, by normal control flow, break or panic, the deferred functions associated with the scope gets called, and if there's a panic and one of the deferred functions recovers from the panic, the program continues from the point after deferblock.

Such deferblock is consistent with what Go already has, so programmers don't have to rethink about how to use defer.

The new functionality missing from the language is to break out of a deferblock to an outer label, which cannot be easily implemented by using a closure.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions