Twelve Go Best Practices(talks.golang.org) |
Twelve Go Best Practices(talks.golang.org) |
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 level. By the third my brain is trying to crawl out my eyes and strangle me, even on code that doesn't have a potential exit point at every level.But that style doesn't make much sense in Golang. It makes even less sense in Ruby and Python, which have structured exception handling; if you're coming from Pythonistan, the idea of nesting conditions is probably entirely alien.
something like:
invswitch err !=nil {
case 'err := binary.Write(w, binary.LittleEndian, int32(len(g.Name)))': return err
case _, err = w.Write([]byte(g.Name)): return err
case err = binary.Write(w, binary.LittleEndian, g.Age): return err
default: return binary.Write(w, binary.LittleEndian, g.FurColor)
}Back at Microsoft, the following pattern was used quite extensively in the OS group:
int someFunc() {
DWORD error = ERROR_SUCCESS;
error = foo();
if (error != ERROR_SUCCESS) {
goto Clean0;
}
error = bar();
if (error != ERROR_SUCCESS) {
goto Clean0;
}
....
Clean0:
return error;
}I find deeply nested code ugly and unreadable, others nest eight levels deep and love it. Some people even claim they find parentheses soup (LISP-like syntax) readable. Personally, I cannot comprehend that.
I find that anything more verbose (EDIT: I'm having difficulty thinking of the right word for what I mean here - hopefully you understand anyway) is useful while you're writing the code, but actually increases the difficulty and length of time to understand what's going on when you come back to it.
Symbolic representations of relationships - including parentheses among many others - just make more instant sense to me. To the extent that a characterisation of the Lisp family as "parenthesis soup" just does not make sense to me -- it implies an arbitrary jumble of symbols, while for me, as none of the parentheses could possibly be moved, they're in a perfect and immovable pattern.
I'll admit, though, that without [ and ] I would sometimes be more than a little lost.
I think I might enjoy an APL-like language if I ever had time to learn it...
Lisp is read like python, you read indentation, ignore the parenthesis.
From a pedagogical point of view, showing the first transition from the anti-pattern is a good practice, because once you learn that, those first transitions are composable. So, while I agree that the correction for this isn't the best end-state, its an appropriate way to show how to correct the nested-conditionals-with-error-returns anti-pattern.
Its pretty much the natural naive composition of conditionals. I think eventually most people come to the point where the rightward-marching blocks become irritating and they look for ways to avoid them, and in the case of conditionals where one branch ends in a return there is an easy way to do that, but that particular case (and, thus, the solution) may not be as familiar to people coming from languages where error returns aren't idiomatic (though you do run into it elsewhere, just not as often, so its less likely to result in deep nesting if you don't pay special attention to it.)
It would help very much if editors supported this better (e.g. single keystroke inversion of an "if"). Go with its easy syntax would be a particularly good target for automatic rewrites for code like the above, or even just editor-level hints for better style.
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 use Go for anything. What a huge step backwards.
Most of the time when error checking, returns (or in C, gotos) are fine and lead to more readable code that's easy to make sense of and easy to step through with a debugger.
Please don't fall into the trap of believing that I am
terribly dogmatic about [the go to statement]. I have the
uncomfortable feeling that others are making a religion
out of it, as if the conceptual problems of programming
could be solved by a simple trick, by a simple form of
coding discipline!
- Dijkstra (1973) in personal communication to Donald Knuth , quoted in Knuth's "Structured Programming with go to Statements"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.
There is nothing wrong with multiple exit points as long as:
a. the language has a mechanism for scoped resource allocation (ie. defer, finally, with, unwind-protect or "RAII")
b. the early exit doesn't happen in the middle of a long and complex function
Because a lot of people make up code as they go a long.
Think of it as story telling vs. ordering. When story telling something happens and the your characters/code react to the outcome of that event, but stays in the same context.
Instead it would be better to write code as your being ordered around by an old drill sergeant. He'll always tell you to do one thing and one thing only. When you've completed that task, you given the next instruction and are no longer in the context of the previous action.
It's something I thought about often when reading code, both that of others and my own and it's the best explanation I've been able to come up with.
(Of course, good programming languages provide a better solution than pyramid-of-doom nesting)
vi!
Naïve, absolutist positions in areas of long-standing debate between programmers of great experience and the highest imaginable competence just makes you look ridiculous.
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.
[citation needed]
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...
1. The file I/O makes the case for including exceptions in the language. Specifically, adding one-off types to deal with exceptions is a bug, not a feature. There is a good case against exceptions but that ain't it.
2. On slide 5, it appears to show that you have to use a switch statement on a generic to get polymorphism because the language doesn't support overloading. Again, looks more like a bug than a feature.
Also, is the "break;" implicit in Go? At first glance, it looks like a coding error.
func (g *Gopher) DumpBinary(w io.Writer) error {
err := binary.Write(w, binary.LittleEndian, int32(len(g.Name)))
if err != nil {
return err
}
_, err = w.Write([]byte(g.Name))
if err != nil {
return err
}
err = binary.Write(w, binary.LittleEndian, g.Age)
if err != nil {
return err
}
return binary.Write(w, binary.LittleEndian, g.FurColor)
}
could be written like this: func (g *Gopher) DumpBinary(w io.Writer) {
binary.Write(w, binary.LittleEndian, int32(len(g.Name)))
w.Write([]byte(g.Name))
binary.Write(w, binary.LittleEndian, g.Age)
binary.Write(w, binary.LittleEndian, g.FurColor)
}
if the language supported exceptions.e.g. in http://talks.golang.org/2013/bestpractices.slide#25 , the first case in the select will trip regardless of which value arrives, yet the sender was still made to choose one.
A channel may be closed with the built-in function close; the multi-valued
assignment form of the receive operator tests whether a channel has been closed. [1]
Given that is available, why is the use of a separate bool quit channel preferred? switch v := v.(type) {
case string:
w.Write(int32(len(v)))
w.Write([]byte(v))
default:
w.err = binary.Write(w.w, binary.LittleEndian, v)
}
Great way to alter control flow based on the type, without a ton of ugly casts cluttering things up.I created these slides using code.google.com/p/go.talks You can actually find the source code and the slides in it.
https://code.google.com/p/html5slides/
But it's outdated. And the new version claims to support mobile.
_, err := potentiallyErroringOperation()
// with idiomatic nil
if err != nil {...}
// with monadic Option
if err.isDefined() {....}
EDIT: fix typoEspecially given that with Go's lightweight lambda syntax a functor type would be easy to work with, I'm disappointed with its exclusion.
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, or seeing it as trickier to implement and acceptable to do without in 1.x, that's quite valid.
Honestly, who thinks this stuff is a good idea?
You don't need the scroll wheel to navigate the presentation. Click on the right side of the slide to go forward, on the left side to go back.
> Honestly, who thinks this stuff is a good idea?
People who are making presentations to deliver in an in-person setting where putting them on the web for everyone is a secondary use, not the primary use? I mean, looking at that, it would be a lot better as the visual component of an in-person presentation than it is on its own (though its not without value on its own.)
I think it's a good idea, btw :-)
You can run code on your slides, check http://talks.golang.org/2012/concurrency.slide#14
http://golang.org/pkg/sort/ (See example 1 -- you have to write that for every concrete slice type you want to sort; it's not enough to write it once. And god help you if you also want to sort other collection types.)
I tried hitting space, and another slide scrolled in! But I wasn't done reading, so I hit shift-space, which is the common idiom to reverse the direction of space. But it scrolled in the same direction. Now I'm two slides behind. After some more thrashing, I found that I could use the arrow keys to navigate. Delete also goes back.
Please don't make me use trial and error to figure out your UIs, Google!
func init() {
http.HandleFunc("/", errorHandler(betterHandler))
}
func errorHandler(f func(http.ResponseWriter, *http.Request) error) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
err := f(w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Printf("handling %q: %v", r.RequestURI, err)
}
}
}
func betterHandler(w http.ResponseWriter, r *http.Request) error {
if err := doThis(); err != nil {
return fmt.Errorf("doing this: %v", err)
}
if err := doThat(); err != nil {
return fmt.Errorf("doing that: %v", err)
}
return nil
} if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
all over the place, as the error return type isn't part of the standard Handler signature.The decorator approach lets you return errors from your handlers, and then have your actual error handling centralized; you may want to send an e-mail to the ops team, send it to a third party exception management web service, etc.
Well, Go 1 has some of the problems of Java 1: no generics, typecasts from interface{} here and there, simplistic GC. Reasons are probably similar: this all is good enough for version 1, and can later be improved upon.
> is the "break;" implicit in Go?
"break;" is implicit in Go.
My only problem with using generics in this context is that you can't catch type-conversion errors at compile time.
Seems like a step backwards with only downside. I get why exceptions are a double-edge sword. I'm not clear on why undermining compile time type safety is an feature.
class Gopher:
def dump_binary(self, writer):
"""Write this Gopher to given writer, raise IOError on error."""
binary.write(writer, binary.LittleEndian, len(self.name))
writer.write(self.name)
binary.write(writer, binary.LittleEndian, self.age)
binary.write(writer, binary.LittleEndian, self.fur_color)
I can see the argument for Go-style explicit error handling, but having this as the first best-practice example just doesn't sit right or sell it very well.Edit: Okay, I guess I should have read the next slide before commenting. Still, the "one-off utility type" is longer and more complex than the original error handling (so I wouldn't do it unless you're using it elsewhere as well).
Exceptions are included in the language, they are called panics, and the go convention is that libraries don't expose them in the public interface, but can use them internally (and, of course, application code can use them.)
> Specifically, adding one-off types to deal with exceptions is a bug, not a feature.
One-off types aren't used for error handling in the example, they are used for abstracting a writing pattern that works like binary.Write for writing non-string values and like io.Writer#write for string values. Sure, the special type's write method also swallows errors, but, except that the mechanism by which it swallows errors would look different, the use of the one-off type and its write method would be pretty much the same with exceptions/panics as with error returns from the underlying library functions.
In his example, he uses a one-off type to isolate the caller from having to explicitly check if each individual write failed. I got no problem with that.
But it seems like it a work-a-round.
It's just an odd choice for an example. The take-a-way seems to be that the basic class libraries need a wrapper that makes them easier to work with and that, you the developer, should build these wrappers so you know exactly what the policy is.
The substantive difference between Go and, e.g., Java with regard to exceptions is that Go builtin and standard library functions panic in a much narrower range of circumstances than Java's standard library. Go seems to prefer that the decision that an error condition is treated as a panic is generally left to user code that is written with more awareness of what is exceptional in the context of the role of that code than standard library code has.
So, you could write the code in pretty much exactly the way you propose in Go; you'd just need to write a wrapper function around binary.Write that panics on errors.
That makes little sense in the context of ioWriters and, frankly, most contexts.
How often do you write code where you deliberately want to be oblivious of IO errors?
Technically, the language does support exceptions. That said, they're in the "please never use this, ever."
/pedant hat off
The spirit of your comment is right, however -- the wonky code resulting from error handling, just like the "compile error on unused vars or imports," is something most new Go users find jarring.
No, its not. The convention is that any use of panics within libraries should be internal, and that libraries' exposed interfaces should use error returns. [1] Use of panics internal to libraries, or use of panics within application code that is not creating a library for others to consume, is not discouraged.
func (g *Gopher) DumpBinary(w io.Writer) {
// Ignore all errors
_ = binary.Write(w, binary.LittleEndian, int32(len(g.Name)))
_, _ = w.Write([]byte(g.Name))
_ = binary.Write(w, binary.LittleEndian, g.Age)
_ = binary.Write(w, binary.LittleEndian, g.FurColor)
} func panicBinaryWrite(w io.Writer, b binary.ByteOrder, data interface{}) {
if err := binary.Write(w, b, data); err != nil {
panic("Error in binary.Write")
}
return
}
func panicWrite(w io.Writer, data interface{}) {
if _,err := w.Write(binary.LittleEndian,data); err != nil {
panic("Error in io.Writer#Write")
}
return
}
func ignoreErrors(f func()) {
defer func() {
_ = recover()
}()
f()
return
}
func (g *Gopher) DumpBinary(w io.Writer) {
// Ignore all errors
ignoreErrors(panicBinaryWrite(w, binary.LittleEndian, int32(len(g.Name))))
ignoreErrors(panicWrite([]byte(g.Name)))
ignoreErrors(panicBinaryWrite(w, binary.LittleEndian, g.Age))
ignoreErrors(binary.Write(w, binary.LittleEndian, g.FurColor))
} def ignoreExceptions[A](a: => A): Unit = try {a} catch {case _ =>}
def dumpBinary(g: Gopher, w: Writer) = {
ignoreExceptions binary.Write(w, binary.LittleEndian, int32(len(g.Name)))
ignoreExceptions w.Write([]byte(g.Name))
ignoreExceptions binary.Write(w, binary.LittleEndian, g.Age)
ignoreExceptions binary.Write(w, binary.LittleEndian, g.FurColor)
}
Though honestly I think a better solution is monads. //returns Validation - either success, or the first error (which stops processing)
//return values are directly accessible, because later code won't run unless earlier code succeeds
for {
_ <- binary.Write(w, binary.LittleEndian, int32(len(g.Name)))
_ <- w.Write([]byte(g.Name))
_ <- binary.Write(w, binary.LittleEndian, g.Age)
_ <- binary.Write(w, binary.LittleEndian, g.FurColor)
} yield {}
//Runs all the operations, ignoring errors
//type system will force you to check a return value before you can use it
binary.Write(w, binary.LittleEndian, int32(len(g.Name)))
w.Write([]byte(g.Name))
binary.Write(w, binary.LittleEndian, g.Age)
binary.Write(w, binary.LittleEndian, g.FurColor)You'll always be in control of your code's control flow that way. You'll never have some random library 5 levels beneath your code throw an exception that you didn't know about, causing your function to return prematurely, resulting in your function accidentally leaving some file handle open, a mutex locked or some similar problem (I realize in Go this should be handled via defer anyway, so would probably not be an issue in practice). In short, you know exactly what error cases you should be thinking about and are forced to explicitly reason about whether or not you care about it.
A few languages fix some of these concerns with checked exceptions, but checked exceptions have their own limitations and drawbacks. Of course, regardless of the approach taken, lazy programmers will always do the minimal amount of effort required to ignore errors/exceptions.
http://golang.org/src/pkg/text/template/helper.go?s=576:619#...
func must(err error) {
if err != nil { panic(err) }
}
func (g *Gopher) DumpBinary(w io.Writer) (err error) {
defer func() {
err, _ = recover().(error)
}
must(binary.Write(w, binary.LittleEndian, int32(len(g.Name))))
must(w.Write([]byte(g.Name)))
must(binary.Write(w, binary.LittleEndian, g.Age))
must(binary.Write(w, binary.LittleEndian, g.FurColor))
return
}Here is how you would return all leaf values in a binary tree in Scala:
case class Node
case class Fork(left: Node, right: Node) extends Node
case class Leaf(value: Int) extends Node
def getValues(node: Node): List[Int] = node match {
case f: Fork => getValues(f.left) ++ getValues(f.right)
case l: Leaf => List(l.value)
}
EDIT: Would actually be more idiomatic in Scala to extract the values from the case classes rather than just matching on type...yet another wonderful feature of pattern matching. Coded as follows: def getValues(node: Node): List[Int] = node match {
case Fork(left, right) => getValues(left) ++ getValues(right)
case Leaf(value) => List(value)
} @tailrec
def getValuesFromNode(node: Node, accValues: List.empty[Int], nodeList: List.empty[Node]): List[Int] = node match {
case Fork(left, right) => getValuesFromList(accValues, nodeList ++ List(left, right))
case Leaf(value) => getValuesFromList(accValues ++ List(value), nodeList)
}
@tailrec
def getValuesFromList(accValues: List.empty[Int], nodeList: List.empty[Node]): List[Int] = nodeList match {
case x :: xs => getValuesFromNode(x, accValues, xs)
case _ => accValues
}The compiler should be making these checks, not the developer.
I think it gets more interesting when using several, often complex (struct) types in the same switch statement.
v is not being reassigned, it is being declared (note := rather than =). But its a little complicated, because type switches are a special syntax construct that is similar to, but different than, regular expression switches. See, "Type switches" in the language reference [1]
[1] you'll have to scroll down a little within the section on Switch statements: http://golang.org/ref/spec#Switch_statements
Perhaps it would have been cleaner to use "*" or some other operator symbol instead of the reserved word "type"
Over time programmers have learned that one entry point, multiple exit points, is OK. But I feel that it is a good habit to flag that with a comment in capital letters so that someone skimming the code can't miss it.
Just being pedantic here. RAII is more specific than the defer statement that Go offers. Strictly speaking, defer is not RAII.
Except I doubt we're going to see generics in Go version 2, whenever that may be. The sentiment against it has been pretty strong in the golang-nuts mailing list.
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.
I think this is what people will ultimately focus on when considering Go. Many of the complaints are a product of type weaknesses in the language, voiced by people who had assumed that a modern static language wouldn't have that fault. Others tend not to mind because they lack that expectation, and regard the dynamic behaviour you can get as a feature. The argument about shared mutable state goes the same way, but for some concurrent but non-parallel code it might be convenient.
I can easily see people picking Go when moving from Python. But not when moving from a static language with a stronger, safer type system.
Also, as burntsushi points out, it does require more sophistication in the type system. I doubt they're trying to sell a naively simplistic type system (sophistication often makes it easier to use), but when Go was announced the feature they seemed to be selling the hardest was short compilation times. I think that feature is the seed of this behaviour.
More safety usually requires a more complex type system. Such a type system is usually more expressive and can make more guarantees about your program, which is a pro. But of course, it is also more complex, which is a con.
Another option is monadic style, which will pass error checking along. I believe it will work well for this example, and can be implemented in Go, most likely (I don't know Go, bur it seems so).
Of course, the real problem with this code is that it is not decomposed properly. Nested error checking is the first sign of it, as somebody else already rightfully noted in the thread.
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.
I would think that some of the most important best practices would relate to the best means of dealing with situations where the approach users coming from other languages might naturally seek to apply are not the most appropriate, either because the other-language feature they are likely to have used does not exists (or works differently) or because of features in the target language that allow a better approach than in other languages.
> In his example, he uses a one-off type to isolate the caller from having to explicitly check if each individual write failed. I got no problem with that.
You'd do that if the library function threw exceptions, too. Eliminating repeated try/catch blocks (or calls to inline functions with deferred recover calls in go) and eliminating repeated if/then blocks are pretty much the same thing.
I want to like that language but I just keep going seeing things that make me pause.
Really I'd like a C-dull or a C-- (keep a small subset and get close to the metal as C)
The existence of error returns means that the IO library function "not panicking" and the calling code being "oblivious of IO errors" are not equivalent.
I think the motivation for the Go convention of keeping panics internal and reducing to error returns in library APIs minimizing the potential downsides of the way unchecked exceptions are not part of the declared interface of functions and yet have a major effect on control flow. A convention of using panics (which amount to unchecked exceptions) only within logically bounded units is, IMO, a sensible approach to this.
(You could do this with checked exceptions, which require additional syntax in declarations. When you already have support multiple valued returns, I don't see that checked exceptions get you much that's worth making signatures more complicated.)
Seriously?
Does your test-suite exercise every potential I/O error and timeout on every single of your I/O calls?
They're just a little too easy to abuse and are often used for non-exceptional cases. So while checked exceptions improve on exceptions, they are still exceptions at the end of the day.
The solution I prefer is a monad with some light notation (Haskell do, scala for/yield). So it looks like:
def doSomething(): Validation[Whatever] =
for {
result1 <- callThatCanFail()
result2 <- callThatCanFail2(result1, 4)
intermediate = callThatCantFail()
result3 <- anotherCallThatCanFail(result2 + 4, "Hello", intermediate)
} yield result3 + result1
The <- syntax clues you in that this computation is going on in some kind of "context" - if you're working with futures it might happen on a different thread, if you're working with collections you're iterating over the elements. Here we're working with a validation, and our computations only happen if the previous one succeeds.Is this goto-like? You could argue so - once one computation fails, the rest turn into noops and we pass immediately to the end. But to my mind this is like replacing an if/else with polymorphism - our validation context is an object with certain behaviours, and we're calling methods on it with our functions as parameters, which will behave differently depending on which subtype our context instance is.
Compare how smalltalk didn't have a conditional control flow statement. Rather, the boolean type has a polymorphic method that takes a block and then executes it or not.
The other nice thing about this approach is it makes the language simpler and more regular because it's implemented using standard language constructs rather than a special statement. E.g. you can write a "gather" function that takes a collection of monads and returns a monad of the collection, and this is generic in the monad (so the same function works to turn a List of Futures into a Future of a List, a List of Validations into a Validation of a List, etc.)
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.
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 to do a check against the error or we will run this logic even if the write did not work. This is an incredibly easy trap to fall into.I'd go as far as to call that an anti-pattern - you are hiding the control flow in non-obvious ways.
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 || y
In English: if x (still) equals its default, evaluate y and assign the result to x"One could even shortcut this to
err = Write(int32(len(g.Name)))
|| Write([]byte(g.Name))
|| Write(g.Age)
|| Write(g.FurColor)
return err;
Advantage: that's how the shell works, too. Disadvantage is that 'logical or' is not what one associates with "run items until first failure", but that can be learned.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. That would get you something like:
index,err = SEQ([
Write(int32(len(g.Name)))
, Write([]byte(g.Name))
, Write(g.Age)
, Write(g.FurColor)
]);
I like the first idiom better, though. do := func(n int, err error) {
if err != nil {
panic(err)
}
}
do(string.Write(/* 1 */))
do(string.Write(/* 2 */))
// ...
do(string.Write(/* n */))
In fact, the pattern I've found is to declare throw-away lambdas [2] that I then call (few lines later). I haven't looked at all the pros/cons of doing this, but so far I find it's a very versatile way of doing.[1]: https://github.com/aybabtme/graph/blob/master/digraph.go#L61 [2]: https://github.com/aybabtme/graph/blob/master/digraph.go#L10...
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?
As long as you don't leak panics outside of your package, it's OK to use them for non-local error returns.
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.
By and large, the best programmers eschew nesting in favor of early returns. Invariably (in my experience) those who argue against early returns are inferior programmers (and not only by virtue of lacking taste in this particular debate).
Yup.
> Invariably (in my experience) those who argue against early returns are inferior programmers (and not only by virtue of lacking taste in this particular debate).
Yup.
def with_error_handler
if error = yield
puts "error: #{error}"
end
end
def do_things
error = do_this
return "error doing this: #{error}" if error
error = do_that
return "error doing that: #{error}" if error
nil
end
with_error_handler do
do_things
end
Having said that, I did need to read the Go version more than once to grok it. I theoretically like Go's syntax for defining functions that take functions, but in practice I find it quite hard to scan, especially if there are more parameters on top of the function, or the passed function has multiple returns, or (god forbid!) it takes a function itself - it can all become quite a lot of bookkeeping.Which are pretty important, and not specific to python.
You'd want generics if you wanted the chain to return a result though (or if you wanted intermediate steps to thread results through).
Do you really belive that only people working on low level register transfer level things "accomplish things"? That's souds like a very old assembler argument.:-)