Live data from Hacker News

Twelve Go Best Practices

talks.golang.org

101–110 of 153 posts

Re: Twelve Go Best Practices

#101
post #80

if err == nil { _, err := w.Write([]byte(g.Name)) if err == nil { err := binary.Write(w, binary.LittleEndian, g.Age) if err == nil { return binary.Write(w, binary.LittleEndian, g.FurColor) } return err } return err } Why does anyone have to tell people not to do this? How does it enter anyone's mind as a thing to do in the first place? I've been known to go too far to minimize nesting. I get twitchy at the second lev…

Short circuit returns are the devil - they make it much harder to factor out part of a function into a smaller function. A function should have one entry point and one exit point; that's the whole point of structured programming. If you're going to return from some random point in the middle of your function you might as well be using goto. (Of course, good programming languages provide a better solution than pyramid…

> Short circuit returns are the devil

No, they're not.

> they make it much harder to factor out part of a function into a smaller function

They generally make the function smaller, that's what guard clauses are for.

> A function should have one entry point and one exit point

That's absurd and makes for very ugly and unnecessary code.

Re: Twelve Go Best Practices

#102
post #92
post #84

Earlier quoted context omitted.

No way, the next slide is the very definition of evil unmaintainable code . Go through what is happening quickly: bw := &binWriter{w: w} bw.Write(int32(len(g.Name))) bw.Write([]byte(g.Name)) bw.Write(g.Age) bw.Write(g.FurColor) return bw.err If an error happens on L2, we will still run writes L3-L5. Why is this bad? Because in the future, we might come in and add logic after the writes complete. We have to make sure…

I would prefer to see something like (probably not legal Go, but would work in C, if err is of type bool): err = Write(int32(len(g.Name))) err |= Write([]byte(g.Name)) err |= Write(g.Age) err |= Write(g.FurColor) return err; If err is of type int with 0 == noError, this will require some new syntax if you want to return the correct error code (which you should want to do). I would suggest introducing x ||= y x = x ||…

So basically, monads.

Re: Twelve Go Best Practices

#103
post #78

Earlier quoted context omitted.

Of all the code in there this part confused me. What exactly is being switched on? It looks like v is being reassigned to the type of v, then the type of v is written out (instead of the value).

"type" is a magic word in Go, and in that example. It's highly idiomatic -- it's inconsistent with the rest of the language (Using "type" instead of an actual type), but it makes sense once you memorize the idiom. Sort of perlish -- there are two different operations that look basically the same, and the correct one is chosen based on context (in this case, the context is "is there a type name, or "type" literally?)…

> "type" is a magic word in Go, and in that example.

Specifically, type switches are a specific syntax construct that look very similar to normal switches, but switched on something that looks like a type assertion with "type" in place of an actual type.

Re: Twelve Go Best Practices

#104

if err == nil { _, err := w.Write([]byte(g.Name)) if err == nil { err := binary.Write(w, binary.LittleEndian, g.Age) if err == nil { return binary.Write(w, binary.LittleEndian, g.FurColor) } return err } return err } Why does anyone have to tell people not to do this? How does it enter anyone's mind as a thing to do in the first place? I've been known to go too far to minimize nesting. I get twitchy at the second lev…

Because the alternative is worse. If you test-and-return after every call, your function has multiple exit points and is far less maintainable. If you nest like this, you at least have a chance of maintaining a single exit point in your function (even though this example fails to do so). This is why the Lord invented exceptions, which it seems that Go does not use. This one example is enough to convince me to never u…

> your function has multiple exit points

That's a good thing.

> and is far less maintainable

I'd say it's more maintainable.

> you at least have a chance of maintaining a single exit point in your function

Maintaining a single exit point is IMHO an anti-pattern; it makes ugly unmaintainable deeply nested code.

Re: Twelve Go Best Practices

#105
post #80

if err == nil { _, err := w.Write([]byte(g.Name)) if err == nil { err := binary.Write(w, binary.LittleEndian, g.Age) if err == nil { return binary.Write(w, binary.LittleEndian, g.FurColor) } return err } return err } Why does anyone have to tell people not to do this? How does it enter anyone's mind as a thing to do in the first place? I've been known to go too far to minimize nesting. I get twitchy at the second lev…

Short circuit returns are the devil - they make it much harder to factor out part of a function into a smaller function. A function should have one entry point and one exit point; that's the whole point of structured programming. If you're going to return from some random point in the middle of your function you might as well be using goto. (Of course, good programming languages provide a better solution than pyramid…

> A function should have one entry point and one exit point; that's the whole point of structured programming.

I think a reasonable primary source for "the whole point of structured programming" is Dijkstra's "Go To Statement Considered Harmful"[1]. It's a short article and once you get past his writing style, his point is simple and lucid.

In the first part he lays out a simple question: given a program text, how much information do you need to track of to correctly identify where the program is currently executing at some point in time and how it got there? Roughly, if you were paused in the debugger, how much state does the debugger have to hold in order to be able to resume where it left off?

He's asking this because the smaller amount of state required for this, the easier it is for a human to look at a program text and figure out what can happen while it's running dynamically.

If all your language had was assignment statements, it's simple: you basically just need a single line number. Adding "if" and "switch" for branching doesn't add any more complexity. And, of course, reading code like this is pretty trivial.

If you have procedure calls (and by implication, recursion), you need a stack of those numbers, which is exactly what the callstack in your debugger holds.

When you add "while" and "for" for looping, you need to keep track of how times you've gone around the loop.

Now, if you add "go to" everything goes to hell. If you're on line 10, did you get there because you were on line 9 before, or because you jumped to it, or some random combination of those? It's a total mess.

Then he says:

> I do not claim that the clauses mentioned are exhaustive in the sense that they will satisfy all needs, but whatever clauses are suggested (e.g. abortion clauses) they should satisfy the requirement that a programmer independent coordinate system can be maintained to describe the process in a helpful and manageable way.

Here he's saying you can add any other "clauses" (flow control constructs) to a language that you want as long as you don't add to the amount of state you need to store to keep track of how you got there.

For example, adding an "unless" statement that works like "if" but only has an "else" clause instead of a "then" clause is peachy. You don't need any additional data to keep track of where you are.

You know what else doesn't require adding any additional data? Early returns.

So as far as Dijkstra is concerned, no, avoiding early returns is not at all the point of structured programming.

[1]: http://www.u.arizona.edu/~rubinson/copyright_violations/Go_T...

Re: Twelve Go Best Practices

#106
post #92
post #84

Earlier quoted context omitted.

No way, the next slide is the very definition of evil unmaintainable code . Go through what is happening quickly: bw := &binWriter{w: w} bw.Write(int32(len(g.Name))) bw.Write([]byte(g.Name)) bw.Write(g.Age) bw.Write(g.FurColor) return bw.err If an error happens on L2, we will still run writes L3-L5. Why is this bad? Because in the future, we might come in and add logic after the writes complete. We have to make sure…

I would prefer to see something like (probably not legal Go, but would work in C, if err is of type bool): err = Write(int32(len(g.Name))) err |= Write([]byte(g.Name)) err |= Write(g.Age) err |= Write(g.FurColor) return err; If err is of type int with 0 == noError, this will require some new syntax if you want to return the correct error code (which you should want to do). I would suggest introducing x ||= y x = x ||…

You can panic inside Write() and recover at the top of the DumpBinary() function. This has been demonstrated by Rob Pike and Andrew Gerrand in a talk at Google IO.

As long as you don't leak panics outside of your package, it's OK to use them for non-local error returns.

Re: Twelve Go Best Practices

#107
post #80

Earlier quoted context omitted.

Short circuit returns are the devil - they make it much harder to factor out part of a function into a smaller function. A function should have one entry point and one exit point; that's the whole point of structured programming. If you're going to return from some random point in the middle of your function you might as well be using goto. (Of course, good programming languages provide a better solution than pyramid…

> Short circuit returns are the devil No, they're not. > they make it much harder to factor out part of a function into a smaller function They generally make the function smaller, that's what guard clauses are for. > A function should have one entry point and one exit point That's absurd and makes for very ugly and unnecessary code.

> They generally make the function smaller, that's what guard clauses are for.

My point is: frequently, one wants to extract a part from the middle of a function to make a new function. This is very easy (and can usually be done automatically) unless said part contains a return.

Re: Twelve Go Best Practices

#108

Earlier quoted context omitted.

> Go occupies an interesting space. In my mind, I see it as competing simultaneously with C and Python. I suppose that the developers didn't see a place for an Optional type within that realm. I would assume that everything that hasn't been implemented in Go 1.1 is not implemented because the devleopers of Go "didn't see a place for" it. Now, either not seeing it as more important than the theings that did make it in…

You make a valid point, but for something as fundamental as nullability, I think that's baked into the core language spec. It's possible that we could see an Option type in the future, but the fact that it's not an integral part of the language now means it would be unreliable and defeats the purpose of eliminating NPEs.

> You make a valid point, but for something as fundamental as nullability, I think that's baked into the core language spec.

Well, its certainly out for 1.x; I wouldn't presume to assume how much or little flexibility there will be for 2.x if/when it happens.

> It's possible that we could see an Option type in the future, but the fact that it's not an integral part of the language now means it would be unreliable and defeats the purpose of eliminating NPEs.

Assuming that its not part of a breaking change, sure; but the no-breaking-changes pledge only applies to 1.x. If there is a 2.x, it will be because a need is seen for breaking changes.

I think that beyond a handful of core features, keeping Go 1.x small was a key goal, and getting real production usage experience with the small 1.x to decide on future directions.

Re: Twelve Go Best Practices

#109
post #92
post #84

Earlier quoted context omitted.

No way, the next slide is the very definition of evil unmaintainable code . Go through what is happening quickly: bw := &binWriter{w: w} bw.Write(int32(len(g.Name))) bw.Write([]byte(g.Name)) bw.Write(g.Age) bw.Write(g.FurColor) return bw.err If an error happens on L2, we will still run writes L3-L5. Why is this bad? Because in the future, we might come in and add logic after the writes complete. We have to make sure…

I would prefer to see something like (probably not legal Go, but would work in C, if err is of type bool): err = Write(int32(len(g.Name))) err |= Write([]byte(g.Name)) err |= Write(g.Age) err |= Write(g.FurColor) return err; If err is of type int with 0 == noError, this will require some new syntax if you want to return the correct error code (which you should want to do). I would suggest introducing x ||= y x = x ||…

> An alternative could be to have a language construct that takes a sequence of lambda's, executes them in sequence until the first one that fails (if any) and returns the index and the result code of the failing lambda.

You could write a function in Go as it is that takes a sequence of lambdas and does that, so why would you need a language construct?

Re: Twelve Go Best Practices

#110
post #84
post #20

Earlier quoted context omitted.

The very next slide cleans it up using a "utility type" which reminds me of the "monadic" Haskell solution.

No way, the next slide is the very definition of evil unmaintainable code . Go through what is happening quickly: bw := &binWriter{w: w} bw.Write(int32(len(g.Name))) bw.Write([]byte(g.Name)) bw.Write(g.Age) bw.Write(g.FurColor) return bw.err If an error happens on L2, we will still run writes L3-L5. Why is this bad? Because in the future, we might come in and add logic after the writes complete. We have to make sure…

The only problem I see is that binWriter is poorly named: it should be called unreliableBinaryWriter (alternatively, and possibly better, its Write method should be called UnreliablyWrite.)

The purpose of the type and its method is to provide a mechanism to (1) abstract different writing methods for different data types, and (2) silently swallows but records errors to provide a try-but-don't-care-if-I-fail write where you can check for errors later.

Its not a problem that adding logic after those writes that assumes that they were reliable produces incorrect behavior, though it is a problem that the code where the type and its method are used doesn't clearly reflect the intentionally unreliable nature of the operations.

Post reply on HN