Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proposal: Go 2: change defer to execute once out-of-scope #27762

Closed
deanveloper opened this issue Sep 19, 2018 · 3 comments
Closed

proposal: Go 2: change defer to execute once out-of-scope #27762

deanveloper opened this issue Sep 19, 2018 · 3 comments

Comments

@deanveloper
Copy link

Currently, defer runs once the function exits.

I argue that it would be much more intuitive if defer ran once it was out-of-scope.

For instance, a "solution" to a classic concurrent programming example, adding 2,000,000 to an integer, one at a time, concurrently

func main() {
	mux := new(sync.Mutex)
	wg := new(sync.WaitGroup)
	wg.Add(2)

	var x int
	go addOneMillion(&x, mux, wg)
	go addOneMillion(&x, mux, wg)

	wg.Wait()

	fmt.Println(x)
}

func addOneMillion(x *int, mux *sync.Mutex, wg *sync.WaitGroup) {
	for i := 0; i < 1000000; i++ {
		mux.Lock()
		defer mux.Unlock()

		*x = *x + 1
	}
	wg.Done()
}

Intuitively, you would expect the loop to run 1,000,000 times, but every once in a while it would need to wait for the other goroutine to finish its operation.

Instead, the program gets stuck. This is because a goroutine runs mux.Lock(), but doesn't run mux.Unlock() until the function is ready to exit. But before that happens, mux.Lock() gets run again.

One of the other most common uses for defer is to free resources. Resources should be freed as soon as possible in order for optimal performance. A resource cannot be used outside of its variable scope (obviously), so therefore a defer should free the resource once it is out of scope.

For instance:

func main() {
    // ...
    if condition {
        r, err := getSomeResource()
        if err != nil {
            panic(err)
        }
        defer r.Close()

        // do stuff with `r`...

        // under this proposal, r.Close() would be run right here
    }
    // we cannot do stuff with `r` as it is now out of scope,
    // so it should be freed at this point.

    someLongRunningOperation()

    // in current Go, r.Close() would be run all the way down here. What a waste!
}
@gopherbot gopherbot added this to the Proposal milestone Sep 19, 2018
@DeedleFake
Copy link

Can't you do this already by making the inside of the for loop an anonymous function? For example,

func addOneMillion(x *int, mux *sync.Mutex, wg *sync.WaitGroup) {
	for i := 0; i < 1000000; i++ {
                func() {
		        mux.Lock()
		        defer mux.Unlock()

		        *x = *x + 1
                }()
	}
	wg.Done()
}

I think in a number of cases making defer scope-based would result in a lot less clear code, if not break things entirely. For somewhat contrived example,

func LogSomething(w io.Writer, useSecondFile boolean) error {
  if useSecondFile {
    file, err := os.OpenFile(SecondaryLogFile, os.O_WRONLY | os.O_CREATE | os.O_APPEND, 0644)
    if err != nil {
      return err
    }

    // Under this proposal, this would be impossible. Even if you move
    // file into the parent scope, you still can't conditionally defer
    // it. You'd have to provide a default dummy value with a no-op
    // Close() method in order to defer it outside the if statement.
    defer file.Close()

    w = io.MultiWriter(w, file)
  }

  // Log something or something.
}

@randall77
Copy link
Contributor

Sometimes you want the defer in a loop to wait until the end of the function.

func readAndJoin(files []string) []byte {
    var readers []io.Reader
    for _, file := range files {
        f, _ := file.Open(file)
        defer f.Close()
        readers = append(readers, f)
    }
    var b []byte
    .. read from readers into b ..
    return b
}

@deanveloper
Copy link
Author

You guys are right, there are quite a few examples where a scope-based defer just doesn't work (especially conditional defers...) which I didn't think about. There is also a pretty basic solution, which while it is a bit inconvenient, is a lot less inconvenient than the solutions required for trying to weasel around a block-scoped defer.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

4 participants