I've written a bit of code in Go, and the problem I have with it is primarily it feels really outdated for a "modern" language - every time I use it I feel like I'm dealing with something written by someone who really hated Java in 2005. There are features that could be added to the language that would make it more readable and less error-prone without compromising the simplicity of the core language. Generics are the famous example, but the one that really gets me is the the lack of nullable type signatures. This is a great way to avoid an entire class of bugs that nearly every modern language I've used has evolved a solution for except Go.
Another issue I have is the reliance on reflection. In general, I think if you have to rely on reflection to do something, that usually means you're working around some inherent limitation in the normal language - and the resulting code is often far less readable than the code would be in a more expressive language. Lots of Go libraries and frameworks are forced to use it in a lot of cases because there's just no other way to express some really basic things without it.
I really want to like Go. There's a lot I like - the "only one way to do something" approach means that code always feels consistent. Errors as values is a far superior approach to exceptions. I had to write some for a job interview project a while back and it felt really refreshing, but every time I try to use it for a personal project, I don't feel like I'm getting anything out of it that I couldn't get out of say, Rust, or modern, typed Python.
If you’re the type of engineer who prides themselves on the raw amount of code you write, then Go is for you. If you’d rather focus on solving problems creatively and expressively, Go is not your tool.
I don't mean this as slight against those people that really enjoy writing lots of (Go) code. It’s just my observation after being in a few different contexts where Go was the language of choice. Personally Go is too verbose for me and this is especially painful/apparent when you get to writing the multitude of tests required to ensure your code works since Go’s type system/compiler doesn't lend you much in the way of helping ensure code correctness.
>If you’d rather focus on solving problems creatively and expressively, Go is not your tool.
>It’s just my observation
Well, my observation after dealing with js and ruby projects (ruby was my main language a few years ago) - those creating and expressive languages lead to fewer lines, sure, but in the end you are dealing with a pile of crap nobody want's to touch because some of the developers were so creative and wanted to express themselves more than they wanted to solve the problem.
Go is a tool if you actually want to solve the problem and make sure people after can quickly understand how and why you solved it. Rather than stand in awe of your creation.
> Go is a tool if you actually want to solve the problem and make sure people after can quickly understand how and why you solved it. Rather than stand in awe of your creation.
I disagree with that.
The problem in those cases is not expressivity. It's complex features that are very easy to misuse and have no alternatives within the language.
And Go is littered with those. It allows for lots of "cleverness" around Reflection and Empty Interfaces, which effectively turn it into a poorly-specified ad-hoc dynamic language. Same for templates. This is the same problem of Meta-programming in Ruby, and the same problem with complex OOP architecture that plagues some Java projects. It's all avoidable, of course.
Those features are not "expressive", quite the contrary, but they lead to the same problem of "pile of crap nobody want's to touch because some of the developers were so creative and wanted to express themselves".
It takes as much discipline within a team to avoid this in Golang (or any other "non-expressive language") than in more expressive languages.
On the other hand, lots of new ES6 features give it more expressivity without necessarily adding complexity and in fact making the code easier to read and more reliable.
>And Go is littered with those. It allows for lots of "cleverness" around Reflection and Empty Interfaces, which effectively turn it into a poorly-specified ad-hoc dynamic language.
I strongly disagree with that. You can do it, you can do clever code through reflection. But it is actively discouraged unless it's something required to solve the problem, e.g automatic JSON marshalling/unmarshalling.
No professional Go dev is going to immediately reach for empty interfaces or reflection without seeing what the solution looks like with verbose type safe code.
Maybe in your experience this doesn't happen, or maybe we have different thresholds for what we consider abuse, but it's definitely a thing for a lot of people.
Also, overuse of reflection doesn't happen overnight, or only because of inexperienced programmers.
In most complex projects it happens because someone wants to add more complex abstractions that exist in other languages to help with the work and to reduce programmer error and the only way to do it is via Reflection and Empty Interfaces, or maybe via templates.
All of the Go projects where I work have a very strong "empty interfaces are bad" culture (noobs who don't know better will get called out in code review), and the only reflection I've ever run across is for marshaling/unmarshaling. I really don't see these things being overused at all. When I first started using Go, I did perpetrate some empty interface crimes, but it really only takes one experience getting bitten by thwarting the static typing before you learn to avoid that.
I'm glad you are having a good experience, but people work on different projects with different requirements and different constraints.
In Go, empty interfaces and reflection, or templates, are for some cases the only possible way to solve a large class of complex problems. If you don't encounter those problems then it's all good, but some people do.
You haven't also taken into account the possibility of people having to maintain pieces of Go code acquired or forked from somewhere else. Maybe my team was the one who inherited the code from someone who "perpetrated some empty interface crimes".
Not all Go development is greenfield development.
This is one of the things I dislike the most about the Go community. Every single criticism or suggestion to the language is dismissed as being user error, without even taking into consideration the use case or the different experiences one might have, or if the codebase was gotten from somewhere else, or even acknowledging that there are other programming styles. It is a giant echo chamber with people constantly saying "works on my computer".
I dunno, that seems a little unfair. I’m not saying empty interfaces aren’t a problem just because I’ve managed to avoid them, I’m just saying it is quite possible to write reasonable Go by knowing what language features are misfeatures. Obviously if you’re often running across and having to deal with code laden with empty interfaces, that’s a major problem and put in that situation, I’d probably have the impression that the language encourages bad behavior. I’m just saying that so far, I haven’t encountered it all that much so I don’t really perceive it as a problem. If you want to paraphrase this as “works for me,” so be it, but it seems like a particularly uncharitable interpretation.
I do think it’s a crime that the standard library contains some empty interfaces in critical packages that seem egregious. The fact that you can accidentally pass an rsa.PublicKey (instead of a pointer to an rsa.PublicKey) to a function that takes a crypto.PublicKey interface and not find out until runtime is hard to forgive.
Anyway, I’m not just saying “works for me” but I am saying that I’m not going to let what dumb things someone else might do with the language change my enjoyment of it. This view is likely influenced by the fact that my particular projects aren’t allowed to pull in 3rd party dependencies without a thorough review, so the problem of dependency fan-out that may pull in some unfortunate code is reduced significantly.
And TypeScript and Flow these days allow to properly type the wast majority of JavaScript “dynamic typing” patterns making reflection usage in Go and Java to look ridiculous for a supposedly statically typed languages.
>It takes as much discipline within a team to avoid this in Golang (or any other "non-expressive language") than in more expressive languages.
I think this will differ from team to team. I've been working within two different companies as a Go dev so far and haven't seen any Reflection misuse issues.
The difference I see here: while Go does indeed have those features-about-to-turn problems you will be called on not use them too much or use them at all from around every corner. They are there as a necessarily evil.
At the same time meta-programming and every thing that comes with ruby's dynamic expressiveness usually is the one of the selling points.
Go has its flaws and tradeoffs, and simply things that can and will be misused but you don't see articles that promote them as something that should win you over some other language.
> Go has its flaws and tradeoffs, and simply things that can and will be misused but you don't see articles that promote them as something that should win you over some other language.
I disagree there. I can think of two examples of things I consider very easy to misuse in Go, but are promoted by the community as being the superior solution to problems in articles and posts all the time: Go Error Handling and Go Code Generation.
Go error handling has its downsides but how can you misuse it? If your code may generate an error = return an error to handle elsewhere. If the code you are calling returns an error = handle it.
I also shy away from sloppily "expressive" scripting languages like JS and Ruby. When I talk about expressive I'm referring to Lisp, Scheme, Rust, Haskell, Scala, Kotlin, etc. I do agree Go has a specific use as a technology that can be deployed in high-turnover environments to mitigate devs who have an attention span that lasts until they have to write tests and deploy their code. I mean this is exactly why Google likes Go. It's a least common multiple that can be picked up easily by anyone as engineers whirl around in their machine. You're not wrong. I, however, like to enjoy writing code and understand I have to test it and maintain it throughout its lifetime (or don't have the luxury of handing it off to a salivating crew of new grads looking for promotion-worthy work once I get bored) so I gravitate toward languages that are fun to write and easy to maintain. For me, those tend to be languages that offer formal macros/meta-programming, sound type systems, support higher-order programming, and preferably enforce memory safety. It doesn't mean my code is littered with that "look how clever I am let's admire my code greatness" stuff.. I prefer to keep most of it boring too focusing more on whether it's visually readable and logically easy to understand. But it's nice to have powerful features when they really matter and it's nice to know you can count on a compiler to help ensure memory safety and type correctness.
Rust has to be one of the least expressive languages ever made, so that’s a pretty weird one to group along with other actual expressive languages. That’s not a full-on dismissal of Rust, since expressivity isn’t its main goal.
Also, type system ‘soundness’ is a pretty empty desire. It’s easy to create a type system that is formally sound, yet not useful. This is the same argument as ‘type correctness.’ Java programs were ‘type correct’ before Java had generics, and the type system was extremely limited.
So types are an inherently meaningless goal, because types can mean so many things.
Rust is very expressive. It has a useful macro system, powerful generics, and generalized traits which means if you implement your types canonically you're using them via essentially the same patterns almost everywhere.
Don't confuse not-expressive with articulate. Rust requires your to be articulate about what you're doing so you don't gloss over ownership details with fancy one liners like is common in other languages. You can be both expressive and articulate. Not worrying about memory ownership, while possible in other languages, is not the hallmark of expressiveness. For example, imagine a C analog to a Rust program that implements all the same checks and memory discipline that Rust does.. Rust is way more expressive, relatively.
I see what you mean with ruby, it encourages to be clever, and I had some exposure to work on somebody's "clever" code.
I'm not as familiar with js, but feels similar to me as well.
What makes things worse they are both dynamic languages, but there's a middle ground.
It feels like Rust for example (I'm still planning to learn it) looks like have the right amount of functionality. Java or even C. I think what you experienced is the extreme case of it.
This is just the old conflict between ops and dev. A software dev wants a little code to express a lot, while maintaining some central guarantees, and doesn't really care about edge-cases beyond not having to code them explicitly. Needless to say, a few edge cases will turn out not to be what the dev intended, no matter how smart they are. Tests focus on the central guarantees and nothing else.
Ops is the next guy who comes in, who is only ever supposed to keep everything running. Changing things is second priority, if it's anywhere at all on the priority list. (S)he will HATE any edge case they don't know exactly what happens because it can be bad, and it's the source of all their work. They want to manually code every edge case, and have that covered by a test.
C/C++ ("smart" C++ specifically), Haskell, Lisp, ... are for the software devs.
Go, Java, C#, ... are for the software ops folks.
A large bank should probably be using Java. Someone trying to start up a new bank may prefer Haskell.
I don't see this conflict resolve any time soon. Personally I find the large bank situation somewhere between soul-crushing and suicide-inducing. But I've met plenty of people very happy in such situations, and great for them. I may even consult for them, because for 1-2 months or so software ops can be quite interesting. Figuring out how to introduce changes in a software ops org is ... usually a challenge in addition to figuring out how the software system should change.
You always come out well, because there's always large improvements that can be made because nobody's doing that. They're busy triple-checking that the next parent-teacher conference of employee 271 doesn't interfere with the oncall schedule.
And frankly, I could (and probably can still) use a few lessons in "this edge-case fuckup is not acceptable and you missed it !". I've been humbled several times by steering a large problem past all security measures of a software ops organization in the past.
>Ops is the next guy who comes in, who is only ever supposed to keep everything running.
>Go, Java, C#, ... are for the software ops folks.
Can't say anything about Java or C# but one of the reasons we use Go is because the requirements change often and you can adapt your code quickly.
At the same time our code (my main field is systems integration) is required to keep running obviously. New requirements should not change this fact.
So on a spectrum I'd say Go is 7/10 OPS. Subjectively.
Obviously Go probably is not the type of language you should chose for your MVP or prototyping. Unless you are sure you have a full picture in your head already.
If I was trying to justify Go's verbosity in this case I think I would say "a property check on the dict incurs a cost, by making it somewhat more verbose you encourage people not to check more times than needed".
At least that's the kind of thing I've heard about other verbose things you need to do in Go.
There’s a lot of things wrong with this. First, this has nothing to do with the actual reason for the verbosity. The actual reason is that a value may or may not be present in a map, that’s a universal problem. Most languages just return nil or an Option value, but Go returns an error and forces you to handle it. No comment from me on whether that’s good or bad, but that’s the reason the code is like that.
The real problem is you’ve just made up a reason that the code is that way, and the reason is also crazy. No language purposefully makes something verbose simply to discourage doing it because of a performance concern. Even if they did, key lookups are constant-time complexity and are among the most efficient things you can do in all of programming. So that wouldn’t even make sense as something you’d want to discourage.
Err... Go does exactly what "most languages" do too! You can write:
value, _ := map[key]
and if map[key] doesn't exist, value will be set to the "zero value" of the map's type. So Go doesn't force you to handle the error. The actual reason for the "verbosity" is just that you can't simply write
if(map[key]) { ...
because map[key] returns two values (and "if" wants to have a boolean expression). But if you really have to do this so often that it's bothering you, you should question yourself why...
and if map[key] doesn't exist, value will (still) be set to the "zero value" of the map's type.
The actual reason you cannot simply write:
if(map[key]) { ...
aside from the extraneous parens, is because Go syntax does not permit implicit default values/truthiness in its 'if' statements like that (as you say "if wants to have a boolean expression"). That's the real reason for the verbosity here.
But you certainly can write:
if map[key] != "" { ...
if the map contained strings, or:
if map[key] { ...
if it contained booleans, etc. (For what that may be worth: checking for default value isn't the same as checking membership in all instances, of course).
Because map[key] can in fact return one — or two — values, depending on its usage context.
Last time I wrote Java was before it had nice things so I may be slightly out of date in my assessment of its verbosity. I really wish it had decorator syntax like Kotlin does:
class Foo(val delegate: Collection) : Collection by delegate
{
override fun length() = delegate.length() + 42
}
val foo = Foo(delegate: LinkedList())
foo.add(1)
foo.add(2)
for e in foo {
print(e)
}
foo.removeAll()
foo.length() // = 42
I think a lot of the verbosity I remember was due to the sheer size of the type of interfaces you'd encounter in practice and an inability to easily compose them without implementing your own set of forwarding decorators (io stream style). That and getters/setters or rather the lack of any language-level support for dynamic properties.
Do you really want to inherit all the functions of Collection? Isn't this the anti-pattern everyone complains about on inheritance vs composition? This just looks like it's confused about whether it is a collection or has a collection.
Personally, I'd just rather take a collection by composition and only expose the small part of the API I actually want.
I guess though this is the standard decorator pattern you'd normally see from GoF.
I'm a C++ developer who has been writing more and more go code over the last year, and go's iota enum (alongside generics - having to write a search method for every different slice type gets grating after a while) is my biggest gripe with go. They really aren't any better than just global constants
This feels largely like a mischaracterization, and does not align with my experience. I'd change it to say, if you are focused on solving the problems creatively rather than using the language creatively, Go is a viable option. But if you're interested in creative usage of the language itself, Go is not right for you.
I get what you're poking at and I think we're mostly saying the same thing. However I want to point out that at the level of the stack we're discussing, the application of the chosen language is essentially: plumbing. The user-facing "creative" bits I think you're referring to (those in the system at large, which may or may not use Golang middleware) are most likely a react or mobile app. I'm not trying to devalue the self worth of individuals who write Go code by suggesting that the problems their products are solving aren't creative and fun. Not at all. Rather, my point is that the application of their Go code to build yet another solution for processing requests and serving responses ends up as pretty mundane and verbose imperative program logic that requires non-trivial amounts of effort to properly test and verify. Creativity in the "plumbing" slice of the stack to me looks like concise, readable, expressive code that leverages a type system to model a problem ___domain and then incorporates expressive logic governing how the types interact, the correctness of which is mostly enforced by the compiler itself and where testing is only required to ensure properties the type system can't capture or to make sure traits of a given type are implemented correctly. Go just simply isn't a language that allows for that level of "creativity" in building what would otherwise just be pretty mundane service plumbing.
I agree with this. Go works like my brain, and I don't mind that. Sure you can do X in some fancy way in other languages, but that just leads to feature creep. I've never read Go code I didn't understand at first glance. I cannot say that about a single other language.
And this is totally fine if you're okay reading and writing noticeably _more_ code to solve the same problems. There's nothing wrong with favoring verbosity over expressiveness, it's just not my style.
I disagree that expressiveness lends to feature creep, I've personally never seen that happen, but that's another topic.
For me, the verbosity of Go would be a lot more justified if it aslo had some sort of memory safety system in place (similar to Rust) that could help prevent concurrency related errors. I understand that you're supposed to used channels for everything in Go, but last time I wrote a channel heavy concurrent server the compiler did nothing to prevent me from mutating shared state across different go-routines and I was unable to land on a pure channel based implementation (I needed a run_once to initialize some shared state and a WaitGroup to track active listeners, and this was the case in all the other impls I audited/referenced as well). I just don't feel like I'm getting a lot of value in exchange for the verbosity when writing Go.
That's a completely fair assessment. I don't fault 'you guys' at all because I don't expect your brain works like mine. Read: I don't think you're wrong and I'm right...never.
What I fear myself is feature creep. A design by committee that ruins the language for me, and is never good enough for you.
If sharing state, you're much better off using locks and test with go's race detector. There'll be subtle bugs or contention though, unless you don't share state.
If not, you might try 0mq, immutable lists or any other facility. Channels are just the in-process idiomatic way to communicate state, but not required at all.
In my somewhat brief experience with Go I realized that one better avoid channels even for supposedly idiomatic cases. For example, try to implement a priority queue with channels. Or why it is not possible to use the select statement against a file descriptor? Another problem is proliferation of unnecessary threads that are entirely avoidable when using mutexes.
Channels are also hardly foolproof. You need to internalize the (non-obvious) channel axioms[1], and there are many other pitfalls and annoyances like:
- Needing to determine ownership of closable objects passed across channels.
- Defining what special token (or OOB signaling mechanism) is used for marking end of data.
- Synchronizing who is closing the channel
Go channels are quite painful to use and often a simple lock would do better. This write-up[2] summarizes the issues with them quite nicely:
can't really fault you on that either. features/looks etc. it really comes down to wether it matches how you think. I can make big claims about functional languages being superior for x y and z but if I'm being really honest with myself. its more about that fact that it fits how my brain already models problems.
Please omit the last line. Go is actually far more verbose than modern java when you are comparing anything bigger than a single snippet of code. Modern functional Java is fairly concise. Go is not - though maybe this will change with generics and functional packages after Go 2.0.
Go's weak type system encourages use of dynamic type ops (such as casting interface{}, etc) and obviously the compiler is out of the loop at compile time so it can't help.
Explicitly, I believe you, but I'd bet you've often implicitly cast to interface{} (even some very legitimate cases like json.Marshal() are still technically casts to interface{}).
>> the problem I have with it is primarily it feels really outdated for a "modern" language
It's mostly not about the language. An exception is of course when moving from a dynamically typed interpreted language to a statically typed compiled language.
The success of projects depends much more on other things than the programming language. It's about
- Processes and standards, like following a well defined structure, testing, documentation, ...
- Maintainability of code. It must be easy to read, to understand syntactically and to build a mental model of the code
- Long term reliability and stability of the eco system
- Easy and reliable tooling
- Developer efficiency, e.g. compile times
Go shines in many of the aspects. Especially in maturity, stability, amazing standard lib and tooling. As you mention Rust: This is exactly where Rust falls short. Rust is a great language with amazing people behind it. But there are reasons why its adoption in the broad real world is very, very small. The reasons are not the language. So I always feel it's a bit invasive when Rust promoters enter Go or Java threads by telling how much better Rust as a language is.
In this example Go has additional benefits being statically typed and compiled, very fast and with build in concurrency support.
Go isn't for programmers, it's for managers. Because of the simplicity of the language, it's easy to get people onboarded with, and it's fairly difficult for a single team member to go astray and move the codebase in a direction which will be unmaintainable. It's a kind of lowest-common-denomenator language which is easy to read, debug, and maintain, even for a mediocre programmer.
That said, I think you are right about explicit nullability. A language like Go with this feature, as well as named arguments and limited ADT's could be very compelling for Go's use case.
And yet it's a top-5 most-loved language among programmers and there's no real evidence that it's popular among managers (discussed in detail here: https://news.ycombinator.com/item?id=27175297). Moreover, lots of open-source software exists in Go--if only managers like the language, what's the theory for why all of this software (including major software, like Kubernetes) exists? Is it managers building it in their spare time? Even then, those managers are acting as programmers (not managers) in that capacity.
With respect to the implication that only managers care about the pragmatic aspects of software development (collaboration, onboarding, readability, debugability, maintainability, etc), where did this meme come from? I get the meme that there are many programmers who value abstraction above all else, but I'm not familiar with the meme that all programmers are abstraction maximalists.
I completely disagree with this statement. Some of the most high-performance modern platform-level code written is currently written in Go.
It was a Google presentation that examined their efforts to convert their download site to Go (from C/C++) that got my attention. It's easier to read, has a simpler mental model, and faster than its C/C++ cousin.
I'm not sure that the points you raise contradict my thesis. Could it not be the case that a language designed primarily for making large software teams easier to manage would also lead to high-quality software at large organizations?
If the thesis is that "Go is for managers", then yes. I would wholeheartedly agree to something like "Go is not for developers who want to write clever code that others (or they themselves in a few months' time) may have trouble understanding". But Go also works for large teams (communities) of open source developers who don't have what you would usually call "management", so saying it's only for managers is definitely too narrow a statement...
I basically agree; I think claiming "go is for managers" was a bit flip of me to say, but what I generally mean is that Go is more optimized for operational concerns than for the joy of programming. It's also not a pejorative: there's a lot of good reasons you might choose Go and why you might like it as a programmer.
Doesn't Java fit the same bill? (although it was kinda modern at the time)
> I think you are right about explicit nullability
Sadly it's really hard to shoe-horn this onto Go, now that the std lib is widely used. Same for proper sum types (which can then be used for multiple return values).
But the continuation of the nullability mistake puzzles me the most... And that wile Go's be designed by big name lang gurus: what were they thinking?
> Doesn't Java fit the same bill? (although it was kinda modern at the time)
Yes, I think Java was an earlier attempt at this.
> Sadly it's really hard to shoe-horn this onto Go, now that the std lib is widely used. Same for proper sum types (which can then be used for multiple return values).
I agree, I think these types of features are much easier to design around than to add into a mature ecosystem later
> But the continuation of the nullability mistake puzzles me the most... And that wile Go's be designed by big name lang gurus: what were they thinking?
Is it just a timing thing? It seems like explicit nullability came onto the mainstream a few years after Go debuted, but maybe it was already discussed in academic circles before then.
> Is it just a timing thing? It seems like explicit nullability came onto the mainstream a few years after Go debuted, but maybe it was already discussed in academic circles before then.
I know OCaml[1] has it ('93) and Haskell[2] ('90). Also the claim that implicit nullability is a mistake was made in 2009[3] (around the same time Go was released).
Given the seniority of the designers of Go, I expect they had knowledge of this.
... if "it" is even a thing. I would say that it's not, rather implicit nullability is the thing. It's just not something you'd do in a typed language unless you really specifically chose it. That's how Hoare could claim it was his billion dollar mistake. Implicit nullability was a thing he (and subsequent language designers) opted in to.
> Errors as values is a far superior approach to exceptions.
Why is that? I have never seen a cogent explanation for why this is the case.
I can tell you why exceptions (as implemented in Java) are cool: You can write code as if every function call is successful, as opposed to adding a line (or more) of error handling code after every function call, which makes it harder to follow the logic.
I write Java, but I prefer errors as values exactly because you can't only consider the happy path. It really makes you think about the appropriate response to this specific failure. You can do that with exceptions, but in practice it's exactly like you say: all error handling is delegated to some generic catch-all, which in a web app usually just gives a generic 500 Internal Server Error.
If I encode my errors as values (usually with Either), I have to decide how to gracefully fall back if a failure occurs before I'm even allowed to use the successful result. Maybe I just hide the part of the view that needed that data. Maybe I show an appropriate error message. Maybe I email operations to let them know about the problem. Whatever I do, I have to actually think about it and not just assume that the error will be caught somewhere by someone. The result is usually a dramatically improved user experience when failures inevitably occur.
Exceptions tend to pass the buck so far down the line that there's no context to make an appropriate decision. Values tend to force a decision early, when you still have enough context. (Obviously both can be used against the grain, but the question is which pattern is easier.)
> It really makes you think about the appropriate response to this specific failure.
I beg to differ. It forces you think about whether but not why the function itself has failed. The "why" is embedded in the type of the error (or exception) itself, but Go does not force you to examine the type of the error; indeed, sheer muscle memory compels you most of the time to just write if err != nil again and again.
> I can decide how to gracefully fall back if a failure occurs before I'm even allowed to use the successful result.
You could wrap every line of Java code in a try/catch if you wanted to (it wouldn't be idiomatic, but it's definitely possible). You're just not forced to.
For what it's worth, you also shouldn't email your ops team directly from production code. It doesn't scale. You should log the error, and your monitoring stack should handle altering the relevant team (full disclosure: I work for such a monitoring stack company). It's very rare that you actually want to recover from errors, as that's typically a pattern that leads to silent failures and difficult to diagnose issues.
Yeah, I'm not endorsing Go's approach to errors, just the idea of errors as values. I can't speak for Go, but other languages make it very obvious that to handle an error you have to inspect its type, and thereby get at the "why".
The lack of forcing to handle (checked) exceptions is exactly why I dislike Java's model. Until I've checked for an error state, I want to have an Either that may or may not have the data I'm looking for (and if it doesn't, has an explanation). In a truly exceptional situation I can crash and give a 500 error, but checked exceptions are by definition supposed to be recoverable, and in a production codebase I don't want to be able to lazily avoid recovering from them.
You're differentiating between Java the language and Java the ecosystem. Java's tooling is so strong that you can use static analysis tooling to fail builds that throw checked exceptions, if you want. See e.g. https://rules.sonarsource.com/java/RSPEC-1162
That… sounds like exactly the opposite of what GP wants? If you can’t throw checked exceptions then you can only throw unchecked exceptions, whose catching and checking is even less enforced by the compiler.
GP is differentiating between library code and application code. You wouldn't turn on the static analysis to fail the build on checked exceptions in library code, since you want to force the application to keep track of known error modes in libraries and decide where it's appropriate to handle them. But you would turn them on in application code in order to prevent application code from using exceptions-as-control-flow, which is an anti-pattern and one of the reasons why people who value Go's error handling sometimes hold their opinions because of scarring they suffered from anti-patterns that were in Java codebases they worked with.
I'm not sure where all these comments which specifically mention Java are coming from. In Java you MUST catch or forward all exceptions besides those derived from RuntimeException, and you MUST specify them in the type signature. RuntimeExceptions are equivalent to go panics, so you don't have to catch them.
Java never lets you ignore an non-panicking exception. Unlike Go, where you CAN ignore the error value.
Besides that, Java forces you to explicitly state what type of exceptions each function can throw.
Java exceptions are stricter than Go, not vice versa.
Now, if we were talking about .Net or JS, or Python or mostly any other language with exception I'd get this criticism, but it is patently false when it comes to Java.
I think Java in particular sticks in a lot of people's minds because, while the ideas are there, the execution is not - the standard library sets a poor example that is generally followed elsewhere.
An attempt to read from a Reader, for example, can fail with a java.io.IOException. The javadoc for that lists 31 direct known subclasses, including CharConversionException and JMXServerErrorException; and there is always the possibility of a custom subclass from somewhere else in your application or a 3rd party library.
You can't do anything sensible with such a broad error (like decide if retrying might be sensible), so you end up either propagating it in your type signature, wrapping it in a RuntimeException, or ignoring it.
For what it's worth, I believe many I/O exception sub-classes came from before the time wrapping exception was established as a best practice in Java (as it has been in Go recently, with the introduction of fmt.Errorf("%w")).
I agree that the Java standard library (especially the older parts of it) is quite bad. The Go standard library, whatever issues it has[1], is still pretty solid.
Unfortunately, the questionable quality of the Java standard library and the Java EE libraries back in the day, have led to some of the bad patterns we see nowadays in Java, that are not necessitated by the language, e.g. gross abuse of inheritance.
I still want to point out here that error values are not superior to exceptions, since:
1. Java shows exception handling can be forced to be explicit as well.
2. Handling Go errors is NOT forced. In fact, you can always ignore the functions' return value or assign the error part of it to a `_`. This is far less explicit than an empty catch block.
Can you please elaborate how the pattern that you describe doesn't pass the buck to a portion of code that has less context for the cause of the failure?
> You can write code as if every function call is successful
How does writing code as if every function call is successful (when you know that some functions will fail) not lead to some other bit of code further up the call stack having to make a decision about an exception that it doesn't have the context for? A bit of code which probably was written by a different developer who didn't anticipate what you were going to do?
A library knows what happened, but not what to do about it. It doesn't know who the caller is. It could be an interactive user or a microservice RPC or a Spark job or a debugger.
There have been frameworks like the Common Lisp Condition System to let a caller dictate a recovery/retry policy, but they never caught on. In practice "do no harm, give up, and report the error" is what almost everyone wanted, and most languages support it without punitive effort.
> Can you please elaborate how the pattern that you describe doesn't pass the buck to a portion of code that has less context for the cause of the failure?
Copy/pasting another person's answer:
You could wrap every line of Java code in a try/catch if you wanted to (it wouldn't be idiomatic, but it's definitely possible). You're just not forced to.
You can handle some of the exceptions in the same function, without going to the extreme of wrapping every line of code in a try/catch block.
> You could wrap every line of Java code in a try/catch if you wanted to (it wouldn't be idiomatic, but it's definitely possible). You're just not forced to.
Yes you ARE forced to. Unless the exception derives from RuntimeException.
Look at any piece of Java code and try to guess the cyclomatic complexity of it. It's not simple. Because every line, and every function call can fail. And that failure is one more place where the execution tree of your code branches. You can't see it unless you check every method call in your code.
In Go -- every error is obvious, and you get a sense of a cyclomatic complexity of any piece of code by just going through it quickly.
So, that's it for me. The complexity of the code is visible.
I think it's a little bit complicated to explain but mostly it boils down to this: errors are real. Java methods kind of let you ignore them through declaring exceptions, with this idea that, well, somebody else will deal with it. Golang functions make errors feel more present. They force you to think about how you're going to handle the errors up front, and to question whether or not you even should error in a particular instance. It's actually helped change the way I think about functions. There are many behaviors that in Java I would not have even considered making them idempotent but in golang making them idempotent is both easier and, as it turns out, more robust.
The patterns for error handling the golang have introduced are admittedly verbose, but they do lend a certain element of confidence that once the code is written, the errors should be handled. Of course a programmer can ignore the errors explicitly but doing so is different than forgetting to catch a thrown exception, because the programmer must go out of their way to write code ignoring the error. It feels like there's more agency around the decision.
The difference is that with exceptions sloppy code is the default.
As you write code in a language with explicit errors, the language makes you acknowledge that the code you call can error out. This makes you stop and think what to do about the error. You can choose to ignore it, but that's a conscious decision that the language forces you to make.
With exceptions, there's no such feedback mechanism from the language/compiler. In order to write robust code you yourself must have the discipline to add exception handlers around the appropriate calls.
In short, defaults matter. It's simply easier to write correct, robust code when you don't have to go out of your way to do it.
> It’s really hard to write good exception-based code since you have to check every single line of code (indeed, every sub-expression) and think about what exceptions it might raise and how your code will react to it.
Only checked exceptions. And the integration of checked exceptions is so bad, and the split between checked and unchecked so arbitrary, that most codebases have sworn off of them.
the fundamental tenet in Go is that every error should be handled. Here is an excerpt from Dave Cheney that clarifies this:
"For the truly exceptional cases, the ones that represent either unrecoverable programming mistakes, like index out of bounds, or unrecoverable environmental problem, like running out of stack, we have panic.
For all of the remaining cases, any error conditions that you will encounter in a Go program, are by definition not exceptional — you expect them because regardless of returning a boolean, an error, or panicing, it is the result of a test in your code"
I think the point is that in Go, you expect failure. Failure is not exceptional. It's a first class part of the logic.
Java conditions you to view failure as an exception to the rule, and the happy path as the real code (which is the attitude you express in your first comment). Lots of us have since observed that this approach to failure leads to errors because programmers ignore failures, allowing them to be handled by a catch-all at the top of the stack, which typically just dumps a stacktrace and calls it a day.
The paradigm espoused by Go sees errors as just another program state, and one whose implications are just as important to consider as the desired behavior. This forces programmers to consider all the implications of a failure, rather than just adding another `throws` clause.
> Lots of us have since observed that this approach to failure leads to errors because programmers ignore failures, allowing them to be handled by a catch-all at the top of the stack, which typically just dumps a stacktrace and calls it a day.
That's not typical Java code. You could do that in quick & dirty code, but I haven't seen such code in production code.
> That's not typical Java code. You could do that in quick & dirty code, but I haven't seen such code in production code.
It sounds to me like you're an extremely lucky person. I've seen too much of that sort of thing in production code, including a catch-all for Throwable at the top that didn't even dump a stacktrace.
Anecdata: but I see this pattern all the time in production code. It’s not limited to Java: I’ve seen it in TS/JS, Python, and C++. I haven’t (yet) seen it our Go codebases.
I'm working with some Python that follows this error as value convention and it's awful. The code likes to catch exceptions as soon as possible and replace them with strings like "not found". Exceptions are useful things, they tell me what broke and where. Instead I get a "not found" error value, and find out that happened because the endpoint called a service that returned a "item not found" error value, and then a few layers of obscure string error values later I find out some logic did division by zero or something.
In other words, I also don't understand the hype around error values.
So, back to Python, what error value is suitable for replacing an exception? I mean, Python is flexible enough to do Go-style error values, but what does a good error value look like? Maybe I just haven't seen good error values?
If an error happens way down in the weeds and I initially return a very specific error value, do I just pass it up the stack forever? If so, how is that different than traditional exceptions? Do I replace the specific error value with more general error values as I move up the stack to higher-level code? If so, then I am throwing valuable information away and am likely to end up with something like "not found" as my error value at a the top -- not very useful.
In Go, I would do one of:
* Pass the error value up unchanged
* Wrap the error inside another error
* Pass a new error
* Swallow the error and do something else to cope with failure
I would probably also do some error logging, capturing the state of relevant local variables, especially if it is one of those cases where "see an error" is unexpected.
Thanks for the answer. It sounds like pretty much the same thing good exception handling would do.
I suppose they're two sides of the same coin. Both can be done well or poorly.
I do resent the idea that error values are automatically better or easier to understand, because I'm dealing with some poorly done error values now and wishing for traditional exception handling.
I guess the differences I see are between "with exceptions, you can accidentally forget to handle the errors in the right place, and you end up handling them elsewhere" vs "with error values, if you forget to handle the errors, you don't handle them at all" on one side. And "with exceptions, you do't necessarily have the immediate feedback that one may come" vs "with error values, you need to choose between sticking them in an error value variable, or explicitly ignoring it" (and with Go's fondness for "you assign to a variable that is then never used? That's a compile error!" it essentially means "you either handle the error or you intentionally disregard it".
Either is fine, I think. But, don't mix both in the same codebase. And with exceptions, I would really like Common Lisp-style restartable errors, to get the flexibility I feel I have with error values.
> I've written a bit of code in Go, and the problem I have with it is primarily it feels really outdated for a "modern" language
Go is a language to get the job done. It's for the masses. It's this type of language you write a blog post on how you have done this and that and why instead of a scientific paper. It's really the boring mortar.
Exactly this. Go is the language you use when you need to get shit done, reasonably performant and easily distributed to different operating systems.
The language actively hinders any attempts to be fancy or "expressive". You write (or copy/paste) the same pattern(s) over and over again.
On the other hand, you can pick up pretty much anyone's Go codebase and quickly see how it works. No need to figure out what flavour of metaprogramming or other fancy crap the original author was a fan of at the time of writing.
It's boring, it pays the bills. You can be expressive with other languages on your own time.
> Errors as values is a far superior approach to exceptions
No, it's not. Unhandled errors that aren't noticed just lead to undefined/nondeterministic behavior and some really nasty bugs. And moreover, I literally cannot understand how it is possible for any programmer to not realize this (compare: elixir's approach, which instead embraces errors and restarts processes instantly).
It feels like Option types could be done quite easily - basically slices with a hardcoded capacity of 1. Could compile down to a pointer type. But it would make linting way easier. Of course, the real power of Option comes from Map.
Give Go a map and option and I'll be a happy gopher.
At that point why not use something F# or Rust? Both can provide comparable or better runtimes, very decent ecosystems, and they are already much better languages.
Because Go as an ecosystem has some really attractive properties that few other languages offer in the same combination: portability, stability and a strong compatibility guarantee, quick onboarding for new learners, a robust and scaleable runtime with GC that rarely gets in your way, minuscule startup footprint, reasonable direct control of memory layout when you really need it, a fairly large developer base and corporate backing/funding that's not going away any time soon
So I have to repeat the question: why not F# or Rust then?
None of that is too uncommon, with the notable exception that Go is better at the "quick onboarding" part since it goes out of the way offering no new concepts or syntax that have to be learnt; something that the comment I was replying to would like to change by introducing Optionals and Functors.
Yeah, I have mixed feelings about it as well. There are some good things, but there are also ugly warts.
For example if you try to use some numeric types that are not int, int32, float64 (for example float32 or uint16) you'll be in a lot of pain due to constant casting back and forth. Yes generics could solve this one too.
I also couldn't make myself enjoy programming in it. I think it's because it tried to be simple, so anyone can learn it. Because of it is kind of rare to say that you created with a creative way of solving specific problem.
That's very different to for example C, to which Go (at least early on) was compared to.
One of the reasons I love Go is that it's just so easy to read Go code written by other people on my team or third party libraries. There have been tons of cases where I'm working with another library and I just step into their code in my editor to understand how something works. And the code is always so easy to follow.
It's very rare that I come across weird patterns or someone trying to be very clever. Its always straightforward code.
This is one of its biggest advantages to me as well. I have had to support many production systems, written in varieties of languages, over my tenure and in my experiences the systems that live the longest are those that are the most readable by new team members; people transition in and out and its important for them to learn quickly. And while a person's language experience matters to some degree, it is more the underlying ___domain that takes the most time. Languages that simplify on the number of solution patterns and focus on key ones being "idiomatic" helps to lower the mental load someone has to overcome to learning that ___domain. If you know C, Java, JS, etc. you can pretty quickly figure out how to read Go, and that matters.
Is Go perfect at this? No. I too would love to see some higher level functions exist for to help reduce boilerplate. For example, this proposal: https://github.com/golang/go/issues/45955 to add Filter, Map, etc. to slices. That seems like a practical set of functions to add to minimize boilerplate while at the same time not breaking away from simple idioms.
This is precisely what I’m afraid off. I don’t want library to become a mess of chained lodash style functions. The verbose style of writing your own loops is very readable
> The verbose style of writing your own loops is very readable
That depends entirely on what style of programming you're most accustomed to. I've primarily written functional code the past several years, and even in that relatively short time, it's become for me in general easier to read chained/piped/composed function expressions than equivalent loops.
But sure, I can understand if you don't want that flexibility in Go. It's a fair position if you want the language to be confined to a certain imperative style of programming.
However, it's interesting to note that even if one can do loop-based imperative programming in Haskell and OCaml if one so desires, it's not idiomatic. Presumably, same would apply in Go.
Agreed. I think that's probably one of Golang's strengths (and I've used it a bit professionally several years ago). I can understand if they want to keep it hyper-standardized.
For devs who want a more functional approach, there are a number of alternatives, including Rust (which is not a fully functional language, but has many aspects thereof).
While generics will allow for such to occur, IMO if the "idiomatic style" is to only leverage it for specific patterns then it should not really hurt general readability for the most used frameworks/libraries. There will, of course, be some tradeoffs that authors will make, but overall I'm hoping it enhances readability, especially for the code my team and I write at least. Looking at the proposal, there is a lot of practical debate about naming, consistency with bytes methods, etc. and such honest debate keeps me optimistic.
With big python projects it's often difficult to figure out what code is going to be executed next due to inheritance and mix-ins.
With Go, it's usually pretty straightforward. Yes, there are interfaces, but they're used in places where people actually want the choice of code to be dynamic, rather than people just having fun building up crazy hierarchies in the name of DRY.
So, with Go I find it very easy to read because of high code locality and the ease of following to the correct destination with function calls.
For things that are already local, "ease of reading" is just code for "familiar". Go is strictly easier to read because tracing through is strictly simpler. Anything else is just spelling and those barriers disappear with only minimal experience.
This is true for particular types of python, but not true for most of the python code I work with, for example. I'll grant you "encouraging non-complicated architecture" is a good thing, but Go swings a bit too far in the other direction, for example `set(foo) - set(bar)` in python is an extremely expressive (and efficient) way to give me the things in foo that aren't in bar.
In go, you can either do this as a nested for loop, which is harder to read and n^2 instead of n, or explicitly loop over each iterator and convert to a map (which, to be fair, is exactly what python does too), and then write the map-diffing yourself, which is easy to screw up and should probably be thoroughly tested to ensure you didn't make any mistakes in your implementation.
In python its one line of clear, expressive, known-to-be-correct, code. Granted the generics proposal should do a lot to improve things here, but I disagree that go is "strictly easier", because there's often far more to trace through, as there is far less abstraction provided for you by the language, so instead of having to perhaps unwind a single complex expression that uses some advanced language features, you have to unwind the pseduo-reimplementation of those abstractions in the local project.
I think you can summarize the go flavour of "readability" as:
Fewer constructs for the price of higher Signal-to-Noise ratio.
Go eschews almost any kind of syntactic sugar that might be unfamiliar to you.
The endemic "if err != return err" issue is also highly indicative of this choice.
I really find that quite hard to stomach. For small apps, this can be acceptable, but for any Go app that gets large enough, this trade-off becomes untenable. You have to scan through too much visual noise to get a clear understanding what the code is doing.
Go is a language for writing microservices that handle http requests and/or client-side networking "clients" to an api served by said microservices. My guess is the tradeoff is okay in 95% of those scenarios.
This has been my experience. I love the fact that the standard library itself is written in Go, so it's easy to see what exactly is going on and even learn some idioms that way. It's refreshing for me personally as I had a JavaScript background before learning Go
Is the primary Python implementation self hosting? Ruby? JavaScript? PHP? Perl? It's common for compiled languages to be self hosted, but less common for scripting languages. (Not that Go is a scripting language, just pointing out that many popular languages aren't self hosting.)
Even beyond the question of self hosting, I would say that it's rare for much of a scripting language's standard library to be written in that language... especially performance critical parts of the standard library. There would just be too much performance left on the table.
The person you're responding to specifically mentioned they came from JavaScript, where the standard library implementations weren't written in JavaScript.
(PyPy is a notable exception to this rule of thumb... perhaps as part of its desire to prove how good the JIT is, it seems to be mostly written in Python, which is neat.)
Yet decades old dynamically typed language like scheme and lisp are all self-hosted, even their own compilers. I can bet you that quite a few scheme implementations will beat python in performance.
Haha this must be ironic or Stockholm syndrome. With duck typing and channels, missing basic modern language constructions... it is definitely not known for readability.
Lack of expressiveness improves readability: in a team setting not everyone 'expresses' themselves in easy-to-parse ways. Code is written once, but read many, many times.
I used to work on a team that maintained a large Perl codebase: Perl can be both expressive and terse which decreases readability, especially at times when a colleague felt like expressing their cleverness/individuality in a 'brilliant' one-liner using obscure language features. When the clever code is buggy, you'd have to fix the edge-cases in an even more 'cleverer' way, or unroll it into a readable function.
Lack of expressiveness does not guarantee improved readability. It can only prevent abuse of language expressiveness that could harm reability.
But expressiveness, when used correctly, can also improve reability. Which is more readable?
longNames := users.filter(x -> len(x.name) > 10)
longNames := make([]User)
for _, x := range users {
if len(x.name) > 10 {
longNames := append(longNames, x.name)
}
}
The only world in which the second example is more readable is one where developers never heard of filter, but have fully memorized (and are not confused by) all 3 variants of make(), the strange syntax of append (including all of its shared data pitfalls) and just got used to mentally parsing this behemoth pattern above as "filter".
This world is probably real in some corners of the wider world where Go is used, but it is not the only possible world, and I don't think it's a good one.
Both of those are equally simple to read as it is a very, very simple example. Simple examples are never the issue. The issue is when there are complex things involving edge cases and fiddly bits where there's a nasty hack in the middle of it that can't be removed easily or cleanly or both.
In theory there's no difference between theory and practice. In practice Go is a lot younger than Haskell, to take that as some kind of comparison, and there seem to be a lot more successful projects written in Go despite that. I like Haskell, I haven't even be bothered to learn Go yet on my own. But lets be sensible about readability. Simplicity almost always wins or at worst ties, as in the simple filter above.
Everyone writing serious Haskell tends to end up writing new additions to the standard library to make it work. Reading Haskell can be challenging on this basis. There absolutely is pyrotechnical showing off. I don't see this kind of issue at all in Go - for good or ill. I'd be astounded if Go is 5% of the fun of writing Haskell. I'd be more likely to choose Go than Haskell if I have a large project than needs to work in short-ish amount of time. (And when isn't the amount of time short-ish?)
Not to mention the item index, which is so often unused as in your example, as the first position in `for` loop bindings. Would it be so hard to have a `range_indexed` version that includes the index? Maybe I don't write enough Go code, but I also tend to think the first item conveys more importance which is why I often double take when I read Go for loops. "Wait why are they ignoring... oh yeah it's just the index".
It would be kind of a fun experiment to require a rough estimate of LoC written in a given language be included in feedback about that language. These comments are kind of a cesspool of poorly written feedback that wraps ambiguous complaints about "readability", "maintenance" and even the silly take of "Go is a language for engineering managers", whatever that means. I suspect most of the negative comments on any language come from people that have spent practically no time writing it, and thus are inherently uninformed.
Of course, if you hate a language, you're not going to write lots of code in it (unless you have to, and then I would expect your feedback to be pretty negative but at least informed). Feedback from beginners/programmers external to a language is super important for the success of that language of course, but lots of debates about things like programming language are cluttered with feedback that isn't really made in good faith. Someone that doesn't like static typing is going to leave feedback about Go that may not directly say "I just don't like static typing", but ultimately is as simple to reconcile (and thus, should mostly be discarded).
I have written hundreds of thousands of lines of plain old C, as well hundreds of thousands of lines of Go over a career spanning three decades. I've used Go in earnest since 2013.
I would pick Go for any large project in a heartbeat.
I have to concur that the haters who post to HN probably have never used this language in earnest.
Here's what you get out of the box:
* Extensive standard library
* Cheap concurrency
* Fast compilation
* Built-in test/benchmark framework, including coverage and race detection
* Built-in heap and CPU profiling
Of all the complaints the one I understand the least is the reservations about performance. It is within 10-20% of C, and callouts to C and assembly are possible. (My 8-way multiplexed MD5 routine was written in AVX assembler and lives on in minio.) Extracting that last 10-20% is two or three sigmas away from the norm of how programming languages are used these days.
The objection to generics is similar. The lack of generics shows up -- once in a long while. It doesn't prevent me from being immensely productive with this language.
Looking back at the last few years I've wondered if I could have accomplished what I did in Go by using a different language (C in particular) and the answer, for me, is "not a chance".
I recently spearheaded the creation of a new Go service at my company. If it was Node or Python I could easily imagine a team getting sucked into weeks of bikeshedding the API framework, the test framework, linting tools, formatting tools, the TLS stack, research spikes on packaging and distribution, etc.
With Go, all of these problems were basically solved by the strength of the standard library. We could not be happier.
Networking prowess of Go is often underrated in these discussions. Building an web application with server, router, mux, auth etc. all from standard library and just deploying the binary blob on the cloud (irrespective of the architecture) to get your application running with less effort than competing programming languages/frameworks is itself enough for most use cases.
I think having options is just sign of more mature language.
In python you could also implement the API framework, do unit testing, TLS, packaging using standard library. And similarly there are 3rd party frameworks for Go as well.
There's always tradeoffs, to be sure. What many are trying to convey is that Go's defaults tend to be the stronger and simpler options.
However, for a lone wolf project, I'd lean more on something like gorm, than just hand-craft and maintain lots of sql.
Another misconception is that it's all for inexperienced programmers. But you need decades of experience to appreciate all the opinionated tradeoffs already made in Go.
It's just one more tool though, and nowhere near FP, which is a different beast altogether. Lots of features around Go and ecosystem is clearly inspired from Haskell. That might be another good tool from the other end of the spectrum.
Go is just new, so it had opportunity to build what we already know. Python is older than Java. When it was first released there was no World Wide Web even, not mentioning REST.
I used to use Java because it was fast enough, have good libraries and was productive enough. Go is almost as fast, the standard library has most of what you need and its ability to compile to a single binary makes deployments simple.
I also look back and think, could I have done all that in Java? Probably... but I also feel it would have required more effort.
Java has come a long way. It's pretty easy to program in a functional style in Java these days. Go seems to go out of its way to make that hard. Maybe generics will help.
Go is more about explicit imperative style, defaulting to good/known performance characteristics. Would advice against trying FP with it, unless just for testing it out.
The difference is of course the JIT -- with Go the single binary means you can disassemble it to find out what it's trying to do. JIT adds a layer of mystery.
Also, with Go it's largely possible to avoid heap allocations in performance hotspots. With Java I'm not so sure.
I agree in general, but in this particular case (escape analysis [1]), you won't see what's happening in the ByteCode output.
That being said, it's still very easy to see how the code will be JITed by running it through JMH (or just making sure it runs enough time to be optimized). The JVM and IDEs like IntelliJ contain tons of tools to review that.
To get back to the issue at hand, from my personal experience, if you have many small varibales, your app will perform better in Java than Go. The escape analysis in Java is probably more advanced than the one in Go, and in Java you could also use a generational GC if you prefer throughput to latency.
[1] To be more accurate, the actual optimization the JVM does is called Scalar Replacement, and it's not even about allocating variables on the stack, but rather about inlining primitives:
https://shipilev.net/jvm/anatomy-quarks/18-scalar-replacemen...
The argument above is being able to reverse engineer the machine code that will be executed by the processor. Java bytecode isn't that. That's why he said "the JIT adds a layer of mystery".
I suspect parent may be better able to divine what the Go compiler intended by looking at the produced assembly, vs. knowing or predicting how the JVM will behave by looking at bytecode/decompiled Java code. Not that it is impossible, it's an additional skill that's bourne of familiarity with Hotspot/Graal/$JVM
It's awesome to see all this Java expertise on this thread but at its essence -- a jar file doesn't define the program that's going to run. It's the union of a jar and the JVM it runs on. With Go, the binary is the whole story.
So even if I can predict how a particular JVM will treat a given jar, I'm still stuck if my code might be deployed across a range of JVMs.
I say all this having written orders of magnitude more Go, with apologies to the Java programmers.
>So even if I can predict how a particular JVM will treat a given jar, I'm still stuck if my code might be deployed across a range of JVMs.
This seems like an odd objection to me. I'm not the biggest Java expert, and I don't currently use it professionally, but I've never written Java that was meant to run on a runtime I not only didn't control but did not know the version of. In theory this was supposed to be a big feature of Java, in practice the community is moving towards bundling the JVM when distributing apps, precisely because versioning is such a fiasco.
Why not just ask the current jvm what version it is and act accordingly? I mean surely you can advise the user / customer what JVM's your code is compatible with, which really translates to what JVM's you have tested your programming on, right?
I'm obviously surrounded by folks far smarter than me, so I'm posting something probably grossly uninformed to see what the experts think!
>Why not just ask the current jvm what version it is and act accordingly?
I would! That's more or less what I was saying. But earlier on in the life of Java, there were technologies like Web Start and applets that could be embedded in webpages and the idea was that they would run on the client much like JavaScript does today, and you would have little idea of what the client environment was like, other than "it's a JVM." That turned out to be a bad idea for a number of reasons, most famously security, and the situations in which you can't just bundle your JVM are dwindling.
Fair enough! Still, I'm thinking many javascript pages query to find out the browser environment and also act accordingly. I do agree with the security complaint, however I also like web apps.
I do think that you are sort of implying that web applets can't determine which jvm they're running under, is this really the case? Was there any reason that the java webapp folks prevented the applet from inquiring of the java version?
This page chats about the version stuff for applets, it seems even with applets there was some options for picking the java version:
> I do agree with the security complaint, however I also like web apps.
Java applets are basically dead at this point, it’s all JS.
> I do think that you are sort of implying that web applets can't determine which jvm they're running under, is this really the case? Was there any reason that the java webapp folks prevented the applet from inquiring of the java version?
I never wrote applets but the point I was trying to make is that your environment is fundamentally unknown in that context. You could query for different versions and so on, but you would probably never know for sure because you don’t control the runtime environment (everything from the JVM down to the OS). This was supposed to be a big selling point of Java and it has for the most part turned out to be more trouble than it’s worth. That’s all.
>... a jar file doesn't define the program that's going to run. It's the union of a jar and the JVM it runs on. With Go, the binary is the whole story.
I was saying the same thing - albeit poorly perhaps. I'm no Java expert either, but once had to download 'jad' off sourceforge to decompile a buggy 3rd party lib :-).
The performance measurements I've seen point to Go being faster. To me it seems also to make more sense since Go is compiled to native code, whereas Java still needs JVM to run the non-native binary.
Can you elaborate in what kind of cases Java had better results?
Tight loops usually where it’s really dealing with math and not business logic. The JVM GC was tuned for throughout over latency which may be a factor.
The problem with not having generics is the systems you build instead. They either throw away type safety and use reflection, which causes hard to debug issues, or they lean on code generation, which is slow and hard to debug.
I agree that the lack of generics only shows up once in a while and doesn't hamper productivity as much as might be expected, but I'm still very excited to see them added to the language.
In theory yes, in practice if your Sort array function takes a list of interface{} instead of a well defined type is not going to cause any type bugs. Might be slow, might be ugly, but lack of types on functional/generic stdlib functions doesn't make your code buggier if the original
implementation is sound.
I'm cautiously optimistic about the addition of generics to Go.
If all goes well, we'll get some nice stdlib stuff and 3rd party functions for handling different kind of data with a common api.
But if the - you know what kind of people - discover it in earnest, we'll get a hellscape of C++ templating proportions when people go way overboard with it.
Sincere question out of curiosity, since I have not been exposed to Go at all.
How do container types, like arrays or hashmaps work without generics? Do you basically have to declare and implement types/methods for each different contained type?
I hardly ever need generics directly. If I have to write an algorithm for 2 types, I can copy and tweak as easily as I can deal with the added complexity of programming with abstract types.
BUT, I need generics all the time indirectly. I've worked in lots of languages over the decades, and I highly value languages with mature/debugged/optimized standard libraries of algorithms and data types (esp. "containers").
Go has an impoverished subset "built-in", and that's all it offers.
One of the best things about Go is the high quality of its standard libraries--where it has standard libraries--so there is a gaping hole where the container and algo libraries ought to be. Experts in these algorithms could take advantage of Go's concurrency for example to write library algorithms that provide a combination of performance and safety that I couldn't create quickly if at all. They could, that is, if they had generics in the language, but they don't, so I'm on my own (for now).
When people (repeatedly) claim that Go doesn't really need generics because they (the claimants) hardly ever need generics, I have to wonder whether their knowledge of DS and algos is especially high or especially low: that they could whip up a debugged and optimized algo as easily as calling it from a library or whether they aren't even aware of the DS/algo that they really ought to be using.
Personally, it’s very rare that I need to reach for a specialized algorithm. I’m not sure if that’s primarily the problem space I work in (services providing user identity and access management), but I never find the need to go beyond a brute force search or need a data structure other than an array or built in hash map (sometimes a concurrent hash map). Most of the optimization I do involves eliminating network calls and implementing caching.
Others have answered, but I just wanted to add, your question is usually where beginners/non Go users start to pearl-clutch. I would advise, if you're curious, to give it a shot for a while. As with all new things, try to not just write the language you're used to, but in the syntax of the language you're using, and instead embrace patterns that exist in a given language.
Lots of people find the things you're concerned about missing to not really be the end of the world.
Go supports a handful of built-in generic types, like hashmaps. What Go currently lacks is user-defined generics, though that is actively being worked on.
Arrays/slices and maps are in fact provided as built-ins that function like generics, e.g. []foo and map[foo]bar work as expected, as do associated helper methods.
But those are the only container types with built-in support like that. (Channels can similarly be of any type)
They use hack of "interface{}" which does type checking at runtime (slow). And you then cast it to a proper type.
If I remember correctly (haven't used it in a year) Go cheats a bit, and it has few functions in standard library that do have generics (like make), it just doesn't provide this functionality to the developer.
If you can avoid interface{} you should, ie. by declaring interfaces to types and using them explicitly. You can then mix your own types, and still be protected by the type system.
That’s not really true, since built-in containers are special sui generis generics in Go, whereas in an untyped python they get by being untyped, and in python’s statically-checked type system (as implemented in pyright, mypy, etc.) they use general-purpose generics.
That complaint is as old as the language --- the first piece about Go I ever read, after my friend Yan convinced me to pick up Go, called it out for having "generics for me and no generics for thee". I've never understood why I'm meant to care. I get, if you want to write your own generic containers, being irritated that there aren't universally available generics (yet). I don't get being upset that the language has some special-cased generics. It's not personal. They're not rubbing salt in your wounds. They just need a map type that works.
At any rate: the person asking the question is wondering how you manage to have performant containers with reasonable interfaces in a language without generics. That's a good question! If you've worked a lot in C, you might be imagining a horrible mess of void-stars or something. But no, the ergonomics are basically those of Python, with extra typing.
Given that they stated "Do you basically have to declare and implement types/methods for each different contained type?" in the comment, I don't think they were wondering if you end up using void*/interface{}.
I really don't understand how the python comparison makes sense. Since those particular containers are special cased, the ergonomics are also the same as java (and I'd argue more like java than python). Other than ownership, I don't think go's maps are any more python like than C++s or Rusts either.
> reasonable interfaces in a language without generics
and with static typing. Python doesn't fall into that category. It either doesn't have static types, or does have generics. The answer (as others have mentioned) is that go does support generics for a few particular container types.
I’m not making a complaint, I’m pointing out a fact without a value judgement.
> At any rate: the person asking the question is wondering how you manage to have performant containers with reasonable interfaces in a language without generics.
Uh, I don’t see either “performant” or “with reasonable interfaces” there, the question is: “How do container types, like arrays or hashmaps work without generics? Do you basically have to declare and implement types/methods for each different contained type?”
The answer is, for built-in containers, “no, because they use special-purpose generics”. For custom containers, its basically “yes”, but you probably only want a single contained type with an appropriate interface anyway. In neither case is “basically the same as Python” particularly accurate, though I suppose the former case is similar to typed python using generics in the same way as any language with generics would be, while the latter is loosely similar to bare Python or typed Python using protocols instead of generics. But only loosely.
> The answer is, for built-in containers, “no, because they use special-purpose generics”.
Unless you want to implement utility helpers working on them then it’s yes again, because Go doesn’t have generic functions either, so while it does have a few builtin generic containers, you can’t operate genetically over them.
I've also been mostly using Go over the last 6-8 years. Before that it's mostly been C++, pretty "modern" towards the end.
Go would definitely be my preferred pick for distributed/server applications even though the previous large (mixed C and C++) project I was on was a large distributed application and we did just fine. In fact I'd say our productivity and quality in C++ in that other team was just as good as any I've seen (which is not a random/double blind sort of thing, who knows if that project started in Go from day 1).
I would say that in a large project the language choice has some impact but there are many other variables. A well managed C++ project with competent developers can work well. A less than well managed with so-so developers in Go can be disaster zone. There are lots of other moving pieces, testing, build automation, CI/CD etc.
There are certain areas where I would prefer not to use Go. Basically areas where you spend a lot of time up or down the abstractions. I would prefer not to implement a video codec or a signal processing system in Go. Firmware, motion control etc. would also probably not be a good fit. Going the other direction there are things I can do really quickly in Python (lessay grab some data from some services, do some manipulation, draw some graphs), generally smaller utility like pieces where I want to use data with various random types, use third party libraries. I wouldn't want to write a large project in Python because it's slow and is dynamically typed.
I would also beg to differ wrt/ 10%-20% performance difference. You tend to rely a lot more on GC and produce more "garbage" in Go and the optimizer isn't as good vs. the best C/C++ compilers, I'd say it's more like 50%-150%. But for a lot of stuff it doesn't matter. If you're just making database queries then most of your CPU is sitting there anyways and the delta on the caller side isn't a factor.
Go is pretty nice in it's zone. It's got rough edges but all in all it's fun to use. There are occasional annoyances (like those times where you implement another cache for the 10th time because you can't build a good generic cache or those times where you need a priority queue and interact with those clunky library implementations, so yeah, I'm in the generics fan club). But that hasn't stopped me from using and loving Go. I don't miss those C++ core dumps ;) or dealing with memory management (even with smart pointers and containers it can be a pain). Go's more dynamic aspects (reflection etc.) also make some things easier vs. C++. People can learn Go really quickly which is another bonus, learning C or C++ can take years.
If you can find a team of competent C++ programmers, most definitely you should go with C++ for all tasks.
But if you've got a pool of people who have never touched either C++ or Go, I'd pick Go for the project. It's orders of magnitude easier to teach generic programmers the basics of writing decent Go than C++.
100% on the team making the most difference. (An aside: a software project I've been admiring recently is the Apollo Guidance Computer's. To the moon and back on a 15-bit 1s complement machine and a few kb of memory. It's a case where the team was certainly better than the available tools.)
On the perf side: I'm curious to know where Go sits now relative to C assuming you elide away all heap allocations in your inner loops. In the early days Go's codegen was based on plan9's simple and simplistic C compiler's back end. Things have gotten much better since then and to my knowledge it's within striking distance of C. But I could be wrong.
for webdev work, the standard library might not be that fast, but it's fast enough, and if you need even more speed the fasthttp third party Implementation offers good speed: https://www.techempower.com/benchmarks/
Sort of supports my pulled out of thin air numbers (50-150%). In C/C++ you got those lovely SSE intrinsics ;) But look at the binary tree entry as well, that sort of x13 win for C++ really shows you how C++ lets you get sheer power with decent abstractions.
should we add the amazing declarative marshalling and unmarshalling to/from json and xml and possibly others? or is that part of the interaction with the outside world, ffi and marshalling in one bag?
It depends a lot how you're getting it and what you're doing with it. The existence of projects like rapidjson, simdjson, … indicates that even without GTAV-style mistakes, JSON enc/dec can absolutely be your limiting factor. Even more so if you're on a local network (e.g. between machines in the same DC) where bandwidth is… essentally infinite, for all intents and purposes? When you've got 10Gb/s or even 100Gb/s between your racks, odds are IO is only the limiting factor for very short messages (lots of framing overhead).
As for XML, it's an absolute bear to encode and decode, documents are often large (dozens, hundreds, even thousands of GB), and the format is complicated enough that writing truly fast parsers is difficult. Being CPU-bound when XML is involved is routine, especially if you need a DOM.
It’s not that Go is not a great language; The problem is that it’s a very conservative language that is not picking up low-hanging fruit. Their maintainers also have a nasty habit of being condescending to other people’s use cases; E.g., f-strings, unix shebang as comment, ... .
I find your characterization of critica of Go, highly uncharitable, but unfortunately all too common with Go apologetics.
If we look outside of Go, I see a lot of criticism of C++, Java, PHP and Node.js, but I rarely see the critics being attacked with "You just didn't write enough C++ code" to qualify for a critic license. I find this approach hypocritical.
For what it's worth, I wrote several software systems in Go, and deployed them in production. At least one of them is serving millions of daily active users.
Some things I'm happy about (and these were my reasons to use Go in the first place), like:
1. No JIT = no warmup
2. Portable static binaries
3. Solid tooling
4. (Mostly) high quality static library
What I really find frustrating in Go is not the verbosity (Rust is probably equally verbose, and I get over that), but the utter lack of type safety.
You see, most critics of Go (definitely the people complaining about lack of genrics), DO NOT complain about static typing. In fact, I don't remember ever seeing anyone complain about that. What people often complain about is _not enough static typing_.
Due to lack of genrics and other limitations of the language, Go essentially forces you to use empty intrrfaces, stringly-typed data, weak enums and runtime type checks all over the place. This leads to fragile code, and makes it very hard to have the same safety guarantees you could have in Java - let alone in a more FP-oriented language like Rust or Haskell.
> I find your characterization of critica of Go, highly uncharitable, but unfortunately all too common with Go apologetics.
I might suggest that if you're the type to use the phrase "Go apologetics", you misinterpreted my post and also emit uncharitable feedback quite often :)
> If we look outside of Go, I see a lot of criticism of C++, Java, PHP and Node.js, but I rarely see the critics being attacked with "You just didn't write enough C++ code" to qualify for a critic license. I find this approach hypocritical.
If you scroll up, you'll find my request was simply to include the level of involvement a programmer has with a given language as part of their critique. Critique away! But as with all things, when novices critique things they don't understand, they're mostly just being negative for negativity's sake.
Go into a C++ space and include "I've never written C++", followed by any criticism and let me know how serious your criticisms are taken. I can wait :)
> Due to lack of genrics and other limitations of the language, Go essentially forces you to use empty intrrfaces, stringly-typed data, weak enums and runtime type checks all over the place. This leads to fragile code, and makes it very hard to have the same safety guarantees you could have in Java - let alone in a more FP-oriented language like Rust or Haskell.
We can disagree - the purpose of my post was not to prove Go was a "good" language or not. These bits of feedback sound like you're following antipatterns, but you're welcome to program in Go however you'd like.
My experience in Go in production is limited to ~150 lines of code I wrote to contribute a small feature for one Terraform provider.
The provider in question is a very thin wrapper over an API library. The feature I added contained almost no business logic of its own and would probably take 15-20 lines in a more expressive language.
Based on that experience, I would not choose Go for a project of my own (unless it could directly benefit from Go's strengths, i.e. unless it required fast startup, portability and concurrency).
This is a pretty good example of feedback provided by language novices. You have almost no experience with the language, complained about an aspect of it (line count), and then said you wouldn't use it again, unless you would (as you defined in your closing parens).
I'm not trying to diminish your feedback at all. You like what you like, don't what you don't like, etc. Nobody is forcing otherwise. It's just an interesting pattern that you can see with parallels in almost any language. Much like when novice Clojurists say "I wouldn't use Clojure because of all the parens" - there are things that, for most novice+ users, just kind of end up not mattering a ton. The things inexperienced users think are a big deal (in your example, line count) usually end up not being a big deal.
Infix notation with all the parens is the primary reason I never got comfortable with the lisps. My mind never worked like that. So I really believe there are valid personal preferences that keep people from liking languages right from the start. (I loved forth though. My mind took to it immediately.)
Sorry, in my post I never said or implied that having personal reasons for not liking something was invalid. Distinctly the opposite. The point I was making is mostly that not liking something doesn't mean it is less functional or successful. The parens in clojure are something to acclimate to, not a forever debt. But time and time again you'll see posts discounting the entire language because of them. Much the same with Go and lack of generics/verbosity.
But there is an order of magnitude quantitative difference here that is hard to argue will go away with familiarity (assuming the 10x line count difference estimate is right). Fitting 10x more functionality in the screenful of code you are looking at is a big cognitive advantage.
I’ve not used Go, so I have no opinion. I know some folks that really like it, though, and I respect their opinion.
Same with Rust.
I programmed PHP for twenty years (not full-time). I never learned to like the language, but got fairly good at it.
I now program pretty much exclusively in Swift. It is full-time. I’ve been writing it since it was announced.
I like Swift quite a bit.
One of the things that I’ve learned, is that there aren’t actually that many shops that have done industrial-scale Swift projects. Lots of people have done fairly small ones, but very few really big ones (like you will find with ObjC).
Writing a half million lines in any language gives you some authority.
> Writing a half million lines in any language gives you some authority.
Totally.
In this blog post, Khan Academy says "It is performant enough for us and our team likes it". In this comment section, there are people insisting it's a bad language while having never used it. Not just "Haven't used it at that scale". Legitimately have never written a line of Go. This is part of why I made my post, to try to highlight the silliness of a lot of the "feedback" being slung around.
At the same time, I would expect the people who like it the least to have written on average fewer lines of code in Go than people who like it. It's probably like that for most languages; people tend to not heavily use tools they don't like. I've written some Go, but I have enough experience with enough languages to know I wouldn't like it any more if I were to use it more.
"I don't like it" == "It is a bad tool for me". That's about all anyone ever means when they call something bad. Or it's at least all they can mean. I just think you can have a valid and informed opinion of some tool without having written 500k lines in it.
I wrote 5000 lines of java for a simple multi-user chat room server. I had a separate Java client. I replaced the server with half a page (~200 lines of Erlang) as my first attempt at using Erlang with the exact same functionality.
I still write copious amounts of Java for work and Java is still horrid.
Using an existing websocket library, that chat client, with multithreading, should be a handful of lines in Java. Even writing the raw socket handling yourself, with multithreading, should only be a few classes and maybe 500 lines...
I have spent 3 years being employed to write Go, and i think the language is abhorrent. Once I moved to a job where I didn't need to write Go, I quit smoking.
I appreciate you adding context to your negative comment. I wouldn't really call it feedback, though, as you kinda just said "I hate it" a couple different ways.
Writing code that does a lot of IO gets very repetetive quick - `if err != nil` is just stupid. Different libraries have different conventions on matching on errors. Then there's the inability to know if an interface pointer is valid. Comments that change the way the code is built. The type system felt like it was forcing me to expose either interfaces that are either too restrictive or too open.
So the whole "quitting smoking" thing was...a happy coincidence? Did you go cold turkey? What were the nicotine withdrawal nightmares like? I had fierce ones when I quit
Go's ___domain is CRUD microservices, for which abstraction is generally distraction. If that's all you use it for, of course you are going to like it.
What you're trying to say is that Go's ___domain is mostly serverside. That's true. But it sounds too useful and ambitious, so you've modified it to "CRUD microservices". Which is silly and false, but works better as a slur on the language.
What's dispiriting here is that there are cogent arguments against Go --- decisions Go could have made differently that probably wouldn't have cost the project much in terms of its other goals of compilation speed and simplicity, but make it more annoying to work in. I switch between Rust and Go every day and miss pattern matching almost every time, for instance.
Instead, though, we get the same dumb innuendo. "It's a language for managers". "It's a language for CRUD apps". "It's a language for junior programmers". Never mind that the kernel of truth in those statements is shared by languages like Python; you can't really get away with trashing Python like that, or you'll be haunted by the ghost of one of Peter Norvig's terrifying shirts. Which is a way you know not much is really being said here, except that there's some weird turf war between Go and whatever language its haters are advocating for.
Of all the subdomains of serverside apps, I think "CRUD microservices" are one of the worst fits you get for Go. Fine, CRUD monoliths are even worse.
Go can get you good performance on a CRUD app, but that's probably the only advantage you'll get. On the other hand, Go doesn't have the facilities to build the frameworks that make CRUD apps so easy on languages like Java, Ruby or Python.
Go is a much better ___domain fit for microservices that don't do CRUD, for server and network control plane (k8s, istio, docker), configuration systems,CI/CD tools. So many things that are definitely not CRUD.
As a side note: People love to vilify CRUD apps, but they also require skill to do right. I've seen way too many botched attempts that ended up expensive to maintain and an absolute performance disaster.
A heap of sand minus one grain is still a heap, but eventually it's a DMA ring.
To use wikipedia's definition of CRUD, "[CRUD] are the four basic operations of persistent storage." etcd is persistent but a DMA ring is not?
But fundamentally the main way k8s resources are implemented is by observing the current state and desired state and updating the current state to be closer to the desired state, sometimes performing other actions (which are often updates on a different resource's setting)... so whether "other actions" are enough to make it not-CRUD is to the eye of the beholder.
You wouldn't write a monolith in Go? I think it's fair to narrow it down to microservices (maybe just "services" though?). A lot of programming these days is translating one thing into another thing (there are several packages out there for making protobufs more efficient, f.ex.; which I think is the most I can say on the topic?). It's not a slur if it's actually good at doing something we do a lot of.
I've recently tried using it as a "better shell script for gophers" but it's still too early to say.
We've all heard the nulls, pattern matching, etc arguments already. I just think it's not that big of a pain if you are only doing CRUD and that's why it makes sense in its ___domain.
EDIT: I've fallen out of love with Python because it's bad at large programs (distributed or not). Didn't think that's controversial.
"Maybe just services" concedes the whole point. Yes, Go is not a good language to build a web browser in (and that's an important application ___domain, and we all suffer from not having a popular portable language to do that in; it's why we all run goofy Electron apps on our desktops now). But the space of all possible serverside apps is huge, and encompasses a pretty big chunk of what people actually build professionally.
There's so much more than untrusted-client<->server apps even if we find ourselves in that local maxima. Or maybe it really is the One True Way, but I think more of appliances with firmware, the Linux kernel, the apps on my phone, mainframes, 3d games, FPGAs, PL research, supercomputing... there's so many exciting applications for software! And I don't feel Go is a good match for most of it and will just cause people frustration to try.
I'm most familiar with Kubernetes. This[0] would not exist if the language had sufficient expressive power. But Prometheus, Kubernetes, etcd... they're distributed CRUD. There's so. much. yaml (runtime flex types) and I believe that's in part due to the inflexibility of the language itself.
The whole thing of Kubernetes is to take a goal state as data and abstract away the procedures that make it happen. You were always going to communicate with it in a data serialization format, not a programming language. It happened to choose YAML.
> You were always going to communicate with it in a data serialization format
Perhaps, it's hard to argue that a data serialization format is worse than previous alternative approaches (OLE?), but specifically a schema-less one should be up for debate? (Clearly I've become a protobuf, um, fan? Might be a bit far)
That’s true, but you appear to be a test engineer at Google. Absolutely nothing wrong with that, but it’s also not the same as writing large scale services in Go so I don’t think the experiences are suuuuuper overlapping. I’m a designer and have worked at Google and seen lots of this first hand.
I didn't claim to be writing large scale services? I'm on the team with people who are though.
The code I write is adjacent: refactoring the tests so they run faster with less flakiness (guess what Go makes extremely annoying: mocking concrete types from dependencies! You must either wrap it in your own code (and continuously, manually, maintain that abstraction), give up, or alter the upstream) and writing internal services and tools. Tools are primarily in Go so we can share libraries.
Linear search through a slice is almost always going to be faster than a map operation, because N is almost always small and slices have great cache locality.
If your slice has a million entries, it's definitely a different story.
> If your slice has a million entries, it's definitely a different story.
Searching through a map is much faster than you think. At 20 entries searching through a slice of strings is already slower than searching in a map. For ints this threshold is 100 entries. Feel free to test on your own machine, my benchmark code is here: https://gist.github.com/JelteF/1251f180966eb974d7732ea6c3d3b...
Using a map might not be faster as others have pointed out, but it's often more readable. The core library does this in several places. A map[someType]bool makes for nice, readable code:
The lack of an ordered map keep me using slices where I'd otherwise reach for a map. This is one of the things I'm looking forward to once generics are in Go.
You're conflating Ns here. The N in O(N) is the number of elements already in the collection, not the number of items you are inserting. Since we're talking about the asymptotic case, it's always assumed that N >> the number of elements you're inserting. In general, inserting into a hashmap should be O(1) with a constant time hash. The choice of a hashmap over a list depends on whether the constant time hash is slower than searching through the entire list.
"For a hashmap, the operation of inserting N items is O(N) and yes that N is the number of items you are inserting."
This is not a proper application of big O notation. Big O notation is asymptotic. It's a shorthand for considering the performance of the algorithm as the number of elements tends to infinity. If you insert a single item into a linked list we don't say that is O(1) because it's a single item; you still need to iterate over N items already in the list before you can insert it. The more items in the list, the more items you have to iterate over before the insertion occurs.
But if you don't believe me about the hash map, here's an algorithm for inserting in C++ using linked lists for collision resolution:
template <class T>
bool HashSet<T>::insert(T item) {
// Hash the item
unsigned long ix = this->hash(item);
// Find the item in the appropriate bucket. If it's already there, return false.
if (this->array[ix]->findItem(item) != -1) {
return false;
}
// If the item is not in this bucket, insert it.
this->array[ix]->insertAtHead(item);
return true;
}
The only linear-time operation in this algorithm is findItem() on the linked list, but for a properly sized hash table, the linked list in question here should have very few items in it compared to the rest of the table (n_linked_list << N_hash_table). insertAtHead() is O(1) always. hash is O(1) always. The one thing missing in this example is resizing the table if it gets too full, which is O(N) and generally requires re-hashing all the items. Ideally you could make the table large enough when you initialize it to avoid this altogether, and then you can maintain that O(1) insertion time. This is why O(1) is the best case, while O(N) is the worst case. But O(1) should also be the common case.
Therefore if I have 1 item in my hash table or 1 million, it's still going to take the same amount of time to insert the next item. That's what we mean when we say inserting in a hash map O(1); it will take the same amount of time to insert the first item as the millionth item. Even though, yes, you have to call insert a million times. That's obvious. But that's not what asymptotic complexity analysis and big O notation is for.
With something like a linked list, if you try to insert a million items, it will be fast for the first one and slow for the millionth one (if you are inserting without a tail pointer). The rate at which it gets slower is linearly proportional to the number of items in the list. That's what we mean when we say inserting into a linked list is O(N).
If you still think inserting into a hash table is O(N), then answer me this: what's even the point of a hash table?
My guy, we're talking about inserting N items to a data structure. Or executing an algorithm where the input is a set of N items.
The N here is the number of items you're inserting.
There's another number that we can consider a constant that's irrelevant to this analysis, M, the number of items in the data structure. M doesn't affect the complexity, as don't other numbers you can come up with, like D, the number of donuts you ate today. The complexity of our algorithm in this case is unaffected by changing these numbers.
An algorithm's complexity is talked to in terms of its input. Not in terms of if my input wasn't actually N sized but it was 1 and instead another hidden input which is the pre-existing data structure was sized N.
So, now, the algorithm of inserting a set of N items to a hash map is O(N), because it's broken down into N independent operations where each is constant time.
Same as, despite addition being constant time, the algorithm of adding the N items of a set is O(N).
Okay, I see where you're confused. You're saying the following algorithm is O(N)
for n in N
hashset.insert(n)
Which... okay, but that's not exactly what big O notation is for, because it doesn't tell us anything about insert(). It's not a useful analysis, of this code, because it's the same for any set of N items. The algorithm for inserting into a list is the same
for n in N
list.insert(n)
Okay, so does this mean inserting into a hash set and a list have the same computational complexity? No, of course not. In your post you've already admitted my point: "it's broken down into N independent operations where each is constant time." Inserting into a hash set is constant time. Agreed.
> "The N here is the number of items you're inserting...There's another number that we can consider a constant that's irrelevant to this analysis, M, the number of items in the data structure. M doesn't affect the complexity"
Take this example. Let's say I have a linked list of M = 1 million items, and I want to insert N = 2 items into this list (without a tail pointer). How many iterations will I have to do: N, or NM? It's the latter of course.
But you've just told me the only thing I have to think about when analyzing an algorithm's complexity is the input set. The number of items in the list doesn't matter. Clearly that's not the case given this example.
Let's think about the hash set. I have M = 1 million items in the hash set and N = 2 items I want to insert. How many iterations will I have to do, 2 or 2 million? For a good hash set, just 2. Now, does this mean that inserting into a hash set is O(N) because N = 2? No, that's a misuse of asymptotic complexity analysis, because again, the N in this notation is thought to be tending to infinity. Since N << M we say the complexity of this algorithm is O(1). As the number of items in the data structure grows, the amount of time inserting doesn't change, even though you have to loop over N items to insert each one into the list.
> An algorithm's complexity is talked to in terms of its input. Not in terms of if my input wasn't actually N sized but it was 1 and instead another hidden input which is the pre-existing data structure was sized N.
Yes, but you can't stop your analysis of a data structure there, because the data structure has state. As you process the input set, the set inside the data structure grows, and depending on the data structure, this will change the overall complexity of the algorithm.
You can't say that we just consider the input set, because then there's no such thing as a constant time algorithm, since you always have to loop over N items at minimum. In a proper asymptotic complexity analysis of a data structure, it's absolutely essential to consider M, the number of elements in the data structure, what happens when you add an additional element, and how that changes over time. Otherwise, all algorithms are O(N) at minimum, and there's no such thing as a data structure that gives constant time insert.
As I asked you before, if inserting into a hash set is O(N), what's the point of a hash set? Because inserting into a list is O(N), so why would I ever use a hash set over a linked list? What is even the point of this data structure?
“This context” is also not known except one specific way in which the data is accessed, so blithely saying “I’d use a map instead” and being sure that’s the right answer in all cases is pretty uninformed.
It helps to assume the person who chose a slice did it for some actual reason and not because they are an idiot.
I use Go at work and Rust at home. They're very different languages but I enjoy them both. I do find that Go is more of a "get shit done" language and Rust is more on the creative side.
I just wish that Go would have optionals. Pointers cause too many bugs. Is this parameter a pointer because it'll be mutated? Or because it's allowed to be null? Only way to find out is to run it or to inspect the functions code. You shouldn't need to view implementation details to find out
I'd also rather have optionals/non-nullable types in the core syntax than generics. Although, I must say, I haven't seen a nil pointer crash for a long time.
You must be good at avoiding it or your programs are simple. Go is not much better than C in this particular case.
I forgot specific detail how it was possible (I noticed it 1-2 years ago) but in Go you could have nil value when using references. It was somewhere around implementing an object.
> Our former .NET engineer now says, “Being able to call a function that doesn’t return an error and know for sure that it must succeed is really nice.”
Except when it panics. The article lacks substance honestly.
That being said, it further cements the notion that golang has been a replacement for Python and Ruby, not Java/C#/C++ as originally planned.
Go can easily replace Python or Ruby (because it is faster and libraries are available to do what you need). Go can replace Java and C# especially because concurrency is easier and performance is on par, if you can get along with the inferior libraries Go provides. Go cannot replace C++ in any function, because its compiler is just too primitive, low-level things are impossible and libraries will never be on par with what C and C++ have.
But generally, Go is nice because there is sufficient tooling to do most things nicely and the language is primitive enough to make the tooling work in almost all situations. Which is different from C++, because tooling there is always too stupid for templates, macros, inheritance and pointers. And it is different from Java where tooling is unusably complex, slow and enterpricey. Also, Go didn't accumulate too many "surprise" misfeatures yet, so code can be more reliably written and reviewed than in any of the other languages the parent named. Panic can be seen as the rare instance of such a misfeature, but usually it is only used to signal typically unrecoverable problems like "out of memory". So it is far from the footgun that exceptions are.
Rust has great tooling compared to C++, at least on mainstream platforms. It's a no-brainer choice for green-field projects where low-level development is required.
And the misfeatures of C++ are the stuff of legend, so Rust does way better from that POV as well.
> And it is different from Java where tooling is unusably complex, slow and enterpricey.
That's very subjective. In my opinion, nothing comes close to the excelent tooling that you can find in the JVM ecosystem. From IntelliJ to SonarQube, from VisualVM to Java Flight Recorder, etc. I'm not even talking about frameworks and the support that IDEs like IntelliJ offer for them, which is high quality.
If code can panic it should be documented. Code you write shouldn't ever panic without good reason during the regular runtime of your application. A panic means that the current goroutine has encountered an unrecoverable error, which is usually only the case for bugs such as out of range indexes or nil pointers.
The point being that an exception in C# can be any error. You can throw an exception if the weather is wrong.
A panic is fundamentally something that should never happen. You should catch panics at the root of your application for the sake of keeping the application running. But at no point should you ever expect a panic to be thrown like you would an exception. Errors are for that case.
Not handling panics is OK if you own your own code. But if you're sell an application that panics you can guarantee that's going to cause pain while supporting clients. An error log with details can be as useful but won't make the client think you're incompetent. I say this having given Go applications that panic-ed to clients before and they freaked out about these errors that weren't actually a big deal (a minor case in a controller of a web server).
Except that every nil pointer dereference panics. Every out of bounds access panics.
Is that also prevented by culture?
Let's not forget that, for better or worse, exceptions in Java are checked. The types of exceptions that are errors in Go, must be explicitly handled or forwarded by the caller in Java - there is no way around it. The the type of exceptions that are panics in Go are just unchecked exceptions in Java: you don't have to explicitly handle them, but you can use the same mechanism for doing so, and handling them is more graceful, easier and culturally enforced than panic/recover. I have many criticisms of Java, but I'd argue Java culture is much more graceful in error handling there.
out of bounds panics aren’t all that common in practice. Yeah they’re easy to write, but usually if you have a slice you’re going to range over the whole thing or loop over it up to its length anyway. It’s not common to handle slice indexes on their own and keep them hanging around outside of the context of a for loop. The major exception to this is cases where you want the first element of a slice and didn’t check that the slice is non-empty first, which sucks. But in any case, it doesn’t come up all that often in production code. You can range over a nil slice or a slice of zero items safely.
Nil pointer dereferences are way more common, as is assignment into a nil map. Those I see coming up a lot in newer Go programmers’ code.
that person is talking about application level error handling. In most cases if a functions panics generally the caller isn't expected to do anything about it other than perhaps have a panic handler at the top level of the app that logs things and lets it crash. So in general you don't need to think about catching or handling panics in most of your application code. It is the equivalent of a method that you are calling from your java code returning an undocumented NPE.
Speaking based on direct experience, I doubt that is happening in Financial industry. I wrote a utility in Go that is heavily used in our build workflow. I could have written it in Python too but my objective was to have a dependency-free fast binary in a statically typed language with a decent standard library and Go checked all the boxes. The language is decent and I don't have any huge complaints.
Would I now choose Go to replace our core applications that are written in Java? Nope. Why? My personal main reason is that in pursuit of keeping things simple and homogenous, even everyday things take too much code. I can get the same functionality in Java in basically half loc while keeping the code totally boring and devoid of any interface-madness-programmer-creativity-expressivity black-holes. When you want to type less and deliver quickly, that is important.
Would I use Java to write a utility with objectives similar to what I mentioned in my first paragraph? Nope. Go is totally fine for it.
COBOL is still used and the language gets releases. 220 billion lines of COBOL? We are still in the era of more things are becoming programmable and we are writing code for those new things, not consolidating and fixing up the old code.
Panic is like sigsegv in C. Email the stack trace to the team that owns that server and if the numbers are high no more features till the stability is fixed.
Why would anyone want to use a programming language that uses product types instead of sum types for the return types of functions which can return errors? Or why would anyone want to use a language that doesn't even let you write a type safe hash table? Also, the fact that Go suffers from the billion-dollar mistake is downright inexcusable. Go is way too error prone for me to sleep comfortably at night, because the lack of a reasonable type system is a constant liability.
Yes, I've worked on several Go systems. Nothing I mentioned about the language is an assumption; these are well-documented properties of Go. I've also have experience in a handful of languages which have more disciplined type systems (Rust, Haskell, Ocaml, TypeScript, , Kotlin, ...), and it's a night-and-day difference in terms of safety and reliability. Being able to confidently know that you've handled all error cases, that null pointers aren't hiding in your data, etc. is useful for safety.
Go is 100% worth it. If you're seeing these posts on HN then you should know that the majority of Go posts here are just "why Go is bad" posts. You'll rarely find an upvoted post about why Go is good.
From a Go dev who came from .NET, Go is one the best tools I've learned in a very long time. Code is incredibly readable, I never come back to an old code base and have to wrap my head around the weird abstractions like "MapperServiceFactoryFactory" because they're no idiomatic. Code can be verbose, but the verbosity is what aids its readability.
I haven't had any issues maintaining large Go codebases, the only issues you come across are the same regardless of the language you use. And that's things like ensuring consistent style across a team of developers.
There's obviously more to style than code formatting, but yeah, every Go codebase should rely on gofmt. I'd go so far to say that every language should have an equivalent to gofmt. There are so many advantages, the least of which is eliminating meaningless diffs in git where (for example) one person used spaces and put a new line before the opening brace of a function, while the other person used tabs and put the opening brace on the same line as the function definition. That drives me crazy in other languages!
I maintain hundreds of thousands of lines of Go code, for a few years now. I would not pick another language for that.
The important properties with maintaining code bases like that are language regularity (most code is written the same way and you don’t have to make personal “taste” decisions on things), good documentation and online communities, well-maintained libraries, tooling features for versioning and namespacing, deployment simplicity, etc. Go nails those things.
I agree that Python is great for interviews. I would not use it for a large codebase maintained by a bunch of people for years, unless there is a really compelling reason.
- language regularity; black has been the default for years now, and if you want to enforce a different style, there are other tools
- documentation and online communities; not even a contest, no language comes close to Python in extensive documentation and online support
- libraries; much the same, Python standard libraries are world-class
- versioning & namespacing; Depending what you need from this, Python is again more robust, namespacing in Go is a disaster.
- deployment simplicity; ...alright, you got this one. Python deployment is harder than it should be. Still, I find my Docker images about the same complexity between the two; more complex deployment in Python, but no need for compilation
By regularity I mean much more than just style. Ironically Go does “one obvious way to do something” much better than Python. When formatting a string, should I use % operator, str.format(), or f-strings? If I want to make a data obj, should I use a regular class, dict, namedtuple, dataclass, or TypedDict? If I’m changing each element in a list do I use a list-comprehension, or loop, or map()? Even unit testing in Go is mostly converged on the stdlib framework, in Python it’s constant back-and-forth between unittest, pytest, and nose. Static typing? Mypy, PyRight, Pyre, Pytype, with varying degrees of IDE support. Speaking of IDE support, which of the 3 major LSP servers should you use? How do I specify dependencies, with raw pip, Conda, pipenv, or poetry? When setting up my dev env should I use virtualenv or venv, or do I not have to use it bc of pipenv or Conda?
(I only actually use one of each of those with my own code, but I have to know about them all to work in the ecosystem.)
Also IMO, Python’s stdlib comes nowhere close to the quality of Go’s.
Somehow despite all this I still enjoy writing Python, and use it sometimes for my personal stuff. But in a team setting on a large code base? Nah.
I'll say this as a huge Rust fan: If you're thinking about learning Go, just go ahead and learn Go. One of Go's strengths is that it's very quick to pick up, especially if you already know at least one statically typed language. You can get a good sense of it in a weekend, and then you can make an informed decision about whether you'd like to keep working with it or try something new. By comparison, even experienced programers take at least a few weeks to start feeling comfortable and productive in Rust.
Make sure to write tests in Go before you decide. I liked Go a lot more in the honeymoon period after just learning the language before getting into the weeds of the amount of code required to ensure correctness. While rust takes longer to pickup, I strongly believe it pays dividends down the road.
I hope you didn't get an impression like this ("Go not being worth it to learn") from my post. We think it's great and that our Go is going to be much easier to maintain over the long term than our Python was.
It's all rock solid and easily understandable by the whole team.
Thank you for the reply. I didnt get that impression from your post at all, but from the larger comments & posts on HN and programming subreddit. I have wanting to learn Go, but seeing so much negativity around it was making it harder to start learning.
Go may be more polarizing than a lot of languages. It makes tradeoffs that aren't as exciting, which turns people against it, I think.
There are plenty of legitimate reasons to choose a different language, so it makes sense to figure out what's important for oneself and go from there. The only problem is that it's hard to know all of the aspects that may be important without trying a tool out... so we do, quite reasonably, turn to blog posts with other people's opinions.
I do think it's good to differentiate the opinions of people who have really worked with the tool vs. those who are just e.g. complaining about Go's error handling because they're used to exceptions but they haven't done any significant work with Go.
For deciding on whether or not to learn a language: I'd say it depends on _why_ you want to learn a language. If it's for getting a different job, then look at the kinds of jobs you want and learn the languages/skills to do that. If it's for pure knowledge, then learning languages that teach you something new are useful. Go's concurrency might be interesting in that regard. Rust's borrow checker is certainly interesting.
It is of course worth learning, very much like Java, PHP, C, or Cobol. These languages are going to be with us for a long time, just because of the large amount of important code written in them.
If you want fun, you might consider other languages, like Ruby, or Clojure, or Rust, or Scala, or Elixir, or... But they are not going to be as much the foundation of corporate software, exactly because they are more expressive and require higher qualification to handle well. The languages from the first list, though, while more boring, provide a more certain piece of bread and butter.
Don't think the HN bubble is real life. go, much to my surprise, has been pretty well accepted by companies that used to do Java and Python exclusively (at least in my experience w/ projects).
It's still insanely frustrating in many ways, but if you need code that is fast (i.e. faster than Python), typed (I don't think type hints are a valid replacement), where concurrency is not a never ending nightmare (or at least not any more than it needs to be), and compiles to native binaries, go is a great language to use that doesn't scare large companies away.
What do you want to do? Rust would be a great pick for "systems" code, or anything where you need to talk to existing c/c++ code. Pretty good for CLIs if you're the sort of person who enjoys puzzles.
Definitely go for kotlin if you want to write a GUI. I'd also recommend Kotlin (or maybe go, never used it) if you want to write a server. Rust has ambitious but not there yet server frameworks, and it's quite hard to go off the beaten track with most Rust server frameworks (say if you want to write your own little cache layer).
I'm just a student, but I have the impression rust isn't a good choice for maximizing career prospects. Still writing it, because I love it.
As someone who just took a job writing rust recently, I have to disagree with your take on the career prospects. I specifically looked to switch to Rust in my last job search, and I had plenty of companies to apply to. The market also is only getting bigger, with all of the FAANG(M) companies now employing a significant number of rust engineers.
The Rust jobs also appeared to be blockchain related, so I wouldn’t count on that being a place that could get you to retirement. I would expect Rust is a requirement so a marketer add a bullet point in his or her slide deck.
Yeah, there are fewer rust jobs in general because it’s a newer language, and blockchain is one of the areas it gained its earliest traction. Of the companies I applied to when I was looking for a rust job, though, only one out of five did anything related to blockchain.
I don’t know what Dice is, but Indeed, stack overflow, angel list, and google’s job aggregator all had quite a few rust jobs when I was looking. I also kept a spreadsheet of companies I had heard about who were using and hiring for rust over the course of the last couple of years at my previous job. In addition to FAANG and Microsoft, there were quite a few smaller shops like 1Password and Signal.
And the job I wound up taking has nothing to do with blockchain, fwiw
That's really exciting to hear. I'm struggling with a cs degree (especially the mathier bits) at a mediocre school, and I think the sort of jobs looking for Rust are looking for a more traditional background. Still, I hope I'll get a job in Rust someday.
I know this is only anecdata, but I graduated from a state school (absolutely nothing fancy) with a biochemistry degree and switched to a programming job a couple of years later. I worked in Perl, Python, and JavaScript mostly for years, while picking up whatever interested me on the side, which included Rust.
The company I joined didn’t expect people to already be rust programmers, and I think about half of our engineers are from non-CS backgrounds. Experience counts for way way more than your degree once you’re a few years into the field.
So, keep an eye out! There’s plenty of stuff out there for whatever interests you, and most programming (even in a language like rust) is not super heavy on meatier CS concepts most of the time.
Go is my favorite language but I'm still learning Rust. Would highly recommend either. I hate Python as a language but use it for web scraping and the occasional data transformation.
Is rewriting everthing from scratch ever a good idea? I can understand the need to get off Python 2 but migrating the entire stack to Go sounds very risky and hard to justify.
I blogged about this[1] as we were starting the project. The short story is that moving from App Engine 1st Generation+Python 2 to App Engine 2nd Generation+Python 3 would have _also_ been a ton of work, but would have given us fewer benefits out the other end.
We reduced risk in the project by making it very incremental[2]. All of this Go code has been incrementally put in front of users without anyone really noticing.
As a non-profit, we absolutely did need to justify it to our senior leadership and board. They've been super supportive because they know exactly what we're dealing with and how this benefits us for the long term.
I wonder if the Khan Academy engineering team has come to have any regrets about the change from a monolith to multiple services (micro or otherwise). Do you feel that the move away from a monolith was forced by the transition off of Python 2, and would have been best avoided otherwise? Do you miss the simplicity of an integrated application where you don't have to deal with the added complexity and failure modes of multiple communicating services?
To put my cards on the table, I feel that most web applications are best implemented as a monolith with server-side rendering, not a back-end API (e.g. REST or GraphQL) plus front-end code consuming that API. My thinking on this is largely influenced by DHH's promotion of the "majestic monolith" pattern, e.g. https://m.signalvnoise.com/integrated-systems-for-integrated...
I actually think that we're not yet getting all of the benefits that we'll ultimately get from services. For example, we have one deploy queue still, so we'll likely benefit when teams can deploy even more quickly because they're not waiting on other teams. Multiple services also makes it so that a deploy of one service gone awry is less likely to cause problems on the whole site.
I also think that the maintenance will be better over time. It'll be easier to swap out the guts of an individual service. But, we haven't gotten to that yet.
We _have_ already gotten benefits from certain things being in separate services. The best example of this is with our "content" service which, as you might guess, provides content data and metadata to the rest of the services. Since this is a separate service, it's able to store a whole bunch of content directly in instance memory, so it can respond to queries very quickly. This part didn't work as well in the monolith.
I don't think I'd argue for services in general for a smaller team (Monolith First[1]), but I do think that splitting some bits out that have very different operational characteristics (different traffic patterns or uptime needs) can be worth it, even for smaller teams.
Breaking a monolith to microservices generally involves rewriting the monolith piecemeal and gradually replaced in production. This is opposed to a Big Bang rewrite, where the entire product is set to be replaced with something written from scratch. The Netscape 5 rewrite is the classic example given by Joel Spolsky, as the first (and so far only) "Thing you should never do"[1].
On the other, extracting parts of the monolith, rewriting them and replacing them piecemeal is essentially a distributed version of refactoring, which is generally agreed to be the right way to handle legacy code.
String in Kotlin can never be null.
String? (with a question mark) can.
Kotlin String == Go string
Kotlin's String? == Go *string
They're fully equivalent in this regard.
If you wanted to note where Go strings are actually superior to Kotlin strings I'd give:
1. Go strings are slices. For immutable data that's pretty cool. I like it less when it comes to mutable arrays.
2. Go strings are single-byte UTF8, instead of wide-char UTF16. That's strictly superior.
Both of these limitations are probably due to the JVM, not Kotlin itself.
> you can follow the logic always 100% of the time
You can see the bare bones if conditions and for loops sure, but what are they actually trying to do? golang code is very thin - it takes a lot to write to convey a particular piece of logic. What a few lines of map/filter/accumulate/group by in a more expressive language like Java or Kotlin becomes dozens of lines in golang. It becomes challenging to extract the underlying logic and to follow it. Java and Kotlin are strictly superior.
Isn't item (3) more of a policy thing rather than a language thing? Granted, I actually never coded in Kotlin (and very very little in Go), so if it's "encouraged" in Kotlin, then your point (3) makes sense.
This is actually one of our reasons for choosing Go! We recognized that we'd have to come up with a fairly detailed style guide for Kotlin to reflect how we wanted to generally approach things so that it's possible to jump into another part of the codebase and not get lost.
I've worked with systems where some of the things were defined as a DSL within Kotlin (with implicit receivers and a shitton of extension functions), so you can definitely shoot yourself in the foot.
I think it's ameliorated by being designed by an IDE developer, so all that magic has control+click to go to definition. I've debugged some hairy nested implicit receiver code with jetpack compose pointer events, and found it very easy to read exactly what was executed.
I'm a beginner though, and kotlin was the first language where it occurred to me I could just read the source of a library.
I feel like go by itself is very nice but dependency management across inter-opping projects really sucks. I feel like an idiot every time I run into dependency versioning issues and have to spend half a day figuring out what mixture of versions of each dependency to set. Any advice?
i don't think dependency management is great in any language. Maybe cargo and rust win at that? But I happen to know go's well at this point. Ruby is terrible! And don't even get me started on node.js. But recently I got a go build error and had to just put in printlns: https://andrewarrow.substack.com/p/installing-go-from-source...
This is really just semantics, because nullable strings do exist in go: *string. The difference is you pay a surprisingly severe performance penalty for them.
You might have missed the part of the article where they describe the weaknesses of Go that they've discovered while porting their Python code, but they downplay them to make the whole thing sound more like a success.
The developers say positive things after having spent months on this effort. It's just as likely a psychological defense mechanism against melancholy as it is an actual optimism for the new Eldritch Horror that's been unleashed on them. Especially if they weren't given a choice on what language they would be using.
> Our engineers come from a variety of programming backgrounds, so we certainly do have a diversity of opinion on Go vs. other languages. That said, Go is doing the job we “hired” it to do when we started Goliath, and we’re thankful for the team driving its continued evolution and the community that has built up around it!
If you were calling a reference for a potential new hire, would you consider this a glowing review?
Only one of those was actually said. The article is not a glowing review of go. The carefully chosen snippets of text conspicuously say nothing about how the engineers, at the end of the day, like using the language.
I'm not very familiar with KhanAcademy besides the beginnings of it where it was just a youtube channel. What are they doing now to warrant half a mil lines of code?
At least in mathematics, after each video, they have a fairly sophisticated set of tests that are quick to pass if you’ve understood the concept, or that adapt in difficulty to your level of understanding and attempt to fill in any gaps. It’s quite impressive. Kids can definitely use it to catch up or work ahead on their own if they are interested.
I use Go for small things like CLI tools, simple web services or instead of scripting things with nodejs.
I don't really miss generics as many seems to do. My main critique is the error handling and the lack of control flow handling. It is very easy in Go to accidentally let the code continue to execute after something went wrong. Exceptions don't have this problem cause you only catch them were you now the problem can be handled.
This comes up often in Hacker News discussions, though usually without the qualifier I've quoted above, which I appreciate you adding. It's really easy to underestimate how much there is beneath the surface of almost any product.
People start by looking at Khan Academy and thinking "it's just a collection of videos, and even those are hosted on YouTube." But we've also got exercises and articles. Mostly created in-house, in a CMS that can handle the needs of creating math and science content. Translated into dozens of languages mostly by volunteers. Organized into courses based on, in some cases, regional curriculum.
We need to keep track of which content a learner has worked with and which skills they've mastered so that we can help them keep improving.
We connect with The College Board to help with specific SAT prep based on test results you've had.
Accounts can be logged into using Clever, which is common in school districts. They can be connected to Google Classroom, and teachers can assign content (and manage classes on the site).
We offer higher-level reporting for school districts, plus special tooling to get all of the district's students loaded into Khan Academy.
Around all of these features are the inevitable tricky edges. All of this needs to be able to handle millions of users every month.
In the process of doing this port, we _are_ actually deprecating some old and outdated stuff. Our site is 10+ years old. But most of what we have are features used by lots of people all the time.
I wonder if there's redundancies in terms of MVC patterns or there's just a lot of worker and irregular processes that don't fit neatly into MVC.
Another thing is to be careful about deprecation and feature roadmapping that's developer-whim-/assumption-led instead of customer-led. It's too easy to say "nobody uses this" or "it's not important because nobody uses it" when it can be vital to certain users.
(600 moons ago, I took the SAT-I without studying or prep: 800 math. English... let's not go there.)
I don't think we have many redundancies, especially after all of this upgrade work.
When we deprecate user-facing functionality, that's done with our product managers making the call, and involves usage data and a lot of coordination with our user support people. It's always a tough call, because someone out there won't be happy, but we have to choose the best path forward we can see at every given moment.
That's true. Can't please all of the people all of the time without an infinite amount of effort. "Perfect is the enemy..." I do try to think about important but less popular features (like bulk operations for heavy users) and accessibility concerns like deafness, blindness (screen readers, contrast, and fonts), colorblindness, photosensitive epilepsy, dyslexia, nonnative speakers (more icons and pictures), and reasonable device support.
They are referencing the LoC metric to show that they are not using it for some toy CLI app used by 2 members in the team and therefore justifying the reason for this blogpost. 500k LOC doesn't seem that unreasonable, not sure how you can call it "over-engineered" without even knowing what the system is actually doing or having access to their architecture.
This is a silly.comment unless you know the application and the ___domain. Excellent answer on all the things the go into an Internet facing app. Plus Education is a tricky area because the outcomes are complex - what are the paths to a certificate, diploma, certificate. Khan Academy probably wound up writing the original Python code themselves instead of a COTS product.
I'm not a big fan of LOC as a metric. You certainly can't compare languages. But the point that this is a large app and the builders are happy with their product and choice says a lot.
Agree, 500kloc seems excessive. Maybe they're counting dependencies somehow? Or maybe there's a lot of hidden complexity but in the case of Khan Academy I don't have enough imagination to see where.
It's the nature of golang to be verbose like that. It has very low code density, meaning it takes writing a lot of code to express something that could have been expressed in a much more concise manner in a stronger modeling language like Java or C#.
I'm strongly of the opinion that people make much too big a deal out of line counts and character counts. I'm very doubtful that there will be a significant difference in quality between two equally-complex programs which differ only in that one is more terse.
The line count determines how much code I have to speed-read at 3 AM because prod is down. My issue with boilerplate is that we have lots of tools for generating it but no well-supported way to summarize it back to something human-readable.
I should be focusing on a high-level description of the problem, and the compiler should be filling in as much of the solution as it possibly can. My brain is made of meat and will never get any faster, so this is the only way for the profession to progress.
The codebase should be as small as possible: least surprise, readability, security, new features, and debugging.
The other way for the profession to progress is to eliminate humans (mostly) with self-programming machines (effectively, a technological singularity).
> The line count determines how much code I have to speed-read at 3 AM because prod is down.
I disagree, it’s purely a count of newline characters per my original point (e.g., a third of the lines are just `}` or `]`).
> My issue with boilerplate is that we have lots of tools for generating it but no well-supported way to summarize it back to something human-readable
In my experience, it’s pretty easy to understand `if err != nil { return err }` and the like at a glance. Similarly a for loop that maps a `[]T` to a `[]U`.
I also dispute whether a long chain of .map().foldr().etc() is intrinsically more intuitive than a for loop, especially when the code needs to short circuit. Don’t get me wrong, I like using Rust-style iterator combinators in my hobby time, but it’s because they make me feel clever not because they are more readable than Go-style for loops.
TL;DR, I agree that our brains aren’t getting faster and that readability is important, I just don’t think newlines or the lackthereof are the measure of readability.
> it’s purely a count of newline characters per my original point
It's not simply that. Compare in golang:
var res int
if <some condition> {
res = foo()
} else {
res = bar()
}
to what other languages offer like
let res = if <some condition> { foo() } else { bar() };
The golang version is both more verbose, and more error prone.
These things add up. Let alone how it takes dozens of lines of code in golang to implement what essentially is a sequence of map/filter/groupBy. The logic in languages that support the latter immediately stands out, whereas in golang you have to read it line by line to figure out what's going on.
What's ironic is that even with the proposal to add some of those to golang, they're still going to be vastly inferior to the implementations we have in Java/C#/Scala/etc. because they don't compose in golang, and are still going to be very awkward to read.
I definitely like the ternary version, but to my original point I think people make too big a deal about it. With respect to map/filter/groupBy, I rarely find that solutions are clearer when expressed as chains of iterator combinators like these rather than a single for loop, especially when you're dealing with multiple collections and dealing with errors (short-circuiting). I'm writing a lot of Rust lately, and I definitely find myself dealing with this a lot in practice.
Yeah, I knew about that. Tricky bit is figuring out the best way to plug that into our theme and have that particular feed only show up for engineering posts.
Regardless, it's totally worth doing because people reading our engineering posts are far more likely than the average person to want the feed!
It is a language for people who like to get things done and get reasonably performant code for free. Relative to folks grinding in some of these dynamic languages doing contortions to deal with basic things like concurrency, twisting themselves into unimaginative ways to get that code coverage bump from 90% to 100% (the hardest part) just because their language cannot provide a semblance of guarantee that code "works" or has no syntax errors unless it is ran, unable to refactor without confidence unless the entire team is willing to commit to 100% test coverage and even with the test coverage the refactorings taking orders of magnitude more time than Go as now you need to parse through your test results to see exactly what your simple refactoring of changing a method name or moving a method from one file to another broke and then fixing it as opposed to the superior tooling of Go (or some other similar statically typed language) just doing all that for you in the press of a few keystrokes. It is also a language for those who like seeing a lower memory foot print, a lower cpu foot print, a simpler distribution story (literally copying over a binary to the target system)
The verbosity of Go (and even Java for that matter) is 100% worth it for me for all those reasons. It allows me to iterate fast, make changes with confidence and not have to feel guilty that my test coverage is only 92% because I am not going through the trouble of covering some branches of code that returns an error message (that would otherwise require annoying effort to trigger in your tests) as the compiler guarantees that all those
`return errors.New("some error")`
will execute if the error condition is ever hit in production which was not the case when I was working in python unless I went through the effort of having my test cover that bit of code.
After four years using Go all day professionally, I agree with all of these pros, and yet, the code volume to impact ratio is just so bad. Everything is very straightforward and understandable, the next step is always clear, I can spend an afternoon grinding out those 500 lines without thinking too much. But then I look back at what was actually accomplished in that time and it feels pathetic.
Go is a language for teams. All the cogs in the machine will spin, they will almost never get jammed, but nobody's fingers are fast enough to be particularly more productive than anyone else. Maybe this is the gray reality of mature collaborative software development. But every day I work in Go I get this gnawing feeling that software development could be so much better, that we could have been done weeks ago.
>Everything is very straightforward and understandable, the next step is always clear, I can spend an afternoon grinding out those 500 lines without thinking too much. But then I look back at what was actually accomplished in that time and it feels pathetic.
I sometimes feel that also, but strongly suspect it is a way of thinking that omits very important parts of the equation. When you build things, most of the time you're not spending writing code, but deciding what code to write. That is the work you want to optimize and Go makes that simpler, I think, as it is often obvious how something should be done in Go. With e.g. Python, there are too many options and that, I suspect, actually slows you down. For me, using Go feels like steady progress all the time, while e.g. JS development involves short bursts of productivity followed by long contemplation and sometimes backing out or refactoring a lot of stuff. Could of course also indicate my JS skills suck I guess ;)
Not to mention maintenance - consistent style consistent implementation makes digging into 4 year old code from some random contributor oh so much easier
My read on Go versus other options, having used it for a year or so and then switching back to other old favorites(Python, Haxe, Lua), is that it really doesn't want you to redefine the language. Go code is Go everywhere and asks you to work with a largely homogenous set of concerns everywhere.
That is great if you are building the environment for your application, but it's not great if you are building the application, because a differentiated app will always possess a unique application "vocabulary" - it's a thing that gets thought about in terms of a medium that is not the host language. Go resists extension, so most teams opt for primitivism rather than developing proprietary compilation mechanisms to express their spec. Everyone gets worried about the risks of generating code and how to maintain that: it's a "known" to have a larger fungible uniform codebase. The result is that "slow and steady" feeling when working with Go to do relatively high-level things - it lets you do it, but you have to work hard.
I think this is exactly right. I have separately used Go for more system-level code that pretty much directly wires together the standard library. Exec, x509, tar, gzip, flag, os, io, various encodings... here there is no contest. Go is a killer language for networked UNIXy stuff.
These tools also compose really well, despite the absence of generics, because they speak the common language of byte arrays. Now that I think about it, Go is a language for processing byte arrays in the same way that LISP is a language for processing lists. Go is just more subtle about this specialization, and invites you to use it for other things it is less good at.
But line counts are nearly meaningless. Something like a quarter or a third of those lines are going to be a single closing brace character. So mostly we’re saying that Go’s style involves a lot of beeline characters, but that doesn’t tell us much about even boilerplate much less complexity. And to be quite clear, Go does have more boilerplate than many other mainstream languages, but line counts are a particularly bad way of measuring it.
Citation needed. As far as I know, line count is the only thing that's ever been shown to be correlated with bug count. (And that aligns with my experience, because a line with just a brace increases the number of screens you need to page through to understand a given piece of code just as much as a line with many more characters on).
Correlation isn’t on trial, causation is. That said, I think there may be some merit to the hypothesis that more code on the screen at one time makes it easier to understand a program; however, I could also see that working in the inverse, i.e., more noise (as opposed to whitespace) to scan through between the bits you care about.
The thing is... that's all the major languages. Even the ones that aren't necessarily like that, the culture / style ends up like that. The Python I'll write and pass review at work is vastly different than the Python I would like to write, because I know the next person isn't going to understand my brilliant __metaclass__ and nested decorator approach that cuts 10k of imperative bullshit down to 20 lines - or worse they will understand it enough to make a change that causes an unacceptable performance degradation and not enough to realize that they did it.
There's the famous line from GLS about Java:
> We were not out to win over the Lisp programmers; we were after the C++ programmers. We managed to drag a lot of them about halfway to Lisp.
And probably there's some equivalent idea about Go not trying to win over the Clojure programmers, but rather dragging the enterprise Python/Ruby/JS/Java programmers halfway to C.
> my brilliant __metaclass__ and nested decorator approach that cuts 10k of imperative bullshit down to 20 lines - or worse they will understand it enough to make a change that causes an unacceptable performance degradation and not enough to realize that they did it.
That's just Python and its endless edge cases, caused by everything being special-cased into the interpreter.
If you were using Lisp, the macro equivalent of your ________dunder________ method would likely have no edge cases, so the next guy who makes a change with only a partial understanding wouldn't end up introducing that performance degradation.
>But then I look back at what was actually accomplished in that time and it feels pathetic.
Surely that must depend on the problem space you're working in.
My impression is exactly the opposite of yours, though I must confess it took me a bit of time to build up an armamentarium of patterns that I could apply to a problem. This seems like the flip-side of the "go is a simple language" coin.
> these dynamic languages doing contortions to deal with basic things like concurrency
I get the concurrency gains, esp with older versions of dynamic languages without native support for coroutines.
But the test coverage argument seems quite contorted. If you're accepting 92% coverage because the "return an error" lines are not worth testing (fair) then why is that harder to achieve in, say, Ruby or Python? Is the argument that your 92% coverage in Go is _effectively_ 100% because compiler?
Because Ruby and Python force you to unit test for typos.
I like and use both of them but if you're trying to argue that the testing problem in Python is the same as that of Go or Rust, that argument will fail.
> if you're trying to argue that the testing problem in Python is the same as that of Go or Rust
Wasn't trying to make that case, but I can see how my comment could be read that way.
I was genuinely trying to understand what the parent comment meant. Specifically whether they were saying error control code is _the_ critical but hard to test part of a code base (which I don't think is typically true), or rather error control code is just an example among many of less frequently executed paths in code, which categorically is easier with a static type checker. I think they meant the latter and I don't disagree with that.
Yeah the compiler guarantees that a certain class of errors can never happen and thus you don't need to go through significant effort to cover things like error returns.
Additionally with a dynamic language where the codebase has less than a 100% test coverage I would find it very hard to refactor anything without sweating bullets. The compiler is giving you a free test suite.
Sounds like you just want a statically typed language, because you've been scarred by Python. I'm sorry that you had to deal with what sounds like poorly maintained medium or large Python code base.
After you get some experience with Go, you might want to explore other modern statically typed programming languages (I suggest Kotlin and Rust).
When you know the strengths and weaknesses of each, you'll be able to pick the best one for the job. And the concepts learned often translate from one language to the other, which will make it easier to pick up new ones in the future.
Rust does not solve the problems that I deal with in the kind of work I do. I have no interest in picking up Kotlin. Java/Go/C# solve all the problems that Kotlin solves without the interop headaches and I pick between them for production code depending on the team and the platform we are targeting. For the projects that I am working on right now I specifically want Go because of its faster startup time than Java/C# and the ability to spin up 10s of thousands of threads without breaking up sweat which is needed for the sort of work I currently do.
The constraints of fast startup + "green threads" + easy deployment are actually a sound argument for using Go. The simplicity of the language isn't.
Kotlin and Rust also have the "green threads" [1] that Java lacks, both of them can also produce a native binary giving you fast startup. So the claim that "Java solves all the problems that Kotlin does" is not completely true.
Finally both Kotlin and Rust can let you write safer concurrent code, because their type systems actually can enforce constraints on what's shared between threads, and what's immutable and safe to accesss. Go gives you nothing in the type system, but there is a thread sanitizer.
There’s a pretty strong consensus among people who have used Go professionally (irrespective of whether or not they liked the language on balance) that the ability to look at any project and immediately understand it is a Good Thing, and this property follows directly from the language’s simplicity.
That doesn’t mean it outweighs all other criteria—we can say that there is a tradeoff between readability and expressability or something like that—but it absolutely follows that simplicity is a merit.
To boot, some other things that I would prioritize well above any particular type system feature set (at least features in excess of what Go provides) include: ecosystem, reproducible build tooling, general tooling, performance, a solid standard library, friendly community, static compilation by default, easy learning curve. Note that “tooling” must also be simple to be a boon—I don’t want to have to learn a DSL to script my dependencies, wire up test targets, build static artifacts, etc.
Why am I not surprised that such low level of passive-aggressive attacks on Go are coming from a Rust enthusiast. I don't understand this stance. It's as if they feel threatened by Go when in reality Go doesn't even try to compete in Rust's niche. They sit distant appart in many spectrums.
It's hardly constructive comparing these two languages due to their different objectives. Yet there's this constant belittling of Go as if it was a menace to Rust somehow.
In professional environments, this kind of emotionally loaded tribalistic judgement has not place. Much less in engineering.
> Here's a take: Go is a language for engineering managers who occasionally contribute a ten liner. You can learn just enough in between your many meetings to make a simple change. You can feel proud and happy with how easy it was for you to contribute. And you can dump the verbose, repetitive drudgery onto the people below you in the org chart, it's their job to deal with this after all. But in a language that you can easily understand, you'll make sure to push for that.
Counterpoint: There are lots of engineers who are pretty excited about Go. Go ranked #5 in the 2020 StackOverflow Survey's "Most Loved Languages" section, for example, and I've never even heard of an account in which the manager lobbied to use Go for a project--it's almost always bottom up. Seems like it's not just managers who appreciate being able to come into a project and quickly come up to speed and contribute. It's strange to me that you portray that property so negatively.
It was also the #21 (37% of respondents) most dreaded. For context, the “love” rating was a 62% response, so that means it’s something like a 2:1 love to dread ratio. At my current company it’s definitely leadership pushing for Go and not the other way around, and lots of startups have chosen Go early and are stuck to it inflexibly.
Edit: I retract this. Looks like the metrics are simply inverses (even though they shouldn’t be).
> It was also the #21 (37% of respondents) most dreaded.
This could trick someone who doesn't know that there are 25 languages in that survey and that the most dreaded chart is just an inverse of the most loved chart. You could also say that Typescript is the #24 most dreaded language (32.9% of respondents)... it's also the second most loved language.
Rust is the #25 most dreaded language (because there are 25 languages on that list).
It's important to note that Rust is not used nearly as much commercially which means that most of the people who use it are enthusiasts who like the language. So when you have a metric like
> % of developers who are developing with the language or technology and have expressed interest in continuing to develop with it
you then get a very strong selection effect which is the main reason why Rust has been at the top of that survey for a number of years now.
This is deeply silly (so is that "dreaded" metric). Go is here less dreaded than Julia and Swift, two enthusiast-driven languages, as well as Ruby, and is just 2 slots up from Python.
You gave away the game when you pointed out it was the "#21 most dreaded". What is anyone supposed to do with that very specific number?
The whole survey is fuzzy data of course, it could even be the same people dreading a language as loving it. That’s why I provided the percentage respondents. Citing either one as the value of a language is of course dubious, but if we’re acknowledging one metric we may as well acknowledge the other.
It would be one thing if Go was one of the top 5 most loved and one of the top 5 most dreaded languages. That would suggest (imperfectly, but meaningfully) that it was a polarizing language. But it's the #21st most dreaded language; it is less dreaded than Julia, which is a pure enthusiast language. Your data point actually doesn't show anything.
I think you'd be on rhetorically surer footing just arguing that these surveys don't mean anything.
The fact of the matter is that while Go certainly has a squadron of haters on message boards (it made several design decisions that were almost precision-targeted to alienate PL connoisseurs), huge numbers of working programmers really like Go. It's silly to argue that isn't true.
It is looking like I was wrong about the dreaded metric and that it’s actually just an inverse. If so, I have a strong disagreement with the way this data was collected. That said, what matters is the actual qualities of a language rather than how popular it is.
Like I said previously, citing any of these values to prove a point is dubious. That said I have retracted my top level comment and admitted that my analysis is wrong. What more do you want?
Props for admitting your error, but falling back to “there must be a problem with the way the data was collected (because it contradicts my priors)” isn’t particularly admirable.
It's pretty ridiculous that in order to be in the clear I would have to agree with the way the survey was conducted and presented when it obviously causes confusion, but okay.
No one suggested you have to agree with the survey’s methodology. It’s one thing to identify specific problems with the survey and quite another thing to cite the survey yourself, then when your argument is rebutted, to argue that there must be something methodologically wrong with the survey.
The survey isn’t wrong because it says something I don’t want it to, it’s wrong because it says something other than what it reads as. Elsewhere I said it was either the methodology or the presentation. If they had measured dread and it said something other than I wanted it to, fine. If they hadn’t measured dread but hadn’t presented it as if they had, also fine. But as it is, the survey is misleading because it shows dread as though it was an actual quantity and it’s not. For instance we know that in psychology, you can’t measure unhappiness, invert the numbers, and call it happiness, because happiness and unhappiness are not mutually exclusive.
The survey doesn’t say how the data was collected, but there has to at least be a middle “neutral” option because the two numbers don’t always add to 100%. Semantically they are opposites, but so are love and hate, and we know that a person can love and hate another person at the same time, or so the songs tell us.
I think you’re right, it was confusing because sometimes it was only to 99% but that’s probably rounding. Disappointing and a poor survey methodology or at least poor presentation. There are definitely some languages I both love and dread, like Java and it would be nice to see those as separate metrics.
> lots of startups have chosen Go early and are stuck to it inflexibly.
Is the implication here that it's a common experience among startups to pick Go and regret the decision? Can you give any evidence to support this (e.g., a survey of startups that chose Go and whether or not they regret their decision)?
Or perhaps the implication is that Go is uniquely difficult to migrate away from? I have a hard time believing that--it's expensive to change languages across the board except for a handful of special cases that were designed for interop from the start (e.g., Java and Kotlin, C and C++, etc)--I don't see anything that makes Go uniquely difficult to migrate away from.
There’s no implication intended, it’s just related information that I have which could spark a discussion. (I obtained it through speaking to a lot of companies I’ve been interviewing with in the past weeks.) I’m sure Go is possible to migrate from, it’s just that companies that have it are decided on it and not interested in alternatives.
This isn't so much a "take" as a "regurgitation" of the most hackneyed PL-fight slur against Go, that it's a language for managers and not for programmers. Of course, large numbers of programmers who work in Go by choice for personal projects argue against that. But the argument thrives because there's a kernel of truth to it, in that if you wanted to pick a programming language based purely on expressive shininess, you'd write in OCaml.
What makes the argument superficial is that the list of things PL-connoisseur languages have that mainstream languages lack is long, and can be used to exclude almost all programs; the argument proves far too much, and isn't really saying anything. The fact is, it's not managers making people pick Go and Python; the assertion is false.
There are real arguments you can bear against Go. This just isn't one of them.
"But the argument thrives because there's a kernel of truth to it, in that if you wanted to pick a programming language based purely on expressive shininess, you'd write in OCaml."
Eh, I think OCaml is actually the least shiny language in its reference class: Haskell, Scala and Rust are all shinier.
I think that taste for languages varies a lot, and certainly plenty of excellent programmers find Go is to their taste and statically typed functional languages are not. But I think it's also true that there are lot of people who like Go with Java/Python/JavaScript as their only points of comparison, and who would actually prefer OCaml or F# if they tried them.
> if you wanted to pick a programming language based purely on expressive shininess, you'd write in OCaml.
Oh no you wouldn't. OCaml has at least one pretty serious problem in common with Go, only it's worse because even the basic math operators are functions. Therefore, you x + y to add integers, but x +. y to add floats, and there's no way around that because lol no generics.
> Bingo, Go is geared towards junior programmers. Somebody who exits the university has to be able to feel productive within two weeks of using it. That's literally the cornerstone of the entire design and all things follow from it (C-like syntax, attempt to minimize the number of language constructs, and the heavy focus on imperative programming which is what they teach in college).
Your two critiques opine how easy it is to pick up Go. Clear code, like documentation, is drugery. It's not exciting. It's boring and that's fine. It's what I personally want. And looking at GitHub and Gitlab repos it seems a lot of people want it too.
I'm always impressed by how effortlessly this claim of the extraordinary readability of Go code is thrown around by the language's apostles, given how substantial that claim is, and that it's only backed up by anecdotal arguments with no context about things like the problem solved or the size of the code base involved.
Simplicity is a good thing. Oversimplification in fundamental spaces like the framework for expressing your ideas is a tempting, seductive short-term win that will squeeze the life out of you when it's too late.
Here are some ways in which Go code is harder to read and reason about it because of the language's low capacity for abstraction:
* The need to inspect loop bodies to understand which simple algorithm is being used. Is it a sum, or a find?, or something more complex? Well, let's look for all the ifs and breaks and accumulators, and reason about it, before we can be sure.
* The difficulty in verifying guarantees about your program, for example ensuring that the caller doesn't append to a slice passed to a function if we want to append to that slice ourselves.
* The tendency to have logic spreading multiple screens vertically, because of verbose error handling, and data transformation. You need to keep a part of a function in your short-term memory in order to understand the whole (or zoom out and get a magnifying glass).
* The difficulty of managing and verifying the correctness of concurrently executed code. A wait group needs to be incremented with the number of the joined goroutines manually. There's no abstraction over common operations, like merging channels, and their manual implementation is error prone and complex.
* The need to roll your own implementation of menial tasks, like cloning a map, or getting a list of map's keys. This introduces distractions into the code, almost meaningless sections of it that nevertheless need to be understood and classified as unimportant by the reader, and take up precious visual field.
* No support for immutability makes it less obvious to the reader which parts of the gadget they're looking at can move.
* Almost every more sophisticated data structure needs to have a custom implementation, that needs to be audited for bugs.
I think this belief in the readability of Go code might come from the misconception that if a tool that you use to build something is simple, the resulting thing will be simple too. I can forgive that mistake, but the misconception has to be cleared up. We can't have unsubstantiated arguments about simplicity till the ends of time.
I have some text based operational transform code I port to each language I learn to get a feel for it. (And I’ve got a version in C, JS, rust, swift and go). The go version is clunky. It’s longer than the others, and the lack of parameterised enums (or any type of simple polymorphism) makes it much harder to understand and reason about. The code is longer and uglier than the rust and swift equivalents. And it runs slower than the C and rust code. (I didn’t benchmark swift). After my experiment I find I have little appetite for the language.
I feel like programming is like electrical engineering. A lot of people graduate expecting to design transmission systems and power stations. But most of the work in the field is simple stuff like wiring up a new light switch at grandma’s house. Go feels like a language designed for that group of engineers - boring as a feature, reliable, simple to reason about, anti-clever (so whoever comes in after you can understand your work). It’s a predictable language for code-as-content. It’s great that people who do that kind of work have better tools. But I also accept that that’s not the type of software engineer I am. I want my code to be clever. I like to push at the limits of expressibility in a language. For the types of work I do (lately implementing various CRDTs), small, tight, fast code is better than simple, verbose code that any tom, dick or Harry could comfortably maintain.
> I want my code to be clever. I like to push at the limits of expressibility in a language. For the types of work I do, small, tight, fast code is better than simple, verbose code that any tom, dick or Harry could comfortably maintain.
Code written this way is often smaller and tighter. But that doesn’t make it unmaintainable. Is Haskell unmaintainable because of its dense, clever use of the type system? No way. But I’d personally struggle to maintain something like that because I don’t have enough familiarity with Haskell to do so. Likewise I wouldn’t trust most programmers to maintain the rust code I’ve been writing lately - not because it’s bad rust, but because I’m making heavy use of the language’s features. That raises the skill floor. In comparison, if you gave me some crashing Go code I could probably debug it even though I don’t know much go.
I find most Java code bases awful to maintain because there’s always so much code. Simple changes take a long time to make because there’s so much code to understand and modify. Give me the tight rust any day.
The rust compiler is a great friend for the kind of code I imagine you’re talking about. Expressing ideas in the type system makes the code easier to maintain, because you can’t compile code that deviates from your invariants. I’ve been reviewing rust code at work for over a year now, much more recently having switched to a job where the main language is rust. Knowing in code review that the code compiled successfully is a huge benefit for readability and review, IMO, because you don’t need to go around checking any of the invariants the type system can catch.
There are also use cases for dynamically generating code, or having higher kinded types that Go just can't touch. Anything that resembles data engineering is unnecessarily tedious.
One of my favourite tools for teams these days is Hasura, which is written in Haskell. I could not imagine a product like this being written in Go.
Specific reasons go is more readable than your run of the mill language off the top of my head:
1. Culture of small api surfaces. The number one cause of complexity is having large apis with many arguments and knobs and whistles. Every time you interact with a large api you pay a decision/comprehension tax. Go culls this dramatically.
2. Small api in the language itself. When reading and writing the language itself you do not have to scratch your head at new creative constructs. This is the most commonly cited reason, and it’s a real reason.
3. No inheritance. The lack of immense hierarchies of data structures means it’s dead simple to find out where the implementation for a given function lives.
4. Gofmt
5. Simple error handling. Easy to wrap with all the contextual info you would need to fix a bug.
6. preference for short names. This visually lightens the page when you’re reading code, enhancing clarity.
7. Packages. You always know when code is being imported from somewhere else and precisely where it’s imported from, as you must reference the package every time you reference its code.
8. No constructors.
So no, it’s not a misconception, go actually is very readable and more so than most languages.
Agree, this is something many other langauges/systems can learn from.
> 2. Small api in the language itself. When reading and writing the language itself you do not have to scratch your head at new creative constructs.
Mixed feeling here. Sacrificing expressiveness isn't always worth it. The right tradeoffs are difficult to find, but IMO go is too far in the direction of offering too few useful constructs.
> 3. No inheritance. The lack of immense hierarchies of data structures means it’s dead simple to find out where the implementation for a given function lives.
I actually disagree here. Its no easier than any other language (and often harder), because your function or struct will accept some interface I, and call a function or functions of that interface. Finding the correct implementation of the interface is a matter of figuring out where/how the dependency was injected. Even with good codesearch and cross-reference tools, this can be difficult if there are more than 1-2 implementations of an interface.
Lack of multiple inheritance is absolutely a good thing, and composition in general does have value though.
> 4. Gofmt
Yes.
> 5. Simple error handling. Easy to wrap with all the contextual info you would need to fix a bug.
Mixed, I think explicit error handling (as opposed to raise/catch) is fine. I think better support for concise simple cases (the try macro, better Result types like in rust, or something like Google's ASSIGN_OR_RETURN macro) would all be strictly better for the language.
> 6. preference for short names.
I abhor this. Perhaps it depends on the exact culture, but at Google for example, the result of this is names that are only abbreviations, which requires mental translation to what the thing should be, making reading new code harder. The canconical example of this would be `fpb` vs `foo_pb`. This is maybe okay if you're dealing with a single proto, but if you have a number of related ones, you end up with abbreviation soup trying to remember which of rpcpb, rcpb, crpb, and bcpb you want.
> 7. Packages
This is, I think, a thing that only matters if you're coming from C/C++. So like sure?
> 8. No constructors.
I guess, I'm not sure how this aids readability, I guess you mean that it forces you to have named factories for any complex instantiation, instead of default or magic constructors?
> I'm always impressed by how effortlessly this claim of the extraordinary readability of Go code is thrown around by the language's apostles, given how substantial that claim is, and that it's only backed up by anecdotal arguments with no context about things like the problem solved or the size of the code base involved.
> I think this belief in the readability of Go code ...
I guess you'd be the one to know since you're the one who originally made the claim.
To be fair, Some of the items in your bulleted list are downsides to the language. It can't be all things to all people.
Readability is not a characteristic of Go code. People are either confusing readability with lack of features or too biased by what they have been working on.
That's a take alright. I wouldn't say it's particularly good though.
Why shouldn't all languages be easy to understand? Why would anyone want a hard to understand language unless it provides benefits significant enough to offset that? Newsflash, most don't. Go has your cross platform, it has your simple code, it has your single executable and it has your performance. That suits the needs of most developers should they be interested in Go at all.
The fact that someone with little knowledge can contribute easily to any Go project is a huge benefit to the language. It's simple enough and the code most people write is idiomatic enough that you can drop into any project and know where to start. That is something I can't say of any Java or C# project I've looked at before.
Smart engineers underrate, and are even insulted by (I.e this comment) simplicity. If the language is simple enough that a manager can write in it, maybe it also means that smart engineers can dedicate their limited brainpower to solving business problems not language puzzles. I assert that this is true in gos case.
- the language with the best and most complete standard library
- the language that is the easiest to read due to: the lack of expressiveness/generics/OOP/etc., the forced convention (by the compiler, go build, go mod, etc.), the fact that gofmt can't be customized, etc.
- due to this it is the easiest language to maintain or contribute to, because you won't spend hours trying to decipher what it's doing. You can't be too clever when you write Golang. In many languages the complexity is on the reader, but in Golang it is much more balanced. People complain about Golang being harder to write, but how boy they should really start appreciating how easy it is to read.
- and in turn one of the most secure language you can use because it's extremely simple to audit and review code in Golang. As an ex security-consultant I've reviewed many codebases in many different languages and Golang codebases were in general the most secure one (albeit nil dereference bugs).
But there's a downside to putting things like that in the standard library. Python also went for a "batteries included approach" but it hasn't been all sunshine and rainbows. There's a reason why the Python standard library is known as the place where modules go to die: https://leancrew.com/all-this/2012/04/where-modules-go-to-di...
Javascript has a built in crypto library now too. Mind you, Go’s standard library is way better designed than javascript’s. A lot of Javascript’s API is an inconsistent mess. (Eg btoa, new TextDecoder().decode(str), string.length, etc.)
The prevailing idea even within the Go community today is that probably too much of that is exposed and instead the stdlib should be focusing on higher-level primitives.
> http / html templates / json-rpc
Go's HTTP stack is very good. Most languages do have a worse one, though.
The templating language is interesting, I'm glad it's there, but I think there will be widespread dissatisfaction among developers coming from other languages within in the next few years and you'll see it languish in favor of a 3rd party library. I would not choose a language based on its stdlib templates (and if I did I'd choose Perl).
I prefer easy things over complex things. Only a fool would prefer complexity for complexity's sales. Repetitive, boilerplate is easier to understand and work with than highly abstract, extremely clever code.
For personal projects I'd say write the code however you want. But for professional work, I think the emphasis should be firmly on code readability. I'd almost always forego some beautiful abstraction that's hard to grok without the necessary background in favor of dumb abstractions that are easier for most people to understand.
> Repetitive, boilerplate is easier to understand and work with than highly abstract, extremely clever code.
I think much of the reason for disagreement is that this isn't universal. I find boileplate heavy code much harder to reason about than abstracted code. Especially if the abstractions are either standard ones (map, filter, etc), or well-documented.
I guess there's a happy medium. Personally I find Java too abstracted. Go not abstracted enough, and that lanaguages like Rust and JavaScript strike a good balance.
It's for large teams and people who do lots of code reviews. You don't have to question the language during the review and can focus on the code. For example, when you see a language construct like a loop or an operator you know that it is what it says on the page and that it was not overridden by someone.
There is some complexity that needs to be in a language. Why? Because applications are complex. The complexity will therefore either live in the language or in you. If it lives in you, the language is being unhelpful. For a prime example of this, look at Assembly. It provides nothing and is as simple as possible; it’s just a transparent wrapper around machine code. Yet it’s incredibly complex to write Assembly code.
I'm not sure what kind of duplication you're thinking of.
_Some_ of the services tax is taken up by our use of Apollo Gateway. When the client requests data that comes from multiple services, Apollo will actually pull that data together.
Beyond that, the only additional code required due to services is that it's more verbose to make a cross-service GraphQL call than it is to make a function call into another package. Through tooling, we've reduced that verbosity where we could. I don't think it's anywhere near 200,000 lines of overhead, though.
I want to like Go but it's just too much of a mess of a language, and the power it gives you isn't enough to justify it. C++ is at least fast. You can see this in the proposal for generics[0], where they have to tap dance around the fact they didn't consider generics in the language from the beginning.
This is vapid criticism, and it's only going to invite language flame wars. There's certainly room to criticize Go, but just saying it's "a mess" isn't contributing anything to the conversation.
Not sure why this is downvoted. It pretty clearly demonstrates the point that there is a such thing as being too simple, and that simplicity for its own sake is not a virtue.
Just want to add that Go is ready for webdev pretty much out of the box, you don't need a framework to work with HTTP requests, routing, basic auth etc. There are things like gin/mux that makes it more convenient, but absolutely not necessary.
It's also weird to use C++ to support the argument that Go's generics are too bolted-on (as though C++ generics resemble a holistic upfront design). I like C++ and everything, but I've also used it and thus know better.
The whole point of Golang is that it is easy to read, contribute to, and maintain. This is because it is less expressive, and you have less freedom when you write code. In turn this can make writing code harder, but that's the trade off you have to take if you want to make reading code easier.
I firmly believe that people have managed to write amazing and large applications without generics, and the addition of generics will simply kill one of the biggest feature of Golang: simplicity.
Could you elaborate on what power it give? After working on several projects in a golang monorepo, it's exceedingly weak at modeling, which results in verbose code that is also thin - meaning that it takes a lot to write to say a few things. You can see the for loops and if conditions in front of you, but it's difficult to extract the underlying logic its trying to convey.
The code bases were a complete mess to work on, and even new projects are suffering because of it - no lessons learned can fix it because the language is so weak.
Engineers who complain about that are usually the ones who want to write extremely clever solutions that nobody will be able to understand nor maintain after they have left.
>You can see the for loops and if conditions in front of you, but it's difficult to extract the underlying logic its trying to convey.
That's what abstraction is for. Do you not use functions or something? Do you not group related ones in packages, and pay attention to how they model your ___domain?
What you're pointing out seems more like bad programming practice than a bad language. Even PHP doesn't have this problem.
I said it _doesn't_ give you enough power to justify the baggage, apologies if I wasn't clear. Iteration/modeling is one of my gripes in fact. The only way to copy a map is a for loop, really?
Sorry about that. You wrote that the power it gives you does not justify the mess of the language. I agree that the language is messy, but that it also lacks any power whatsoever worth speaking about :)
Could you list some of its powerful features? The only thing that gets a passing mention is goroutines, and even they are not implemented in a great manner.
Portable binaries, good cross-compilation support, easy to make shared libraries for use in, say, Python code for easy performance gains, goroutines and channels (they are a mixed bag).
Apart from these, the language does make it easy for novices to write performant one-offs similar to how Python enables them to write slower one-offs. I personally hate writing in it, but I will when I think it makes my life easier.
Another issue I have is the reliance on reflection. In general, I think if you have to rely on reflection to do something, that usually means you're working around some inherent limitation in the normal language - and the resulting code is often far less readable than the code would be in a more expressive language. Lots of Go libraries and frameworks are forced to use it in a lot of cases because there's just no other way to express some really basic things without it.
I really want to like Go. There's a lot I like - the "only one way to do something" approach means that code always feels consistent. Errors as values is a far superior approach to exceptions. I had to write some for a job interview project a while back and it felt really refreshing, but every time I try to use it for a personal project, I don't feel like I'm getting anything out of it that I couldn't get out of say, Rust, or modern, typed Python.