Mat Ryer, a Go developer and author, writes about Go code style, HTTP services, open source projects and APIs.
HN commenters cite his talk and posts on writing Go HTTP services and his code style, which they debate on mutation and branching.
Some important context re: my style...
> Any code anywhere in the function can mutate any of the variables
Regardless of the language I'm using, I never mutate values. Counters in loops or some other hyper-local variables (for performance) might be the inconsequential exceptions to this rule.
> More functions help scopes stay small, and data flow is more explicit.
Just write your big function with local scope sections, if needed (another local exception to the rule above). Eg, in JS:
let sectionReturnVal
{
// stuff that sets sectionReturnVal
}
or even use IIFE to return the value and then you can use a const. "A function, you're cheating!" you might say, but my goal is not to avoid a particular language construct, but to maintain locality, and avoid unnecessary names and jumping around.
> A side benefit is that "more functions" helps keep indentation down.
This is important and I maintain it.
See "Align the happy path to the left" (https://medium.com/@matryer/line-of-sight-in-code-186dd7cdea88)
It is also worth noting that solving this problem with function extraction can often be a merely aesthetic improvement. That is, you will still need to keep hold the surrounding context (if not the state) in your head when reading the function to understand the whole picture, and the extraction makes that harder.
Using early returns correctly, by contrast, can actually alleviate working memory issues, since you can dismiss everything above as "handling…
I worry that a lot of developers who have been using languages like Java or C# see Go and immediately feel the need to point out that Go doesn't support language features that they've come to rely on, and so they see that as a weakness or a flaw.
With Go, you're getting a lot of high-level language features like anonymous functions, while also gaining the control that you're not able to get in higher level languages like Python or Ruby or Java.
I think this works out great for cloud computing, where performance is a major requirement, but the technology is expanding rapidly enough that a step up from C or C++ is very necessary.
A lot of the complaints I hear are related to abstractions, too, and I understand those to an extent. I also think that if you're complaining about the lack of generics, maybe you're not thinking about interfaces correctly. A perfect example is the beautiful simplicity that is the [`io.Reader`](https://medium.com/@matryer/golang-advent-calendar-day-seventeen-io-reader-in-depth-6f744bb4320b).
So, whenever someone says something like the OP:
> As a simple language to wire pieces together, Golang is fine, but it really doesn't give you the tools (imo) to be productive for anything more complex than simple apps.
I take that as meaning, "I've never seriously used Go beyond writing a hello world app. It doesn't have the features I rely on to be productive, therefore I don't think it's a capable language for larger projects."
I will say though,…
The section that you're linking to in Effective Go does not state anywhere that "branches are clunky, they are so intentionally to make the points where your code branches much more obvious".
It only promotes the concept that of using guard-clause style ifs that return an error and have no else clause, gradually eliminating each possible error condition.
This style of programming was later termed in the Go community "Aligning the happy path to the left"[1][2]
[1] https://medium.com/@matryer/line-of-sight-in-code-186dd7cdea88
[2] https://maelvls.dev/go-happy-line-of-sight/
I wholeheartedly agree that this style makes code more readable and I have adopted it - and encouraged others to adopt it - in other languages besides Go. This style would stil be followed with the try or check/handle proposals. For instance, let's take the example:
f, err := os.Open(name)
if err != nil {
return err
}
d, err := f.Stat()
if err != nil {
f.Close()
return err
}
codeUsing(f, d)
f.Close()
return nil
With the simple try proposal that would be:
f := try(os.Open(name))
defer f.Close()
d := try(f.Stat())
codeUsing(f, d)
With OP's variant of the try proposal, using a question mark this is arguably even more readable (less noisy):
f := os.Open(name)?
defer f.Close()
d := f.Stat()?
codeUsing(f, d)
As you can see, these proposal reduce - not increase - nesting, so they are completely in line with what Effective Go is saying.
> error check has == instead of !=
And you return the error as-is if its nil; seems fine to me?
Your second example is also a bit... far-fetched because it tries to be smart by putting the error checking in the same line. There's also the "line of sight" guideline which states that the happy path should be left-aligned, while edge cases - like errors - should be in indentation [0]. See also the law of least astonishment [1], which your final example does not conform to.
Yes it will compile and yes it is an issue but that's not because of the language. It's because of an attempt at cleverness by writing more compact, of having the happy path in an if, and (arguably) by reusing variable names - but this seems pretty common, and longer variable names like idk, `osOpenError` and `fileStatError` seem to be discouraged in favor of reuse.
So I'm not sure if you're getting your point across like this.
[0] https://medium.com/@matryer/line-of-sight-in-code-186dd7cdea88
[1] https://en.wikipedia.org/wiki/Principle_of_least_astonishment
This is exactly correct. The cost of holding the entire object in memory, for any significantly complex statement, is going to add up quick if you're dealing with large numbers of users/pageviews/etc. The cost of pre-calculating everything in the object for any complex math similarly gets large when you're dealing with something large-scale. Early returns matter a lot with scale, and make [code line of sight](https://medium.com/@matryer/line-of-sight-in-code-186dd7cdea88) much clearer -- which matters a lot of if your team scale is larger, as in many companies that might have multiple teams working on one shared codebase.
Good linters correctly highlight all missing semicolons as a symptom of sloppy, negligent, careless programming (or pointless syntactic showboating). It's also technically syntactically valid to omit braces around single statements after if, else, for, and while statements, but idiotic and dangerous to do that, too.
https://wiki.c2.com/?FixBrokenWindows
https://www.rtuin.nl/2012/08/software-development-and-the-broken-windows-theory/
https://blog.codinghorror.com/the-broken-window-theory/
https://medium.com/@matryer/broken-windows-theory-why-code-quality-and-simplistic-design-are-non-negotiable-e37f8ce23dab