Half a million lines of Go
blog.khanacademy.org
blog.khanacademy.org
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.
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.
Go is essentially the new Java.
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.
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 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.
Mutexes go a long way, and underpins channels anyways.
- 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:
[1] https://dave.cheney.net/2014/03/19/channel-axioms [2] https://www.jtolio.com/2016/03/go-channels-are-bad-and-you-s...
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.
I can simply do dict.contains("foo") in Java vs having to go through a verbose hoop:
if val, ok := dict["foo"]; ok {
//do something here
}
Common operations like filtering/transforming a collections are very concise in Java - I'm certain doing so in Go will take atleast 5x as many lines: collection.stream().filter(...).map(...).collect(...)
Don't get me started on having to go through iota hoops to declare enums. 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.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.
At least that's the kind of thing I've heard about other verbose things you need to do in Go.
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.
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... value := map[key]
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.
I guess in this case the real reason is trying to force you to handle the error, and that the error and value are returned separately.
>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.
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.
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.
>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.
Like everything human, programmers can be polar opposite without anyone being "obviously wrong."
Whatever rolls the ball.
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.
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.
Ruby also is a very expressive and clean language. I just don't get why you put Scala or Kotlin on unsloppy side.
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.
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.
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.
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 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.
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.
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.
Agree
>Go Error Handling
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.
Do you have any examples? Not a golang programmer myself.
If the formater allowed to write in one line if err != nil { return err }, it would drop the number of code lines by the third.
Give Go a map and option and I'll be a happy gopher.
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.
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.
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.
Sure, you can write sloppy code in Java, but I am sure you can write sloppy code in Go too.
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.
See https://devblogs.microsoft.com/oldnewthing/20050114-00/?p=36...:
> 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.
... which leads us straight to favorite exceptions-caused bug: https://nedbatchelder.com//blog/202001/bug_915_solved.html:
> Inside tempfile.NamedTemporaryFile, the error handling misses the possibility that _TemporaryFileWrapper will fail.
You could ignore those, but similarly you could ignore err in Go as well.
https://dave.cheney.net/2014/11/04/error-handling-vs-excepti...
https://dave.cheney.net/2015/01/26/errors-and-exceptions-red...
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.)
This is not true, and therefore your explanation is unsatisfying.
> 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?
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.
Yes you ARE forced to. Unless the exception derives from RuntimeException.
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.
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.
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.
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.
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.
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.
[1] https://fasterthanli.me/articles/i-want-off-mr-golangs-wild-...
"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"
That's true in Java as well.
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.
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.
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.
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.
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.
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.
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.
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.
Old code always compiles.
The stdlib of Rust, I find it superior, very much so, to the one of go. Even though this is mostly due to the different type system.
The tooling around rust, it is just great, what tool you feel missing in Rust?
Few years ago, rust was lacking libraries, but they are really catching up quickly.
They are different languages that shine in different scenarios, but maturity, stability and stdlib is not where Rust is lacking behind.
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.
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.
OTOH it will pay your rent.
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.
Then there's Rust designed around low-level code and memory safety, and a much more complex syntax.
I think it just goes to show that any language (modern or otherwise) is a consequence of its design decisions (or lack of).
Can you elaborate on that? Usually the argument on which one is better is that it depends on use case.
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.
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.
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?
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.
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.
https://en.wikipedia.org/wiki/OCaml
https://en.wikipedia.org/wiki/Haskell_(programming_language)
... 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.
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.
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's very rare that I come across weird patterns or someone trying to be very clever. Its always straightforward code.
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.
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.
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.
I used to write Python. I was very happy writing Python but dealing with other peoples Python bullshit is not something I miss.
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.)
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.
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.
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?)
We use Ruby/Rails and Go pretty heavily at work it's much easier to jump in and grok Go code than rails (written by the same engineers mind you).
Go removes abstractions (to a fault sometimes) so, anecdotally, it's pretty damn easy to just read and figure out what's happening.
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.
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.
I'm not a fan, but it fits with google's goal when creating the language.
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).
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 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".
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?
But those are the only container types with built-in support like that. (Channels can similarly be of any type)
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.
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.
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.
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.
Lots of people find the things you're concerned about missing to not really be the end of the world.
Or you might hate it, and that's also okay.
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.
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.
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.
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.
Later
But see below, I think I misconstrued this comment.
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.
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 :-).
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.
I'm obviously surrounded by folks far smarter than me, so I'm posting something probably grossly uninformed to see what the experts think!
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.
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:
https://docs.oracle.com/javase/8/docs/technotes/guides/deplo...
You've made a nice argument for bundling the jvm so maybe I should change my inclination there, especially since I do like avoiding depencencies.
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.
Modern Java is meant to be packaged with the JVM via jpackage. You will know exactly what you're running on.
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 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?
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.
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.
number crunching wise, Go is on par with Java and C#: https://benchmarksgame-team.pages.debian.net/benchmarksgame/...
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.
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++.
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.
Zero Go-related issues during the whole project. Just drop in one executable and one config file and it just works.
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.
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.
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.
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.
Time will tell.
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.
What didn't you like about it?
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.
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)
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.
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.
This is too close to my work life that I don't wish to comment on this project. EDIT: but it is a fantastic accomplishment
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.
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.
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.
Indeed... like Python novices complaining about significant white-space.
Still a no-go though (jk).
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...
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.
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.
if err != nil {
return err
}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 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.
func inSlice(needle someType, haystack []someType) bool{}It helps to assume the person who chose a slice did it for some actual reason and not because they are an idiot.
For a hashmap, the operation of inserting N items is O(N) and yes that N is the number of items you are inserting. So flat out wrong on that one.
The rest is kind of obvious.
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?
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).
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?
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...
You can run it with:
go test -bench='.' -benchtime=1s slicemap_test.go
The benchmark code is based on the one from this blogpost: https://www.darkcoding.net/software/go-slice-search-vs-map-l...
After looking at that code though I didn't trust the results too much anymore, because of various issues with it. So I decided to fix those issues.
if haystack[needle] {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 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.
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.
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.
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.
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 sure you're wrong, though...
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.
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.
And the misfeatures of C++ are the stuff of legend, so Rust does way better from that POV as well.
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.
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.
It’s a language for getting things done. It isn’t for language buffs. And that is a huge plus for me.
Python has been around since 1994 (2 years before Java: 1996). It's heavily used in Data Science and Science in general.
The C API allows you to write code in most compiled language (C, C++, Rust, ...) and Python serves as a glue between business logics.
Go simply does not serves the same purpose. There is no winner/loser here, just different use cases...
But it's for a rewrite, so:
- you can use the most modern language features. Python 3.9 is less dense than Python 2.7 (E.G: a silly 2.7 example would be https://0bin.net/paste/xpnCov-3#3s2AgDReXpeqG4knegGwdnYNi523... which is 37 lines, the equivalent in 3.9 is https://0bin.net/paste/b4DVZIS5#372LGWNLBJfDUNQGAMsDlJPNlkVy..., down to 25 lines). Also note that it's not just less lines, but way shorter, yet clearer, lines. Something LoC fails to take in consideration.
- you can use the most modern stack. E.G: fastapi is less dense than flask as soon as you use data validation, plus it adds typing for free.
- you know very well the problem to solve so you get it right the first time.
In the article, they clearly measured success by 1:1 porting of the entire code base from Python to Go as closely as it makes sense.
Was looking at my next language after python. Rust is one , should something like kotlin be the other ?
For interviews python seems to be nice and fast to type.
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
(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.
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.
It's all rock solid and easily understandable by the whole team.
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.
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.
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.
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.
Java: 12,879 Golang: 4,339 COBOL: 321 Haskell: 18 Rust: 2
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.
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
So I don't see why go is not worth to learn.
However depend on what engineer are you, IMHO go is one of the top mindshare language for backend services.
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.
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.
[1]: https://blog.khanacademy.org/go-services-one-goliath-project... [2]: https://blog.khanacademy.org/incremental-rewrites-with-graph...
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.
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.
[1] https://www.joelonsoftware.com/2000/04/06/things-you-should-...
1) if thing vs if (thing) kotlin makes you add un-needed ()
2) a string is always "" vs null!
3) you can follow the logic always 100% of the time vs. getting lost in a maze of clever kotlin.
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.
The nice thing about Go is that its simplicity permeates the entire ecosystem.
If you use bundler and Gemfile.lock everything 'just works' in Ruby.
Node/npm/yarn is a nightmare
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.
> 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?
Ships code on time, on spec, performance exceeding expectations. Sure.
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.
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.
TLDR; it’s more than videos and exercises.
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.
That's an awesome outlier.
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.
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 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.
The other way for the profession to progress is to eliminate humans (mostly) with self-programming machines (effectively, a technological singularity).
if err != nil {
return err
}I threw in the towel and went to Rust.
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.
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.)
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.
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.
YMMV
khanacademy.start()
Just 1 LoC! =)https://blog.khanacademy.org/engineering/feed/
should work for you!
<link rel="alternate" type="application/rss+xml" href="/engineering/feed/" />Regardless, it's totally worth doing because people reading our engineering posts are far more likely than the average person to want the feed!
I work with python and node.js but should I give Go a chance?
More generally: you're getting downvoted because your question is impossible to answer without knowing what kind of work you do.
What are the chances if it was a monolith written in Go, it could be closer to 300,000 lines instead?
_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.
[0]: https://go.googlesource.com/proposal/+/refs/heads/master/des...
C++14 specification: 1354 pages.
Go specification: 50 pages.
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.
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.
Nothing to do with the language. Other platforms provide the same features.
> goroutines and channels
Extremely verbose and also error prone. C#'s Tasks are much better abstractions for example.
> the language does make it easy for novices to write performant one-offs
Nothing to do with the language itself, it's just that it's compiled not interpreted like Python.
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.
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.
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.