-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
Discuss: should we provide guidance on function arguments being functions #92
Comments
I agree with your instincts about which of these examples are good/bad. To rephrase (and correct me if I'm not properly capturing the intent), we'd essentially be recommending that folks prefer named or intermediate variables over temporary, unnamed values, because - ostensibly - the language, type, and/or API semantics for the types being passed aren't obvious, and explicitly using variables makes things easier to understand and harder to misinterpret. If we extrapolate from this, we could change it to mean "any potentially blocking call to compute or retrieve a value should not be invoked such as it could be conflated with another blocking call at the same callsite". This could be a combination of channels, functions, and defer: func probablyAnExpensiveFunc() string {
// assume some likely runtime cost
return "foo"
}
func maybeAnExpensiveFunc(s string) string {
// assume some potential runtime cost
return s + "bar"
}
// Is the channel send blocking, or is evaluating the function call blocking?
ch <- probablyAnExpensiveFunc()
// What is blocking here?
ch <- maybeAnExpensiveFunc(probablyAnExpensiveFunc())
// What is blocking here?
doSomething(<-ch, probablyAnExpensiveFunc(), maybeAnExpensiveFunc())
// Will users of defer be surprised?
defer doSomething(<-ch, probablyAnExpensiveFunc(), maybeAnExpensiveFunc())
// How about here?
defer func() {
doSomething(<-ch, probablyAnExpensiveFunc(), maybeAnExpensiveFunc())
} So on one hand, maybe we have opinions about where the language is unclear and could be augmented with basic guidance. On the other hand, this is not unique to Go: one could argue this is a "generic consideration when writing software", like many runtime concerns or using good variable names. Where do we draw the line? Is any potentially-obfuscated blocking bad? Is it even possible to remove all ambiguity, and if not, is it worth addressing any of it? // Is this measurably better?
var (
chanString = <-ch
probString = probablyAnExpensiveFunc()
maybeString = maybeAnExpensiveFunc()
)
doSomething(chanString, probString, maybeString)
// What about this?
var (
probString = probablyAnExpensiveFunc()
maybeString = maybeAnExpensiveFunc(probString)
)
ch <- maybeString As a devil's advocate argument, while I'd generally agree that the above are "better" from a clarity perspective, how much less error-prone are they in reality? In either case, is the net result different? Executing code takes the amount of time it takes, so are we "preventing" anything other than incorrect assumptions? Right now, I'm split between "this is a code review problem" and "we should draft very generic guidance". We do have plenty of guidance already in the vein of "don't shoot yourself in the foot", but also want to be careful not to become unnecessarily prescriptive. The only guidance I think we could give would be something like "avoid ambiguous or 'shadowed' callsite blocking", which could use parameter and/or operand evaluation (function parameters, channel sends, and probably defer) to illustrate the problem, much like you originally showed. Interested in other folks' thoughts. @abhinav, @prashantv? |
I had a bit of an offline discussion with @jeffbean initially, where we first considered: Specific guidance to discourage use of channel operations as function arguments, e.g.,
To avoid the inconsistency, we could make it more general: avoid any complex expressions as function arguments. However, I think that advice has too broad of an impact, and isn't necessarily useful. -- e.g., What I think we want to avoid is complex expressions where the blocking argument is easy to miss, or there's multiple blocking operations like
|
Agree. I think the original phrasing of
or something like
is probably both specific enough to a class of pitfalls and generic enough to not be rigid or over-prescriptive. (Something similar to that phrasing is also flexible enough to apply to both functions and channels since it just describes blocking rather than its cause.) This doesn't address whether we should add guidance for this, though. Agreed that it's probably not valuable to make a special callout for channels, but if we feel that "nested blocking" is a prevalent enough issue, then what we're discussing makes sense. I haven't seen this pop up as an issue in many (if any) code reviews/bugfixes, for what it's worth, but that's only one datapoint. |
I had not considered the debugging aspect or argument ordering. This personally will encourage me to avoid this pattern. |
After some internal discussion, the general thinking is that both (1) channel operations and (2) function call semantics are unambiguous enough that it should be reasonably clear, in all cases, that such expressions have the potential to block; therefore, we'll opt to lean on code reviewers' judgment rather than adding specific guidance here. |
thanks for the input all! |
Wound up here from a private discussion, so apologies for the thread necromancy 🧟 I like this guidance, and tend to express it slightly differently: each line of source code should contain only a single operation, where operation is basically shorthand for anything that can block, as identified above: function calls, chan sends or recvs, etc. It's a simple rule that's easy to follow and tends to make code easier to audit. For example, // Avoid
val, err := f(<-c, someFunc(), fmt.Sprintf("prefix/%s", ns))
// Prefer
var (
foo = <-c
bar = someFunc()
msg = fmt.Sprintf("prefix/%s", ns)
)
val, err := f(foo, bar, msg) |
I'm generally for keeping each line relatively simple, but in trivial cases, such as lnAddrStr := ln.Addr().String()
conn, err := net.Dial("tcp", lnAddrStr)` I prefer the one-line expression, since the |
Sure! It's guidance, not an inviolable rule, and there are plenty of cases where it's probably preferable to inline function calls. Yours is a reasonable example of one of those cases, and — absent more context — the original form is probably just fine. (We can reasonably assume Addr and String will not block.) But I can definitely think of situations where an exploded version would be beneficial! And I'd probably do it like var (
netw = "tcp"
addr = ln.Addr().String()
)
conn, err := net.Dial(netw, addr) |
As it's not specifically called out I am looking to discuss adding our opinion (if any) around function parameters that are not variables or constants.
My motivation for failing this issue and want others' feedback was while doing some simple channel read loop, it occurred to me that we can do a blocking read as a function parameter. My opinion is the difference between
<-event
andevent
in this use case has a massive impact on behavior.My opinion is that the first case should be avoided. As for the others, I'm not sure I have a strong opinion.
I understand this is specific to a channel in this example, and guidance ideally would not be specific to channels, but I also acknowledge that my point of
<-
being the issue is specific to channels.The text was updated successfully, but these errors were encountered: