Hacker News new | past | comments | ask | show | jobs | submit login
Python Is Easy. Go Is Simple. Simple != Easy (preslav.me)
186 points by nalgeon 5 months ago | hide | past | favorite | 303 comments



If you going to compare simple and easy, you've got to mention the Simple Made Easy talk by Rich Hickey. Though debatable if Rich would find Go simple in his sense of the word. An example might be not having an insertion-order map iterator.

https://www.youtube.com/watch?v=SxdOUGdseq4


No matter what your opinion is about Go (as a language), but they nailed it for bigger projects.

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


I used Go for many years. My issue is that it's _almost_ a great language, but in its current version it's just a collection of foot guns that makes it difficult to get shit done.

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.


This roughly lines up with my feelings. Go is a solid improvement over many languages that we inherited from the 70s, 80s and 90s.

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.


Language design is always easier in retrospect. It is harder than it looks, and often harder than the people designing a language realize before it is too late.

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.


> Go is an engineering language - not an academic exercise.

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


Thanks for posting that link. It lead to a lot of interesting background material! I think this quote in particular resonates:

  > those ideas simply haven’t had time to pass through the filter of 
  > practical experience


I remain somewhat unconvinced on that front. There were good implementations from outside the functional programming sphere (which seems to be what people from the Go space tend to mean when they say "academic") already existing at the time. I think the real problem is that the most well-known examples, C++ STL and Java 5 generics, were trying to stick generics into an existing language with a "some things are objects and some things are not objects" type system. My take on the story there is that generics called attention to some deep deficiencies in that kind of type system, and people responded by shooting the messenger.

Languages 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 think the key question is how robust a type system can be while keeping compilation extremely fast. Slow compilation absolutely destroys developer productivity.


You can make it pretty darn robust! OCaml compiles very quickly and has a good type system. Rust is slow due to LLVM, macros, and the compilation unit being the entire crate. None of these are requirements for a good type system


Zig's new compiler is a nice example of how you can do all sorts of inherently slow things at compile time and still have a lightning fast iteration time for developers if you design your toolchain for incremental compilation instead of making the compilation unit be the entire module the way Rust did.


I'm wondering if there will be a language that compiles to Go, like Typescript is for Javascript, that adds a stricter type system (and fixes other issues) and adds a compiler step, but still has all the other benefits of Go.


Conversely I could go longer between compiles if the type system caught more.


Most of the times when I really care about quick feedback cycles is when I'm experimenting to see what approach does and doesn't work well in the first place.

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?"


I think I agree with you, but in trivial projects even the infamous slow compilers are fine. Toy Rust projects and small Java projects get compiled very fast.


It doesn't even have to affect compile times per se, it could be a linter/checker task running separately from the main compiler; most type errors are only relevant during development in my experience. Said experience lately is a lot of Typescript, where all type information is no longer relevant at runtime because it's compiled out or just bluntly removed to make it valid JS (I dunno how the TS compiler works).


Yep this is one of the reasons lots of my friends switched to Go: short compilation time (like good old Pascal days).

Rust & Haskell is a big no :p


But we're not talking about replacing Go with something like Rust or Haskell here. We're talking about really basic, inexpensive things like not repeating Tony Hoare's billion dollar mistake yet again. Or actual structured error handling instead of something that's ergonomically quite similar to classic C-style error handling aside from the relatively incremental improvement of not relying on global variables.


> not repeating Tony Hoare's billion dollar mistake yet again

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.


It doesn't even have to be ML-style option types. Kotlin is a great example of an alternative implementation that arguably has much better ergonomics for procedural and object-oriented code.


I'm not so sure that we're necessarily talking about small, inexpensive changes here. Anything that changes the fundamental character of a language is probably going to have bigger consequences than you might imagine.

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.


Ergonomically, the thing that Go and C have in common about error handling is the thing that the grandparent pointed out - you don't get much language-level support in helping you to remember to check for errors. If you forget, then there's a decent chance that the error condition will be allowed to silently propagate.

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


Error handling is tricky because it is (usually) people who write code, and people are lazy and sloppy. Force people to deal with errors and they'll find new ways to not really handle errors.

I'm not opposed to language support for pushing developers to handle errors. But experience suggests it isn't the magic bullet we sometimes tend to think it is.


Completely agreed.

I'm only responding to point out that I never claimed or even implied that there's such a thing as an error handling magic bullet, just that I think that Go's approach to error handling has been oversold. It compares favorably with an "enterprisey" approach to exceptions, precisely because that method makes it so easy to hide the dead bodies. But I don't love that Go's (and C's) approach makes it so easy to implicitly suppress errors, because that's effectively the default behavior. In a language that forces you to check for errors, by contrast, it's harder to be lazy and sloppy without being explicitly lazy and sloppy, which then means that such behavior is more likely (not guaranteed - again, I'm not trying to claim a panacea - just more likely) to be noticed during code review.


Additionally, codegen and templated metaprogramming is very fast in go. Gen + Compile + Lint + Test on Save with line-by-line coverage overlay in <10s.


I’ve used a number of languages over the years and I have never been more productive than with Kotlin.


> 70s, 80s and 90s

It reminded me of Go vs. Algol-69 http://cowlark.com/2009-11-15-go/


I could not agree more with this. Go is so frustrating to me because there is so much I like but these things you listed make it miserable for me to use.

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.


"Defaults are useful" was IMHO the biggest mistake Go made. I understand that it made the language a lot simpler but this was a simplification that ended up moving the complexity to the user, rather than just removing it.

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.


Not to mention, some things just don't have a sensible or sane default. Needing the zero value to be meaningful has had so many negative downstream effects (nil being one of them) it's crazy people still defend this decision.


Not sure. If you declare a variable as of type string i think it really good that it is empty string as default. The other option is to be null.


I don't think it is. We had a production outage because someone was putting things into a map with the wrong key. Tests passed because they read (using a right key) and checked that they got a valid value. In this case an empty string was "valid". (Basically it deserialized correctly.) Sure the tests could have been better but this also wasn't the only bug due to default values. Imagine if 1% of the time the result of addition was randomly 5, it would cause havoc. Default values are the same. Sure, often they are a reasonable value, but when values that the coder didn't expect enter the system you are going to have a bad time. If the map reads failed tests would have failed, but also the crashing service would have been reverted and the problem found before it caused any real trouble. Not silently corrupting out data for a week.

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.


A huge downside of that it that adding a new (public) struct field will be an incompatible change. And what people will do is add a non-public struct field with a setter method, both to avoid the compatibility break but also because for many of these fields the defaults are just fine (even though sometimes they're not) and no one lines having tons of boring boilerplate.


Having used Ocaml for quite some time, i can understand this. However, i cant tell people (junior devs, or devs who just want to work and does not care about comp-soyery) to learn an ML (+ all the FP idioms) with a straight face.

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.


You really don't need to think about monads or functional programming to have option and result. The focus on explaining these in the context of sum types and monads only serves to distract most people. A list is a monad and people use them every day and they are fine.

> 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.


I like Go for its standard library but truly hate its nil behaviour and default value semantics. Uber effectively built a static analysis tool from scratch to catch nil issues: https://www.uber.com/en-IN/blog/nilaway-practical-nil-panic-...


Indeed. I enjoy working in Go and have written various things in it over the last 8 years. Last year I toyed with Rust (very different learning curve!) and still prefer Go for its simplicity and development speed. But, I would love to have Rust-style enums in Go. The 'similarly named constants pretending to be enums' and 'returning nil if not found' patterns suddenly started feeling wrong.


> a dozen different implementations of functions like "minimum" or "filter"

this is too real and my number 1 gripe with go


And it's funny because go has a whole http server in the standard lib, but not minimum?


It couldn't have a minimum before generics. The function was not expressible in the general case. So they either would have had to add `minByte`, `minInt`, `minStr`, ... or make it a builtin generic. And every builtin is an admission that the language was not powerful enough, so those are kept to a minimum.


I know it sounds funny, but I have used the built in "net/http" in probably fifty projects now, and not a single one of them needed a min/max function.



Since v1.21, which wasn't released that long ago.


I tried Go for about a day, and the exception handling and null-pointer issues were exactly the things that made me lose interest.


Sorry to disagree, but you can not judge a whole programming language in one day (no matter what your skills and experience in other languages are). Most likely what made you lose interest in Go are simply things you did not grasp yet in one day.


> Most likely what made you lose interest in Go are simply things you did not grasp yet in one day

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.


> the lack of sum types

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.


One day is not enough to even get a superficial understanding of any language.

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.


I disagree; you can learn 90% of Go in an afternoon. However, you also do have a point; the power of Go is not apparent in an afternoon, but in longer term and larger projects spanning years or decades, and hundreds or thousands of developers. It was made by and for Google to solve Google's problems, which include millions of LOC written and read by thousands of engineers over the span of decades.

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.


> I disagree; you can learn 90% of Go in an afternoon.

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.


The lack of true exceptions are annoying, but what kind of environment are you writing in that it's so expressly prohibitive to quit after a day? I've written error matching functions (and now there's errors.Is()) that mostly achieve the same things exceptions do, so I'm struggling to match being so overwhelmed by textual errors that you ignore every other improvement with the language over others.


> what kind of environment are you writing in that it's so expressly prohibitive to quit after a day?

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!


Elm is a spectacular "weekend language". You really can learn the entire thing and get a solid feel for it in a weekend.


I can understand why you felt the need to share that, but if you only used it for about a day, aren’t you just reporting your initial impressions? If I used Rust for about a day, I would probably complain about the borrow checker. And if I used Python for about a day, I would have no idea what people are talking about when they complain about package management in Python.


That's true, but the flip side is selecting heavily for public commentary from people who are already invested in—or at least generally open to—the tool in question. But, of course, that means selecting heavily against the people who see immediate, significant problems!

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"...


It's immediately clear (to me at least) what benefits you get from the 'pain' of borrow checking. I'm not sure what benefits you get from null pointer exceptions.


It's pretty clear what the payoff is for the investment in using a borrow checker, and it's quite big. Less so for writing if err != nil on every. other. line. That's just an unnecessary nuisance.


The payoff is the same: you do more work up front, but have fewer weird, hard-to-track-down bugs later. Even rust's "?" operator nudges you towards "just pass the error on up", rather than actually thinking about what you want to do if an error happens each time.


Yes, and that is exactly what you don't want in a "get shit done" language.


I feel like the pointer stuff you could handle safely in a modern language. Because I think pointer and array notation are convertible so one isn't scarier than the other.

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.


Using pointers just to get a 'null' is just an annoying hack reflective of poor design. Instead, "no value" should be able to be directly represented.


In Go that's the zero value, which is usually things like empty string, 0, [], etc; in most cases that's good enough.


> 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.

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


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".

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


To be honest, I used to use Go before generics became a thing.

I'm glad that they implemented some basic library functions 10 years down the line.


Yeah no offense but most of the people online that I find complaining about Go used it a long time ago or got their talking points pre-generics and haven't touched it or really looked at it since then.


A general "min/max" function could be implemented with Generics?


And has, since Go 1.21, but that's very recent so everything written before August this year may have the problem of a min/max utility and the like: https://go.dev/blog/go1.21


My favorite thing about Go is that I can read a dependency’s source and there’s a very low probability that the author favors some totally different 30% of the language than I’m accustomed to, so I find it illegible without great effort. Or has imported libraries to add so many language features that it looks totally alien (fucking JavaScript—it’s getting better these days, to be fair, but there was a decade-plus when you just had no clue what you’d see when you opened up an unfamiliar codebase)


As someone with the misfortune of inheriting a million plus loc Go project I disagree. Go is a nightmare from the duck typing to dependency management.


I think you typed "python" wrong :) Duck typing is a python thing. And the dependency management is a nightmare, constantly breaking. To the article's point - you _must_ run it in a container and defer to the OS to have anything resembling a sane development story.


OP meant structured typing, which is the strong type version of duck typing.

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.


Ok that's totally fair. What's the Go name for it? In my mind Go interfaces are duck typed.


mostly joking - your complaints are big problems in python. Dependency management in Go has been contentious (historically) with "to vendor or not to vendor", glide and dep, GOPATH, etc. But that is all solved. Go mod for the win.

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)`.


The Python typing nightmares you refer to go away on large projects as soon as you start using mypy, which I tend to find most large projects do these days.

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.


Python typing nightmares definitely do not just go away with the use of mypy. There are so so many ways to break out of the guardrails it supplies that are not caught.


I can write stringly typed code in haskell too if it comes to that.


Don't you need stumps which are quite often not available?


What were they actually doing? Passing interface{} all the time and casting it?


Haha you're spot on. In the codebase I inherited that was everywhere.


You can do crazy things in any language if you try hard enough.


The trick to efficient Go is to not try hard, lol


Python is duck typed as it support dynamic type, whereas Go doesn't. Yet you can do polymorphism in Go and aside that difference I think Go interface are duck typed.


Probably structural typing?


I think you typed “JavaScript” wrong…

;)


To be fair, most million plus LOC projects match “misfortune” and “nightmare”.


Go is verbose, a million plus LOC doesn't go as far as it would otherwise.


I don't know, my experience with mega SLOC projects were Java, so verbosity was also a thing.

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); ).


I assume with the duck typing you are talking about interfaces? How has that caused issues?

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.


It was sloppy coding on the project I took over, but it still made me wish for explicit style interfaces. Cleaning up frequently broke the project in non-local ways, because someone would break an interface without meaning to.


Unless you are doing some (possibly unsafe) magic the interfaces are strongly typed, external dependencies are locked, so one cannot really break an interface without meaning to. Or I guess without realizing it immediately.


I ended up ripping out the unsafe magic, but it was a huge pain while it existed.


I assume your only part of a larger larger team working on this 1M LOC project? If you are alone with no previous context/knowledge the language does no really matter, you would be in a world of hurt nonetheless.


I agree with you but that pretty much invalidates Go. The whole selling point is that it is lacking anything to make the language interesting to use but you can drop in and any two Go programmers write the same understandable code.


Don't forget the fact that it doesn't have this overbloated concept of inheritance and polymorphism of let's say Java where you have to look through 6 files to understand what's even going on. Even generics in Go were a heated debate because they make the language more complex. All in all Go was created by geniuses and it shows.


> it doesn't have this overbloated concept of inheritance and polymorphism of let's say Java where you have to look through 6 files to understand what's even going on

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.


A certain number of developers will be able to complicate any language or mechanism you provide them, it's how they feel important. I've notice the there is a group of people who has their entire identity tied up in being "smarter" than everyone else, except they aren't, so they build extremely complex systems, because they think that's what smart people do. They are interestingly often extremely well versed in their tool of choice.


You are mistaken if you think that the abundant use of interfaces etc. is motivated by anything along those lines. As someone who often puts basically everything behind an interface and abstracts everything in sight, it's not because it has anything to do with being smart, quite the opposite; it's the only way I can keep any meaningful portion of the code in my head. Give me a 5000-line straightforward / unabstracted implementation of something and I'll struggle, but give me thingDoer.doThing(), behind an interface so I don't even have to know how to construct one, and I'll be happy and able to focus on the task at hand. This applies even if I'm the one who wrote the ThingDoer interface and its implementation(s), and even if it was only 30 minutes ago.


This is me as well.

Another reason for me for having interfaces is simple mocking in tests.


> They are interestingly often extremely well versed in their tool of choice.

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.


Very well put. "They are interestingly often extremely well versed in their tool of choice." People who are, shall I say unwise, but very good at what they do is one of the most annoying things wrong with the world.


> I've notice the there is a group of people who has their entire identity tied up in being "smarter" than everyone else

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.


This is actually a real gem of a comment. Thank you for that!


You can instantly see when the codebase is Java Coder Go or Python Coder Go, they bring their baggage to the language and it shows.


No one is safe from bad developers. I've seen go SDKs for paid products that were clearly written by people with only a vague idea what the target language offers. I have to assume such libraries were hammered until the compiler just accepted what was given


I’ve seen one Golang codebase like this and it was made by Java people


There is this odd thing where the actual syntax and features of a language are (at least partly) independent of how it ends up being used. A culture forms around the language (that the authors have limited influence over). Java does not have to be used the way most people tend to (factoryfactoryimplfactory!). And some go projects (notably at least early k8s are go in Java style).

A lot of what people comment on is less the language and more the dominant culture.


Yeah, you just described every Go project. Reading Go code is a nightmare of frustrations.


I disagree because I've seen some readable Go codebases but I wholly agree that bad Go codebases are a nightmare.

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.)


> geniuses

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.


Yup.

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.


I'd say that the hands-down improvement over C is simplified threading and proper utf8 support. The utf8 alone (because of the notion of rune) is a good reason to use golang over C or C++.


Ironically, we had more advanced languages than Go in the 70s.


Yeah, but advanced languages are also a problem for long-term maintainability.

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.


Advanced languages can compile quickly - it depends on the feature set chosen. For example, do-notation is an advanced feature not found in most languages, but it compiles quickly as it is syntactic sugar. Operator overload, on the other hand, might require a solver and thus compile slower. It's not a particularly advanced feature, however. Binary optimization passes do not make the language any more advanced (on the user side), but will slow down compilation. Advanced design and compile-times are pretty orthogonal.

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.


Advanced != good. It takes some genius to realize that.


Maybe you could educate them.


It depends on what libraries you use and how you structure your code. Yes, you can create inheritance hell within Java code. But you can also use composition (what Go uses) within Java and create sane code bases.

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.


I remember looking through this insane taxonomy of abstractions of a button to find the code for when it got pressed. There weren’t that many buttons and they had no special requirements. Just trying not to find some real actual code that does the thing. I work on much bigger projects now with much less abstraction and my quality of life is much higher


Go was first and foremost created to serve the needs of Google. Kudos to them for releasing, open sourcing and backing it but it was designed to serve their needs. It’s not a criticism but it explains why it’s quirky in many areas. The authors being genius is not as important as the goals and intentions.


> All in all Go was created by geniuses and it shows.

This is not necessarily a compliment to the language. I'm OK with a language created by merely somewhat smart pragmatists.


They may be geniuses, but some - mainly Ken Thompson - are also luddites, eschewing things like syntax highlighting. I mean they have a point - if you need colors to make your code readable, your code isn't readable.

But yeah, I love how everything is well thought out and they just say "no" a lot.


To know which interfaces a Go type implements (and so where it can be used) requires reading more, not fewer files than in Java.


This was one of the troubles I always had with Java. It was very hard for me to reason about with some of the wildly nested inheritance.


You can write 90s object oriented code in any language.


The way I describe it is that Golang is optimized for reading, other languages are often optimized for writing. If you have to deal with someone else’s codebase, then Golang is a godsend.

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.


I don't think I understand where you are coming from. Reading Go code is pretty painful compared to most languages between the specification and the formatter everything looks identical and there is a lack of intentionality. You can't really reason about method signatures or really tell what source will do given the shape.


I read a lot of code. Reading Go is painful because I have to parse whatever open-coded version of standard algorithms or error handling the developer chose to use that day.


This was exactly my experience when I was working in Go. Each line was easy to understand, but reading actual code was painful because I needed to read so much to figure out what was actually being done. I was basically spending 90% of the time mentally simplifying the code into abstractions so that I could understand the business logic. Then once I understood the business logic I had to zoom back in to check that their inlined reimplementation of various basic data manipulation routines were correct.


Yep. Every layer has to deal with every detail of every problem. So now you get to read dozens of in-line, bespoke implementations of what should be a method call.


I used to read code for a living (security consultant) and I'll ask you this question: you're being thrown in a codebase and you need to understand it in a week, what code do you wish the codebase to be. I'll give you the answer: it's going to be the language you work in, and then Golang if it's not the language you work in.


This is actually a pretty interesting question; hard disagree on Go though. I work in security but I don't do much formal code review, instead I care more about trying to figure out how things work as fast as possible. Go is easy to "parse" in that the language isn't too complicated but navigating the architecture of complex projects is annoying because there is just so much boilerplate everywhere that I have to cut through. It's kind of like how I feel with Java, despite it being one of my strongest languages. Otherwise I actually do know a fair number of languages well enough to comfortably navigate around projects that use them; I think C and JavaScript projects are generally not too bad, C++ can be pleasant or painful depending on how things are structured, and Python is also a toss-up. Rust, Swift, Scala, Kotlin are usually pretty good. Of the languages I really don't know I think I would rank Ruby as one of the worst, maybe TypeScript on the better side?


I've actually found Golang codebases to be the hardest to read, mostly due to the non-locality of effects from channels. It's just to easy to read a line of code and not know where to move next.


I would argue that code with channels are hard to read in any languages, not just Golang.


Brainfuck is also super easy to read, and all codebases look the same. That's not a good thing. A language optimized for reading wouldn't clutter most of a screen with "if err != nil". Something like Rust's `?` would be a far better option.

Agree that the limited formatting options is nice, though.


Exactly. Code is read WAY more often than it's written.

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.


Go is verbose. That slows me down when I’m _reading_ code. Especially when it means the chunk of code I’m reading doesn’t fit on my screen.


Java is verbose, C++ is verbose, Golang is not. Golang actually cared about verbosity. For example, uppercase letter means that your function is exposed, no keyword!


Or when I have to pick out what the actual logic is amidst a forest of error handling. Or mentally deconstruct some complicated loop to figure out it's just doing a map, filter, reduce.


IMO, Go is a mediocre language with great tooling and a great company behind it, and it turns out ultimately if you don't have the latter, the former is irrelevant.


Yes, like Python and Javascript right? Turns out you don’t need a great language when you provide what most people want: great tooling, good resources to ramp up, great stdlib, etc. Believe it or not Golang was the first language to do this, and probably still the only one? (Rust misses the mark on great stdlib)


> great stdlib

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.


I'm pretty familiar with Golang's stdlib and other languages stdlib and I'm not sure what the other commenters are on.


Typescript (which is 99% of current JS above toy scale) has a company behind it.


I dont think there is a language better suited for containerized applications, even web servers more broadly.

Is it the best at everything? No, but it got everything exactly right for its niche imo.


And also the actual container infrastructure itself.


Yeah, I also think people index too hard on the language itself, when the runtime, tooling, and ecosystem are all much more important. Moreover, I think "mediocre" is a good thing so long as it means "fewer features" or "simple". I'm kind of glad Go is a mediocre language in this sense, because it gets out of the way. And Go's runtime is similarly "mediocre"--it's fast enough for 99% of applications, it doesn't use a ton of memory by default, the default GC is low latency, goroutines far easier to use than async/await, all it needs is a Linux kernel (no userland dependencies), and it's all small enough to fit in a static binary. Go also has a pretty good standard library too; no need to pull in dependencies just to marshal JSON or make HTTP requests, and everything compiles to static binaries by default so distributing CLI tools internally is a breeze.

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).


Yes, i can agree. Go is not my favorite language, but when it comes to pragmatism and developer productivity, Go is up there.


Most developers are mediocre, so having a language that can match the skill level of the average dev is a win for many situations, no?


No. A better language would make things safer for the average dev.

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.


A mediocre language is not the same as a language appropriate for developers of mediocre skills. Go could have been made a better language [0] in many ways that would have actually made it easier than it is right now, by avoiding some of the footguns built into the language. This also wouldn't have made the language more prone to being used in a complex manner by metaprogramming enthusiasts and the like. Better often means simpler, not more complex.

[0]: Not that it's not already decent.


> No matter what your opinion is about Go (as a language), but they nailed it for bigger projects.

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.


I think the way it's meant to be read and understood is as follows:

"There may (or may not) be many problems with Go, but building bigger projects is not one of those".


This is actually kind of funny because one of the classical arguments made by gophers eight or so years ago when I first started learning it was that golang's limited abstraction power makes the pain of large codebases obvious enough that teams are incentivized to keep projects small.


I basically mean:

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.


Or the related fallacy, "X was designed to be good at Y, so clearly X is good at Y!" (or worse, "is the best way to do Y!").


Well... It kinda replaced Java for most cloud stuff except for Kafka.


A form of whataboutism.


As someone who mostly writes request-handling services, I just don't think propagating errors through gateway/repository -> controller -> handler layers deserves 90% of my attention. But unfortunately that it is where it goes doing microservices glue type stuff in Go.


Ok, maybe you can help me figure something out that I haven’t been able to.

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?


>I need to make a change across both of these modules. Will my compilation pick up the changes?

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.


> Yes, add a replace directive to A's go.mod and point it to B's directory. It'll pick up changes immediately.

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.


Ehhh... I find it far more similar than not in terms of effort/maintenance, and much less likely to work with [random tool X] tbh. Replaces work with basically everything, and you can vendor when they don't - workspaces have no vendor equivalent AFAIK.

go.work is also rough to use with existing folders / a gopath-like setup, because they're super awkward if you don't have a dedicated folder with dedicated clones with just the stuff you're using in the workspace (which has some nice benefits, but most people are not even aware you can do that afaict, GOPATH caused so much pain that there's now mental scar tissue that'll take a while to clear). Replaces work fine with that if you open two editor instances, workspaces generally index everything or nothing and get confused.

Tooling support has likely improved since I last tried it (around 1.19?), and I 100% agree that it's a viable option worth considering (thanks for the comment!), but I'm not sold on them yet. Better for some things, worse for others.


Ok, I wasn’t able to figure that out when I was in the codebase.

What do you do if you’re working in a fork of the project and you need to make those changes?


Should be the same. You can replace to any location, and with modules it doesn't even need to follow GOPATH naming conventions.

E.g. clone A and fork-B into adjacent folders regardless of fork status or import paths, and change A/go.mod to include

   replace original.com/B ../B
and that should work fine.

With GOPATH you would need to clone the fork into the original path. Which you can do if needed / modules don't prevent that either, but modules don't need any specific locations at all.

---

The official module doc is... huge and doesn't spell things out in an easily-usable form, so you're far from alone in your confusion, but I do think it's worth reading. I routinely find people spending way more time struggling with them than it would take to read the spec and become an expert on it: https://go.dev/ref/mod


Wow. You’re right - that is simpler than I expected. Thank you!


Go is an amazing language. I use it for everything. It’s easily the best new language of our generation for getting shit done


What's strange is that Java predates Go and has all of these attributes. C# too, for that matter.


Backward compatibility (BC)


> they nailed it for bigger projects.

Yeah, indeed, big projects did not exist before!


They sure did! But in what shape do they exist in today? Depending on the language you might find N ways of doing something, where N = amount of devs who has worked on the project. Most likely you have a huge mess.


I'm diving back into python after being mostly away for the better part of a decade having built high scale, highly available systems in Go during that time (including migrating several projects from python and perl to go). Being suddenly back in python is jarring. So much inheritance - abstract base classes and multiple inheritance via mixins, strange coupling in tests via over use of mocking and patching, and a reliance on method overloading to do magic things like make a field actually call out to the database on usage causing strange behavior that is unexpected. I miss Go and I will be pushing for it via the strangler pattern to bring productivity and sense to this project.


I was thrust into a go project after a decade of python.

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.


Python has complex mechanisms to avoid depending on "whatever random stuff is in /usr/lib/python$v/site-packages" like pyenv and whatnot. Clearly that approach comes with a lot of downsides, too. Most newer languages avoid this pattern, including Rust, Zig, and probably others, and languages that don't tend to have workarounds; e.g. npm, bundle, etc. all use a "local" installation by default.

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.


If you have CI/CD then rebuild and deploy isn’t a big deal. If you’re mutating running systems in an uncontrolled way you’re probably doing it wrong


The reason that we have static linking is because it's not "you" updating the affected library, it's more often than not the package manager and more often than not entirely out of your control.


Python code typically doesn't and shouldn't rely heavily on inheritance. It sounds like you're working on non-idiomatic Python code. Bad luck.


Do you mean that the hierarchies don't tend to be very deep in Python? I don't tend to have deep inheritance hierarchies in Python, but I don't tend to have that in really any language I use. If you mean Python code doesn't tend to use inheritance in general, I'm not sure that's true. Django, DRF, the ML/DS packages, etc. all seem to use inheritance as much as any other language...I think? Maybe you're contrasting to those endlessly deep hierarchies you might find in Java? Otherwise, I'm just not sure what the basis is for saying you shouldn't rely on inheritance in Python, given that it is a first-class citizen due to its object-oriented programming model.


True. Thanks for your comment.


Django typically does this.


In a lot of cases it is necessary complexity, between overloading the dot operator, wrapping SQL, and supporting multiple database backends. There is a fair bit of bloat, though.


Indeed. It's quite off-putting.


The sad thing is that many people make language decisions on the beginner topics like syntax , literals, hello world or trivial samples.

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).


My experience translating a codebase from Python to Golang (chat application), is that 20k of Python really does translate to around 40k of Golang to get the same functionality.

And it’s not just due to language but also expressiveness of the library ecosystem.


I very much doubt that.

"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...


It’s not a subjective opinion: I’m saying that I’ve actually migrated a Python application to Golang (real time chat application with a lot of business logic) and it was 2x the line count.

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 have any concrete measurements, but I think this is about right, ± a bit depending on the type of application.

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.


I really think go goes too far to the point of hurting productivity and expressiveness. For example, go is missing any way to write parametric enums (sum types). Sum types make code much simpler and more expressive. The equivalent go code (using interfaces) is uglier, more verbose and more error prone. There’s a lot of features like that which go leaves out - like iterators, optional (nullable) types and so on.

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; unqualified statements that "sum types make everything simpler" are just wrong, because they don't. Whether it's worth the trade-off is a subjective judgement call and a completely different thing.


If all things are a trade-off, what would the trade-off of adding sum types be?

> 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.


> If all things are a trade-off, what would the trade-off of adding sum types be?

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.


> It's not clear to me anyway that sum times are a "slam dunk" type of feature.

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.


> unqualified statements that "sum types make everything simpler" are just wrong, because they don't.

Just about everything is "simpler" than Go's `iota` (which isn't even easier for the compiler implementor).


I never said there aren't features in Go I would rather see removed, or that there aren't features I would like to see added, or that Go is perfect in general.


Súm types (or even just working enums) would replace iota.


Only for some uses of iota. And iota is "simpler" from some perspectives, because you never have to deal with it anywhere except in const (..) blocks (that is, it's a very "localized" feature that barely interacts with anything else).


Are you including the libs in the calculation?

E.g. if in python you can “import map” and in golang you implement map, which one is being counted.


Unfortunately, once you go further I personally still find Java (and Rust) "executors" far more easy to use than Golang's channel synchronization.


What’s also sad is people choosing between two very limited languages, Go and Python. Ultimately they both lead to the same place-orientated programming soup.


And Clojure has "Simple made easy" [1]

[1] https://www.youtube.com/watch?v=SxdOUGdseq4 -> on of Rich Hickey's best talks


I wonder why isn't Clojure more popular.


I tried to use Clojure a few weeks ago. I have two projects for it: the first is that I have a "TOML test matrix" where I compare different TOML implementations against the test suite. This works by feeding a binary a TOML document which then outputs a description of that in JSON. I set that up for a whole bunch of languages.

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.


Because it's not easy.

You need the right balance of "easy" and "simple" and Clojure does not have it.


It's the learning curve.

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.


Syntax IMO. I know it's obnoxious, but the reality is most people read C like languages, not Lisp.


t took me a week to get used to the syntax. Now that I'm not working with Clojure anymore, I find every other syntax somewhat repulsive. When I was looking into Elixir and now OCaml, I found them much harder to follow ¯\_(ツ)_/¯


2 decade ago, I used to love Scheme-like languages, they seemed so elegant.

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.


lisp turns a lot of people off - it does require some retraining of your mind. when people need to get shit done this friction is too great, and so they fall back on other tools.

going to do advent of code in clojure this year though to force myself back into it - it's a great language.


Separately from that, Clojure turns some genuine Lisp people off.


That confuses me. It’s such a great lisp.


I hate everything that runs on the jvm, only use kotlin for Android stuff. I dont think I am the only one. Simplicity=python, performance=go.

Memory usage is absurd and when you cap it, the performance is worse than Scratch.


The added value of Clojure over JavaScript is marginal, and the trade-off is significant.


Sorry, but this is plain wrong. Clojure is a functional first language with an extremely powerful macro system. JavaScript has ad-hoc monkey patching and a heavy toolchain requirement to enable immutability.


And the industry doesn't care, ecosystem is 100x more important (please don't bring up that "seamless interop" fallacy crap).


Yeah, but the amount of boilerplate one must read and write in Go is frustrating, even compared to Java.


In a former life I often bemoaned the amount of boilerplate shovel code I was having to write to move data between my underlying data models and the user interface's displaying them. So much repetitive and simple 'pick up this number, format it to two decimals, put it in this display box'. Then technologies came out featuring data binding, and patterns like MVVM to keep me from having to write all that shovel code. Instead I could just specify bindings directly in the declarative UI layer, bind it to my data object (or view object) and be done. All I had to do was learn the binding language, understand how the binding engine worked, understand the corner cases and details like the difference between one and two-way binding, etc.

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.


With things like copilot, the boilerplate (if err != nil sorts of things) comes out really easily, and you can skim past it when reading.

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.


> With things like copilot, the boilerplate (if err != nil sorts of things) comes out really easily, and you can skim past it when reading.

Easily... but is that simple though.


It's very simple. It's explicit and tells you exactly how errors are handled (if you care), and it follows a very common pattern that is easy to ignore when you don't want to know how errors are handled. Simple and concise are two very different things.

In comparison, languages with exceptions give you a minefield around every function call.


Ok. Then I will spell it out: using an LLM to generate code is not remotely simple.


I will spell it out for you: Almost every language in the world has some sort of template-based autocomplete for common boilerplate that is often used, from class definition templates to if statements. If you aren't using one of these, you're probably working unproductively by thinking about all of your boilerplate instead of just taking it off the shelf. Copilot and its ilk just take this one step further by effectively giving you the same thing but with the slots in the template filled in. I could have said that "with autocomplete it's easy to write" and it would have the same meaning.

The presence or absence of boilerplate (and thus the presence/usability of autocomplete systems) does not imply anything about simplicity.


> I will spell it out for you: Almost every language in the world has some sort of template-based autocomplete for common boilerplate that is often used, from class definition templates to if statements.

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.


The LLM is just fancy autocomplete. You are latching on to the LLM aspect, not the autocomplete aspect, while the latter is the important part. The LLM literally just does a little more than a traditional autocomplete in real life. I said it because it's easy for people to understand what "copilot" actually does and I (wrongly) assumed people didn't go crazy when you mentioned it.


You can also just use templates for this sort of thing.


Mindless AI-generated boilerplate you're not really looking at sounds like a likely vector for bugs.


If you're "not really looking at" LLM output, you are abusing it.


Java boilerplate is way worse AND hidden behind annotations and so many level of abstractions!


That's mostly a facet of frameworks like Spring, not something inherent to Java itself.


There’s a strong culture in the Java community which actively encourages this style of programming. I agree - I don’t think it’s an inherent part of the Java programming language. At least not any more - recent versions of Java have a lot of nice features! But you still see a lot of this overly abstracted, “enterprise” Java code in the wild all over the place. I find it easier to just specialise in other languages.


It’s must nicer to read IMO than a generic-heavy language. Sure Golang is a bit more of a pain to write because of that but it’s a trade off.


Huh? Having defer statements and error returns vs. dealing with exceptions gives Go the win all by itself.

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.)


Go's defer is verbose and unwieldy compared to Python's `with` statement or Rust's lifetime system. In Python, you don't have to manually write any code to close the file, you just do:

    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.


I wish defer was block-scoped rather than function-scoped; that would solve the "defer in a loop" problem. Right now I just wrap the lot in a function for that, which works and isn't too bad, but meh.

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.


Go doesn't really warn you if you forget to handle an error return, which in my experience continuing silently in the face of error conditions is far scarier than crashing loudly.


Check out ErrCheck. It's a static analysis tool that detects exactly this. We added it to our common Makefile that we use for testing/building Go projects. Typically I have it test for this among other things before I commit, and then it runs again in CI scripts before a merge to main is allowed.

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.


The problem with error checking is that Go doesn't cover every case BY FAR.

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.


In practice, how often does your rant on memory safety really apply though? Because I currently feel that you're inflating the argument substantially to make it seem like a much bigger, much more common problem, than it is. For reference, in 5-6 years of Go programming, memory allocation has been a problem for me exactly one time, and it was because I was a noob and tried to push about 60GB of data into a variable at once on a virtual machine with 32GB of memory available to it. And it wasn't a silent error, the systemd service I wrote for the application was crashing each time it attempted to load up that mega-variable until I rewrote it in a sane way.


Well that's the problem with correctness. I've seen this fail, in 20 years (that I noticed) about 20x. Let's assume I caught it 1% of the times I actually saw it so ... about once a week. More on slower networks.

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.


That would not be a compile error, but these sorts of issues are why you should also run `go vet` and `golint` for a healthy codebase


Go is so boring that I found AI tooling to start being far more effective, fwiw


Much less so these days with generics, even in their fairly limited capacity.


Write more libraries of common tasks you have to do constantly. If you're not abstracting your chores, you've been doing it wrong.


Go fought me at every step trying to abstract away boilerplate.


I'm not sure how you mean it fought you. I've been writing libraries and utilities in Go for something like 5-6 years now though, so maybe what you and I perceive as "fighting" differs.


You can see the structure in the boilerplate pretty quickly. And because it's boilerplate, you'll also instantly see when it's missing.


Sounds like a waste of time. I’d rather just not have that code in the first place. I don’t want to write it. I don’t want to maintain it. I don’t want to have to scroll past it when I’m working, and have my monitor made artificially smaller because my code has the same repeated error handling code everywhere. What a waste of brain cells.


Sounds yes, but isn't in practice.

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.


Right; but the computer is much better at fixed patterns than I am. This is the philosophy behind linters, automatic indenting tools in IDEs and gofmt and friends.

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.


Go isn't very declarative. The thing that always gets me with go is how much stuff has to be (or is culturally) imperative code. This isn't article isn't wrong, simple things arent easy and easy things aren't simple. But saying one is better than the other is not right either. Turring machines are simple but we don't use them for anything serious cause it's WAAAAY to difficult to make anything worthwhile.

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.


Python is valuable due to the ecosystem of libraries it offers. The language itself is extremely poor. I think this is not something most Python users are aware of since if you are doing ML, data-science or simple scripting there is little reason to step outside of the ecosystem.

- 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.


I really cannot agree with most of this.

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.


> Slow execution of pure python code - this is the ONLY area where you are perhaps correct... but compared to what?

Compared to… basically everything? Python’s performance is unacceptable, especially given that single threaded processor performance isn’t really improving any longer.


asyncio is quite fast, especially with libuv/uvloop

- https://github.com/libuv/libuv

- https://github.com/MagicStack/uvloop


> Weird scoping rules? This will rarely if-ever impact you in the real world.

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.


Python always felt well designed to me. Slow yes but it felt the the author really thought about how the language would go together. This is mainly a comparison to the other scripting languages available.

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.


Yes it's certainly better, but it definitely slithered out from under the same rock. There are even better languages, designed by folks whose minds where presumably never corrupted by anything on that list.


I could address each of these "issues" but I'd rather focus on the following:

> 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.


> These two statements are contradictory. If it was indeed so "poor", people would notice :)

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?


I don't claim that Python is the perfect language and it will never be replaced, in popularity, by something that is better.

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 :)


You could claim the same thing about JavaScript. It's very possibly more popular than Python, but it's definitely not better designed than Python or most of the other languages in current use.


Yes and that would be a correct claim. Unless one assumes that a language is objectively "good" or "bad", in which case, rationally speaking, we would only be using "good" languages, which is clearly not the case. Ironically the same happens with natural language. In theory we should all be using Esperanto by now, in practice English as the de facto international language is totally fine.

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).


I mean it's not like MATLAB where a company put in millions of dollars to make these libraries - the entire ecosystem was developed by open source contributors because they liked something about Python - sure, it's probably not as sophisticated as some of the other languages, but it has a low barrier of entry, it has users, and people developed libraries - in some cases because that's where users are and in many cases because it's the language they know!

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!


> they liked something about Python

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!


It's definitely easier than many other languages to start learning. Absolute beginners don't want to worry about overflow, signedness, or anything like that. They just want to add some numbers and have it work. Python isn't perfect by any stretch, but having syntax like "for x in y:" goes a long way towards helping beginners not have to care about stuff they're not ready for yet.

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.


I once read that "Python" came from Monty Python's Flying Circus. And that's how I've always viewed the language, i.e. something akin to a Terry Gilliam animation.


Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: