Python Is Easy. Go Is Simple. Simple != Easy
preslav.me
preslav.me
Go code always looks the same, performance is predictable and fast enough for most programs, and its really, really easy to be productive. When i picked up Go i basically got shit done without ever having written a single line of Go.
Its statically typed (with generics yey!) and compiles very fast. Its easy to create small binaries and cross compilation ”just works”.
Go projects usually dont have many dependencies, but IF you need something there always a package for it.
Go takes BC serioisly, 10 year old Go code compiles (usually) fine with the lastest version.
Overall its a really good ”get shit done” language, and a good fit for larger teams
Go doesn't have some of the most library functions, so large codebases shared between teams end up with a dozen different implementations of functions like "minimum" or "filter". Good luck debugging a bug in one of the implementations.
The exception-less error handling would be great if they used sum types instead of (val, error) tuples. Return types are required to have a "default" value if you want to return an error, and good luck finding bugs where you use that value and forget the "if err != nil"`.
Worst of all, they removed most "fun" C things about pointers (like subtracting pointers in the same array) but kept the null pointers themselves. There's no way to ask for a not-null pointer at the type level, so you have to check for nullity everywhere and good luck debugging those runtime panics.
But it also retains a certain "we don't need a robust type system; weak-ish static typing is good enough" ethos that made sense in back then, when compilers were hard enough to write that it was easier to justify making the programmer handle more things manually for the sake of simplifying the compiler authors' job.
This is always a tradeoff, of course, but I think that the optimum balance has shifted even further in favor of programmers. Some of Go's decisions still made sense in the 2000s when the language was first being created. But now, 15 or so years later, I think many of us could be forgiven for wishing for a language that's a lot like Go except that it dared to dream just a little bit bigger.
I think it was a good choice to take smaller steps and try to not be too ambitious too soon. Sure, Go isn't the most sexy language from an academic point of view, but it has a certain conservative and pragmatic approach that does work. It does generally result in code that is a lot easier to read and maintain than is my experience with C, C++, Java, C# and a few other languages I've worked in.
Go is an engineering language - not an academic exercise.
Take, for instance, the approach to generics. They could have designed that in from the beginning, but they showed restraint and didn't. That probably took a fair bit of courage. It is my impression that they hadn't figured out what generics should look like in Go, so they postponed until they had a better feel for how it ought to be done. Rather than risk making choices that would be hard (impossible?) to rectify later.
When you add something to a language there is always the risk that you make it worse.
(I'm not making any qualitative judgements on Go generics since, frankly, I don't feel qualified. I make very sparing use of it because it really isn't that often I actually need to make use of it)
People tend to forget that Java didn't have generics until 1.5 (or 5.0 or however they prefer to version it now) - about 9 years after first being launched. And to be frank, that was not a fun experience at all. Not so much because there was something wrong with the design, but because suddenly a lot of people went overboard and started designing really hairy types that could be hard to figure out and use.
If you consider C++: C++ spent 20+ years flailing wildly and the result was that you got lots of different C++ "traditions", subsets and practices. Sometimes within the same company. Depending on which era or tradition a C++ codebase is from you may have to adjust to a wholly different way of programming from what you are used to or prefer. And as for generic programming: in what world is STL a neat solution? And to this day, compilers are slow, they produce rubbish error messages, the toolchain still feels like a 1970s ad-hoc mess, and there is no definitive way to build things, resulting in lots and lots of additional complexity when trying to tame the horrific tool chain.
Sure, they could have put loads of stuff in Go from the beginning. But I think they would have gotten a lot more wrong if they had. I really appreciate that they are evolving the language slowly and conservatively.
Great summary. I personally think Scala is the antithesis to Go, it being an academic exercise - with complexity and tooling to boot.
re: Java generics, I've got this comment from 2015 bookmarked, it's a great explainer about the decision process behind adding generics to Go and the problem(s) with Java's implementation: https://news.ycombinator.com/item?id=9622417
> those ideas simply haven’t had time to pass through the filter of
> practical experienceLanguages like C# (which I've used extensively) and Eiffel (haven't used in anger), on the other hand, have a consistent type system, and didn't seem to have the same problems with generics that Java and C++ did. Or for a non-object-oriented model that has some other features that might be attractive to Go, such as not allowing implicit specialization, there's Ada.
I care a lot less about it for "simple" errors like typing errors, typos, or whatnot. This is mostly trivial stuff compared to "is this entire approach good or not?"
Rust & Haskell is a big no :p
This is the killer for me. After using Swift for a while, I no longer have the tolerance for languages that don’t have proper option types (and perhaps more importantly, which don’t use option types pervasively and by-default.) It’s just 100% the wrong way to design a language nowadays. Option types are table stakes.
I've been writing a fair bit of C lately. I'd say that the way Go error handling works has exactly nothing in common with C. Despite spending 15 years mostly writing C on UNIX 20 years ago, when returning to C it was striking how little Go's convention of returning multiple values, with the error being the last, has in common with C.
And if by structured error handling you mean things like exceptions: I don't really see how that improves error handling from a readability or security point of view. In Java people can't even agree on whether or not to use checked exceptions (ie neuter any perceived advantage of exceptions), and syntactically, exceptions are a little bit more awkward than return values as you invariably create nested scopes and move the handling of exceptions away from the normal program flow - which is not a readability win.
But I'll grant you that the error type in Go was both a bit vague and there was a lack of sensible stuff to, for instance, wrap and accumulate errors initially. But that has gotten better.
Structured error handling is as opposed to exceptions. I think that the most famous implementation these days is Rust, but if you want to see a version that isn't built on top of an ML-flavored type system, check out Zig's implementation of the concept: https://ziglang.org/documentation/master/#Errors
It reminded me of Go vs. Algol-69 http://cowlark.com/2009-11-15-go/
A basic option and result type could fix a lot of the issues around errors and null pointers. I know languages like Scala, Haskell, and Rust have type systems that are often considered too complex but Go doesn't need all that to add these two.
I have seen so many bugs caused by default values, production outages. Plus the code is harder to understand because you need to consider the default value case, and make sure that it is only left at the default when intended, not by accident. (And the linter probably can't warn you because it isn't wrong to have the default value live in one path through the code).
One of my favourite things in more strongly typed languages is just adding a field to a struct then having the compiler point out everywhere that I need to make changes. The strictness both prevents bugs and saves time. Plus the code doesn't need to worry about invalid structures, you can often write code such that they literally can't exist.
The other option isn't null, the other option is an error. Ideally the code fails to compile. 99% of the time I would rather have an error than have my code silently be wrong. Failure is almost always better than corruption. (Don't get me started on Go's fmt lib that barfs crap into the output if you have your format string wrong, at least that is easy to lint for most of the time) And build failures are almost always the best because then the wrong code can't even make it to production.
Having a (val, err) tuple IS more easy than returning a monad.
Sometimes (most times?) pure procedural code is just the best, and i really hate languages that have feature X but does not support it fully.
As an example i would not be happy with an monadic return type if the language did not have the option to use some sort of bind/return combo, and have full pattern matching support.
Thats why i champion Go for larger teams. Almost anyone can join the team in dive in without much previous Go knowledge. This is a rare feature and i dont know many other languages that has this property.
> Having a (val, err) tuple IS more easy than returning a monad.
Arguments about which concept is easier for people to understand pretty quickly descend into subjectivity but I don't believe this is universally true. Why am I getting a value when there's an error? What do I return as my value when returning errors? Explaining to someone that a function returns (val, err) and only one of these values will be significant is more or less the same as describing a result.
Option is even simpler. It's a list that can have at most one item. Or, you know how in python you can set a variable to None? Here's how to do it in a strongly typed language.
this is too real and my number 1 gripe with go
It doesn’t take a day of use to be frustrated by go’s warts - like having nullable pointers everywhere, the lack of sum types and the awkward error handling. I think it takes more than a day to get used to those problems and work around them. And who knows if the juice is worth the squeeze?
Rust is the same. It takes less than a day to start fighting the borrow checker. Whether the language is worth the discomforts it brings is a question nobody can answer for you.
I think dismissing these criticisms by saying the poster just didn’t understand the language is overly dismissive. These criticisms are real and legitimate.
Wait, Go doesn't have sum types? How do you create any modern programming language and not build it on top of sum types? That seems so odd.
People tend to always exaggerate how little time it takes to learn a language to a meaningful degree. Sure, the first day I tried Go I was able to accomplish something useful, but it took me a few months to develop a basic understanding of how you use Go in an idiomatic manner. I'd say it took at least a couple of years before I could say I "knew" Go.
And it wasn't exactly love at first sight. My initial impression of Go was that it was a bit too much like C, and, coming from Java, I was a bit confused about how you structure things. That takes time to figure out for any language. What sold me on Go eventually was that for my uses (writing multi-protocol server applications that run on multiple architectures), it resulted in code that was very readable and productivity more than doubled because Go is a lot less fussy to work in than Java. Both in terms of encouraging more minimal designs and having a lot less fragility.
So while at first you think "eww, err != nil everywhere", at least in a decade of reading your own or someone elses' code you'll know exactly what's going on.
How many languages are similar? I feel like a lot of languages in the past decade have had a steady migration in things like error handling, so 10 year old Java code is incomparable to today's Java. Whether that's a good thing (the language has evolved and is now more ergonomic) or a bad thing (I no longer know what's going on here) is the big debate.
Learning a language only starts with knowing the formal spec. Then you have to learn how to express yourself efficiently and in mechanical sympathy with the language. That takes time. Worse yet, a significant portion of developers can't do it without a lot of help.
I think it’s admirable to try other languages for a day. I’ve spent less than a day noodling with dozens of languages that seem interesting. I’ve never tried dart, kotlin, elm or Scala. Why not? Is it prohibitively expensive? No. I just haven’t taken the time and the initiative.
More people should spend a day with go, even if they don’t stay around it’s still nice to learn something new!
I've run into this problem repeatedly on Goodreads where wholly mediocre works get high ratings because of aggressive selection bias. I generally like fantasy, but I've stumbled on some painful stinkers thanks to this dynamic.
It's doubly reasonable to value first opinions on languages like Go since they intentionally aren't doing anything fundamentally novel. With a handful of exceptions, Go is a remix of ideas from mainstream languages that we've all seen before. It is about as far from a new paradigm for programming as you can get, and that's the point! Hell, that's the main thing people praise about the language: "I could pick Go up immediately, without needing to learn anything new"...
But yeah not being able to tag pointers as 'can't be null' and have the compiler enforce that ignores everything we've learned in the last 40 years. You shouldn't be able to pass a potentially null pointer to a routine that can't deal with it. You end up with code finding a null pointer without any context that tells it what to do with that. Or it panics.
You can write a simple Option type yourself using generics [1]. It's not strictly idiomatic and you do have to remember to use it, but it works well.
[1]: (my post) https://news.ycombinator.com/item?id=38331565
Does https://pkg.go.dev/slices#DeleteFunc not work for you or do you need it to be called filter? It's there for maps too https://pkg.go.dev/maps#DeleteFunc
I'm glad that they implemented some basic library functions 10 years down the line.
As long as the type supports the same interface anything goes, even it actually supports another one, where the methods have the same name, with different semantics.
As far as "the nightmare of {using interfaces}" -- I have no clue to any controversy there. It is one of the most celebrated features of Go. Not sure where you were going with that. In Python, I do find duck typing to be a nightmare because you don't actually _know_ what you have. Can you change it? Try it and if it breaks you will know. Or you use it like a list but it is a string and that gives strange, unexpected behavior, so you end up doing things like `param=force_list(maybe_string_or_list)`.
So yes, you can change that string and you do know.
It's still better that it's optional and gradual - it's unnecessary overhead on smaller projects, spikes, notebooks, etc. which are what most big projects grow out of.
;)
Go's verbosity is usually found in multiples (if err), not in long forms (int[] arrayOfIntegersWithValuesOneToFive = new ArrayFactory().createArrayWithSize(5).populateWithValues(1, 2, 3, 4, 5); ).
With huge projects that pull in packages i have seen the dependency management become a mess but most of the time its not that bad.
Several Go codebases I've worked on would like a word. Some Go people really love their interfaces and abstractions and making sure every method is only 3 lines and pretty soon you're 20 files and three type hierarchies deep trying to figure out what a single HTTP handler actually does.
Another reason for me for having interfaces is simple mocking in tests.
This has become a red flag to me; over attachment to a tool or paradigm means that they're driven by and making decisions using feelings, not through actual analysis.
I've also seen the opposite where people revolve their entire identity around not being the person and they swing too far the other direction and end up being a masochist and doing things the dumb way because they're too afraid of coming across like a snob.
A lot of what people comment on is less the language and more the dominant culture.
Try comparing the Honk source (clear, direct, modulo the unangst wacky names for everything) with GotoSocial (unreadable, interfaces and abstractions out the wazoo).
(Yes, I know GTS supports the MastoAPI but that doesn't force the hellish onion-layer abomination they've created.)
That's very strong language. Go was created by some smart people that have completely ignored programming language developments that happened after the 70's.
Go made a lot of odd choices from a language design perspective. It's pretty much "C, but with better types and a garbage collector!"
It certainly has its usages and does some nice things. But at the same time, the obsession with compile speed has, IMO, has been a detriment to the language as a whole. We see this in the way generics ultimately entered go after years of causing problems by not existing from the get-go.
Remember, Go was developed at Google - jokingly, while waiting for stuff to compile. They deal with tens of millions of lines of C++ written by thousands of engineers with varying skill levels and career paths. The advancedness of C++ did not reduce the compile times or readability of the code. At those scales you can't rely on being familiar with a particular section of code; you need to be able to drop in anywhere and know what's going on, without having to learn which subset of the advanced language they used for that particular feature.
Go is not advanced by design.
As for maintainability, it's not clear to me that reams of imperative code is easier to maintain than something terse and declarative. In fact, probably the opposite.
Inheritance is a foot gun that got overused in the '90s and early 2000s. But people have learned mostly to avoid it unless really needed.
This is not necessarily a compliment to the language. I'm OK with a language created by merely somewhat smart pragmatists.
But yeah, I love how everything is well thought out and they just say "no" a lot.
I also like that you pointed out that every Golang codebase looks the same. I think part of that is that Golang’s formatter can’t be customized. I pushed for that to happen in Rust but lost the battle.
Agree that the limited formatting options is nice, though.
Go is verbose, that's true. And if the speed of writing code is the biggest hurdle in your productivity you're either a true 100x coder savant or deluded.
Go is boring, boring is good when you need shit to work every time. And especially when you can't pick the top1% geniuses to work on it and might need to bring some rando up to speed to the project AND language quickly.
Many comments say the opposite of this. I am not too experienced, but I do know that go has a great standard library for web apps.
Is it the best at everything? No, but it got everything exactly right for its niche imo.
Go isn't the best at anything, but it gets 80-90% of the way there on everything whereas other languages aim for 100% in one dimension (e.g., performance, static analysis) at the expense of all other considerations (e.g., usability).
You don't give the new guy at work the saw with all of the safety features removed. Or maybe you do, but you're not doing it for his benefit at least.
[0]: Not that it's not already decent.
I don’t understand this template?
1. No matter your opinion on X:
2. Opinion Y is true about X!
If all you have are compliments about X: just go directly to (2). You’re not making a begrudging concession, after all.
"There may (or may not) be many problems with Go, but building bigger projects is not one of those".
No matter if you dislike Go you cant argue with the fact that it is a good fit for larger teams collaborating on a big multi-year/multi-decade project.
I’ve got a project broken down into modules. Module A uses module B. These are both checked into GitHub, so the module path is something like guthub.com/distortedsignal/my-project/src/a or whatever.
I need to make a change across both of these modules. Will my compilation pick up the changes?
What if I need to make a backward-incompatible change in b when b is a fully separate project?
Yes, add a replace directive to A's go.mod and point it to B's directory. It'll pick up changes immediately. You can also vendor + disable modules + change stuff in the vendor folder to quickly experiment, though IDEs tend to get moody about this.
Backwards incompatible in principle: release a new major version of B. Import and use that.
Backwards incompatible in practice: few projects actually do that reliably, IMO because tooling to help you do this or detect the need is somewhere between "bad" and "horrifyingly bad". Most just make the breaking change, increase the minimum version in A, and it'll work for anyone who stays up to date on A (this is often good enough for internal use). Though people updating B separately by hand might break.
Nowadays you just create a go.work file. That's much less cumbersome than 'replace' directives.
If a module is in the go.work it isn't downloaded from a remote git repo, but overshadowed by your local files.
What do you do if you’re working in a fork of the project and you need to make those changes?
Yeah, indeed, big projects did not exist before!
The most jarring thing besides the 3rd party panics, was the idiom that nils are typed. This was different than python, java, JavaScript, c, c++, etc.
Variable loop scoping is also weird.
Writing unit tests is 10 times harder than python.
Go statically links everything, and just that seems like going back 30 years in software development. Any security in a dependency means a total recompile and redeployment. In C land you would just update the affected library.
And while security problems are of course still a thing, there are a lot fewer of them than in C. Even in C it's not as bad as in "buffer overflow of the week" like in the 90s. It's that argument that's 30 years out of date.
Sure a 20 line python app will be 40 lines in Golang, but that doesn’t mean 10k lines of python are 20k lines in golang. And there are way more serious considerations than LOC.
Golang concurrency is amazing. You can reproduce an entire multi-core application stack with concurrent IO and CPU in a single binary.
When you realize how much code is wasted on config, RPC, encode-decode, code-generation, concurrency & locking – you want a language that reduces the need for those things (or makes them really easy).
And it’s not just due to language but also expressiveness of the library ecosystem.
"The man barrier to translation was that, while at 14KLOC of Python reposurgeon was not especially large, the code is very dense. It’s a DSL that’s a structure editor for attributed DAGs — algorithmically complex, bristling with graph theory, FSMs, parsing, tricky data structures, two robot harnesses driving other tools, and three different operator-composition algebras. It became 21KLOC of Go." [1]
[1]: https://gitlab.com/esr/reposurgeon/-/blob/master/GoNotes.ado...
I expect the Golang line count ‘overhead’ gets bigger for typical LoB software that has to address any sort of enterprise mess.
I don't think this is necessarily a bad thing though; Go is a bit more explicit on a number of things and there's less opportunity to make code 'dense'. Both have their own up- and downsides.
The only tradeoff I see is that go is faster to learn, because it has less syntax. But at the end of the day I don’t mind spending a bit more time learning a language if the result is I can write more expressive and clear code. I love go’s concurrency model. It’s clever and simple to learn. I wish they applied the same pragmatism when designing other parts of the language.
> All these things are a trade-off;
In some cases the trade-off is so one sided that its hardly worth the conversation. If everything is a trade-off, do you feel the same way about indenting your code? Or structured programming - aka using if/while blocks instead of gotos?
I think I'd confidently say that indented code makes everything simpler. And if I were given the choice, I think I'd choose structured programming every single time. I also don't often find myself questioning my daily choice to use high level languages rather than writing assembly directly. What else? Mmm... functions? I like those.
I feel the same way about sum types. They feel like an obviously good idea. Try as I might I can't think of any reason not to have them in a language like Go - except, as I already said - that they are another thing to learn when getting started. Having sum types and generics also makes it much easier for the type system to support optional types. And that makes it easier for a language to do away with Hoare's "billion dollar mistake".
If you disagree, I'd love to hear what you think the downsides are.
Harder to write tooling for the language, bit harder to reason about code, possibly slower compiles (hard to claim this one for sure without a working implementation that has wide-spread use), harder to add features or change the language in the future, harder to work on (or implement a new) compiler.
None of these are insurmountable problems of course, and "Harder" means "harder relative to" rather than "hard". It's not clear to me anyway that sum times are a "slam dunk" type of feature. For example I've written a bunch of Go tooling over the years, and I really like that Go makes this fairly easy, partly due to the simple syntax. This is perhaps not something the "average developer does" or even a niche concern, but on the other hand: good tooling makes all the difference.
I'm not necessarily adding sum types to Go; details matter and it would partly depend on those.
Almost every single feature that has ever been added to any programming language was useful to add. I find many of Ruby's features useful, even some of the more esoteric ones, but that doesn't mean it was a good trade-off to add them.
They certainly are for me. I use them constantly in my two main languages - typescript and rust. Expressing similar ideas in go using iota and go's interfaces is far more awkward, inefficient and error prone.
Just about everything is "simpler" than Go's `iota` (which isn't even easier for the compiler implementor).
E.g. if in python you can “import map” and in golang you implement map, which one is being counted.
[1] https://www.youtube.com/watch?v=SxdOUGdseq4 -> on of Rich Hickey's best talks
The second is instaparse, which I'd like to use to quickly test a TOML document against the TOML ABNF.
I couldn't get either to work. As in: I can't even get very simple examples to compile. The tooling is super confusing and weird. I appreciate that a lot of tooling you're not familiar with can be confusing and weird, but I managed to get to a lot of languages for my TOML test matrix to work, including many where I'm not familiar with. For some that took some time and patience, but that's okay. Clojure is the only one thus far where I just gave up (for now anyway).
So at least part of the answer to "why isn't it more popular" is "the tooling isn't very good". Maybe the tooling is "simple" by Hickey's definition of that, but it sure isn't "easy" by my definition, and in the end, "easy" does matter, especially for these kind of things.
You need the right balance of "easy" and "simple" and Clojure does not have it.
There is a very DIY approach with composition, with lots of library choices. This makes it tough for beginners and intermediate programmers to jump right in and be productive.
That plus functional programming, plus immutable defaults, plus Lisp, filter out developers who don't have a lot of experience, dedication, or a mentor.
The more I learn though, the more I love it.
But after struggling hard trying to introduce them into companies, I gave up. In contrast, I introduced Go into 3 companies and numerous teams so far and it was so easy for it to gain adoption.
going to do advent of code in clojure this year though to force myself back into it - it's a great language.
Memory usage is absurd and when you cap it, the performance is worse than Scratch.
Eventually the code taught me that typing, proofing and and maintaining shovel code is often easier than understanding and maintaining the magic incantations of a more 'elegant' solution. My eyes can quickly scan over boilerplate and glean what it is doing pretty quickly. But magic - magic takes understanding - and I only have so many ready spell slots.
Also, I have noticed that a lot of the other stuff that people call boilerplate in Go is actually an artifact of static typing (eg having to unmarshal JSONs before querying them), and is ultimately what prevents you from having Python-style runtime crashes or weird bugs.
Easily... but is that simple though.
In comparison, languages with exceptions give you a minefield around every function call.
The presence or absence of boilerplate (and thus the presence/usability of autocomplete systems) does not imply anything about simplicity.
Which is very conceptually simple. `sout` in Intellij creating Java `println(...)` is very simple. Completely straightforward. Unlike LLMs.
- Go is simple!
- Great, but it takes a while for me to produce code in Go.
- No problem, just use an LLM to help you write it!
I felt that there was a 20% chance that you were being satirical.
And then there is the business of being able to make a type automagically satisfy an interface. (Java may have fixed some of the agony of creating classes just to satisfy a required interface. Last I used Java, it didn't have delegates and whatnot.)
with open('foo') as f:
...
And the file will be closed after that block of code finishes executing. Similarly in Rust if you do "let file = File::open("foo")?;", when the variable drops the file will be closed.If you want to loop over 10k files, both of those handle that just fine, because the active file will be closed at the end of each loop. Go's defer will try to wait until after the loop to close everything. You can make things even more verbose to fix that, but it gets ugly real quick.
defer is more flexibly and explicit though; I rather like that. It's pretty unclear what exactly that "with" does, what you can and can't use inside "with", you can't "just" run any arbitrary code without creating your own class, and you sometimes end up with 3 or more levels of nested "with"s that could have been one defer.
In short, I don't think there's a clear winner. Both are clunky in some scenarios where the other is easier.
In my experience, if you're not checking errors, then you're often just going to crash loudly. Likely at a similar point that the comparable Python would have had its runtime error. In my experience, if Go would have continued silently, the comparable Python might also just continue silently anyway.
EVERY memory allocation can fail. And I mean EVERY.
var x := 5 // where's the error handling?
EVERY kernel call can fail. Even this is still not a 100% correct way to call fmt.Printf("Hello, World!"): s := "Hello, World!")
writtenSoFar := 0
while writtenSoFar < len(s) {
bytesWritten, err := fmt.Print(s[writtenSoFar:]) // and even this is a recent syntax addition.
if errno, ok := err.(syscall.Errno); ret == -1 && ok {
// error signaled
if errno == C.EAGAIN {
time.Sleep()
continue
} else {
return err
}
writtenSoFar += bytesWritten
}
If you don't do this, you will find, for example, that writing large amounts of data to a network socket suddenly only sends half the output to the other side. Plus anything could set O_NONBLOCK on stdout, which would require this. And time.Sleep() is required in some cases where the program redirects os.Stdout to itself.Even this does not take have proper reactions if an OOM occurs somewhere. So it is still not correct.
It's like C. Simple Go looks correct and just chugs along, destroying data instead of crashing. This makes people feel programs run correctly ... but they don't.
This issue, not checking the number of bytes written, usually combined with incorrect EAGAIN handling, is a pretty pervasive problem in network programming. You will find the closer you get to 100% cpu usage, the more common this problem becomes, just like threading bugs. It's one of the ways a service goes from handling 5 Gbit at 90% cpu usage, then handling 5 kbps at 95% cpu usage (because everything suddenly errors out, then retries eat all the bandwidth). It's impossible to find if you don't know what you're looking for.
It's not this issue specifically: Golang programs, like C programs, are strongly incentivized to just keep going with incorrect data when other languages would crash.
You look at patterns every time you read code, if the pattern is clear, it's easy to see from far away. Any disruptions in the pattern stop your progress and you need to zoom in to see what's actually going on.
Given a choice, I’d rather that the compiler does the work to write “if err != nil …” thing, using any mechanism for it - exceptions, or whatever.
You’re right of course. I can deal with patterns. I do so every day. I can type that code over and over and visually learn to spot when I do it wrong. But I’ve got a computer right here in front of me. I’d rather get the computer to do that work and spend my attention focusing on the task in front of me.
It's been a while but golang keeps making simple is better decisions that hurt the language and ecosystem.
Sure, saying just download a dep from git is simple, but it doesn't work. It makes the whole ecosystem fragile.
Sure putting all your deps in a global folder is simple but it means you have to have a tool to manage envs and swap them out anyway to be able to write more than one app.
Simple != Easy but neither of them equal good.
- Weird scoping rules
- Very limited list-comprehensions
- Ability to monkey patch things is a liability
- Mutability by default
- Lack of support for functional programming
- Deployment story remains extremely painful
- Tacked on type-system
- Slow execution of pure Python code
If this sounds like a rant... it sort of is. I find it really sad that so much programmer effort is being put into something with poor fundamentals. Many of these issues cannot be fixed with breaking changes.
Weird scoping rules? This will rarely if-ever impact you in the real world.
The list comprehensions in Python are incredible, in fact people over-use them all the time in really gnarly ways. Like [x for x in [y for y in [z... and the consistency with the same system supporting all datastructures (sets, dictionaries, tuples, etc) is very intuitive.
Monkey patching is a liability? This is like saying a car is a liability because I am free to drive it off a cliff. You're technically correct, but you are the one in the drivers seat. The ecosystem does not do or encourage this behavior, with the exception of things like gevent where it is required.
Mutability by default is common in virtually every language. You can't say it is good or bad, it's just a fact of life. There is a cost to immutable datastructures unless the language is designed from the get-go to utilize them efficiently.
Lack of support for functional programming? Functions are first class in Python. You can pass them around all over the place, as args, put them in datastructures, etc. When you combine this with comprehensions and generators it is very powerful and lets you do lazy evaluation of complex transformations. There are lots of built-in tools in the stdlib to do functional programming. This is just straight up false.
Deployment story remains extremely painful - again false, I do not know why people say stuff like this. Create a virtualenv (built into the standard library), and pip install your requirements. If you are using conda and all the other noise you are going to have problems.
Slow execution of pure python code - this is the ONLY area where you are perhaps correct... but compared to what? For a lot of use cases, the speed of Python is not a problem. When it becomes a problem, you off-load that responsibility to something else. There is also something to be said for writing software quickly, which is a perk of Python for sure.
Compared to… basically everything? Python’s performance is unacceptable, especially given that single threaded processor performance isn’t really improving any longer.
It can be a problem if you have nested loops. Things would have been less error-prone if Python introduced a let keyword.
> The list comprehensions in Python are incredible, in fact people over-use them all the time in really gnarly ways. Like [x for x in [y for y in [z... and the consistency with the same system supporting all datastructures (sets, dictionaries, tuples, etc) is very intuitive.
They have been completely surpassed by those in other languages. For example, you can't do this in Python list-comprehensions:
xs = [
if some_flag:
yield 1
for y in ys:
yield y * 2
]
> Monkey patching is a liability? This is like saying a car is a liability because I am free to drive it off a cliff. You're technically correct, but you are the one in the drivers seat. The ecosystem does not do or encourage this behavior, with the exception of things like gevent where it is required.Only if you are a solo developer. Unfortunately the Python ecosystem is built around things that compose poorly like annotations and exceptions.
> Mutability by default is common in virtually every language. You can't say it is good or bad, it's just a fact of life. There is a cost to immutable datastructures unless the language is designed from the get-go to utilize them efficiently.
It's a mistake, even though it is common. The cost is low in true FP langauges, because they can be optimized away.
> Lack of support for functional programming? Functions are first class in Python. You can pass them around all over the place, as args, put them in datastructures, etc. When you combine this with comprehensions and generators it is very powerful and lets you do lazy evaluation of complex transformations. There are lots of built-in tools in the stdlib to do functional programming. This is just straight up false.
Python lacks do-notation. It doesn't give you tail-call optimization. Function calls are slow. Immutability is very much opt-in. Lambdas can only be one line. If you do lots of FP in Python, you're in for a bad time.
> Deployment story remains extremely painful - again false, I do not know why people say stuff like this. Create a virtualenv (built into the standard library), and pip install your requirements. If you are using conda and all the other noise you are going to have problems.
In other language stacks it's pretty trivial to cross-compile from e.g. MacOS to Linux and things just work. In Python this basically requires Docker. Note that Pip install doesn't give reproducible builds. There's nothing like Go static binaries out-of-the-box, either.
> Slow execution of pure python code - this is the ONLY area where you are perhaps correct... but compared to what? For a lot of use cases, the speed of Python is not a problem. When it becomes a problem, you off-load that responsibility to something else. There is also something to be said for writing software quickly, which is a perk of Python for sure.
Python is about 100x slower than mainstream GC languages such as Java and C#. Having to drop into native code is not always possible and it's extra work besides.
Pick your poison
shell #expand all the strings, but those pipes are nice, why can't more languages have pipes.
awk #not a bad language, but very very small, and an AWKward(see what I did there) implied data loop
perl #can do anything but the syntax is, well it is a real frankenstein monster of a syntax
python #well designed syntax, has a strange block structure but at least it is always nicely formatted
tcl #all the strings! but other than that not half bad
ruby #pretty, but wow, that is a lot of magic
php #just graft on whatever to the language as you need it, don't worry about how it all fits together.> The language itself is extremely poor
> I think this is not something most Python users are aware of
These two statements are contradictory. If it was indeed so "poor", people would notice :) If they instead increasingly adopt it (out of appreciation, not because they are lobbied into doing it) it becomes really difficult to logically demonstrate that it's a poor choice. Software development is a very efficient market. Everyone is (or can be) aware of (almost) everything. So if most people (including experienced devs) gravitate towards a certain technology, the only acceptable explanation is that the technology, as a whole, is good.
Deconstructing and pointing out the flaws of the individual components is a common flawed thought process IMHO. Python is great despite all the issues you point out. To me this is equivalent to comparing individual components when shopping for a product, missing the fact that it's the ensemble of all those (flawed) components that make the product (or the language, in this case) work. The easy syntax, the packages, the community, those are all things that make "weird scoping rules" pretty much irrelevant.
Would they? The history of programming is full of great ideas that took a very long time to reach mainstream adoption. We also did some things that in hindsight were bad ideas, but for a time were very popular.
This is because the programming language "market" is not rational. It moves at the speed of education, not the speed of innovation. People learn a language and they make useful things with it. Why would they stray into niche languages and PL research?
What I'm saying is that considering Python a bad language, just because there are some languages that improve on some of its shortcomings, is just wrong.
As of today, Python is the most popular, hence (as a corollary) the best choice for most people. One day that might change, sure. These aren't mutually exclusive.
I don't agree with the claim about the market not being "rational". Someone who adopts Python even when given requirements that are clearly beyond the language's capabilities isn't going to last long in such market (and neither will their choices).
On the other hand there are plenty of people (myself included) who prefer using Python whenever possible, even though they have been "educated" in the use of other languages (I'd say I'm fairly comfortable with Typescript or even C building non trivial systems). I guess I'm not innovative enough :)
The main difference with JS is that we don't know whether it would be so commonly used if it wasn't for browsers. Still, it seems that the majority of efforts are towards augmenting JS' capabilities rather than finding ways to use alternative languages on the web (yes, I'm aware of WASM and maybe in the long run this statement will be proven false).
You're thinking about the programmer effort put into making certain libraries efficient, but think about the users who are reaping the benefits of low barrier to entry! More people are coding, getting comfortable with the idea of programming, and we should welcome them!
I think it's simply what they already knew. Very few programmers learn multiple languages to any kind of depth.
Is there any evidence that Python is an easy language to learn? I think we've reached a situation where beginners learn Python because it's seen as beginner friendly... because beginners learn Python!
Admittedly, that's just an opinion. I don't know of any relevant studies, but Python wasn't always the preferred language for beginners, and it changed at some point. It feels like there must be a reason for that.
and thus it was shown!
I have been building a platform for deploying internal web applications using this approach https://github.com/claceio/clace. Use Starlark to configure the application, the platform itself is built in go.
Languages evolve, I get it. Certainly, when I first looked at Python and saw that whole integer division thing I said, "They're going to have to ditch that." Many people smarter than I have had a wide variety of opinions as to how well the unicode transition was managed. Overall, though, Python has started to feel fiddly in the way that Perl did: a lot of syntactic sugar, programmers writing "clever" code-golf you have to unpack, hunting for external libraries, and the like.
* Values are immutable
* Functions are pure (i.e. no side-effects)
* Functions are values (i.e. can be passed as arguments to other functions)
We decided on that order, because the mortal part of python[1] can be picked up with less theoretical knowledge and requires less effort and knowledge to become useful imo. Like, if you know json.load, csv.load, some loops and other basics, some comprehensions, you can already do a whole bunch of useful things. And if you're a bit careful about your versions and dependencies, it can be easy to get this running on servers as well. Though you can still make your life a living hell with dependencies in python if you want to.
And later on we want to offer Go for two reasons. First - we have to support some stuff on-prem on a variety of operations systems. Here, go's simple cross-compilation saves a lot of time. And a lot of our stack is written in go - consul, vault, terraform, ... being able to read this code, or even write providers, plugins and such later on will prove useful. Go just requires a bit more programming experience in my book to actually get into, as well as to start understand larger projects.
1: I mostly mean python not touching meta classes, multiple inheritance, monkey patching, binary patching and such. Darkness lies down that path.
There are a ton of "simple" tools that are indeed very simple to understand. That does not making using them at any level of scale easy or low-effort. In fact, IME, often quite the opposite.
> filtered_temps = { entry["city"]: entry["temp"] for entry in temperatures if entry["temp"] > 20 }
is less readable to me than:
> for _, ct := range temperatures { if ct.Temp > 20 { filteredTemps[ct.City] = ct.Temp } }
??
city_temps
.select { |ct| ct.temp > 20 }
.to_h { |ct| [ct.name, ct.temp] }
Or let map: HashMap<_, _> = city_temps
.iter()
.filter(|ct| ct.temp > 20 )
.map(|ct| (ct.name, ct.temp) )
.collect(); filtered_temps = list(filter(lambda e: e["temp"] > 20, temperatures))
IMHO the most readable version (and coincidentally also the shortest).If you skip that,
city_temps
.select { |ct| ct.temp > 20 }
city_temps
.iter()
.filter(|ct| ct.temp > 20 )
.collect();
are still best IMO just due to the natural method chaining of iterators.So that needs reduce:
def filter_func(acc, e):
if e["temp"] > 20:
acc[e["city"]] = e["temp"]
return acc
filtered_temps = functools.reduce(filter_func, temperatures, {})That being said - by Rich Hickey - typing also add a lot of complexity and non-optimal bindings between stuff. Untyped incremental development with just simple scalar types, and a single list, map and set type is wonderful. Large refactorings, not so much.
Adding gradual typing to a project often gives you the worst of the two worlds. You still fear changing stuff, _and_ have lots of overhead doing all the typing. Either your project is fully type checked at compile time or it isn't.
Python match statements are useful for checking structure and de-duplicating the container name:
result = {}
for temperature in temperatures:
match temperature:
case {"city": city, "temp": temp}:
if temp >= max_temp:
result[city] = temp
Oh, and python typing has come a long way: from typing import List, TypedDict, Dict
class Temperature(TypedDict):
city: str
temp: int
def filter_temperatures(temperatures: List[Temperature], min_temp: int) -> Dict[str, int]:
result = {}
for temperature in temperatures:
match temperature:
case {"city": city, "temp": temp}:
if temp >= min_temp: # Most of the time >= is what people mean.
result[city] = temp
case _:
raise ValueError(f"Wrong temperature format: {temperature}")
return result
my_temperatures: List[Temperature] = [
{"city": "City1", "temp": 19},
{"city": "City2", "temp": 22},
{"city": "City3", "temp": 21},
]
print(filter_temperatures(my_temperatures, 20))
print(filter_temperatures([{"typecheck error": "and runtimeerror"}], 20))I don’t think the data supports your VC’s thesis.
>It is not uncommon to leave a Python application to go back to it after a few >months, only to realize that the host environment has changed enough that it is >no longer possible to even to start the application anymore.
Pros:
* Being single threaded makes concurrency simple. No need to think about mutex/semafor/channel/etc.
* Everything is a pointer so no debate on whether to use pointer vs value for variable
* Not to many reserved keywords to learn before being able to write javascript
* I like async/await
Cons:
* Tiny standard library
* Very little standardization between projects
* JS is often compiled/minified which can make debugging painful
* Not so performant. Can't do true concurrency
but what's more complex is this laguage should be backward-compatible to not break the web, so there are often 3+ ways to do the same thing
For people who knows both, I don't know why you would choose Python (except if your domain doesn't have the libraries, like scientific/AI/ML stuff).
edit: for further clarity of my intent to comment