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: remove bare return #21291

Open
carlmjohnson opened this Issue Aug 3, 2017 · 41 comments

Comments

Projects
None yet
@carlmjohnson
Copy link
Contributor

carlmjohnson commented Aug 3, 2017

(I couldn't find an existing issue for this, so please close if this is a duplicate and I missed it.)

I propose getting rid of bare returns. Named return values are great, keep those. Bare returns are more trouble than they are worth, eliminate them.

@gopherbot gopherbot added this to the Proposal milestone Aug 3, 2017

@gopherbot gopherbot added the Proposal label Aug 3, 2017

@ALTree ALTree added the Go2 label Aug 3, 2017

@dsnet dsnet added the LanguageChange label Aug 3, 2017

@awnumar

This comment has been minimized.

Copy link
Contributor

awnumar commented Aug 3, 2017

I would be against this change, for a few reasons.

  1. It's a relatively significant syntactic change. This could cause confusion with developers and contribute to leading Go into the same mess that Python went through.

  2. I do not think that they're useless, why are they more trouble than they're worth? return nils all over the place is a little verbose, don't you think?

@jimmyfrasche

This comment has been minimized.

Copy link
Member

jimmyfrasche commented Aug 3, 2017

@awnumar for 2 this would pair nicely with #21182

@dominikh

This comment has been minimized.

Copy link
Member

dominikh commented Aug 3, 2017

return nils all over the place is a little verbose, don't you think?

Simplicity beats verbosity. A bunch of return nil are a lot easier to understand than a bunch of return where one has to chase the latest value of the variable and make darn sure it hasn't been shadowed (intentionally or by mistake.)

@josharian

This comment has been minimized.

Copy link
Contributor

josharian commented Aug 3, 2017

Note that naked returns allow you to do one thing you cannot otherwise accomplish: Change a return value in a defer. Quoting the code review comments wiki:

Finally, in some cases you need to name a result parameter in order to change it in a deferred closure. That is always OK.

A complete proposal to eliminate naked returns should explain how such uses should be written instead and why the alternative form is preferable (or at least acceptable). The obvious rewrites tend to involve lots of boilerplate and repetition, and while they usually involve error handling, the mechanism is general.

@tandr

This comment has been minimized.

Copy link

tandr commented Aug 3, 2017

I don't think this

func oops() (string, *int, string, error) {
    ...
    return "", nil, "", &SomeError{err}
}

is more readable than

func oops() (rs1 string, ri *int, rs2 string, e error) {
    ...
    e = &SomeError{err}
    return
}

,sorry.

@tandr

This comment has been minimized.

Copy link

tandr commented Aug 3, 2017

OTOH - yes, shadowing sucks. It would not make sense to outlaw it outright (generated code comes to mind), but I would gladly vote for at least prohibition of return values names shadowing. Oh, and make compiler generate some diagnostic in all other cases would be nice too :)

Edit: apparently there is #377 that talks about it

@bcmills

This comment has been minimized.

Copy link
Member

bcmills commented Aug 4, 2017

@josharian I was assuming that deferred functions could still modify the return variables. That semantic seems more or less orthogonal to the syntax of the return statement.

@dominikh

This comment has been minimized.

Copy link
Member

dominikh commented Aug 4, 2017

@josharian You need named return parameters to be able to modify them in a deferred function. You do not need to use a naked return for that. return with explicit values will copy the values into the named return values before the deferred functions run.

@josharian

This comment has been minimized.

Copy link
Contributor

josharian commented Aug 4, 2017

@bcmills @dominikh right, silly me. Mental lapse; thanks for setting me straight.

@leonklingele

This comment has been minimized.

Copy link
Contributor

leonklingele commented Aug 16, 2017

A func without return values should still be able to "bare" return:

func noReturn() {
    if !someCondition() {
        return // bail out
    }
    // Happy path
}
@dmitshur

This comment has been minimized.

Copy link
Member

dmitshur commented Sep 12, 2017

I spent 1.5 minutes looking at the following Go code in Go standard library (from 4 years ago, /cc @bradfitz), in disbelief, thinking there might be a bad bug:

go/src/net/http/server.go

Lines 3158 to 3166 in 2d69e9e

func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) {
tc, err := ln.AcceptTCP()
if err != nil {
return
}
tc.SetKeepAlive(true)
tc.SetKeepAlivePeriod(3 * time.Minute)
return tc, nil
}

Specifically, this part:

tc, err := ln.AcceptTCP()
if err != nil {
	return
}

At first glance, it looked to me that on the first line there, new variables tc and err were being declared using short variable declaration syntax, distinct from the named err error return variable. Then, it looked to me that the bare return was effectively returning nil, nil rather than than nil, err as it should've been.

After about 90 seconds of thinking very hard about it, I realized that it's actually correct code. Since the named err error return value is in the same block as the function body, the short variable declaration tc, err := ... only declares tc as a new variable but does not declare a new err variable, so the named err error return variable is being set by the call to ln.AcceptTCP(), so the bare return actually returns a non-nil error as it should.

(Had it been in a new block, it would actually be a compile error "err is shadowed during return", see here.)

I think this would've been much more clear and readable code:

tc, err := ln.AcceptTCP()
if err != nil {
	return nil, err
}

I wanted to share this story because I think it's a good example of bare returns wasting programmer time. In my opinion, bare returns are marginally easier to write (just saving a few keystrokes), but often lead to code that's harder to read compared to equivalent code that uses full returns (non-bare). Go usually does the right thing of optimizing for reading, since that's done much more often (by more people) than writing. It seems to me that the bare returns feature tends to lower readability, so it might be the case that removing it in Go 2 would make the language better.

Disclaimer: In Go code I write and read most often, I tend to avoid having bare returns (because I think they're less readable). But that means I have less experience reading/understanding bare returns, which might negatively influence my ability to read/parse them. It's a bit of catch-22.

@creker

This comment has been minimized.

Copy link

creker commented Sep 12, 2017

@shurcooL, interesting example that threw me off as well. For me it was the fact that there're two connection variables tc and c. I thought you made a mistake when copy-pasting the code and instead of c there should be tc. It took me about the same time to realize what that code does.

@awnumar

This comment has been minimized.

Copy link
Contributor

awnumar commented Sep 12, 2017

@carlmjohnson

This comment has been minimized.

Copy link
Contributor

carlmjohnson commented Sep 12, 2017

From the article:

Note that if you don’t like or prefer the naked return that Golang offers, you can use return oi while still getting the same benefit, like so:

Named return values are great! Bare return of named return values are the problem. :-)

@bradfitz

This comment has been minimized.

Copy link
Member

bradfitz commented Sep 12, 2017

@awnumar, that is not relevant. See my comment on Hacker News: https://news.ycombinator.com/item?id=14668595

@pciet

This comment has been minimized.

Copy link
Contributor

pciet commented Jan 31, 2018

I think removing these would follow the approach of explicit error handling. I don’t use them.

Code I’ve reviewed on golang-nuts with bare returns isn’t difficult to understand, but parsing variable scope is an unnecessary added effort for readers.

@ianlancetaylor ianlancetaylor changed the title proposal: Go 2: Remove bare return proposal: Go 2: remove bare return Mar 7, 2018

@kelwang

This comment has been minimized.

Copy link

kelwang commented Apr 27, 2018

I really love bared return, especially when a function has multiple returns. It’s just making the code much cleaner. The biggest reason, I chose to work with go.

Go tool vet shadow can detect potential shadowed vars. If a func has less cyclomatic complexity, plus some test cases. I couldn’t see it will get any trouble. I could be wrong, but I wish to see some examples to demonstrate how bad bared return could be.

@dmitshur

This comment has been minimized.

Copy link
Member

dmitshur commented Apr 27, 2018

I wish to see some examples to demonstrate how bad bared return could be.

@kelwang Did you see my example about ln.AcceptTCP() from 7 comments above?

@kelwang

This comment has been minimized.

Copy link

kelwang commented Apr 27, 2018

Hi, @shurcooL, thx

Yeah, I think you made a great point.
But like you said, it's been 4 years. I have a feeling maybe you already get used to it.

I think it's not really an issue for bared return. But a confusion in multiple vars initialization.

For me, the shadow vet tool usually works very well. I never really worry about that.
Maybe we should fill a ticket in go linter to avoid those kind of confusion. whether rename the err, or declare tc on top first. I feel a linter suggestion should be good enough.

@nhooyr

This comment has been minimized.

Copy link
Contributor

nhooyr commented May 6, 2018

@kelwang In my opinion, if a function has multiple returns to the point where the return statements are getting ugly, a struct/pointer to a struct should be returned instead over a bare return.

@pam4

This comment has been minimized.

Copy link

pam4 commented May 7, 2018

Since #377 has been mentioned, I would argue that the source of confusion in the ln.AcceptTCP example is more about the magic behind := rather than the bare return itself.

I think the ln.AcceptTCP case wouldn't be so bad with a more explicit form of short declaration (proposed in the referenced issue):

func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) {
    :tc, err = ln.AcceptTCP()
    if err != nil {
        return
    }
    // ...
}

By just looking at a multi-variable := you can't tell which variables are being declared: you need to take into account all the preceding part of the block to know that.
An oversight about where the block boundary is, and you may end up with a hard to find bug.
Also you can't fix a multi-variable := to make it declare exactly what you want; you are forced to give up the short declaration or reorganize the code.

I've seen many proposal trying to address specific consequences of this, but I think the root of the problem is just the lack of explicitness of := (I would also argue that, whenever shadowing is considered a pitfall, it is really just multi-variable :='s fault).

I'm not saying that bare returns are necessarily worth it, I'm just saying that what we are seeing is a compound problem.

@spencerpomme

This comment has been minimized.

Copy link

spencerpomme commented Jun 6, 2018

Regardless of the readability arguments, I think the bare return is less logically consistent and elegant. It's somehow a shaky standpoint. Although, obviously I'm not the only one being subjective here.

@ggicci

This comment has been minimized.

Copy link

ggicci commented Aug 30, 2018

@tandr I think returning a small struct object is much more clear than having more than three return values.

@networkimprov

This comment has been minimized.

Copy link

networkimprov commented Sep 25, 2018

Alternative to a purely bare return, although debated and abandoned in #21182 in favor of #19642:

func f() (e error) {
   return ... // ellipsis trigram
}
@kf6nux

This comment has been minimized.

Copy link

kf6nux commented Oct 11, 2018

While I'm in favor of removing it in Go2, I made #28160 to (essentially) remove it in Go1 via gofmt. Folks comments there would be appreciated! 😄

@agnivade

This comment has been minimized.

Copy link
Member

agnivade commented Oct 12, 2018

I have found naked returns to be surprisingly helpful in DB layer.

This is a real redacted production code -

func (p *PostgresStore) GetSt() (st St, err error) {
	var tx *sql.Tx
	var rows *sql.Rows
	tx, err = p.handle.Begin()
	if err != nil {
		return
	}
	defer func() {
		if err != nil {
			tx.Rollback()
		} else {
			tx.Commit()
		}
		rows.Close()
	}()

	rows, err = tx.Query(`...`)
	if err != nil {
		return
	}
	// handle rows
	for rows.Next() {
		var all Call
		err = rows.Scan(&all.Email, &all.Count)
		if err != nil {
			return
		}
		st.Today = append(st.Today, &all)
	}

	rows.Close()
	rows, err = tx.Query(`...`)
	if err != nil {
		return
	}
	// handle rows
	for rows.Next() {
		var all Call
		err = rows.Scan(&all.Email, &all.Count)
		if err != nil {
			return
		}
		st.Books = append(st.Books, &all)
	}
	return
}

There are 6 return statements here. (Actually there are 2 more, I just shortened the code for brevity). But my point is when an error occurs, the caller will only inspect the err variable. If I just had to write return err, I would still be okay, but to match the return signature, I have to write return st, err over and over again. And this grows linearly if you have more variables to return; your return statement keeps growing bigger and bigger.

Whereas, if there are named return params, I just call return, knowing that it will also return any other variables in whatever state they were. This makes me happy and I consider it to be a great feature.

@nhooyr

This comment has been minimized.

Copy link
Contributor

nhooyr commented Oct 12, 2018

@agnivade not sure if this is because its redacted but it looks like your production code would panic in the defer if rows is nil which would occur if the query failed.

@networkimprov

This comment has been minimized.

Copy link

networkimprov commented Oct 12, 2018

@agnivade five of your six returns should evaporate under the forthcoming Go2 error handling scheme :-)

More on the feedback wiki.

@agnivade

This comment has been minimized.

Copy link
Member

agnivade commented Oct 12, 2018

@nhooyr - Oh dear .. looks like we are doing a prod push today .. 🤦‍♂️ 😅

@networkimprov - That's true. I hadn't thought of the new error handling. Personally, I would be okay with this when the new error handling lands. But even then I think this is a pretty major change which is bound to break a lot of codebases. Arguably, we can fix this with automated tooling. But I wonder how many of these major changes should be make for Go 2.

@kf6nux

This comment has been minimized.

Copy link

kf6nux commented Oct 12, 2018

Go2 is given license to not be backward compatible with Go1, is it not?

I agree that tooling could be made to upgrade existing Go1 code to be compatible with this change in Go2.

@bradfitz

This comment has been minimized.

Copy link
Member

bradfitz commented Oct 12, 2018

Go2 is given license to not be backward compatible with Go1, is it not?

It's almost certain we won't use said license. Breaking backwards compatibility is incredibly difficult and risky ecosystem-wise.

@kf6nux

This comment has been minimized.

Copy link

kf6nux commented Oct 12, 2018

If breaking changes aren't currently planned, I'd agree this proposal isn't worth causing a break. I hope we consider the proposal if/when breaking changes are planned. Updating go fix to accommodate this proposal should be trivial.

@networkimprov

This comment has been minimized.

Copy link

networkimprov commented Oct 12, 2018

New keywords are on the table for Go2 error handling, and that would break some code.

@ianlancetaylor

This comment has been minimized.

Copy link
Contributor

ianlancetaylor commented Oct 15, 2018

For Go 2 we can break code when it is worth it, but it carries a heavy cost. The cost of a new keyword is less than the code of removing a language feature, in that it is straightforward to diagnose and fix. It's possible to imagine that the benefit of the new keyword would exceed the cost of having to slightly rewrite code that uses an identifier that is the same as the keyword.

This case is harder to rewrite than a new keyword, but it's feasible. I think the most important question is: do people misunderstand what it means when they write a naked return? Are there real bugs in programs due to the use of a naked return? Even if there aren't real bugs, when people are writing code, do they make mistakes by using a naked return incorrectly?

@carlmjohnson

This comment has been minimized.

Copy link
Contributor

carlmjohnson commented Oct 15, 2018

Are there real bugs in programs due to the use of a naked return?

I'm not sure if it causes production bugs, but for me, when I see naked return, it usually makes me pause to try to figure out what's happening. I get paranoid about shadowed err working in an unexpected way because I'm not 100% confident in my understanding of how they interact. It makes reading the code much slower.

That said, I think #28160 is probably a better fix since it's backward compatible and gofmt has already had several minor changes.

@DeedleFake

This comment has been minimized.

Copy link

DeedleFake commented Oct 15, 2018

I get paranoid about shadowed err working in an unexpected way

I just found out recently that you actually don't need to worry about the shadowing as much as I thought, at least not in terms of it causing bugs. It's actually a compile-time error to use a bare return if any of the return variables are shadowed at the point that the return happens.

And before anyone says anything, yeah, I know that there are a lot of other problems with that example. I was just throwing something together to demonstrate.

@kf6nux

This comment has been minimized.

Copy link

kf6nux commented Oct 17, 2018

#28160 has been closed, which seems to me to close the door on fixing this in Go1.

@ianlancetaylor

This comment has been minimized.

Copy link
Contributor

ianlancetaylor commented Oct 17, 2018

Well, this kind of change was never going to happen in Go 1 anyhow, no matter how one approached it.

@shuLhan

This comment has been minimized.

Copy link
Contributor

shuLhan commented Nov 28, 2018

Maybe, maybe, the problem is in :=, and we should have proposal to remove it.

@kf6nux

This comment has been minimized.

Copy link

kf6nux commented Nov 28, 2018

This isn't just about shadowing. It's about making the reader carry more context than necessary.

Using the code example here: #21291 (comment)

In the first example, the reader knows exactly what's returned with only having to trace the value of err.

In the second example, the reader would need to trace the values of three additional variables.

Put another way

if err != nil {
	return err
}
...
return err

vs

if err != nil {
	return err
}
...
return nil

(returning a variable with a value nil vs returning nil)

Which do you prefer?

I prefer the latter because it's explicit and easier to understand at first glance. Removing bare returns would encourage explicit return values instead of encouraging return values that must be deduced.

@carlmjohnson

This comment has been minimized.

Copy link
Contributor

carlmjohnson commented Dec 3, 2018

Another example of the cognitive burden is that you can't tell a void function (func(arg T)) from a non-void function (func(arg T) ret R) when you look at a bare return. It could be that the function has no return values or it could be that it has named values that are being returned. You have to remember the function signature to know what the return actually means. Not a huge issue, to be sure, but it's one more thing to keep in your head.

@devishot devishot referenced this issue Dec 11, 2018

Closed

return error? #93

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment