Straight exceptions are also goto-like, but there's the slight improvement that they allow you to (automatically) refactor out the middle of a function without changing the behaviour.
The solution I prefer is a monad with some light notation (Haskell do, scala for/yield). So it looks like:
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.
I don't see how the <- clues you in any better than the error handling that things might exit early. It seems from your response that the goto-ness of the go solution is not what you're really objecting to, because your proposal is equally goto-y. As far as I can tell, it's really the if-else verbosity that you don't like, which is fine (and I agree, exceptions are better), but it has nothing to do with the "whole point of structured programming." And, unless I'm mistaken, structured programming also lacked exceptions in its initial formulation.
I think the main thing I'm objecting to is the possibility of multiple paths through the function. Doing it this way there's only one possible flow: a series of calls that pass blocks to validation objects (that then may or may not execute them).
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.)
The solution I prefer is a monad with some light notation (Haskell do, scala for/yield). So it looks like:
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.