Why Go is elegant and makes my code elegant
filippo.io
filippo.io
I like Go because it's practical. I can hammer out the solution to a difficult problem very quickly and that quick solution is reasonably scalable from a software engineering perspective[0]. Go makes it easy to write the first iteration, but its real strength comes from making it easy to write the subsequent n iterations.
That's "elegance" of some sort, but it's elegance in engineering, not in programming, which is an important difference.
You might say it's "elegantly practical"
[0] By "scalable" I don't necessarily mean performance, but the costs of scaling developer manpower on large projects - ie, a "quick-and-dirty" solution in some languages may require expensive refactoring down the line, or cause you to design bad interfaces in your functions, etc.
"I came to Go expecting the elegance of a ballerina, and was surprised to discover the grace of an experienced bar-room brawler."
* while "go get" is nice, it doesn't handle version pinning at all. So for a large team working on a go program, you need another solution. Luckily there are tools like godep[0] that will allow version pinning and putting it's source code into your projects root.
* debuggers (gdb) are only partially supported. Most people don't have problems with this, but occasionally it's an issue.
* Windows support is not as strong as Linux and OSX. But it still works surprisingly well.
EDIT: For the record, I'm using it on Windows
As for windows support, I should say that it's really just as good as the gnu toolchain support is on windows. There is no support for using Microsoft's toolchain on Windows. It can also take some work to make go apps not run within cmd terminal. I saw a blog post about it recently but I can't find it right now. But it also mentions things like the syscall[3] library docs on golang.org are actually for Linux, and it takes some steps to get them for other platforms, see the header comment here[4].
So Windows support is pretty good, but it is definitely not Google's focus. But if you've ever heard about what Windows is like within Google, there is a good reason for that.
[0] https://groups.google.com/forum/#!topic/golang-nuts/YG-APRPw...
[1] https://groups.google.com/forum/#!msg/golang-nuts/DS5ZGswXC6...
[2]https://code.google.com/p/go/issues/list?can=2&q=label%3AGDB
I wouldn't imagine writing a GUI app in Go, Go just isn't made for that and it would be silly since Microsoft's .NET tooling is so darn good. But Go for services and command line apps for Windows it has been fantastic.
That said, I still use Go all the time.
[0] http://www.abtinforouzandeh.com/2012/04/08/Do-Not-Use-Go-For...
For everything else, I think Rust is miles better than Go. It stole a ton of useful features from Haskell/ML and did a great job of putting them together. I actually want to use Rust instead of C++, whereas the only reason I ever really want to use Go is that its decent threading primitives and good standard library make for simpler web apps.
It isn't even good for that. There isn't a single use case where one of: D, ocaml, haskell, rust, or C isn't better than go. Go is the new java, a language that was inferior and obsolete when it was made. And like java, it'll probably be popular for a long time.
Rust seems nice, but its multitude of different pointers and mutable versus non-mutable data really makes for complicated code.
I work on Juju. It's over 200k lines of code spanning several applications that coordinate across multiple machines. The number of times generics would have helped the codebase I can count on a single hand... and none of it is huge stuff, just some very minor parts of the code.
There's no operator overloading, macros, or monkey patching, so you don't have to know about the entire state of the universe to know what one little function will do. There's no destructors or finalizers that magically get called sometime in the future.
There's no exceptions, so you never have to worry that the control flow of your program is going to jump all over the place. There's multiple returns, so you almost never have null pointer exceptions, because you always just check the error before using the value returned.
And of course, channels and goroutines make modern multithreaded code very easy to reason about, because they're just functions and a little pipe primitive. They don't do a lot of fancy schmancy stuff, and that's good. It means it's very easy to tell what go Foo() will do.
There's probably more I can't think of right now.
In what way? It's not a very featureful language, but that doesn't necessarily mean that using it is simpler.
It's certainly not simple to set up! Most languages, I can put my projects wherever I want. It took me a while to find out that I had to put my projects in my Gopath for them to properly compile with third party libraries. There are lots of weird restrictions like that.
>Everything is just pass by value.
Not entirely true. Goroutine capture is by reference, which is why a lot of new people have trouble with goroutines in for loops.
>There's no operator overloading, macros, or monkey patching, so you don't have to know about the entire state of the universe to know what one little function will do.
True, those things are gone, but the things that cause confusion most of the time are still very much present. Go, like most imperative languages, can suffer from global variable overload. Lots of Go code is also so badly typed that it's impossible to tell what things are supposed to be. I can't tell you how many times I've seen `x := poorlyNamedFunctionWTFIsThis()` or `foo(x {}interface){... x.bar()}`
>There's no exceptions
There are `panic()`s.
> There's multiple returns, so you almost never have null pointer exceptions
Rust also has multiple return (because Rust returns via hidden out-param), so I wouldn't really say this makes Go particularly competitive.
Rust also doesn't even have null pointers at all, at least in `safe` code blocks. Rust uses `Option` instead. (Like I said, lots of good features from Haskell/ML.)
>channels and goroutines make modern multithreaded code very easy to reason about
I agree, Go does this well, but let's be fair; a lot of modern languages have primitives like this.
> Rust also has multiple return (because Rust returns via
> hidden out-param)
Not quite. Rust does have multiple returns, but that's just the obvious result of supporting tuples in the language (to say nothing of tagged unions). The hidden out-param you speak of is just there to guarantee return-value optimization, allowing the compiler to construct return values in higher stack frames to avoid the overhead of copying.Yes, but it's (from a user standpoint) practically the same thing!
>The hidden out-param you speak of is just there to guarantee return-value optimization
Yes, which allows for the efficient return of arbitrary-sized structs, i.e. multiple return.
In the ways I described below that line :)
> It's certainly not simple to set up! Most languages, I can put my projects wherever I want. It took me a while to find out that I had to put my projects in my Gopath for them to properly compile with third party libraries. There are lots of weird restrictions like that.
Python, C, and C++ all have magic directories just like that. For ex /usr/local/lib/python2.7/site-packages/
At least Go lets you define where to put it yourself, and the paths are just the URL for the VCS.
> Goroutine capture is by reference, which is why a lot of new people have trouble with goroutines in for loops.
I think you're talking about what happens when you run closures as goroutines in a loop, like this: http://play.golang.org/p/1NX39zk4NI
This is just a misunderstanding of how closures work and what it means to start a goroutine. Yes, closures capture the variables so that modifications inside the closure and outside the closure both affect each other, that's the whole point of closures. Goroutines, when started, may not start right away, and actually generally don't, certainly not faster than this loop can run.
> Go, like most imperative languages, can suffer from global variable overload. Lots of Go code is also so badly typed that it's impossible to tell what things are supposed to be. I can't tell you how many times I've seen `x := poorlyNamedFunctionWTFIsThis()` or `foo(x {}interface){... x.bar()}`
You can write bad code in any language. I don't think that's an indictment of go.
> There are `panic()`s.
Panic is intentionally clumsy and is actively discouraged. Any package that panics as a form of replacement for exceptions will basically never get used by the community. That's a big difference from other major languages that use exceptions for error propagation and control flow.
> Rust also doesn't even have null pointers at all, at least in `safe` code blocks. Rust uses `Option` instead.
From what I've read this is actually just inferior to Go's multiple returns, because options don't tell you why they failed, unless you use multiple returns, in which case it's basically exactly like Go.
Go:
val, err := foo()
if err != nil {
return err
}
Rust: let (opt, err) = foo()
match (opt) {
(None) => return err
}
Is that really any better? In Rust you have to know what match does, what this magic (None) => business is... whereas the Go code just reads like normal C-ish code. Anyone who's ever taken intro to programming would be able to tell you what the Go code does. To understand the Rust code, you have to know a lot of the language internals. And that's exactly what I mean about Go code being simple and easy to understand.> a lot of modern languages have [concurrency] primitives like this.
Not any of the mainstream languages. C, C++, Python, Ruby, C#, Java, Javascript .... some of them can do similar things, but it takes a lot more boilerplate and it's a lot uglier and harder to reason about.
[Edited for clarity]
> options don't tell you why they failed
Options aren't built into the language, they're just a standard library type that any user could define themselves, and we don't recommend them as a replacement for robust error handling anyway. For that we have the Result type, another type that's defined in the standard library, which does indeed contain an error message. > you have to know what match does,
> what this magic (None) => business is...
`match` is just a beefed-up switch statement. And `None` is, to reiterate, an essential component of the standard library, and not magical in any way.Your Rust code also shows that you've never actually used it before. Here's a proper translation of the Go code into Rust:
let result = foo();
if result.is_err() {
return result;
}I'll speculate that he hasn't used a language with a usable sum type either, because I'd expect people who have been exposed to this would take issue with the idea of expanding every 5 line method to 40 lines of mostly error handling.
OP is using panic in the Heartbleed script he wrote. Source highlighted here: https://github.com/FiloSottile/Heartbleed/blob/master/bleed/...
Sum, Max, Min are pain points that surprised me indeed.
As for error handling I argue that it's a good thing. Really, it forces to decide what you do with your errors, and snippet expanders are there for a reason if you need to handle them all in the same way.
I doubt the parent post meant sum as you used it. Look up "sum type" or "tagged union". Explicit + concise error handling are not mutually exclusive.
Here's this in a really ugly language I would never recommend to anyone: http://stackoverflow.com/questions/3496304/how-do-i-process-...
And in a reasonable language: http://stackoverflow.com/questions/13503965/mapping-over-eit...
Edit: See also wting's comment below https://news.ycombinator.com/item?id=7562702
http://play.golang.org/p/7T2s8Dn0Sr
Ideally, on line 57 I'd pass in some kind of "first class" type representation that the switch statement could use to branch into the correct type, rather than a zeroed (or not, Go doesn't care) struct or a string. Admittedly, this could definitely be abuse of Go semantics, so I'm also interested in how I could refactor this into a more canonical Go pattern.
http://play.golang.org/p/vXJcMJ-j6P
Edit: I think I see what you're trying to do with the factory method. You're trying to enforce the ServiceProvider interface on every provider struct. You don't need to do that yourself, the compiler does that work for you. On line 57, you already know what kind of struct you're dealing with so you can safely call .connect() on the provider.
And if you need to pass on the provider to another method, just make sure the method only accepts structs with a ServiceProvider interface. The compiler will stop you when it's not the case.
you already know which provider you want to use, so why not just initialize the struct from there and skip the factory method altogether.
The reason that I want ProviderFactory to generate instances is because I also want it to act as a manager for those instances. ProviderFactory can keep track of all ServiceProviders and perform various operations or broker data between instances.
Here is my previous example slightly improved. http://play.golang.org/p/XoXu9x9r8f
Now, ProviderFactory injects configuration information into ServiceProvider instances based on their concrete type, before establishing a connection. A more complicated app might wrap the NewProvider function into a gorotune/callback that blocks each provider instantiation until a successful connection has been established. This is all beside the point though; all I want is something akin to a special "type" type that the compiler recognizes as a symbol for that type that the factory can use to evaluate which concrete type to produce.
Here's I would do it:
The same for the host they connect to. Shouldn't that be defined in the type? Like make WeatherProvider.Connect() call ProviderFactory.WeatherHost() to get the host to connect to.
You're doing everything inside out. What do the types even do in your example?
This isn't a Go problem, it's programmer problem.
Thanks, but just to be clear, this is not a sample from a real application, it's just a contrived demo designed to illustrate the pattern.
Why does the ProviderFactory hold the knowledge about the query interval for each provider?...Let the query interval be defined on the type, not in the factory.
As previously stated, the example is contrived. If this were a real application it could probably make sense to define the query interval on the specific type, but lets just say, for the sake of illustrating the idea, that the desired interval for a specific provider could depend not only on the type but also the total number of providers already allocated for a given type.
For example, perhaps the ProviderFactory might calculate the total number of WeatherProviders already defined, and reset the query_interval for all WeatherProviders to be (some_weather_provider_specific_constant * total_number_of_allocated_weather_providers). Whenever a new WeatherProvider is requested from the factory, the factory increases the query_interval for all WeatherProviders. In this way, the ProviderFactory acts as an automatic rate limiter for each provider by keeping track of each instance.
With the data structure you have, any time you add a new provider, you'd need to add a new case to that switch statement.
Well yeah, that's pretty critical to the "factory" role of the ProviderFactory, how else could the factory return a variety of types if it didn't have a case/branch for each signal that represents each type? Additionally, if individual logic is required for preparing a certain type of provider, the switch statement is there to accommodate the needs of the specific type before returning it.
That's like the polar opposite of separation of concerns.
How do you figure? In this example, the factory is concerned with all logic related to instantiating and appropriately configuring each instance depending on the type of instance, the state of the app or the state of other instances. I'm not sure what concerns are mixed here. Alternatively, I could export all that logic into a NewFooProvider function for each of the various FooProviders, but the idea is to encapsulate all that custodial work within the factory so that a Provider user only has to request their specific provider and that's it.
What do the types even do in your example?
Nothing. It's just an example.
The same for the host they connect to. Shouldn't that be defined in the type? Like make WeatherProvider.Connect() call ProviderFactory.WeatherHost() to get the host to connect to.
I think you're missing the point. Yes, yes, that design would work fine, but all you're really telling me here is "you don't need to use a factory in this example". What I'm saying is, when I want to use a factory (lets just assume that it makes sense in the design of my app, unless your argument is that a factory is never useful), I have to indicate which type of instance I want the factory to produce by using a zeroed struct or an arbitrary data type, what's going on within my factory is actually irreverent; any example factory snippet could be refactored into something less complex when your only context is 100 lines in a scratch pad.
You're doing everything inside out.
I'll seriously take that opinion into consideration.
Never? That seems a little extreme, no? It's a tool like any other and IMO, it's a sometimes useful idiom that works pretty much identically in Go as it does in other imperative languages. However, I'd be interested in any links you could provide which might elaborate on why a factory is never an optimal solution.
If you know what type you need, why would you need a factory to create it for you? Just create it.
I already explained why. The Factory abstracts away instantiation and configuration details for a complex or dynamic initialization process. I understand you don't agree with that approach, but it's fairly common.
(see page 5) http://www.cs.colorado.edu/~kena/classes/6448/f07/lectures/2...
(see accept answer) http://programmers.stackexchange.com/questions/81838/what-is...
but there's pretty much never a reason to have a function create a variety of types for you.
The capability to define a function's return type to as an interface type would suggest otherwise, but we'll have to agree to disagree in that regard. :)
Is there some reason you can't do that by just having this?
func IntializeFoo(f *foo)
The capability to define a function's return type to as an interface type would suggest otherwiseUh... if you know what type the function is returning you, why do you need it to return an interface? You don't need the interface until you pass it into a function that requires an interface, at which time, the type you've made will be converted for you... why convert before you need to?
Either the consumer should never need to know about types, in which case, you should probably just make it an enum:
type ServiceType int
const (
Weather ServiceType = iota
News ServiceType
)
type Service interface {}
func Service(type ServiceType) Service
Or if the consumer should know about the service types for some reason, you can use the Initialize method above... or to genericize it better, and remove any need for the factory to know about concrete types: func Initialize(svc Service)
If you can avoid having both sides know about concrete types, it'll make your life a lot easier.Or have I not understood your pain?
In Java, you could pass in WeatherProvider.class to tell it that you want it to produce a WeatherProvider instance or a string that said "WeatherProvider". Is that the kind of thing you have in mind?
They're not required -- indeed, I get by without them --, but the topic of the OP is elegance.
https://github.com/mediocregopher/seq
It provides clojure-like data-structures and functions for interacting with them (like map and filter). It's completely immutable and generic, and supports lazy operations.
Other languages, Nimrod for instance (http://nimrod-lang.org/system.html#515) can manage this while still keeping a simple core, including lots of type-inference. Having generics doesn't automatically turn your language into Java.
The map operator in Nimrod has a definition that is clear and simple:
proc map[T](data: var openarray[T]; op: proc (x: var T): S): seq[S]If you're not running into the issues that Go is trying to solve, you don't understand why its solutions are at least somewhat interesting, nor why its trade-offs are reasonable.
It thinks wrong. Making people duplicate code makes it harder to write and maintain large scale software applications.
Actually what you describe is the Java mentality exactly! That's why Java is full of code duplication and is supposed to help with code maintenance.
[Disclaimer: Googler here using Go, yaddi yadda, this is just my opinion]
Downvoting is not something you do merely because you disagree. Valuable, intelligent discussion of complex topics that matter requires some disagreement. Downvoting should be saved for comments that are inappropriate in some manner according to the values of the site.
Do you have any idea why this is happening? There seem to be a handful of people who have recently obtained the ability to downvote, and who are now downvoting any comment that find distasteful -- even ones that are rational and well-thought out. Most of the well-written downvoted comments are eventually upvoted, but these comments are often almost invisible during times of most exposure.
I've actually wanted to try to quantify this, but I can't figure out how to establish the pre-downvoting baseline, particularly since people seem to eventually upvote many of the downvoted comments.
Edit: To be clear, I'm not complaining about being downvoted. I've got more than enough karma, even if I cared about that. I'm complaining about lots of other people being downvoted for writing cogent, thoughtful comments. It's not a positive trend.
Here's a more recent comment from pg in which he describes ideal comments, and the collegial atmosphere he's been trying to promote for HN [1]. In my opinion, downvoting a well thought-out comment made in good faith is not very collegial.
If garbage comments are filtered out through endorsement, then, if you view of downvoting held true, there would not longer be any need for downvoting and the downvoting button should be removed. This is not what was proposed.
If only quality comments make it past the proposed endorsement filter, and the downvote button continues to exist, then the only purpose of downvoting would be to express disagreement.
If that's the case, I can almost guarantee you that your flags don't carry a lot of weight in HN's algorithmic ratings.
Do you know the rating algorithms?
Sample size of 1, but here's a story about what happens when things go wrong:
https://www.kickstarter.com/projects/2066438441/haunts-the-m...
The bit about packages from github.com being edited locally was particularly hair-raising. Really hope the smart folks working on Go figure out a resolution for this kind of thing.
There are practices and tools for insulating yourself from changes to third party packages. Generally the answer is to copy the code into your own repo so that you control when it gets updated. There are tools that will do this automatically for you, and it's what most big Go projects do.
Anecdotally, from using Go for a number of projects... As a rough approximation, I think Go's marginal return tops out at about 1000 lines of code. Past that point, syntactically it is hard to do abstraction (interfaces or generics).
As a result, I use it for scripting but not for larger projects.
What about you all?
https://github.com/shurcooL/Conception-go - Go, 7'500 lines
https://github.com/shurcooL/Conception - C++, 15'000 lines
Which would you rather work with? You can look at the commit activity to see my preference.
Also, I don't agree with your statement. If it were true, it would mean every language clearly sucks.
For example, Option types...
[1]: http://www.sics.se/~joe/thesis/armstrong_thesis_2003.pdf
I'm toying about a new languagte where each function have IN/OUT/ERR channels.
So, is possible to do:
result | err = open('file)
This consume the ERR channel, however, if is not:
result = open('file')
it "trow". Inside the open, any raise is like a return for the ERR channel.
This combined with RAII & scope like in D: http://dlang.org/exception-safe.html
Yes, you MUST. That is just how Go does things. If you don't do this, then of course things are going to blow up at runtime. Your "unless" is the same as saying in a language like Java: unless you rescue the exceptions, the program crashes! Yes, clearly.
Personally, I love Go's error handling. I made a joking tweet one time that over 10% of all lines of code in Packer are "if err != nil". That is still probably true, but I don't think its a bad thing.
We also make Serf, which is powering some pretty large infrastructures out there, and we've never ONCE had a crash in production. Not once. We've had errors, but they were logged and handled. I attribute this to the fact that we were forced to handle every error.
Some people like exceptions, but I've always liked Go's way of things in this department.
func DoFoo(Foo foo) Cat {
bar, err := GetBar(foo)
if (err != nil) { return nil }
baz, err := GetBaz(bar)
if (err != nil) { return nil }
cat, err := GetCat(baz)
if (err != nil) { return nil }
return cat
}
Option types add metadata to a type, essentially combining `bar` and `err` into a single variable. Let `Maybe x` mean a function will return `Something x` or `Nothing`. For example (in hypothetical Go): func DoFoo(Foo foo) Maybe Cat {
bar := GetBar(foo)
if (bar.(type) == Nothing) { return Nothing }
baz := GetBaz(bar)
if (baz.(type) == Nothing) { return Nothing }
cat := GetCat(baz)
if (cat.(type) == Nothing) { return Nothing }
return cat
}
Since this is such a common pattern, Haskell has a bind operator (`>>=`) to take advantage of option types by passing the output as the input of the next function if it's a `Something`, or return `Nothing`. func DoFoo(Foo foo) Maybe Cat {
return GetBar foo >>= GetBaz >>= GetCat
}
If you care about why something failed, use an `Either` instead of a `Maybe`. In a language that supports algebraic data types, you could create your own sum type instead.There's a reason why hideous Java is so successful while beautiful Haskell is a punchline to jokes.
http://www.oracle.com/technetwork/articles/java/java8-option...
Using option types produces more robust code and prevents null pointer exceptions / segfaults. Go's idiom of multiple return values and error is a poor man's Either, which is an improvement on C's return code idiom[0].
[0] C's return code idiom led to the latest GnuTLS bug:
http://blog.existentialize.com/the-story-of-the-gnutls-bug.h...
The one-liner would be harder for me to unpack, in most situations, than a 3-liner. You've got 3 calls, give me space to work with them.
do
a <- GetBar foo
b <- GetBaz a
c <- GetCat b
return cI'm not trying to sell anyone, code however you like -- I prefer the situation of "simpler environment, don't write bugs" to "really complicated environment that makes it theoretically impossible to write bugs". I bet I can still write bugs.
No. Remember we're talking about using a language that is not broken by design instead of go. You have one operator (>>=) and it works on one typeclass (Monad). Do notation (do and <-) is merely syntactic sugar that desugars to using >>=.
>I prefer the situation of "simpler environment, don't write bugs" to "really complicated environment that makes it theoretically impossible to write bugs"
And since those are not the options available, people tell you that those are not the options available. Doing it right is very simple. Your options are "simple and with lots of chances to create bugs" or "just as simple but with fewer chances to create bugs".
To someone who isn't familiar with that particular idiom, perhaps it causes more cognitive load at first. Sometimes you have to learn new ideas and techniques to make the best use of your tools.
On the other hand, once you are familiar with the idiom, what you see there is a concise and easily readable representation of the main algorithm, together with a quickly recognisable, robust way of dealing with the edge cases.
Would you also object to someone using structured programming because it causes way more cognitive load than just writing goto everywhere?
let foo = match some_call_that_may_fail() {
Ok(foo) => foo,
err => return err,
};
The advantage is that the language forces you to handle errors: dropping the error on the floor is quite hard to do, whereas "foo, _ = some_call_that_may_fail()" is very easy in Go. (And if the call doesn't return any value at all except an error, you don't even have to write the "foo, _ =" bit; this is a nasty gotcha and is why external lint tools like errcheck are needed for Go.)I haven't spent any time using Rust, though, so maybe that construction will seem more natural when I get around to doing that.
Anyways, someone upthread called Go 'simplistic' which has both positive and negative connotations, and I totally agree. I'm happy with a simpler setup that solves 80% of problems really well and maybe there are some pitfalls you can fall into if you don't check your error values. As long as I'm not completely hamstrung by the simplicity (generics would be nice), I find the lack of features to be a win when you add up all the non-features that I don't have to think about it.
If you think of it as a big-O problem, you're worried about the coefficient, and Go is worried about the exponent. On a large problem, what Go is worried about solving will matter more. But if you're not on a large problem, that 10% of repetition will chafe you...
Which features precisely have led you to believe this? I can't see any indication of that anywhere. You seem to be creating a false dichotomy: "caring about abstractions and caring about scalability are opposing goals", so that you can ascribe one side of a nonexistent spectrum to go.
>But if you're not on a large problem, that 10% of repetition will chafe you...
Except that is simply not the case. The larger the problem, the more that duplicate code causes problems.
If it's something I "must" do, why doesn't the language force it to happen? Silent failure is a really bad default.
Sooner or later, you have to trust the programmer to not be an idiot. If he/she is an idiot, the language can try to help, but can't really fix it.
Static checked exceptions let the programmer know that there's something that needs to be handled. That's good. It can check that the programmer put a catch block somewhere, but it can't guarantee that the programmer did anything appropriate to handle the problem inside that block. It's better than nothing, but it's not enough. (And if you're going to ask what is enough, I have to reply: Nothing. If the programmer is determined to ignore the fact that there's an error to handle, the language can't fix things.)
That's why you have code review and code linters. If you have a linter, there's no way in hell you'll ever miss this has happened.
Take a beginner's Go code ... do they check errors ? Just like with C the answer is plain and simple ... no, they don't.
And "our language is just bad as java" is a pretty awful sales pitch.
>That is still probably true, but I don't think its a bad thing.
No it is not a bad thing. It is a terrible thing.
>I attribute this to the fact that we were forced to handle every error.
But you aren't forced, that's the problem.
>Some people like exceptions, but I've always liked Go's way of things in this department.
Some people like proper error signaling and handling instead of either of the broken choices you suggest.
Even if this were true, it certainly doesn't lead to clearer code as you have your error handling code weaved into the mainline of the algorithm. Extracting error handling to the outskirts of the algorithm is a win, even if it costs a few extra lines of code.
Anyways, I think that weaving the error handling code into the mainline of your algorithm is in fact clearer. It's 100% clear what's happening at all times, and you don't have to speculate about where exceptions could create a hidden return. Maybe you feel differently, although I'd recommend trying it for a while with an open mind.
Go is made purposely dangerous, to force people to explicitly do the right thing. Java's enforcement of exceptions looks like the right thing on the surface, but it instead encourages people to handle exceptions in dangerous ways when they're in a hurry.
You could say the same thing about lambdas, continuations... basically any high level flow control concept. Higher level flow control concepts are a good thing in just about every case.
>Java's enforcement of exceptions looks like the right thing on the surface, but it instead encourages people to handle exceptions in dangerous ways when they're in a hurry.
This line of reasoning has never made sense to me. Forcing error handling is the right thing. Just because someone uses that information and does something boneheaded with it is not an indictment of the concept. An explicit reminder to handle an error case is loads better than non explicit. It's like blaming the static type checker for someone casting away type errors in a language like C. It's not a fault of the concept of static type checking if people fail to handle it properly.
"But it's a checked exception!" Like 90% of the code I've written in Java (and it's a lot) just says 'throws IOException' up top and moves on. Or, if that's not enough, we catch some dumb ones that shouldn't even be checked (URISyntaxException anyone) and wrap them with RuntimeExceptions. Effectively I've eliminated exceptions.
True, but programming, especially in the large, is about juggling a whole lot of cost-benefit calculations. Continuations done right actually reduce the implicit hopping around you have to think about in a given situation. Lambdas can increase it, but even in those cases, save you a different kind of cost.
Higher level flow control concepts are a good thing in just about every case.
No, they are often a good thing, but they are not a good thing in just about every case. Like any tool used for programming, you use them when the cost-benefit works out.
Forcing error handling is the right thing. Just because someone uses that information and does something boneheaded with it is not an indictment of the concept.
Empiricism. How does it work out in practice? What is the prevalence of bugs caused by the particular boneheaded behavior? What are the effects on difficulty of debugging?
It's not a fault of the concept of static type checking if people fail to handle it properly.
Why not? We could use such logic to blame a huge number of additional aircraft accidents on pilot error. But pilots, like programmers, have limited bandwidth.
The cost to exception handling -- which I acknowledge is beautifully useful at times -- is that it entails an implicit hopping around of control flow. So if you combine that with compiler enforcement of exception handling, is that you force the programmer to pay the price of such implicit hopping, whether it is appropriate or not. Lazy programmers will cause problems in either case, but in the case without exception handling, it's often easier to figure out where you went wrong.
Think of it this way: Enforced exception handling makes the wrong thing to do dead easy, and the right thing to do anywhere from slightly harder to a lot harder. Not having exception handling makes the wrong thing to do zero effort, but suicidal, while the right thing to do is still anywhere from slightly harder to a lot harder.
An even shorter version: with Go's error handling strategy, lazy programmers tend to see the consequences sooner.
Can you offer an example (besides exception handling) where high level flow control concepts reduces readability or increases potential for bugs? I honestly can't think of any.
While I understand the empiricism argument, I'm not sure thats enough to sway me. People can be "easily" enough taught not to swallow exceptions. Also the explicitness of swallowing makes catching it during code review extremely easy. Actually I'd be surprised if lint tools don't already pick it up.
High level flow control concepts communicate intent, which then makes detecting deviations from that intent easy enough for tooling to catch. This is always a win. Without high level flow control, one has to deduce intent from more primitive units, increasing cognitive load and increasing potential for bugs.
Even if I were to accept that the tendency for abuse makes checked exceptions a worse case, I don't see how in-band/manual error code checking is superior to unchecked exceptions. Uncaught exceptions provide much more context and they're tied to the point of inception. Ignored error codes have a tendency to cause problems downstream that can be painful to trace back to their origin. (Unchecked) exceptions seem like an obvious win here.
Yes, there is a "pluggable" pattern in Smalltalk where you just let people park lambdas in instance variables to accomplish customization. Too much of this will result in complaints from fellow programmers about debuggability. Also, method_missing/doesNotUnderstand: wrappers or proxying produce the same complaints.
High level flow control concepts communicate intent
Good ones do. Bad ones hide it.
This is always a win.
Uh, no. Even this can be done badly.
Without high level flow control, one has to deduce intent from more primitive units, increasing cognitive load and increasing potential for bugs.
Sometimes true. Sometimes the cost/benefit works out in their favor.
Even if I were to accept that the tendency for abuse makes checked exceptions a worse case, I don't see how in-band/manual error code checking is superior to unchecked exceptions. Uncaught exceptions provide much more context and they're tied to the point of inception.
Keep in mind that the worst problem is actually with badly caught exceptions. What you are saying is: "Granted that too many tend to use exceptions badly, if people use exceptions intelligently, they are better." For one thing, are you granting it or not? For another thing: If your shop can manage to accomplish that, power to you! Go knock yourself out! The Go team has determined a different cost/benefit analysis in the context of their language. (It has more to do than just with the semantics of exceptions.)
>Keep in mind that the worst problem is actually with badly caught exceptions.
My point here was that if we granted that checked exceptions, i.e. forcing handling of an exception by the compiler, is a net negative, the construct of exceptions themselves (sans forced handling) is still superior to error codes for the reasons I mentioned.
We are not in disagreement. Higher level constructs become a nuisance when they aren't really necessary: In other words, when the cost/benefit doesn't work out. Pluggability and proxying are cited as a nuisance when they should not have been there in the first place, often put there by junior programmers who are excited at their discovery. You can think of exceptions as a higher level construct expected to appear everywhere. This makes them more prone to abuse than proxying, for instance.
the construct of exceptions themselves (sans forced handling) is still superior to error codes for the reasons I mentioned.
Again, this is still relative to cost/benefit in individual situations. In the case of go, there would be other costs external to the simplicity of code.
This is the first Go article I've seen on the front page in a while...
Rails solve common problems for the most popular platform: web.
Go is created to replace C, a system programming language which could be considered as a niche. You see popular Go projects specifically addressing system level stuff (infrastructure: packer.io, docker, etc).
What can I do in Rails that I can't do in PHP?
"Systems programming" != "Systems software".
Systems software is more widely used, but written by fewer people.
I'd go a bit further. Go was written to replace C++ when used for servers at google and it excels in that spot. It is not meant to replace C for kernels.
wow
C is now a niche language?
http://www.tiobe.com/index.php/content/paperinfo/tpci/index....
etc
How does that work?
It rather relies on the assumption that you do have one unique canonical URL associated with your package for this to work, but one of the things about go is that a lot of it is based on pragmatic assumptions that turn out to be true often enough that the edge cases don't matter.
If you use go get, it can auto-download any such publicly hosted packages, which is a nice feature, especially as it can get the dependencies for you as well.
And, under the hood, all of their net.Conn types use non-blocking i/o combined with the goroutine scheduler.
Basically, the philosophy is that rather than do it yourself, just write really simple blocking code using lots of really cheap goroutines, and the runtime non-blockifies it for you. If you really want nonblocking behavior, you can easily spin off a goroutine to do your i/o and then send on a channel or callback or whatever, and if you really want really nonblocking behavior, you can less easily use the syscall package to write your own conn type.
You can have static typing and type checking without compilation. The interpreter can just complain to you when you try to run it.
That sounds more like strong dynamic typing.
Exactly.
> It will still complain if you muck things up when you type them into the repl. Dynamically.
Yeah, because it's a REPL so it has to evaluate every line of code that you give it. How does that work when you have code as a file and you run the main function of that file? You would have to run every line of the code. So what about code paths that are not taken? Do you just give enough input data that eventually all code has been run through? But then you 're not really running the main function, you're running all the code of that program has, with whatever results and output that that might give. Obviously I'm not talking about Haskell here, which already has a static type system.
Is your claim that you don't need a compiler to do type checking? Well, yeah, since type checking is not part of the source code translation. But it often said to be part of a compiler since it it often is often part of the semantic analysis phase. But if you have an interpreter you wouldn't really have a compiler as in a program translator.
I interpreted 'The interpreter can just complain to you when you try to run it.' as "complain to you as you are running it". Not "complain right before you run it".
Yes, it will technically complain before you even run it. Because it will first analyze it and only then run it if it type checks. The entire file, not just the reachable parts.
You apparently tried to build a prototype with a framework that very explicitly tries to provide more than just prototype capabilities. Invoking "one mans trash anothers treasure [big sic]" and then citing this is ridiculous; your failure to choose the right tool for your chosen task does not reflect poorly on the tool, nor does it suggest people who do use it correctly "like bumps in the road."