When a programming language doesn't solve mass/common problems, its popularity will definitely decrease after initial peak.
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).
Systems programming is further down the layers of abstraction than Web Development is. As you go deeper and deeper down these layers there are fewer and fewer people solving the problems that the layers above depend on.
> 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).
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.
Go was created to do the systems programming tasks people used C++ for. But what has resulted is that very few C++ users have switched to Go. It's mostly been Ruby/Python programmers and they are using Go for network programming, not systems programming. It's just another bit of proof that language designers, no matter how brilliant they are, such as Ken Thompson and Rob Pike, almost never know who is going to adopt their language and what it will be used for.
Not sure why C is on top of the list but having said that, I see more Rails/Java/C#/Python/PHP (or in general, web-app dev) jobs out there than a Linux kernel hackers :).
OP has never heard of panic() and recover(), which are awful things that should not exist.
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.
Uh, no. You shouldn't use panic for error handling. You should basically never use panic unless you have some small bit of recursive algorithm inside your package that you want to be able to easily unwind. For general error handling you should only ever use error returns.
panic() is awful. Right. But is so much not a pattern.
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.
Sorry, I don't mean to talk about Sum/Max/Min. I mean to talk about Either<A,B>, which is the appropriate return type for almost every Go function. In the presence of this type, the
"40 lines of mostly error handling" can then be reduced most of the way back to 5 lines using some function or control-flow construct that maps over the left half (or whichever half is chosen to not hold the exception) only.
Many problems need generics to be solved elegantly. Go doesn't have generics. It is completely impossible in Go to write things like user-defined polymorphic container types.
And look how poorly that worked out. People assume software will break all the time, because most of the software they are exposed to does exactly that. It is 2014. There is no excuse for using languages lacking important features from 1973.
What problems do you believe that you'd hit regularly that can't be elegantly solved without generics? I haven't used much Go, but isn't its duck typing implementation appropriate for the most common needs of generics?
Go has no generics. You can either rephrase your program to not need them, or pretend that you're using a less-typed language and write code that can blow up at runtime.
I agree you with you about generics, but downvoted you anyways, because "it gets ugly fast" is not true. I've certainly felt the pain of wanting generics for some data structures, but I've never felt the pain to where I've needed to make specialized structures for more than 2 or 3 types. And it had no real effect on the rest of my code. It was just one of those things where if I had generics, I could remove the 3x duplication of some code. It definitely doesn't ruin the language.
I wrote this to scratch an itch of mine:
There is nothing about go which prevents the use of generics. I wrote the following library to scratch an itch of mine:
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.
Yes, if you totally throw away the type information, then Go will let you write some generic code. Your library uses a _ton_ of `interface{}`, because it has to.
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]
One of the main jobs of a programming language is to reduce the amount of duplication you have to write. It just seems to me like being okay with that is antithetical to progress in practical language development.
That's not what Go thinks its job is at all. Go thinks its job is to make it easier to write and maintain large scale software applications. Removing the amount of duplication that any one developer has to write is not one of the design criteria.
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.
Please throw this Java mentality away. Code duplication isn't an issue, code maintenance is. While lack of duplication might help, it is in no way a necessity for a maintainable code base.
If code is duplicate of some other code maintenance increases 2x. If it's in ten places, it's 10x. If two different version diverge and they should really be the same (but for some reason one has data packed into an array and the other has multiple arguments, etc.) it becomes impossible to fix it without breaking something
... until you find a bug duplicated x times in all your versions and you missed one of the x different unit tests to test them all ?
IMHO when you are fighting your language to avoid code duplication, you either use it incorrectly or something is wrong about the language.
To be honest, I only encounter rare cases where, dammit I would really love generics.
And the Go team never said no to it, they just have more urgent stuff to do first.
[Disclaimer: Googler here using Go, yaddi yadda, this is just my opinion]
What's up with the rampant downvoting the past week or so? I see all kinds of posts downvoted that shouldn't be (like this one). Seems like it has really taken off the past week or so.
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.
I agree 100%, and I've also noted a clear trend toward rampant downvoting the last 2-3 weeks. I've been here for a number of years and have never noticed or complained about this type of downvoting issue prior to the past few weeks.
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.
That's a single comment from pg from the very early days of HN. And that's not how things developed organically. Instead, downvoting comments has been reserved for comments that violate community standards.
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.
My interpretation pg's plan for a comment endorsement scheme was that it actually encouraged downvoting for disagreement.
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.
I do not. I am just pointing out that that's what they are for. Of course drivel comments are also likely to be downvoted heavily, but that doesn't mean that this is the only intent of downvotes.
I think one of the biggest problems is that they make it hard to write good libraries. When libraries cannot easily deal with user types that's just not good. Imagine trying to write an ORM for instance.... in Go you are not given many options...you either have to deal with casting via interface{}, making coding both more verbose and lacking in compiler enforced type-safety, or you force the user to implement your interface on all their types.
When a language has a different feature set, it's often helpful to approach the problem from a different direction. Not all common design patterns apply to all programming languages and it may be more work than it should to shoehorn a concept into a language where it can't fit.
Basically to install a dependency you do something like "go get github.com/mediocregopher/somelib", and the tool will fetch it and put in the appropriate spot on your GOPATH. So the universal name of your package is also the address the package is retrieved from. You COULD host the package multiple places and it wouldn't be true that it would be unique in the whole universe, but that's up to you.
Same way it works in Java: by convention. You use the repository URL on e.g., github in the package name, so if that URL is unique then your package name is unique.
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.
The fully-qualified package name, by convention, includes the web path to its repository, for example: `github.com/smartystreets/go-aws-auth` is unique.
* 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.
A serious question (not saying you're wrong, I'm just genuinely curious): What issues have you run into with Go and Windows? I've been shipping libs and programs written in Go for Windows without issue.
Same here. Good support in LiteIDE. Debugger works from LiteIDE. Been meaning to try this LightTable plugin (https://github.com/toqueteos/LightTable-Go) but haven't gotten around to it yet.
Hey, I'm one of the contributors for that plugin. There's still a lot of work to be done, but I'm finding it fairly usable right now. If you get around to it soon, make sure to grab the plugin from Github and not the plugin manager. The version in the manager is very out of date. And pull requests are very welcome.
As for debugging, there were some comments about this recently on golang-nuts. A discussion about debugging[0]. Also Rob Pike saying GDB support is 'poor'[1] and there are bugs[2] that aren't a high priority to fix. Though, as many people pointed out on the mailing list, print statements and unit tests will cover 95% of your issues.
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.
I still don't see any problems with Windows support here. Debugging is a problem, actually, but other than that everything seems fine. The syscall interface gives you access to call any Windows API you want.
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.
I think elegant is probably the wrong word. Practical is the one I would use. Go isn't Haskell; it isn't there to impress the PL guys. They boiled down the average imperative language and made many decisions for you so that opinions don't have to collide while working on a project. Because the decisions they made, they have very nice tooling to go along with the language.
As the project gets bigger the right word approaches "elegant" but practical does fit. It seems like a language for engineering software, big software like c++'s boost or google's bot. Unsurprisingly, bloggers aren't talking about that scale.
I agree completely. I like Lisp because it's elegant, and the solutions to difficult problems in Lisp are beautiful in every sense of the word.
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.
Go exception handling (or lack thereof) sucks. Things can just blow up silently at runtime unless you check the error code return of practically every statement.
That's the whole point--to force you to actually handle errors properly. You could say the same thing about any language with exceptions that you don't catch.
There is a better way. Check out how Erlang embraces failure with its "let it crash" philosophy, and considers trying to handle every error condition manually ("defensive programming") an anti-pattern. Read the "Error handling philosophy" section of Joe Armstrong's thesis[1] if you want to learn more.
I guess I should've worded that differently. No, it doesn't force you to handle errors, but it makes the possibility of errors much more obvious. With exception handling, you never know what functions may raise an exception; Go is much more explicit in that sense, so if you don't handle the error, it's your own damn fault.
> unless you check the error return code of practically every statements
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.
errcheck will add this as an optional linter. I've adjusted the git hooks on my machine to run errcheck and fail the commit if it doesn't pass (unless I specifically disable it).
If 10% of your lines are repetitions of the same thing that's absolutely a problem with the language. You should find a more concise way to express the same thing (e.g. error monad).
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):
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`.
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.
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:
They don't need to be on one line. If you really want to pretend that having them on one line is a problem, then put them each on their own line. You can even be needlessly verbose if you want to:
do
a <- GetBar foo
b <- GetBaz a
c <- GetCat b
return c
That's much more to my liking, but still, we've got multiple ways of doing option types and now I've seen 2 different operators, which I presume are overloaded to deal with those types somewhere? Or we're dealing with a ton of built-in types and built-in operators to deal with them?
I'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.
>now I've seen 2 different operators, which I presume are overloaded to deal with those types somewhere? Or we're dealing with a ton of built-in types and built-in operators to deal with them?
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".
Now you've got way less code, and way more cognitive load per line.
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?
In a language with option types (such as Rust) if you want to be totally explicit and write your error handling the Go way, it's scarcely more verbose than it is in Go, and totally clear:
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 really can't wait to spend more time learning Rust, but that bit you just put out strikes me as a fair bit more complex than just multiple returns plus "don't ignore your errors, dude, you crazy?". We've got the let-match func() {} construction, the Ok() function (built-in? library?), the => foo vs => return err.. that's returning from the outer function, right? Whereas the => foo completes the let and continues? There's a lot going on in there.
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.
Go cares about scalability in the large. It doesn't care very much about conciseness in the small.
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.
What led me to believe this is Pike's article about the design goals of Go. I don't know enough about Go to know how well it succeeds, but I know what the intent was.
But you are falsely implying that the intent "be good for big projects" is opposed to the concept of "having useful abstractions". It is not. Go fails at being useful for big projects precisely because it fails to have basic useful abstractions. Those are most important in large projects. In a 1000 line program it doesn't matter much if I am repeating code, there's just not that much code anyways. In a 1000000 line project, it is a serious maintenance problem.
You mean something like Java's checked exception? That has its value, but you have some programmers who write empty catch blocks just to get rid of the exception. Forcing the programmer to handle it is not the same as forcing the programmer to handle it.
How is this an argument for silently failing being superior? It seems like static checked exception handling could solve this problem easily, the compiler can just display a warning on an unhandled exception and then pass it up the chain.
It's not an argument for silently failing being superior. It's a claim that too often, static checked exception handling gets subverted, and therefore doesn't work.
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.)
I can see what you're saying, but ultimately there's no helping people who are doing something they don't really understand. I'm more interested in language features that remind me when I forget to do something I wanted to do.
It's actually awesome. At first it feels ugly to write, and you're generating all this unnecessary noise in your code, but then you finish a big project and overall the manual error checking combined with defer leads to less code than the Java or C++ equivalent, and feels safer. Going back to try/finally just feels awkward now.
I don't see how its possible that manual error checking leads to less code than try/catch/finally. Can you give some examples?
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.
I can't really give a good example because I'm not sure how it happens, it just has, for me. Maybe it happens in spite of the error handling.
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.
Sounds like what you're saying is that it makes flow-of-control in error situations explicit instead of implicit. Automagic implicit hopping around to different locations in your code-base is the fatal flaw with so many overly clever programming tricks.
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.
>Automagic implicit hopping around to different locations in your code-base is the fatal flaw with so many overly clever programming tricks
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.
I think the poster was saying that java exceptions don't tend to force error handling to the same degree as the multiple-returns approach does.
"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.
You could say the same thing about lambdas, continuations... basically any high level flow control concept.
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.
>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.
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.
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.
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.)
I don't see how pluggable or proxying support your argument. What would be the alternative that would more clearly communicate intent and avoid bugs? Just avoiding these constructs altogether isn't an answer to this question. Yes, some constructs/patterns should generally be avoided, but when they are necessary higher level constructs like these are a boon rather than a hindrance.
>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.
when they are necessary higher level constructs like these are a boon rather than a hindrance.
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.
Why the strong opinions about blocking? Go discourages, at least me, from doing non-blocking IO. I've heard that's a unix philosophy though. For example, io.Copy will block.
There's a robust syscall package that you can use to do whatever you want.
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.
The Author touches on a lot of nice points, but does not address some of the problems with using Go.
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.
They are not feature equivalent, so it's hard to compare directly. That said, excluding code reuse, to me Go feels like 75%-100% of verbosity of C++ without header files.
Also, I don't agree with your statement. If it were true, it would mean every language clearly sucks.
I think part of his point is that the mere re-write itself should save a huge chunk of that. Quite probably if you were to re-write it in the original language you could still get 25-30% decrease in code size (or more).
I work on Juju. It's over 200k lines of Go code. It works just fine. Generics have only been missed in a small handful of places, and nothing at all critical.
This may be an unpopular opinion, but I hope Go is relegated to web services programming. It's really not a good language for much else. The lack of generic support is really terrible. What you have to do instead (cast things to {}interface) is like casting things to Object and then hoping they have the right methods in Java.
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.
That's mainly the conclusion I came to as well. I find Go nice to use for writing web apps and services. For anything more complex/critical and/or with a high demand for correctness, I find using Rust to be a no-brainer compared to Go.
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.
One of Go's major features is that any particular function is very easy to reason about. The code is very straightforward, there's very little magic, and the code usually does exactly what you expect it to do, even if you're not an expert in the language.
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.
For one thing, it's just a simpler language. There are fewer keywords, there's only one kind of pointer (and no pointer arithmetic). Everything is just pass by value. There's very rarely anything going on "behind the scenes". The code does what it says it does. There aren't any dense one liners that do a whole bunch of stuff. Values are very easy to reason about. You know exactly how values will be initialized, how much memory they take to copy, when they'll be copied.
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.
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.
> 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.
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.
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 love go, and I concur with almost everything the author wrote here. A small pain point in my experience though is that Go lacks an elegant way to pass type information around without using a zero valued (or fully populated, it makes no difference) instance. For example, I have a factory struct with a New 'method' that returns interface{MySignatures()}, but I cannot inform the factory of the concrete type I want it to produce without passing in something like new(ConcreteStructWithMySignatures). Some have suggested I pass in a string, but what is the point of a type system if I have to use strings to reason about my types?
You're probably using Go in The Wrong Way®. It's hard to fit classical object oriented design patterns onto Go. A factory method that produces structs with dynamic types is an example of that.
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.
Well, at line 57 you already know which provider you want to use, so why not just initialize the struct from there and skip the factory method altogether.
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.
I was in a rush and didn't create the most illustrative example.
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.
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.
You're still leaning too much in types. Go is a very lightweight type system. The setter and base type basically scream for "I want to use my Java in Go". I'm not even talking about using a factory.
Your code seems incredibly backward. Why does the ProviderFactory hold the knowledge about the query interval for each provider? Shouldn't the provider hold that information? 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. That's like the polar opposite of separation of concerns. Let the query interval be defined on the type, not in the factory.
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?
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.
Uh yes, probably a factory is never going to be an optimal solution. If you know what type you need, why would you need a factory to create it for you? Just create it. You might need a generic initializer function that sets various values on the type you create, but there's pretty much never a reason to have a function create a variety of types for you.
Uh yes, probably a factory is never going to be an optimal solution.
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.
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. :)
The Factory abstracts away instantiation and configuration details for a complex or dynamic initialization process.
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 otherwise
Uh... 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.
Seems to me that if I've got a factory, I need some way to tell it what I want it to produce. That's either a string or an exemplar or some set of properties/flags. Somehow, you have to say what you want.
Right, and this is what happens in Go. I can use a string or struct as a signal for what type to produce, but I wish I could communicate that type in a "first class" way, rather than using another data type as a proxy signal. Check my reply to melvinmt for a more clear example of what I mean.
I'm still unclear on what you mean by "first class".
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?
I am not a Java programmer but the Java class literal seems like a perfect example. The class literal provides a language mechanism for representing a type of class (in Go's case it would be a type of struct) without the need for an actual instance of the class or another arbitrary data type (like a string or const) that the application is programmed to recognize as a signal for that class.
one mans trash anothers treasure. When I pointed out that Django is an annoying framework because it breaks up your ability to prototype flat by forcing you into some workflow structure I was told "that's a feature". Go is similar. It's annoying but some people like bumps in the road. Enjoy
It's annoying but some people like bumps in the road.
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."
I guess I wasn't clear and just letting out a bit of steam. Still doing it but to be clearer... Go feels more like a framework than a language. could be by design, but didn't expect it. also you are right, "bumps the road" is not fare. Better would be to say, in referring to myself, i guess you can't teach an old dog new tricks ;)
But that's wrong. If I have strong dynamic typing and the program never goes to that part of the code, I don't get an error. What I'm talking about is more like Haskell repl. It will still complain if you muck things up when you type them into the repl. Dynamically.
> But that's wrong. If I have strong dynamic typing and the program never goes to that part of the code, I don't get an error.
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".
No, you just run the static type check on the entire program. Like you run the Haskell REPL on a file and it type checks it for you and executes the main. It can just do this in RAM without generating an executable file.
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.
Having uniquely-named packages is insufficient unless the maintainers of those packages never update them. The point of tools like Bundler is not about name resolution, it's about VERSION resolution.
Sample size of 1, but here's a story about what happens when things go wrong:
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.
The bit about packages being edited locally shows the problem was between the chair and keyboard, not in the language. That's like making edits to some local files and never pushing them up into source control. Of course it'll never work outside your own local machine.
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.
177 comments
[ 2.8 ms ] story [ 241 ms ] threadThis 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://langpop.com
http://www.tiobe.com/index.php/content/paperinfo/tpci/index....
etc
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
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:
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.
https://news.ycombinator.com/item?id=117171
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.
1. https://news.ycombinator.com/item?id=7445761
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?
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.
* 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.
[0] https://github.com/tools/godep
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
[3] http://golang.org/pkg/syscall/
[4] http://golang.org/src/pkg/syscall/syscall.go
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...
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."
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.
[0] https://en.wikipedia.org/wiki/Option_type
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.
I'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?
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.
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.
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 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.
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:
Rust: 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]
Your Rust code also shows that you've never actually used it before. Here's a proper translation of the Go code into Rust:
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:
http://play.golang.org/p/EmxCRkDNLC
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?
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:
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: 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?
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."
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.
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.