Hacker News new | past | comments | ask | show | jobs | submit login
Go is not an easy language (arp242.net)
487 points by jen20 on Feb 22, 2021 | hide | past | favorite | 431 comments



Barriers to entry are invisible. They are invisible to people on the inside and most frequently invisible to people who have a hand in creating those barriers.

I once had a conversation with a founder about their signup flow. This founder had aspirations to be a global player. They said that step 2 of their flow included entering a credit card number. I had to stop them. "What about people who don't use credit cards?" They were nonplussed. "You know, China, much of Africa ..." they had never actually thought about whether people had credit cards because every single person they interacted with had one.

Back to languages. If you've never taught an introduction to computer programming for a general audience you are blind to what does and does not matter about a language. You look at things like `foo.bar()` and think "Yeah that's a simple method invocation" and have no idea how many people you just lost.

Never underestimate how much ease of use matters. We, as a community, have selected for decades against people who care about language ergonomics because they get hit in the face with a wall of punctuation filled text and turn away. We fight over where braces belong while not realizing how many hundreds of thousands of people we've excluded.

Ease of use is the most important part of a language. It's just that the barriers to entry are invisible and so the people who happen to be well suited to vim vs emacs debates get to talk while the vast majority of potential programmers are left on the outside.

We create barriers to entry accidentally because we design for people like us. To us the barriers are invisible.


Programming has and almost infinite depth to it, depending on the complexity you are tackling/modeling. In a way, it is like mathematics in the way you have layers acting as foundations/abstractions for more complex representation. Introduction to mathematics is counting discrete things, then the number line, addition, which is a foundation for/abstracted by multiplication, which is in turn a foundation for/abstracted by exponents, etc. and in no time, you're at the level of multi-variate calculus and n-dimensional hypercubes.

Each level requires notation that is concise (so it can fit in short-term memory to be actually useful) - one cannot expect graduate-level mathematics to share the same notation/symbols as beginner (elementary) maths.

Programming languages have to trade off which level they are biased towards, I do not believe in a universal language that is both easy for beginners while being concise for advanced users.

Back on topic - Go is not an easy language, it is a simple language, those 2 things are not the same.


Go isn't even simple. They tried to be simple, but mostly managed to only move the complexity around, or foist it on to the user's shoulders by not providing a layered, composeable API in the standard library.

There are some kinds of complexity you can't eliminate; only contain through careful, thoughtful architecture. Unfortunately, people often confuse this with the eliminatable kind, and end up making a bigger and bigger mess as they push things around.


What do you mean by "layered, composeable API"? Go does provide abstractions for common use cases in its standard library (`io.ReadAll` for example) and there are interfaces to connect different parts of the standard library (`io.Reader`/`Writer`, `fs.FS` etc.).


> Go is not an easy language, it is a simple language, those 2 things are not the same.

This distinction is fundamental to any sort of design, but is unfortunately lost on a lot of people (especially developers, in my experience). Easy and simple are near-universally conflated.

> I do not believe in a universal language that is both easy for beginners while being concise for advanced users.

Exactly. I really want some languages to be more intuitive and easy to pick up for non-programmers. The average person (whatever that means) does not have the first clue about the machines that run so many parts of their lives.

At the same time, such a language would likely be a bad fit for most professional software development. That hardly means it's without value.

I know languages like that exist, but they're often aimed at absolute beginners and are treated like toys. There doesn't seem to be much middle ground or "transition languages".

Insert joke about how $LANGUAGE_YOU_DISLIKE is a toy.


> Easy and simple are near-universally conflated.

That’s an interesting statement as it doesn’t always work in other languages. In German (my native language) my first instinct was to translate both words as “einfach” which contains both concepts. In fact, in my online dictionary of choice the word “einfach” is the first entry for both “easy” and “simple”. So if Germans conflate these two it might be because of the language they speak :) But more to the point, I’m wondering how universal the distinction between easy and simple is when other languages cannot express that distinction as easily as in English.


Interesting. My native language, Afrikaans, has a lot of influence from both Dutch and German (as well as a bit from English and quite a bit from Bantu African languages). We say "eenvoudig" for "simple" and "maklik" for "easy". I recognize both the (different) Dutch words Google Translate provides me when I translate to Dutch as similar to the Afrikaans words, but Google Translate translates both to "einfach" when I translate to German. May be German for some reason conflates the meanings. That said, homonyms and homophones that conflate meanings are found in most human languages, and often for meanings that are far easier (or is that simpler ;-)) to distinguish than "simple" and "easy".


If we were to agree that a simple task has low complexity to accomplish, and an easy task requires little energy to accomplish, then conflating them is straightforward, particularly when weighing the mental effort to accomplish the task. (Tying a shoelace is simple: four steps. Tying a shoelace with 5 pound weights on my wrists is still simple, but not easy.) Of course, if you don't agree to these definitions, then the intersection of them is thinner.


I mean, Python is the de facto beginner language and also used for professional software development (and of course intermediate data science work). Are you suggesting this is an unwise or unstable equilibrium?


I'm glad you bring that up. I think Python is the closest we have to a "universal language" (even so, it still has some limitations).

I think it works well for beginners because the language itself is so consistent and they have put a lot of effort into avoiding corner cases and "gotchas". And I think it works for professional uses because of third party library support.

To answer your question: I'm not suggesting that at all. I'm honestly not entirely sure how Python balances it seemingly so well. Given the lack of focus in the industry towards "intermediate" programmers and use cases, my slight fear is that Python will be shoehorned into one direction or the other.

Even if the language itself isn't, it does feel like the use-case-complexity gap is growing exponentially, at times.

And not just with Python. Seemingly, you're either a complete beginner learning conditionals and for-loops or you're scaling out a multi-region platform serving millions of users with many 9's of uptime.


Python does this so well because of the extremely full featured and fairly easy to use C API. Advanced programmers can write extension modules for the interpreter and provide APIs to their C libraries via Python, give their types and functions a basically identical syntax to MATLAB and R, and bang, statisticians, engineers, and scientists can easily migrate from what they already know how to use, pay no performance penalty, but do it in a language that also has web frameworks and ORMs. You can do machine learning research and give your resulting predictive models a web API in the same language.

This gets badly underappreciated. I've been working in Python for a while and honestly, I hate it. I wish I could use Rust for everything I'm doing. I can't stand finding so many errors at runtime that should be caught at build time in a language with static type checking.

But I also recognize the tremendous utility in having a language that can be used for application development but also for numerical computing where static typing isn't really needed because everything is some variant of a floating-point array with n dimensions. Mathematically, your functions should be able to accept and return those no matter what they're doing. All of linear algebra is just tensor transformations and you can model virtually anything that way if you come from a hard engineering background. Want to multiple two vectors? Forget about looping. Just v1 * v2. It will even automatically use SSE instructions. Why is that possible? The language developers themselves didn't provide this functionality. But they provided the building blocks in the form of a C API and operator overloading, that allowed others to add features for them.

So the complaints you typically see about dynamic languages simply don't matter. No static typing? Who cares? Everything is a ndarray. Syntax is unreadable? Not if you're coming from R or MATLAB because the syntax is identical to what you're already used to using. Interpreted languages are slow? Not when you have an interface directly to highly optimized BLAS and ATLAS implementations that have been getting optimized since the 50s and your code is auto-vectorized without you needing to do anything. GIL? It doesn't matter if you can drop directly into C and easily get around it.

Meanwhile, it's also still beginner friendly!

EDIT: I should add, editable installs. That's the one feature I really love as a developer. You can just straight up test your application in a production-like environment, as you're writing it line by line. No need to build or deploy or anything. Technically, you can do this with any interpreted language, but Python builds this feature directly into its bundled package manager.


Great rundown! It's love/hate for me too. Python is the worst language for scientific computing, except for all the others. I think Julia's going to take the crown in a few years though, once the libraries broaden out and they figure out how to cache precompiled binaries, to get the boot time down. With Python, it's not so much that you get to write C, you have to write C to get performance. I'll be interested to see whether Julia takes off for applications beside heavy numerical stuff. That seems to be the Achille's heel of languages designed for math/science applications -- it's easier to write scientific packages inside a general-purpose language than vice versa.


This is hands down the best description I've seen of why so many of us persist in using Python despite the language or runtime. I do hope that more alternative language ecosystems will begin to thrive in the numerical space and that we'll see more ergonomic facilities for generating high performance code from within Python itself.


TLDR; Right now Python is almost always easier for numeric Python beginners than Rust is for numeric Rust beginners and even also more productive. I just don't see Python's ease and productivity advantages remaining if Rust can catch up with Python's ecosystem and toolchain. But we'll have to wait and see if that will happen. And when and if Rust is actually (slightly) more friendly to the numeric computing beginner and much more productive in some numeric/scientific contexts than Python, Python loses its current intermediate language position. Especially if similar improvements happen in other domains.

> Python does this so well because of the extremely full featured and fairly easy to use C API. Advanced programmers can write extension modules for the interpreter and provide APIs to their C libraries via Python, give their types and functions a basically identical syntax to MATLAB and R, and bang, statisticians, engineers, and scientists can easily migrate from what they already know how to use, pay no performance penalty, but do it in a language that also has web frameworks and ORMs. You can do machine learning research and give your resulting predictive models a web API in the same language.

You know what's better than "the extremely full featured and fairly easy to use C API": If your language can itself compete with C/C++ for writing the libraries you need. The only advantage Python has over Rust regarding library ecosystem is the first-mover advantage and that Rust makes obvious how terrible the C API is which means people often invent new Rust libraries rather than reuse the old C libraries. The only advantages Python has over Julia are first-mover and that I doubt Julia-native libraries can truly match highly optimized C/C++/Rust libraries performance-wise in most situations where performance actually even matters.

> But I also recognize the tremendous utility in having a language that can be used for application development but also for numerical computing where static typing isn't really needed because everything is some variant of a floating-point array with n dimensions.

* Some numeric use-cases need to work with more than floating points. May be 2D (complex)/4D/8D numbers, may be dollars, may be durations. You lose all units of measurement and they are often valuable. In Python you cannot indicate "this cannot contain NaN/None". * In an N-D array, N is a (dependent) type, so is the size, so is the shape. Julia got this right but last time I checked it had a nasty tendency to cast the dependent types to `Any` when they got too complex. Imagine if you can replace most `resize` calls with `into` calls and have the compiler verify the few cases you still need resize. In Rust several libraries already use dependent types for these sorts of uses, but lack of important features that are only now starting to approach stable (const generics, GATs) makes them very unergonomic to work with. * I see a lot of complex types that should've been a single column in a dataframe get represented with the full complexity of multi-indexes. Juck! Not only more complex, but far less expressive and more error prone. I haven't yet seen Rust go the extra step and represent a struct as a multi-index to get the best of both worlds, but it's what I would love and Rust definitely has the potential. It's just not a priority yet as we are still just implementing the basics of dataframes first. * Things get even more interesting when you throw in machine learning. As a masters degree student, it took me months (mostly during the summer vacation, so I wasn't going to ask my promoter for help) to figure out the reason I'm getting bogus results is due to a methodological mistake that should have been caught by the type system in a safe language with a well-designed ML library. But here the issue is "safe" and "well designed library" not so much as "statically typed", but a powerful type system is required and the type system would catch the error in milliseconds in stead of hours if the it is static rather than dynamic.

> Forget about looping. Just v1 * v2. It will even automatically use SSE instructions.

Many languages have operator overloading and specialization or polymorphism to enable optimizations. In Rust this is again just a case of libraries providing an optimized implementation with an ergonomic API.

> So the complaints you typically see about dynamic languages simply don't matter. No static typing? Who cares? Everything is a ndarray.

Nope. Everything is not just an ndarray. That often works well enough. But when numeric computing gets more complex, you really want a lot more typing.

> Not when you have an interface directly to highly optimized BLAS and ATLAS implementations that have been getting optimized since the 50s and your code is auto-vectorized without you needing to do anything.

Much of those decades old optimizations are irrelevant or even deoptimizations on modern hardware and with modern workloads. The optimizations needs maintenance. In C/C++ optimizations are very expensive to maintain in Rust we cannot only leapfrog outdated optimizations but also much more cheaply maintain optimizations. Also, as we move into more and more datasets that are medium/large/big (and therefore don't fit into RAM), we're getting more and more optimizations that are very hard to make work over the FFI boundary with Python. The fastest experimental medium data framework at the moment is implemented in Rust and has an incredibly thick wrapper that includes LLVM as a dependency (of the wrapper), since it's basically hot reloading Python and compiling it to a brand new (library-specific) DSL at runtime to get some of the advantages of AOT compilation and to try to fix some of the optimizations that would otherwise be lost across the FFI boundary. Note that means now you need to do a very expensive optimized compile every run, not every release compile of the program, though I guess you can do some caching. Note also that it means maintenance cost of the wrapper quite likely dwarfs maintenance cost of the library implementation which is not a good situation to be in. The fastest currently in production python framework for medium/large data is probably Dask, but to achieve that performance you need to know quite a bit about software engineering and the Dask library implementation and do quite a bit of manual calculations for optimal chunk sizes, optimal use of reindexing, optimal switching back and forth with numpy, etc. and to avoid all the many gotchas where something that expect would work crashes in stead and needs a very complex workaround. In Rust, you can have a much faster library where the compiler handles all of the complexity for you and where everything you think should work actually does work and that library is already available (though not yet production ready).

> Meanwhile, it's also still beginner friendly!

* Is it? I admit it's code (but definitely not its toolchain) is marginally better for programming novices (and that marginally is important). Importantly, remember that novices don't need to learn about borrow checking/pointers/whatever in Rust either and that by the time they're that advanced, they need to worry about it in Python as well but the language provides no tools to help them so in stead of learning concepts, they learn debugging. Rust is lagging in teaching novices mostly due to the lack of REPL and fewer truly novice-friendly documentation, IMHO. * But give Rust a good REPL and more mature library ecosystem and I cannot imagine Python being any more beginner friendly than Rust for numeric computing. (When "everything is just an ndarray of floats" is good enough, the Rust would look identical to the Python (except for the names of the libraries used) but provide better intellisense and package management. When "just an ndarray of floats" isn't good enough, Rust would have your back and help the beginner avoid stupid mistakes Python can't help with or express custom domain types that Python cannot express or at least cannot express without losing the advantages of the library ecosystem.

Don't get me wrong. Right now Python is almost always easier and even also more productive for numeric computing. I just don't see it remaining that way if Rust can catch up with its ecosystem and toolchain. But we'll have to wait and see if that will happen.

I can also think of several other domains where Rust is actually potentially better suited as intermediate language than the competitors: * Rust arguably is already there in embedded if you can get access to and afford a Cortex-M. But I think it might actually be capable of beating mycropython in ease on an arduino one day. (At least for programmers not already experts in Python.) I won't go into my reasoning since this is already getting long. One day embedded Rust might also compete with C in terms of capabilities (the same or better for Rust) and portability (probably not as good as C on legacy hardware but possibly better on new hardware). * I think Rust is already a better language than Go for cloud native infrastructure except for its long compile times and it seems like an increasing number of cloud native infrastructure projects also feel that way. In the meantime new libraries like `lunatic` might be an indication that one day Rust might be able to compete with Go in terms of ease of writing front-ends for beginners. * Looking at what happens in the Rust game libraries space, I think Rust can definitely be a great intermediate language there one day. It already has a library that aims to take on some beginner gamedev/art libraries in languages like JS/processing/Go and at the same time, it has several libraries aiming to be best in class for AAA games.


Python abounds with corner cases and gotchas. It may have fewer than JS/Perl, but that really isn't saying much. It may hide them until a test or real-world use shows you you've stepped on them but that's not always a good thing.


> The average person (whatever that means) does not have the first clue about the machines that run so many parts of their lives.

Sadly, I would argue that this is also true of many developers.


It seems like Basic has been the closest we've come on a general purpose language with "easy to pick up features" (at least in widespread use)?


> Back on topic - Go is not an easy language, it is a simple language, those 2 things are not the same.

I often see this point here, but I always wonder what people mean by it. Could you elaborate on that point?


I think the author's example is pretty good.

"How do you remove an item from an array in Ruby? list.delete_at(i)...Pretty easy, yeah?"

"In Go it’s … less easy; to remove the index i you need to do:"

  list = append(list[:i], list[i+1:]...)
So Go is simple in that it doesn't have shortcut functions for things. There's generally one way to do things, which is simple. But it's not easy, because it's certainly not intuitive that "append" is the way to remove an array element.


Not OP, but I'd be happy to try and differentiate, as well.

Simple means uncomplicated, not a lot of pieces. A violin is arguably simpler than a guitar because of a lack of frets.

Easy means it is not difficult. A guitar is arguably easier than violin [0] because it has frets.

It's important to remember that "easy" is very subjective. What is easy for one person might be insurmountable to another.

tl;dr "simple" usually means "easy to understand" and "easy" usually means "easy to do". Both inherently assume some amount of prior knowledge or skill, so neither is entirely universal or objective.

[0]: I'm not trying to say one instrument is better or disparage and guitarists. It's an example.


The primary source for this is https://www.infoq.com/presentations/Simple-Made-Easy/

(I am not entirely sure I agree with its thesis or its applicability to Go, but since nobody had actually linked you directly to the concept, I thought it would be worthwhile to do so.)


To me, what makes Go simple language is limited set of building blocks, and being very opinionated - this mostly relates to syntax and what the core language provides out of the box. With Go, you only need to understand looping, branching, channels, slices and you're mostly good to go.

Ease is measured by how a language may be used to solve a problem. As an example, text-wrangling with Perl is easy - but you may have to do it using complex (i.e. not simple) representation.

Back to Go - channels are a simple concept, but are not (always) easy to work with if you do not put a lot of thought into your concurrency model.

Edit: I just thought of another way to express the difference between "simple" and "easy". The notation of adding "1+1=2" is simple, however proving that "1+1=2" is not easy (at least at the level of elementary students).


> "What about people who don't use credit cards?"

> "You know, China, much of Africa ..."

Oh, you don't even have to go that far. Of all the people I know maybe 1 out of 10 has a credit card. The irony in regard to your post: Most of them got one because "it was needed for something online" at some point. This is in Germany.


Maybe your sample is really biased, but the actual credit card ownership rate in Germany in 2017 was 53% and growing YoY, so it's likely quite a bit higher by now.

https://www.statista.com/statistics/865943/credit-card-owner...

You may need to copy & paste the title of that page into Google and access the page from there to see the data.


A programming language is a tool. I don’t think it should really be optimized or designed around how easy it is to teach someone who is at step zero, basic programming concepts. I do think Go is a decent language to teach up and coming professional devs precisely because it doesn’t hide the real complexity of what’s going on, but I’d probably opt for something a bit higher level as the absolute intro to programming.


I've been coding for 10+ years in primarily ruby and python. I switched to golang recently for work. While its an interesting language, it takes forever to express what I want the code to do, unlike the previous languages.

Go's simplicity forces developers to introduce insane amounts of complexity.


After like 9 months of Go I am already starting to leave it behind for Rust, but one thing Rust still really lags behind Go in is the maturity and feature completeness of major libraries.

The main example in my mind is how powerful Cobra/Viper is to create CLI apps where config can come from multiple sources - files, flags, env vars. To do the same in Rust you need to write a lot of your own code and glue together several libraries.

There's also nothing I can find for Rust that can do database migrations as nicely as Goose for go. Diesel can create its own migrations, but that's only if you're using Diesel and I prefer SQLX over an ORM and Diesel isn't async yet


Ruby and Python are notorious for being difficult to read because of all the foot guns in place. Monkey patching, duck typing, metaprogramming... and that's not even talking about structural limitations like the GIL.

Go is definitely more verbose and less "fun" to write, but it's 10X easier to reason what's going on in a large application.

Of course, it's also the correct kind of solution for some types of problems. If you need a compiled language (many do), the competition to Go isn't Ruby and Python, it's C++ and Rust.


Or Nim or Zig or ...

Actually, I feel like Zig may be most in the compiled-but-keep-it-simple-like-C headspace as Go. I don't know either Go or Zig super well. From what I do know, Zig seems not quite as stubborn as Go about forcing code to be "simple to reason about" (which is also subjective, though maybe less so than "easy to do things").


I would say Zig is interesting in that "safe things are easy and pretty", "unsafe things are difficult and ugly", thus drawing eyes to the code that needs it. It's four lines of nasty looking code for me to convert a slice of bytes to a slice of (f64/f32/i32)... Which the compiler no-ops. This is dangerous because the alignment and byte count must be correct.


I would say both of those are interesting languages, but lack the amazing standard lib of Go and definitely the tooling and industry support.

Personally, I find Swift to be a really great language, but can't deal with the Apple eco-system, XCode, raw Linux support...


Totally agree Re: Swift drawbacks.

Nim's stdlib is actually quite large (like 300 modules). I am sure there are things in Go's that are not in Nim's (like Go's `big`), but I am equally sure there are things in Nim's that are not in Go's (like Nim's `critbits`, `pegs`, etc).

I doubt there is much in Go's stdlib not available in Nim's nimble package system, but OTOH, I am also sure the Go ecosystem is far more vast. I just didn't want people left with the impression Nim's stdlib is as small as Zig's which it is definitely not. Nim has web server/client things and ftp/smtp clients, json and SQL parsers, etc.


> If you need a compiled language

I would wager jvm/clr are probably the main competition for Go at least for product/tech companies and enterprise IT.


Definitely for server-side software, but not for binaries that need to be distributed to end users.


Python is widely considered to be very expressive and very readable.

You can write unreadable code in virtually any language but that's besides the point.


Considered "very readable" by whom? It's pretty well accepted that dynamic/weakly typed languages are more difficult to deal with at scale.


Python is dynamically but strongly typed. “2+True” is a type error; once something has a type, it has a type. There’s no WAT.


Python is more strongly typed than JS/Perl, granted. But it is still very weakly typed overall. Here are some examples:

1. if list(): pass # implicit coercion from collection -> None -> bool. (Very uncommon weak typing and a terrible idea.)

2. a = 123; b = 4.5; c = a + b # implicit coercion from int -> float (Common but not universal weak typing. More often hides bugs than helps with ergonomics and readability but sometimes a worthwhile tradeoff.)

3. a = 1 + false # implicit coercion from bool to int (Common weak typing in scientific languages (for masking) and older C-family languages (for bit twiddling). However, that's still bad language design. Libraries/syntax sugar should special case masking and bit twiddling. You should not have global coersion between bool and int.)

4. etc.


Variables can change type though. This is working python code:

  x = 1
  y = 2
  print(x + y)

  x = "no "
  y = "types"
  print(x + y)
Whereas this will not even compile in Go:

    x := 1
    y := 2
    x = "no"
    y = "types"


That's the difference between static and dynamic typing, not the difference between weak and strong. The values cannot be used as if they were another type.


It has nothing to do with the difference between strong and weak typing and also has nothing to do with the difference between weak and strong typing. It is about Python not disambiguating between variable shadowing and variable reassignment.

Here's a comment of mine that explains why Python is weakly typed: https://news.ycombinator.com/item?id=26354039.


So what's the type signature of `print()` then?

I do think my example code demonstrates why Go is easier to reason at scale than Python. Python is conflating assignment and type declaration. Go has = and :=, so it's crystal clear what's going on.


> So what's the type signature of `print()` then?

I like the way Rust makes clear what are the different possibilities:

In Rust we use a macro to handle variadic arguments, but if it was just a case of supporting different types, not of supporting an arbitrary number of arguments we would've had three options for the type signature:

1. Monomorphic duck typing: `fn print(arg: &impl Debug)`. Here the compiler simply generates multiple print functions, one for every type the function gets called on. The exact concrete type is known at compile time.

2. Polymorphic dynamic dispatch (with dynamic sizing) duck typing: `fn print(arg: Box<dyn<Debug>>)`. Here the compiler generates a vtable and allocates on the heap. Only a type approximation is known at compile time, not the exact concrete type, but it still counts as static typing.

3. Dynamic typing: `fn print(arg: Box<dyn<Any>>)`. Note `Any` in stead of `Debug`. Full dynamic typing with the type completely unknown at compile time. Juck! But occasionally useful for prototyping or for FFI with dynamically typed languages.


> what's the type signature of `print()` then?

Dynamic typing doesn't rule out polymorphism.

    void print(PyObject objects...)
where `PyObject` is a base type.

Additionally, you could perfectly well have constants, or require variables to have a fixed type, in a dynamic language. You would just pay a cost at runtime to check the type on assignment.


I would argue that you pay the cost at runtime but you also pay a cognitive overhead cost while writing in a dynamically typed language. Refactoring in particular is a lot more difficult.


I agree. But this particular example ironically has nothing to do with dynamic typing.


This has nothing to do with either dynamic or weak typing in Python. It's just Python not disambiguating between shadowing and reassignment.

Here's a comment of mine that explains why Python is weakly typed: https://news.ycombinator.com/item?id=26354039.


Bad example, booleans in python are integer subclasses since forever... 2 + True = 3. Also, False in [1, 2, 0] evaluated to True. But you're still right saying python is strongly typed.


Companies use golang for web services, which directly competes with Ruby and Python.


And Java, which (I feel like) is used for bigger, long-term projects.


> the competition to Go isn't Ruby and Python, it's C++ and Rust.

Crystal is a pretty solid alternative to golang.


I don't think you can write that much of a rant using the term "ease of use" without explaining your view on what "ease of use" is; does "use" really only mean a beginner using it for the first time? You mention languages with punctuation, what alternatives do you see instead? You claim "ease of use is THE most important part of a language", but why is it the most important?


> Back to languages. If you've never taught an introduction to computer programming for a general audience you are blind to what does and does not matter about a language. You look at things like `foo.bar()` and think "Yeah that's a simple method invocation" and have no idea how many people you just lost.

I keenly remember in college when they were first starting to teach me C++ and I asked something like “Ok: I hear what you’re saying that this is a function, and that’s a parameter, but my question is how does the computer know that you’ve named it [whatever the variable name was]?

The teacher had no understanding that this was a conceptual barrier.

Of course now I know that the answer is “because order of the syntax tells it so“, but stuff like that made those classes much harder than they needed to be.


> Ease of use is the most important part of a language.

It's really a matter of who your target audience is and what they're trying to achieve, though, isn't it? You might make a language easy for the whole world to use, but simultaneously make it hard for specific tasks. Likewise, a language might be easy to use for experts who are trying to achieve a specific task, but difficult for newbies. That's totally okay.

BASIC was super easy to understand and helped me get into programming, but there's no way I would use it for anything serious today.


I feel completely the opposite. While I do agree that we should make the barrier to entry as low as possible. A lot of times the barrier to entry is in conflict with the usefulness of a tool. We should lower the barrier to entry without losing any usefulness and not any further.


> Barriers to entry are invisible. They are invisible to people on the inside and most frequently invisible to people who have a hand in creating those barriers.

I just want to say that I really, really like this phrasing.

I have a lot of opinions on how programming languages could be improved, and however much I disagree with Rob Pike on types, I still think Go hit a real sweet spot.


> You know, China

Another thing I find very interesting is Go is very popular in China

https://blog.jetbrains.com/go/2021/02/03/the-state-of-go/

the US ranked #7, it barely had more devs than say, Belarus.


But the metric was credit cards in China


Going by their first example, the fact that I couldn't write

   list.delete(value)
without realizing it involves a linear search is considered one of the benefits of Go.

That said, I agree Go could use a bit for ergonomic and handy methods, while still remaining efficient.

The example on Concurrency also has a different answer. Go gives you all the tools, but there are many different ways their problem can be solved. By explicitly avoiding a "join" method and having any default 'channels', Go makes all these possible. What is missing is some of the popular patterns that emerged in the last few years need to make it to into stdlib or some cookbooks.

1. If you want a worker model, I would start n=3 go-routines and they all receive from a single channel. They don't need to much around with a channel of buffer 3, as in the example.

2. If the workers already return data from a compute, the read from driver serves as the wait.

3. In other cases, there is sync.Waitgroup available to synchronize completion status.

4. End of work from the driver can be indicated via a channel close. Closed channels can still be read from until they are empty and the reader can even detect end-of-channel.

Designing concurrent system is a bit complicated. Some tutorials do make it sound like a `go` keyword is all you need. All of these can fixed by improving std-lib or cookbooks.


> Going by their first example, the fact that I couldn't write list.delete(value) without realizing it involves a linear search is considered one of the benefits of Go.

I think this is mostly okay. Most experienced programmers will realize this is a linear search and that it may be slow on large arrays. And turns out that in the overwhelming majority of the cases that's just fine!

For other cases it's not-so-fine, but the mere presence of "list.delete" doesn't really stand in the way of implementing another, more efficient, solution.

Overall, I certainly think it's better than implementing the same linear searches all the time yourself!


And it's not exactly gatekeeping to think that anyone who uses a method on any data structure on their platform should know the (rough) complexity of it (At least in the family constant, linear, or better/worse than linear). Removing a value from an array-backed list that isn't at the end is usually not a good idea.

My main problem with having "remove items by value" in standard library apis is that it assumes a notion of value equality. Having that notion inserted at the very "bottom" (Like Equals/GetHashCode in .NET for example) is a mistake that has caused an infinite amount of tears.

I much prefer this situation where the user must provide his own implementation and think about equality. It's boilerplate, but the boilerplate is useful here.


> I much prefer this situation where the user must provide his own implementation and think about equality.

But that doesn’t scale beyond the first user. Every subsequent developer now needs to read implementations to understand what the code does thanks to a lack of standardization for these functions. Consider if there was a smaller standard library with fewer interfaces and conventions, it would become a lot harder to understand a number of concepts in Go, by design. That’s fine, but conventions are what made Ruby on Rails projects so successful that they scaled up to being the monolith monstrosities most of us with startup experience ended up knowing them to be.

Note that I’m suggesting something akin to C++’s standard library where algorithms are already written for you to use and compose with. Yes, the drawbacks are a slower compile time, and some conventions like constexpr can really complicate things, but… I can’t say that a larger standard library or a larger set of conventions would make Go harder to use assuming the implementations hide a sufficient amount of complexity to outweigh the overhead of learning of them in the first place.

What functions provide more value than the overhead required to learn them? Delete is one such function, mutable immutable data structures generally are likely another. Yes, the documentation needs to specify big O complexity, but it can still be easy to read. For example: https://immutable-js.github.io/immutable-js/docs/#/List

The only way to get easier than that would be to use the same syntax for immutable operations as mutable ones: https://immerjs.github.io/immer/docs/introduction

I recognize that the Go community finds the built-in standard library restrictive now, but that’s no reason not to support a versioned standard library that ships separately but contains common functionality. I can only point to TypeScript for how such a system might work, given the large community project that exists to provide and publish types for that language, without actually publishing them with the language or compiler itself, excluding browser dom etc.


> But that doesn’t scale beyond the first user. Every subsequent developer now needs to read implementations to understand what the code does thanks to a lack of standardization for these functions.

There is no standardization. It’s a hidden piece of logic that developers slowly and painfully understand.

If I you have to pass an equality function when removing an item from a list by value (or equivalently when creating a set or dictionary) it would always be explicit. That doesn’t mean it can’t be standardized. A framework can provide typical implementations (and often does!) such as “ReferenceEquals” or “StringComparison.Ordinal” etc.

Another unfortunate side effect of the “equality as a property of the type” is that you can only have one such equality. And if you are passed a Set of dogs you still can’t know for sure whether the equality used is actually the one declared in the Dog type of at Set creation (or possibly in the Animal base class). It’s a mess. And it’s so easy to avoid - the default virtual equality is simply not necessary.


This is why I tend to like the example shown by the Immutable javascript libraries. They implement find-by-value using a library-specific calculation of equality built on primitives, but if you need something custom, you can pass predicate functions to, for example, perform your own equality check.

I think we're agreeing here, despite the initial disagreement. Standards don't have to solve every edge case, nor do they have to integrate with existing language patterns such as "equality as a property of the type" though a standard function could have multiple variants. The Go way of doing this might be like how regular expression function names are built.

Also, the library doesn't have to implement delete that way. If functional programming and predicate functions are an encouraged design pattern, you could replace delete-by-value functions with a filter function which could clearly indicate what happens if more than one value is found as well as how the equality check is performed but glosses over using slices to update the array. Some might say it's slower https://medium.com/@habibridho/here-is-why-no-one-write-gene... but it doesn't have to be slower, it's just tradeoffs in how the go compiler and language currently works vs could work.


Why would introducing value equality of all things be a problem? I think the opposite is true: many languages force you to write error-prone boilerplate because they lack a good definition of value equality built into the language and ecosystem.

Go in particular is worse on this front than any language more high-level than C. It defines equality for a very limited subset of built-in types, with no way to extend that notion of equality to anything that is not covered; nor any way to override the default equality assumptions. This makes it extremely painful whenever you want to do something even slightly advanced, such as creating a map with a struct as key when that struct has a pointer field .

And since pointers have lots of overloaded uses in Go, this turns a potentially small optimization (remember this field by pointer to avoid creating a copy) to a mammoth rewrite (we need to touch all code which was storing these as map keys).


I’m sorry for the “gatekeeping”, but do you really want to work together with someone that doesn’t know the language’s standard library or won’t even look at the documentation? And instead it would be positive that he/she writes the very implementation of something?


This problem transcends documentation of any given language's standard library. list.delete(value) in any programming language is a degenerate without an ordering or hash built-in to the underlying data structure.

If you ever need to delete based on value, it's a smell that a list is the wrong data structure. I'm sure there are cases in constrained environments where a linear search is heuristically okay, but generally in application development list.delete(value) is a hint that you're using the wrong data structure.


Good point. I guess the reason the author finds removing from lists to be relevant is because there is no Set structure in the Go standard library. They should use a `map[t]bool`, but that is non-obvious to many programmers.


My Google search suggests that maps in Go aren’t ordered. If so this doesn’t work in place of an array.


`map[t]struct{}` saves you a few bytes per entry. Just use the `_,found := foo[key]` form of lookup


Unfortunately, map[t]bool (or map[t]struct{}) only works for special structs that happen to have equality defined for them. It's not a general purpose solution, it's just a hack.


map[t]struct{} so that the values don't require any space.


> Most experienced programmers will realize this is a linear search and that it may be slow on large arrays.

Experienced programmer can think that values in a list with such operation are indexed by a hash and delete by value is O(1) unless there is a big warning in documentation. Novice programmer probably haven’t seen such combination of an array and a hash table and will assume O(N) as taught in a college.


Experienced programmers, if perf matters to their project, should think about their data structures, memory size, and access patterns. It's not clear why an experienced programmer would think an array defaults to wasting memory on a hash table. Why use an array, if they need lots of values and to delete them at random?


Experienced programmers will know that such a thing is possible, but I've never seen any (non-JavaScript, non-PHP) language where primitive arrays involve a hash table.


And even in JS and PHP (and Lua) where arrays and hash tables are technically the same data structure, a hash table used as an array isn't actually indexed on elements, just numbers, so you don't get O(1) search.


Experienced programmer will know whether the vanilla array of the language they use feature this kind of convoluted data structure.

Which, for virtually all languages, is “obviously not”.


Developers in other languages know the performance characteristics of list.delete because there is only one implementation throughout 99% of their codebase. Go you have to look at the implementation every single time to make sure its doing the right thing.


Haskell documentation uses the Big O notation everywhere which I really love. I wish Go and many other languages did the same.

An example would be https://hackage.haskell.org/package/containers-0.4.0.0/docs/....

  size :: Map k a -> IntSource
  O(1). The number of elements in the map.

  member :: Ord k => k -> Map k a -> BoolSource
  O(log n). Is the key a member of the map? See also notMember.

  lookup :: Ord k => k -> Map k a -> Maybe aSource
  O(log n). Lookup the value at a key in the map.
And so forth...


Redis does the same; e.g. [1]:

> Time complexity: O(N) where N is the number of elements to traverse to get to the element at index. This makes asking for the first or the last element of the list O(1).

I agree this is something more documentations should do when possible; it doesn't even have to be big-O notation as far as I'm concerned, just a "this will iterate over all keys" will be fine.

Ruby's delete() doesn't mention any of this, although you can easily see the (C) implementation in the documentation[2]. In principle at least, this doesn't have to be O(n) if the underlying implementation would be a hash for example, which of course has its own downsides but something like PHP may actually do this with their arrays as they're kind of a mixed data structure? Not sure.

[1]: from https://redis.io/commands/lindex

[2]: https://ruby-doc.org/core-3.0.0/Array.html#method-i-delete


Redis's documentation is wonderful, I wish every system I use were documented even half as well.


In the C++ standard the complexity of algorithms is a requirement for a compliant implementation. Many text books skip it, but better documentation references it. Taking the "delete element from list" example from this thread https://en.cppreference.com/w/cpp/container/list/remove states that there is a linear search.


Andrei Alexandrescu has a scheme to encode these in the D typesystem on his website. It didn't get merged into the standard library in the end but you can happily do it in your own code


My experience is that developers in other languages have no idea of the performance characteristics of list.delete. It seems low-level and therefore obviously fast.


"Fancy algorithms are slow when n is small, and n is usually small." Rob Pike, creator of Go.


Not at the scale of his employer.


You don't need to be Google scale. Google scale is just "medium large startup" scale replicated across a zillion regions. Gmail has an enormous amount of data behind it, but it's not like calling foo.sort() is going to be iterating over billions of accounts.


But isn't usually when using lists performance is dead anyway (due to cache misses).


"List" is usually a generic term for any data structure that can hold many items, and that has an API for adding or removing items. This can be implemented using an array, a linked-list, a hasthable, or a more advanced data structure.

Also, in languages with GCs, linked lists do not necessarily cause more cache misses than arrays, due to the way allocation works (this is especially true if you have a compacting GC, but bump-pointer allocation often makes it work even when you don't).


Linked-lists, maybe, but `list`s in Python are just resizable arrays.


resizable arrays of pointers to objects stored elsewhere, which is going to blow through your cache anyway, especially when you start accessing those objects' attributes. so it's more a memory management strategy than a performance optimization.


But when removing an item it’s not necessary to visit every element, just re-arrange the pointers (and in CPython, modify the reference count of the item that was removed).


When removing an item by value (being the use case in this thread), you do need to visit the elements to check the value.


Yes but the problem there is not that your list is backed by a linked list or an array, it's that every single value is boxed and needs dereferencing. The difference between no hops and one hop totally trumps one hop vs two hops.


You can write a list data structure with performant 'delete' properties if you're willing to maintain a sort or a hash table. There would be people here bitching about memory usage if the stdlib did that natively. Here's a solution: don't use list.delete. You're using the wrong data structure if that's your solution to whatever collection you're maintaining.


Downvotes on comp sci 102. Nice.


We have this saying in my country "if grandma had a mustache she'd be a grandpa".


I like my country's version better: "if grandma had wheels she'd be a bicycle".


Which language/country is this?

I will use this in English regardless. I like it.


Mexican Spanish. I don't know if they also use that saying in other Spanish speaking countries besides Mexico. In Spanish it goes: "Si mi abuelita tuviera ruedas, sería bicicleta".


If your aunt had nuts she'd be your uncle


That has the same basic idea as the mustache version, but is pithier. I still prefer the surrealism of the bicycle.


You'd probably do better on HN to substitute 'decrying' for 'bitching'.


Noted, thanks. Didn't realize that was the issue.


Yes!

Perfect example: Datetimes. In Golang, if you want to convert a string to a datetime (or vice versa), you _need_ to look at the godoc for datetime because it uses very specific permutations of "Jan 2, 2006" that you _have_ to include in your code. This is much more confusing than how this would be done in Python (provide the format of the date using ISO8601 notation) or Ruby (provide the string, get a Datetime, done).


> without realizing it involves a linear search is considered one of the benefits of Go.

The primary problem that I think the author was trying to get at is that go lacks tools for building abstractions. In a language with generic functions (for instance), it would be possible to implement a delete_at function completely in user code. The fact that you cannot reflects poorly on the language.

Of course it will always be possible to write functions that have arbitrarily slow time complexities. That's something you have to be aware of, with any function; it's not a reason to avoid including a linear-time function to remove an element at a given index of an array.


That’s a deliberate choice in many ways, and it means you don’t end up building castles in the air, or worse, working within someone else’s castle in the air. See the lack of inheritance for a good example of a missing abstraction which improves the language.

Abstractions are pretty and distracting but they are not why we write code and each one has a cost, which is often not apparent when introduced.

That’s not to say Go is perfect, there are a few little abstractions it would IMO be improved by having and the slice handling in particular could be more elegant and based on extending slice types or a slices package rather than built in generic functions.


I don’t see why a well-built standard library would be in the way. In the case of delete, well write your own linear search if you are into, though it gets old imo fast. But will you also write a sort algorithm by hand? Because I am fairly sure it won’t be competitive with a properly written one. Also, what about function composition? Will you write a complicated for loop with ifs and whatnot when a filter, foldl and friends would be much more readable?


Go actually does contain a sort package in the stdlib: https://golang.org/pkg/sort/ with the most ergonomic function being sort.Slice

As for a complicated for loop instead of the functional equivalent: I too am a fan of functional patterns in this case, but the for loop equivalent in my opinion is just as readable in 99% of cases. A little more verbose, that's all. Matter of taste.


Seems to me there are a lot of different types of collections which all have a delete operation. Feels like go forcing you to hand roll a delete forces you to violate the principal that a bit of code should care as little as possible about things it's not about.

If code breaks because list is now a hashmap, that seems like an anti-feature.


> the principal that a bit of code should care as little as possible about things it's not about

Can you find/quote where that principle comes from? I'm genuinely curious.


I think it is mostly based on OOP paradigm, like principle of least knowledge.


It's a motivation for things like OOP, Generics, and macro's.


I agree filtering and many generic operations on slices would be nice, I think they have plans for it once generics lands. I don’t agree they are huge problems though, they are much simpler than sorting algorithms for example (which are included).

A range vs a series of filtering operations is an interesting question in terms of which is easiest to write or, crucially, for a beginner to understand after writing - not convinced foldl is easy for beginners. If you like abstractions like that, Go will be abhorrent to you, and that’s ok.

It is easy to build your own filter on a specific type, I’ve only done it a couple of times, it is literally no more than a for loop/range. Where I have written extensions and would appreciate helpers in the std lib is things like lists of ints and strings - contains, filter and friends would be welcome there in the stdlib (I use my own at present).

For filter etc I think I’d use them but am not convinced they would change the code much - the complexity is in what you do to filter not the filtering operation itself which probably adds about 3 lines and is very simple (set up new list, add to list in range, return list). For example in existing code I have lots of operations on lists of items which sit behind a function and for hardly any of them would I delete the function and just use filter in place at the call site, because I have the function to hide the complexity of the filtering itself (all those if conditions), which would not go away. I wouldn’t even bother rewriting the range to use filter because it’s not much clearer.


> and it means you don’t end up building castles in the air, or worse, working within someone else’s castle in the air. See the lack of inheritance for a good example of a missing abstraction which improves the language.

Very well said. If only more developers would understand this.

Yes I know, creating interfaces, inheriting everything from the void, adding as large amount of layers to create the beautifully constructed reusable structure is a nice drive but it might happen no one will reuse it as doesn't want to dive itself trough infinite level lasagna with rolling ravioli around it (and - thanks god, go doesn't have try/catch - throwing exceptions trough all the layers to force you to go down the guts of the construct to figure out what it means). Or, as it often happens, will be forced to reuse it, complicating his life and is code.

I do understand that inheritance, even operator overloading has its meaning and is extremely useful. But it has another side - everybody are overdoing it as "might come handy later" and the code becomes a case for "How to write unmaintainable code"[2].

When I am coding I am not doing it for philosophical reasons and go has until now succeeding being a very helpful tool without much of "meaning of life" complications. And i would love to see it stay that way.

If you are forcing me to read the documentation / code (presumably I know what I am trying to solve) to be able to use the "beautiful oo construct" and forcing you to read your beautiful design, you have failed making it and I have seen it in java everywhere. I just hope the same people making everything complicated more than it need to be wont skip from java[1] train to go train. I really don't want them anywhere close.

[1]https://github.com/EnterpriseQualityCoding/FizzBuzzEnterpris...

[2]https://github.com/Droogans/unmaintainable-code (and 20 others)


Yeah, this. I always found myself creating beautiful OO constructs that were invariably overfitted to my understanding of the state of the problem at that time, and were really hard work to change once I understood the problem better (or it changed).


Yeah I’ve fallen into this trap and worst of all, I’ve had my team fall into it and not find their way out.

Abstractions are a great idea, and Go has plenty of these (io.Writer is ubiquitous), but most business logic is dead boring and doesn’t need it.


The advantage here isn’t that it’s ergonomic, it’s that if I get an “unknown” array I have no special knowledge about, I can simply use list.delete instead of having to write a linear delete and sleep soundly in the knowledge that it’ll have well-known performance characteristics for that case. If I know something special about an array, I’m still free to write my own deletion algorithm, and nobody would fault you for that.


> What is missing is some of the popular patterns that emerged in the last few years

Erlang has had these "popular patterns" for decades. And all languages that ignore them, making devs reimplement them, poorly, with third-party libs and ad-hoc solutions.


It has been said that Go4 software design patterns are signs of language defects.

Most all of them can be implemented as single lines of code in Python.


Most languages codify common idioms from earlier ones; I don’t see how most of the patterns are any different.

I’m sure there were a lot of assembly programs that used the “call stack pattern”. I’m sure there are a lot of C programs that abuse struct packing to do inheritance (the “class pattern”, sometimes with crappy vtables) or the preprocessor to do “templates”. And even in Python, you’ve got to use the visitor pattern due to single dispatch.


I'm just learning Go myself, where can I learn or reference these popular patterns that have emerged over the past few years?


What comes handy is this [1] link, but I will update if I get any better links or others might chime in.

[1] https://blog.golang.org/pipelines

It is a very long doc, but that also shows that concurrency has so many patterns one might like.

My own pattern is typically

1. Decide level of parallelism ahead of time and start workers (that many `go X()` invocations 2. Setup sync.Waitgroup for the same count 3. Create two channels, one for each direction. 4. Job itself needs some sort of struct to hold details

Most of my jobs don't support mid-work cancelation, so I don't bother with anything else.


Your 1. and 2. are merged in the almost-stdlib package errgroup: https://pkg.go.dev/golang.org/x/sync/errgroup.

It also uses context (useful for long running, concurrent jobs) and handles mid-work cancellation


Study the generic reader/writer implementations in the io module. (On my system, those sources are in /usr/lib/go/src/io.) The io.Reader and io.Writer interfaces are very simple, but very powerful because of how they allow composition. A shell pipeline like `cat somefile.dat | base64 -d | gzip -d | jq .` can be quite directly translated into chained io.Readers and io.Writers.

Another example of this is how HTTP middlewares chain together, see for example all the middlewares in https://github.com/gorilla/handlers. All of these exhibit one particular quality of idiomatic Go code: a preference for composition over inheritance.

Another quality of idiomatic Go code is that concurrent algorithms prefer channels over locking mechanisms (unless the performance penalty of using channels is too severe). I don't have immediate examples coming to mind on this one though, since the use of channels and mutexes tends to be quite intertwined with the algorithm in question.



This looks like some C++ or Java developer trying to shoehorn OOP concepts into a language that is not OOP-first. Just to pick out one particularly egregious example:

> Registry: Keep track of all subclasses of a given class

Go does not have classes.


Since simpler languages are easier to work with, let's just remove goroutines and channels from Go. Then all these problems will just go away.


It's okay. They will add generics soon, someone will immideately write a generic array.Delete() function, it will klog compiling and linking, and in couple more iterations we will have another JVM.


Linear search if it's not sorted. If it's sorted binary search will be much faster. So you need a flag for the function to tell it to use one of the two. This is why writing generic functions can be tricky. Programmers may use something inefficient just because it makes their lives easier.


linear search can be faster than binary search for small arrays, depending on your processor architecture.

writing generic functions is difficult, so it's nice if a language allows people to do so, otherwise you get N inefficient and/or buggy reimplementations of the function in every project needing it. (not sure if that is your point)


Linear search is also quite a bit faster than maps for small arrays (in Go at least, last time I tested it).



my point was that the author uses an example like search and called go not an easy language. so i was trying to give an example where it's not necessary a linear search or search is trivial ...


If your developers hand write a binary search to delete an element you are extremely likely to end up with bugs. So it would be nice if there was a generic binary search too.


That Go doesn’t have the equivalent of `std::lower_bound` is pretty ridiculous.


Binary search won’t be any faster (in big O) because it’s backed by an array: you’ll still need to copy every element after the removed one from slot n to n-1.


Does Ruby track that a list is already sorted? If not, you are still adding cognitive overhead for the programmer to track it


You can sort by many things. Knowing where you sort (usually as far up the chain as possible) is important


Yes. That very example shows why itis hard to write fast program in Ruby. It's not even the interpreter (which is slow), but it's that Ruby is pure magic.

I was never able to understand what happens, when I looked at a particular snippet of Ruby code, if it was not me who wrote it. With Go, understanding others people code is a trivial task.


What part of

    list.delete(x)
Is hard to understand if you didn’t write it?


If we're still talking about Ruby here: List could be _anything_. It might be a list of numbers, it can be a dataset in a remote server, it can be a web page parsed into a list of sentences, it might be list of people in Active Directory.

You literally can't know what's going on under the hood without popping it and looking for yourself.

Ruby's infatuation with clever magic code is what turned me off it years ago. You can get up to 80% in 20% of the time compared to other languages, but then you spend the 80% of time you have left fighting against the magic to get the last 20% done properly. There's way too much stuff You Just Need To Know.


I don’t think that example tells the whole story. You have to zoom out and consider what code based look like when this trade off is made repeatedly by devs. When “easyness” or DRYness becomes king you get ravioli code, and it becomes unintelligible. Keep it simple


My persistent impression of the Ruby ecosystem is that everyone optimizes for clever one-liners so hard that all other priorities go out the window.


As much as I don't personally enjoy writing Go. This is a huge benefit. There's a decent amount of conformity around the "right way" of doing things. I'll admit, it has definitely made me consider my approach to solving problems in other languages much simpler. I'm not back to writing loops in JavaScript when I use it. It really bugs the functional programming folks but... no one can argue that it's readable, and it's pretty fast.


But no one said Go was easy. It's just simple and easy to reason about. There's a huge benefit to something doing exactly what it says. No hidden behaviours, no overly complex syntax, nothing like that.

I came from C# where I have gotten increasingly frustrated with the amount of additions to the language. Like we now have classes, structs and records. We now have async/await but we also have channels. This is what having a huge toolbox causes. Some people are going to write code using channels, me others will use async. Some people will use features that others don't care about.

I think there's huge benefit in a simple language. In Go I know that there's only 2 ways to do concurrency. You either use go routines and mutexes or you use go routines and channels.

Generics will bring a lot of helper functions that weren't possible before which will remove a lot of repetitive code.

But otherwise I am super happy with writing Go code. All my pet peeves of something like modern Java or C# are gone.


> But no one said Go was easy.

Lot's of people have said go is easy. In particular that go code is "easy to read" is an oft-cited benefit of go.

Also, if you go to golang.org and the first thing you see is:

> Go is an open source programming language that makes it easy to build simple, reliable, and efficient software.

(emphasis mine)


Go is easy != easy to read.

I personally do think Go is easy to read. The fact that I don't get bitten in the ass by hidden behaviours and there's no inheritance to step through 5 files of. The fact that everyone's Go code looks more or less the same (unless you're an absolute beginnner) because there's only so many ways to do something. Compared to C# where reading someone else's code means I have to take a shot to calm my nerves and then take a guess at which permutation of language features they decided to use today.

A simple language is easier to read. This doesn't mean it's going to be easy for anyone who isn't a Go developer. There's no claims of that. But given a week to understand syntax and getting used to reading files, I don't think there's many places where you'd get stumped.

>Go is an open source programming language that makes it easy to build simple, reliable, and efficient software.

This still holds true for me. A simple language builds simple software. No noob Go dev is going to have a good time, but that's true in almost every language. Once you understand how everything works, how interfaces work, how channels work, you can simplify down most problems and build them with ease. Needing to write a few extra lines of code doesn't mean you can't write simple, reliable and efficient software.

Neither of those quotes say Go is an all round easy language. It's not like you can throw it at a baby and get back Kubernetes. It's not easy to do custom containers, it's not easy to do manipulate lists, it's not easy to do a bunch of things you'd do in one line in something like C#. But I think these are small hurdles for the mental burden you're relieved of when you see that the rest of the language is equally as simple and benefits from it.


As someone who reads a lot more Go than I write, Go is awful to read.

Reading Go means that I keep having to read idioms (as posted in the article) and parse them back into the actual intent, rather than just reading the intent directly. But I can't even trust that, since the nth rewrite of the idiom might have screwed it up subtly.

Reading Go means that I can't easily navigate to the definition of a function, because it implicitly merges folders into a single namespace, so I have to try them one by one.

Reading Go means that I usually don't have tooling that can jump to definition, because there are umpteen different dependency management tools, and Gopls only supports one of them.

Reading Go means that even when I have tooling available, and it feels like working today, jump-to-definition becomes impossible as soon as interfaces are involved, because structural typing makes it impossible to know what is an intentional implementation of that interface.


>Reading Go means that I can't easily navigate to the definition of a function, because it implicitly merges folders into a single namespace, so I have to try them one by one.

Not sure what you mean here

>Reading Go means that I usually don't have tooling that can jump to definition, because there are umpteen different dependency management tools, and Gopls only supports one of them.

There's 1 dependency management tool though. It's go modules. Unless you live 2 years in the past?

>Reading Go means that even when I have tooling available, and it feels like working today, jump-to-definition becomes impossible as soon as interfaces are involved, because structural typing makes it impossible to know what is an intentional implementation of that interface.

You just right click in VS Code and click "Go to implementations". Or if you use Goland it's right there on the gutter.

Reading comments like this really makes me wonder if anyone complaining about Go has actually used it.


> Not sure what you mean here

In Rust, if I see a call to `foo::bar::baz()`, I instantly know that `foo/bar.rs` or `foo/bar/mod.rs` (and only one will ever exist) contains either a `fn baz()` (in which case I'm done) or a `pub use spam::baz;` (in which case I can follow the same algorithm again).

In Go, if I see a call to `import ("foo/bar"); bar.baz()`, all I know is that ONE of the files in the folder `foo/bar` contains a function `baz`, with no direction on which it is.

> There's 1 dependency management tool though. It's go modules. Unless you live 2 years in the past?

As I said, 99% of my interaction with Go is reading the code that other people wrote. I don't make the decisions about which dependency management tools they use.

> You just right click in VS Code and click "Go to implementations". Or if you use Goland it's right there on the gutter.

I use Emacs, but VS Code still uses the same broken Gopls.

> Reading comments like this really makes me wonder if anyone complaining about Go has actually used it.

Can't really say I disagree, I guess.


> In Go, if I see a call to `import ("foo/bar"); bar.baz()`, all I know is that ONE of the files in the folder `foo/bar` contains a function `baz`, with no direction on which it is.

This is a code smell for poor file organization in your "foo/bar" module - in a well organized project, it should be obvious which file in a module contains a given function[1]. Go doesn't force file=module paradigm (preferring the folder to be the basis), however, it doesn't preclude having one file per module, if that's what you prefer. If you're reading someone else's poorly organized code, you can always use grep.

1. Say, you have an `animal` module, the first place you check for `NewCow()` is `animal/cow.go`


> Go doesn't force file=module paradigm (preferring the folder to be the basis),

Yes, this is what that complaint was about?

> however, it doesn't preclude having one file per module, if that's what you prefer.

> 1. Say, you have an `animal` module, the first place you check for `NewCow()` is `animal/cow.go`

This whole subthread was about reading other people's Go code. Of course your own code is going to look organized to yourself!

> If you're reading someone else's poorly organized code, you can always use grep.

Yeah, ripgrep tends to be what saves me in the end. Still annoying to have to break it out all the time.


> Yes, this is what that complaint was about?...This whole subthread was about reading other people's Go code.

So the complaint, restated is "Go doesn't prevent other people from writing bad code?" Am I getting you right? If so, well, I have nothing to say about that.

edit: I do actually have something to say. I just remembered having to work with an 82,000-line long Perl module file that would defeat any IDE. Fun times. No language can save you from poorly organized projects, whether the modules are file-based or folder-based.


I would say it's closer to "Go doesn't do a really simple thing that nudges people towards writing more readable code, while having basically no tradeoffs".

Considering the far more tedious tradeoffs that Go does force on users in the name of supposed readability (for example: the lack of generics), I'd consider that a pretty big failure.

I don't expect them to be perfect. I do expect them to try.


FYI: Go's lack of generics was not related to readability - the Go team didn't have a solution they liked, so instead of saddling the language with a half-assed solution forever, they waited for a more elegant solution. Also: the design of Go generics was recently approved (as in the past month).

It's no secret that Go is targeted at "programming at large". Go's design favors larger, fewer modules, how those modules are structured is left to teams/individuals. I may be lucky to work with a great team, but I always find the code where I expect to find it. When I'm starting from scratch, I utilize modules, <$VERB>er interfaces, structs and receiver functions: I cannot remember ever running into an ambiguous scenario where I'm unsure where a particular piece of code ought to go.


Like the parent, I read more Go than I write, and I have never seen a single-file-per-module (unless the entire project is one file). And sometimes it's obvious what file a function is from (like in your examples), but a lot of times it isn't. For example is `GiveFoodToCow` in food.go or cow.go? Maybe there are patterns that experienced gophers know, but that adds cognitive load. And would having a 1 file per module paradigm have made go any more complicated?


food.go and cow.go do not belong in the same (sub)module, IMO. That said, each team (and project) have unique sensibilities - consistency and familiarity help here. My gut feeling is that the "belongingness" of feeding a cow is closer to the cow than the food, unless your food.go is full of "GiveFoodToCow(), GiveFoodToChicken(),...GiveFoodToX()" which is gross. With that complexity, you're better of with a Feeder interface and implement Feed() in cow.go (and chicken.go, etc). If you cannot distill the logic to a single interface, you're probably better off with a receiver (or regular) function in cow.go, because having a list of GiveFoodToX() with different args in food.go is the worst possible design you could go with.

> And would having a 1 file per module paradigm have made go any more complicated?

I was being facetious. Under normal circumstances, no one should use one file per module in Go, but if Rubyists are feeling home-sick while writing Go, the option is available to them ;)


> In Rust, if I see a call to `foo::bar::baz()`, I instantly know that `foo/bar.rs` or `foo/bar/mod.rs`

In Rust, you would see `Bar::baz()` and have no clue where `Bar` is defined. The common style is `use something::somewhere::Bar`.


but if you see `Bar::baz` you can just go to the top of the file, and find the `use something::somewhere::Bar`, and then know what file `Bar` is defined in. In go, the import line only tells you what module/folder it is in, and you either need some tool or some intuition on how the module is organized into files to know what file to look for the definition in.


And then in Rust, that file ends up just saying `pub use ...`, and the real code is in a different module.


Let's go with "Go is easy if you use an appropriate IDE"

Emacs is great for many things, but Go was clearly designed with GUI IDE tooling in mind. So if you decide to forfeit that huge benefit, of course it'll be a sub-optimal experience.


> Go was clearly designed with GUI IDE tooling in mind.

Surprisingly not. One of Go's authors commented many times in the early days that Go shouldn't need an IDE, only a text editor. He even commented that syntax highlighting/coloring were unnecessary distractions.


He said:

there was talk early in the project about whether Go needed an IDE to succeed. No one on the team had the right skill set, though, so we did not try to create one. However, we did create core libraries for parsing and printing Go code, which soon enabled high-quality plugins for all manner of editor and IDE, and that was a serendipitous success.

Which I read as they considered IDEs important.


I'll accept that as a reasonable conclusion. Then he also said the following:

https://usesthis.com/interviews/rob.pike/

https://groups.google.com/g/golang-nuts/c/hJHCAaiL0so/m/kG3B...

(and some other stuff about syntax highlighting from around that time (?) I couldn't find.)

So maybe it was a bit of both.


To be fair, we then had to throw all of those "high-quality" plugins in the garbage where they belonged, and go with Microsoft's Language Server architecture instead to really have a usable Go IDE experience in something like Emacs or vim (GoLand from JetBrains is of course much better, and that might be using some of the built-in Go tools?).


> Go was clearly designed with GUI IDE tooling in mind.

I would like to see what happens if you say this to Rob "syntax highlighting is for children" Pike's face.


What makes Emacs not a GUI IDE?


I happily use golang in vim using gopls. I don't see an IDE being a requirement at all.


This is pretty much an IDE though (which is not a bad thing!)


Gopls works great with Emacs.


The first one is an issue, not just for reading but also for writing; you'll find slightly similar ways of doing the same thing, and Murphy's (Real) Law kicks in "If there are two or more ways to do something, and one of those ways can result in a catastrophe, then someone will do it."

The rest of the issues seem to be tooling issues, not an issue with Go itself. There are tools out there that do the right thing (personally I'm old school and use gotags, warts and all), so I'd suggest get better tools that work for you (or write one if you can't find one you like).


> Reading Go means that I keep having to read idioms (as posted in the article) and parse them back into the actual intent, rather than just reading the intent directly. But I can't even trust that, since the nth rewrite of the idiom might have screwed it up subtly.

This. Also, this idea that simple(r) = easy to read just doesn't hold. Is brainfuck easy to read? It's very simple. Even without being so extreme, we could simplify go replacing loops and structured ifs with "if-gotos". Or remove list enumeration as in range(my list) and only allow using integer indexing. Would that make go easier to read or reason about?


> Reading Go means that I keep having to read idioms (as posted in the article) and parse them back into the actual intent,

What? I never have to do this...

> I can't easily navigate to the definition of a function . . . I usually don't have tooling that can jump to definition

What? You command-click on it... ?

> jump-to-definition becomes impossible as soon as interfaces are involved, because structural typing makes it impossible to know what is an intentional implementation of that interface

What? You obviously can't jump-to-definition of an interface, but in what way is this a limitation?


I think c# is an unfair comparison in this instance.

You are right, c# does too much. But Go makes it far more work to do basic things - list comprehension for example (what is everyones obsession with for loops?). An opinated language with one way to do things sounds great, but not at the expense of basic niceities you get in other languages.


For what it's worth, I'll take for loops over list comprehensions any day of the week. I just find it a lot easier to read.


So you are claiming this

  arr2 := []int{}
  for i := range arr1 {
    if arr1[i] > 9 {
      arr2 = append(arr2, arr1[i] * 2)
    }
  }
Is more readable than these?

  var arr2 = arr1.Where(x => x > 9).Select(x => x*2);
  var arr2 = from x in arr1 where x > 9 select x*2;
  arr2 = [x * 2 for x in arr1 where x > 9]
  (setf arr2 (loop for x across arr1 when (> x 9) collect (* x 2)))
I honestly can't imagine by what measure any of the latter ones could be harder to understand the former. And as the complexity of the expression increases, I only see the advantage increasing typically (though often it pays to split it into multiple comprehensions, just like a loop that does too much).


Personally? Yes (except for the first example you gave, which I don’t classify as “list comprehension”).

It’s not inherent complexity though, just personal preference. I grew up writing for loops so they’re second nature to me and I grok them instantly in a single pass, whereas some list comprehensions require a re-read (particularly if they’re nested).

Different strokes, that’s all.


Fair enough!


Agreed! Check out pythons move with walrus operators inside list comprehensions - I think u can do that now - starts to be line noise


It's exactly this sort of comment that gives go lovers a bad name.


"A simple language is easier to read." "A simple language builds simple software".

It's become a cliche to bring up brainfuck here, but it really is a direct refutation of these ideas, being a maximally simple language that produces maximally complicated code.


Go _is_ easy to read. I can read almost any go codebase, even giant ones, and see what's going on pretty quickly. Same with C - the Linux kernel is surprisingly easy to understand once you know the basic data structures it uses. There is a lot of benefit in using simple/limited languages from a readability point of view.


Maybe for small pieces of code, but getting the overall higher level feature goals of code in go is hell (for me). The lack of function overloading to generalize intent for generic work is abysmal and outdated. It pains me to no end that their built in functions are overloaded, but I can't do it in my own go code.

Generally speaking if you're modestly familiar with a language it's rare that it's difficult to read small pieces of code. The hard part of programming on a team is writing code such that high level intent is quickly apparent with APIs that support that intent and make it easy to understand where edge-case handling is happening vs direct feature intent


Reading the kernel is relatively easy since some people worked hard on a structure, which is relatively clean.

There are many macro C code bases with tons of function pointer fun (for example when doing OOP) where you have a hard time to find things, till you spent considerable time learning the choices made in that code base.


Yeah true, macro heavy code can be really difficult, as you can make your own metalanguage with macros. The ruby codebase is a lot like that, but I still find it pretty easy to read. But I can imagine someone going crazy with macros could really make it hard, but that's not the norm in my experience.


The data structures in C require thorough documentation to have any chance to keep your foots. And keeping the preprocessor wizards in check. But I agree that Linux authors have done a consistently good job.

In fact, a kernel is even easier to read than other programs whose authors worked as hard, because having no external libraries at all also helps.


simple != easy.


Go is easy to read


> No hidden behaviours, no overly complex syntax, nothing like that.

Arrays vs. slices and standard functions dealing with slices are full of weird behaviour, unnecessarily complex syntax and obscure features. Like, why are there even arrays at all? Which slice functions return a copy and which ones don't? Why is append() so braindamaged to make a copy sometimes? Why do I need make() as often as I do when mostly the language knows all make() would do?

Go still has lots of footguns, even right at the trivial basics.


"Like, why are there even arrays at all?"

Because while Go is garbage collected, it also provides modest control over memory layout. Arrays are allocated chunks of contiguous memory for holding a fixed number of things. Slices are something that sit on top of that and let you not worry too much about that.

You almost never want arrays yourself, but they are a legitimate use case that the language has to provide because you can't create them within the language itself. But the right answer for most programmers is to ignore "arrays" in Go entirely.

"Why is append() so braindamaged to make a copy sometimes?"

(Tone: Straight, not snark.) If you understand what slices are and how they are related to arrays, it becomes clear that "append" would be braindamaged if it didn't sometimes make copies. Go is simple, sure, but it was never a goal of Go to try to make it so you don't have to understand how it works to use it properly.

It is a fair point that quite a few tutorials don't make it clear what that relationship is. Most of them try, but probably don't do a good enough job. I think more of them should show what a slice is on the inside [1]; it tends to make it clear in a way that a whole lot of prose can't. "Show my your code and I will remain confused; show me your data structures and I won't need to see your code; it'll be obvious."

[1]: https://golang.org/pkg/reflect/#SliceHeader


Of course Go has hidden behaviors and overly complex syntax, like any programming language before or after.

For example:

  xrefs := []*struct{field1 int}{}
  for i := range longArrayName {
    longArrayName[i].field = value
    xrefs = append(xrefs, &longArrayName[i])
  }
Perfectly fine code. Now after a simple refactor:

  xrefs := []*struct{field1 int}{}
  for _,x := range longArrayName {
    x.field = value
    xrefs = append(xrefs, &x)
  }
Much better looking! But also completely wrong now, unfortunately. `defer` is also likely to cause similar problems with refactoring, given its scope-breaking function border.

And Go also has several ways of doing most things. Sure, C# has more, but as long as there is more than one the problems are similar.

And I'm sure in time Go will develop more ways of doing things, because the advantage of having a clean way to put your intent into code usually trumps the disadvantage of having to learn another abstraction. This is still part of Go's philosophy, even though Go's designers have valued it slightly less than others. If it weren't, they wouldn't have added Channels to the language, as they are trivial to implement using mutexes, which are strictly more powerful.


I don't think this is a good example of proving the point that Go is difficult to read. Isn't it expected to internalize the semantics of basic language primitives when learning a new language, or does people just jump in guessing what different language constructs do? IMHO you learn this the first week when reading up on the language features.

range returns index and elements by value. The last example does what you asks of it, it's like complaining something is not returned by reference when it's not. Your mistake. Perhaps some linter could give warnings for it.


I think this is actually a very good example of the inverse relationship between logical complexity and language complexity.

A language that has implicit copy/move semantics is easier to write (since it's less constrained), and more difficult to read (since in order to understand the code, one needs to know the rules).

A language that has explicit copy/move semantics, is more difficult to write (since the rules will need to be adhered to), but easier to read (because the constraints are explicit).

Although I don't program in Golang, another example that pops into my mind is that slices may refer to old versions of an array. This makes working with arrays easier when writing (as in "typing"), but more difficult when reading (as in understanding/designing), because one needs to track an array references (slices). (correct me if I'm wrong on this).

In this perspective, I do think that a language that is simpler to write can be more difficult to read, and this is one (two) case where this principle applies.

(note that I don't imply with that one philosophy is inherently better than the other)

EDIT: added slices case.


I don't think you can truly internalize the varying semantics of which operations return lvalues and which return rvalues. At least, I haven't yet been able to in ~2 years of Go programming, and it's a constant source of bugs when it comes up.

The lack of any kind of syntactic difference between vastly different semantic operations is at the very least a major impediment to readability. After all, lvalues vs rvalues are one of the biggest complexities of C's semantics, and they have been transplanted as-is into Go.

As a much more minor gripe, I'd also argue that the choice of making the iteration variable a copy of the array/slice value instead of a reference to it is the least useful choice. I expect it has been done because of the choice to make map access be an rvalue unlike array access which is an lvalue, which in turn would have given different semantics to slice iteration vs map iteration. Why they chose to have different semantics for map access vs array access, but to have the same semantics for map iteration vs array iteration is also a question I have no answer to.


> and they have been transplanted as-is into Go

Hum... I don't think anything can return an lvalue in C.

Your first paragraph is not a huge concern when programming in C, declarations create lvalues, and that's it. I imagine you are thinking about C++, and yes, it's a constant source of problems there... So, Go made the concept much more complex than on the source.


I think Go has exactly C's semantics here. The following is valid syntax with the same semantics in both:

  array[i] = 9
  *pointer = 9
  array[i].fieldName = 9
  (*pointer).fieldName = 9
  structValue.fieldName = 9  
  pointer->fieldName = 9 // Go equivalent: pointer.fieldName = 9

  // you can also create a pointer to any of these lvalues
  // in either language with &
Go has some additional syntax in map[key], but that behaves more strangely (it's an lvalue in that you can assign to it - map[key] = value - but you can't create a pointer to it - &(map[key]) is invalid syntax).


> I don't think anything can return an lvalue in C.

Btw, I was curious to check , here is an example of a function returning an lvalue:

  struct test {
    int a;
  } global;

  struct test* foo() {
    return &global;
  }

  int main (int argc, char**argv) {
    printf("%d", global.a); //wil print 0

    foo()->a = 9;
    //or (*(foo())).a = 9;

    printf("%d", global.a); //will print 9
  }


What exactly is the difference here? Is the `x` variable being updated in the second sample so that the stored references all point to the same item?


Yes, that code is equivalent to

  xrefs := []*struct{field1 int}{}
  var x struct{field1 int}
  for i := range longArrayName {
    x = longArrayName[i] //overwrite x with the copy
    x.field = value //modify the copy in x
    xrefs = append(xrefs, &x) //&x has the same value regardless of i
  }
The desired refactoring would have been to this:

  xrefs := []*struct{field1 int}{}
  for i := range longArrayName {
    x := &longArrayName[i] //this also declares x to be a *struct{field1 int}
    x.field = value
    xrefs = append(xrefs, x)
  }


> Like we now have classes, structs and records.

IMO records are a great addition, especially when it comes to point-in-time/immutable data. They also provide an improvement to developer efficiency, as basic data classes can be represented with a single line record definition.

> We now have async/await but we also have channels. Some people are going to write code using channels, me others will use async.

I'm not really sure what you're talking about here. C# has no concept of a channel. If you referring to the "System.Threading.Channels" package, it exists for those in the community that would benefit from it, and still uses familiar async/await syntax. It's also a very niche package that is unlikely to have significant adoption, so there's no real concern of the pattern "segmenting" the community.


> It's just simple and easy to reason about.

These aren't the same thing. Brainfuck is "simple" in the same sense - no hidden behaviors or complex syntax - but it's virtually impossible to tell at a glance what a Brainfuck program does, or say with certainty how it will react to different inputs. Complexity has to live somewhere, and the more restrictive the language, the more complexity is moved to the program structure. Conversely, every special-purpose construct in a language is a place where you don't have to rely on reasoning about an unboundedly complex Turing-complete system, and can instead go and check the documentation for what it does.


I don't entirely disagree but I can't help noticing, you mentioned two ways to do concurrency in C# and two ways to do it in Go.


Actually Rob Pike said that Go was supposed to be "easy to understand and adopt": https://www.youtube.com/watch?v=uwajp0g-bY4


> But otherwise I am super happy with writing Go code. All my pet peeves of something like modern Java or C# are gone.

I'm always confused by this. Why do people consistency cherry pick specific languages to compare then when those languages offer fundamentally different paradigms? I understand comparing C/C++ vs Go, but isn't obvious that C# is going to be fundamentally different than Go?


C# and Java are much closer to Go than C++. C is also closer from another perspective (simplistic syntax).

C++ is about as far from Go as you can imagine a language. Maybe only Prolog would be a worse comparison point.

C++ is designed with one goal in mind: no special compiler built-ins. If it is possible to do it at all, it can be done in a 3rd party library. C++'s design philosophy is Library first. Bjarne Stroustroup explicitly advocates this: don't write special case code. Write a library that solves the problem, then use that library to achieve your special case.

Go doesn't think writing libraries is a useful endeavor for most programmers: they should be writing application code, and let their betters design the tools they need, build them into the compiler, and stop wasting time designing your own abstractions.


> C++ is about as far from Go as you can imagine a language. Maybe only Prolog would be a worse comparison point.

https://commandcenter.blogspot.com/2012/06/less-is-exponenti...

/headscratch

EDIT: I think you're misinterpreting syntax of the language with use case. e.g. low level vs high level programming language


I'm not talking about either syntax nor use case, but about language philosophy, the approach to problem solving embodied in the language - the "paradigm" as you called it. As that post points out, even inside Google, the C++ committee members had a vastly different vision than Rob Pike on what makes a language good. C++11 is essentially universally loved in the C++ community as a giant step forward, as are most subsequent revisions. Rob apparently hates them.

Even the article shows that Go and C++ are fundamentally at odds philosophically, in terms of their paradigm:

> Jokes aside, I think it's because Go and C++ are profoundly different philosophically.

> C++ is about having it all there at your fingertips. I found this quote on a C++11 FAQ:

> The range of abstractions that C++ can express elegantly, flexibly, and at zero costs compared to hand-crafted specialized code has greatly increased.

> That way of thinking just isn't the way Go operates. Zero cost isn't a goal, at least not zero CPU cost. Go's claim is that minimizing programmer effort is a more important consideration.

If you want to compare them on use cases, Go is far too slow and memory hungry to be used in most domains where C++ is actually necessary. That is, for any C++ program that could be re-written in Go, you could also re-write it in Java or C# or Haskell, bar a few problems around startup times and binary sizes.

And that is what has been seen, again as the post points out: C++ programmers are not switching to Go. Ruby and Python and (Java and C#) programmers are.

Also, the designers of C++ have never made any effort to think about how easy the language is to learn, unlike the designers of Go, Java and C#. In fact, all of these languages share a common heritage: they are all designed as answers to C++, in slightly different ways.


> That is, for any C++ program that could be re-written in Go, you could also re-write it in Java or C# or Haskell, bar a few problems around startup times and binary sizes.

Nearly any program can be written any language. I can write in functional programming using Ruby, but why on earth would I do that when Haskell is "out of the box" useful for an application where functional programming makes the most sense.

> Ruby and Python and (Java and C#) programmers are.

Dynamic, object orientated based programmers are switching to Go when the use case suits. My understanding is that the syntax and philosophies are much more aligned to people of those backgrounds (Erlang -> Elixir is a great example of this) and gives them access to a lower-level language without having to learn many of the common more "computer sciency" things that are involved with those languages (C++/C).


> Nearly any program can be written any language. I can write in functional programming using Ruby, but why on earth would I do that when Haskell is "out of the box" useful for an application where functional programming makes the most sense.

That's only true in theory. In practice, you can't write a program that has soft realtime constraints for example (e.g. a game engine) in Go or Java, you have to write it in C++ or C or maybe Rust. This is true in general when you need utmost performance: while in general it's easier to write the same functionality in Go or Haskell or Java than in C++, the opposite is true when you need to extract as much performance as possible: it becomes easier to work in C or C++ or Rust than in Go or Haskell or Java (of course, this is a generalization; there are exceptions, though the rule holds pretty well overall).

> My understanding is that the syntax and philosophies are much more aligned to people of those backgrounds (Erlang -> Elixir is a great example of this) and gives them access to a lower-level language without having to learn many of the common more "computer sciency" things that are involved with those languages (C++/C).

Yes, because Go is is much closer to this paradigm than to the the paradigm of (Modern) C++. The only programming languages whose vision is similar to C++'s in my opinion are Rust and Common Lisp (even though of course Common Lisp is applicable to different hardware constraints, it being garbage collected and dynamic, it still has a very similar philosophy at its core).


Yes it's obvious, but it doesn't prevent OP from liking go more than c#/java.

Furthermore, its very likely that the languages people 'cherry pick' are actually languages they use everyday. So they just compare 'new everyday language' to 'previous everyday languages', and just tell you whether they're happier or not when they write code. The point is not really about the language theoretical properties and merits, but how people experience the language in real life.


>This is what having a huge toolbox causes. Some people are going to write code using channels, me others will use async.

I don't know if this is a problem. Redundancy is good. If simple languages ruled, we would all be speaking Esperanto.

>Some people will use features that others don't care about. I end up having this argument with product managers who insist that because most people only use 60% of the functionality of a product, we don't need to implement the remaining 40%. You need more than one way of doing the same thing.


On the contrary, every instance where there is more than one way to do something is a failure of the language. It's OK, all languages have failures, but the ideal is pretty clearly a set of precisely orthogonal features, where there is neither repetition nor exception.


I am pretty sure I have read something similar to "Go is easy" a multitude of times.


We solve that with communication to use a to the team well known subset of the language and internal trainings if we see potential helpful features.


But for most programmers, easy outweighs simple in the hierarchy of values.


Which is a problem. (Though, i will say i think go is neither easy nor simple, but i believe that is an unpopular opinion here at HN)


Simple > easy should be common sense for anyone who ever worked in a team. Code is written once and read many times (and usually not by the author).


As someone without a ton of experience with Go, a good amount the Go code I have encountered "in the wild", has actually been more difficult to read and understand than code in more complicated languages because I have to read through and understand all of these patterns. Hopefully the addition of generics will help with that. But IMO the simplicity of go actually hinders readability.


My personal experience is very different. Of course I have seen bad Go code in the ~5 years I do Go development professionally.

But when compared to previous monstrosities in C++ or Java with exceptions cluttered everywhere, deep inheritance trees that are absolutely useless..

then Go code is an absolute breeze to read and work with. The one thing I see frequently and dislike a lot in Go code bases is the frequent usage of interfaces for single structs just for the sake of mocking for unit tests.

Often I see cases where you could just strip the whole layer of crap and unit test the real functions themselves. But nobody seems to think about that. It seems that this "write interfaces for everything and then mock/test this" pattern is dominating currently.


> But when compared to previous monstrosities in C++ or Java

Perhaps part of it is the languages we are comparing to? I'm comparing to languages such as rust and scala, and to some extent python and ruby.

> with exceptions cluttered everywhere

I prefer errors to be part of the return value to exceptions, but I also find repeating

    if err != nil {
        return nil, err
    }
for almost every line of significant code in go functions pretty distracting. I much prefer rust's `?` operator. Or haskell's do notation (or scala's roughly equivalent for/yield).

> deep inheritance trees that are absolutely useless.

uh, you can have deep inheritance trees in Go, and not have them in Java or C++, I'm not sure what your point is.


> Perhaps part of it is the languages we are comparing to? I'm comparing to languages such as rust and scala, and to some extent python and ruby.

I think you should compare to languages that have a similar purpose / area of usage. In my experience that is C++ and mostly Java. I wouldn't dare compare dynamically typed and interpreted languages with Go.. what's the point? I don't have much experience with Rust so I cannot compare it and additionally it's rarely used in companies. Scala I just don't like personally. For my taste it just "is too much of everything".

> ... err != nil ...

In the beginning I was thinking the same. Over time I got used to it. When I write it I use a snippet. And clearly reading the flow of the error has been beneficial to me a lot of times. Yes it is verbose.

> uh, you can have deep inheritance trees in Go, and not have them in Java or C++, I'm not sure what your point is.

I am sure you know that there is no inheritance in Go so I am not totally sure what you are getting at. My point is that I think OOP by composition is a lot clearer than by inheritance. Also composition is not overused in Go as say inheritance is overused in Java.


> I think you should compare to languages that have a similar purpose / area of usage. In my experience that is C++ and mostly Java.

The languages I listed first were rust and scala, which serve very similar purposes to c++ and java. In fact, rust is closer to c++ (no GC, more direct memory control) and scala is closer to java (runs on JVM) than go is to either.

> Over time I got used to it. When I write it I use a snippet.

Which is my point. You have to get used to things like this, which probably adds about as much cognitive load as having something in the language to reduce this noise (although I think this is an known problem in go and may be improved in a future version).

> I am sure you know that there is no inheritance in Go

Fine. Replace "inheritance" with struct embedding and/or interface hierarchies, and you can get a similar effect. My point is you can have overly abstracted designs in either language.


> The languages I listed first were rust and scala, which serve very similar purposes to c++ and java. In fact, rust is closer to c++ (no GC, more direct memory control) and scala is closer to java (runs on JVM) than go is to either.

The overlap in purpose and usage for Go and Java is gigantic. Also I don't understand why it matters whether Scala is closer to Java or whether Rust is closer to C++. We were comparing Go and X right?

This whole argument tree is a bit nonsensical.. I compared Go projects to Java and C++ projects which I have worked on. All of the mentioned languages are very common in companies these days and are used for similar topics. Why bring other languages in to this?

> Which is my point. You have to get used to things like this, which probably adds about as much cognitive load as having something in the language to reduce this noise (although I think this is an known problem in go and may be improved in a future version).

You always have to get used to some quirks in any language out there. It adds a few lines writing the code but reading it is way easier IMHO and not cognitive load. Opinions may of course vary on this.

> Fine. Replace "inheritance" with struct embedding and/or interface hierarchies, and you can get a similar effect. My point is you can have overly abstracted designs in either language.

As I already wrote, embedding is rarely used and was not a problem ever in my experience and yes interface spamming is a problem.

However Java e.g. has these abstraction complexities already baked into the standard library and encourages the overuse of abstractions IMO.


> I am sure you know that there is no inheritance in Go

https://golangdocs.com/inheritance-in-golang


This tries really hard to look like an official resource, but it seems to be run by some random IT consultancy.

What they call inheritance is a narrow syntactic sugar intended to forward method calls to a member without having to write something like

  func (x Outer) Frobnicate(val int) {
    x.inner.Frobnicate(val)
  }
by hand. It's arguably not inheritance because Liskov substitution is not permitted:

  type Outer struct {
    Inner
  }

  func process(i Inner) { ... }

  var x Outer
  process(x)       // ERROR: type mismatch
  process(x.Inner) // OK


This is a bad article; struct embedding is not inheritance, and mistaking it for such is a classic case of "Java/Python programmer tries to use Go and shoehorns concepts from those languages in Go" that the post mentions.

This entire website seems pretty ... meh. It's like the W3Schools of Go.


Could you share an example?


Not the poster you're responding to, but practically every time someone wants to write code that mimics `map` or `filter` and gang is a 5-7 line function (at least). Something that would have been a 1 liner in languages like Java or C#. It gets tiring and distracting very quickly to jump around verbose implementations which amount to nothing more than standard streaming functions.

Another example is the lack of annotations for validations. In Java or C#, you'd annotate the function argument with something like `@Validated`, and it's taken care of. In golang, calling the validation code would have to be done manually each time that's another 3-4 lines (including error handling).

Yet another example is that golang lacks a construct similar to C#'s `Task`. You can't execute a function that returns a value asynchronously (if you care about that value) without creating a channel and passing it.

golang also lacks pattern matching and discriminated unions (aka algebraic data types). Java and C# are getting both (both have pattern matching, and ADTs are in the works as far as I'm aware).


Have to agree with you. I wished for sum types & pattern matching more than for generics.. but who knows what the future brings.


this is why I have moved to Rust. I worked in Go a number of years ago, but after becoming proficient in Scala, I've decided sum types and pattern matching are where the sweet spot is. The JVM has its own issues though and I'd like to have a performance-conscious language in my toolbelt that doesn't sacrifice expressivity. Hence: Rust.


RemindMe! 10 Years "Are we there yet?"


You got me excited about Java having pattern matching, but all I could find was a proposal.



Thats just for instanceof they're working on growing the capabilities in another JEP. Switch expressions are EXTREMELY powerful though.


I think anyone can agree with the author that Go definitely lacks conveniences you're used to in other modern languages.

I just think we also tend to dramatize how much that matters.

I also think Go's benefit really is that it's simple and that you have very few tools that let you do anything other than focusing on solving your problem, and like the author I do think goroutines are the exception to that.

Where I work, we don't even use them. We use plain ol' mutexes instead.

When coming from higher level languages, Go does feel frustrating. In Javascript, you're writing `const [a, b, c] = await Promise.all([x(), y(), z()])`. In Go, you're writing 40 lines of WaitGroup code. It's easy to go ughhh.

But I think a nice way to appreciate Go's conservative middleground is to go back to writing some C/C++ code for a while, like some Arduino projects. Coming from that direction, Go feels like a nice incremental improvement that doesn't try to do too much (with perhaps the exception of goroutines).

Go's performance is also particularly stand-out which makes up for many of its convenience shortcomings. It's fast enough that I've written some code in Go where I would have written C not long ago. And writing C involves quite a bit more concessions than what Go gives you, so in comparison, Go kinda spoils you.

Go has plenty of annoyances too though. Not having any dev vs prod build distinction is annoying. Giving maps a runtime penalty of random iteration in order just to punish devs who don't read docs is annoying. It's annoying to have crappy implementations of things like html templating in the stdlib which steal thunder from better 3rd party libs. Not being able to shadow vars is annoying. `val, err :=` vs `val, err =` is annoying when it swivels on whether `err` has been assigned yet or not, a footgun related to the inability to shadow vars. etc. etc.

But it's too easy to overdramatize things.


> I also think Go's benefit really is that it's simple and that you have very few tools that let you do anything other than focusing on solving your problem

I feel just the opposite: Go has very few tools, which forces you to often have to solve language games instead of focusing on your problem.

You want to remove an element from a list? Instead of writing that, you need to iterate through the list and check if the element at position i has the properties you want, and if it does, copy the list from i+1 (if it wasn't the last!) over the original list. This is NOT the problem you were trying to solve.

You want to store a map from some struct to another? Instead of writing that, you have to create a key struct that is comparable for equality from your original struct, which probably involves string concatenation and great care to avoid accidentally making unequal structs have equal keys; and then two maps, one from keys to the key struct and another from keys to the value struct, and of course client code also has to know about this explicitly.

You want to pass around some large structs, or iterate over slices of them? Well, you'd better start using pointers, and start being careful about what is a copy and what isn't, otherwise you'll pay a steep performance price, without any syntactic indication.

You want to work with sets of values? You'll have to store them as map keys, perhaps doing all of the black voodoo described above. And of course, you can't do a reunion of two sets, you have to iterate through the keys of the first map and the second map and add them to a third map.

All of these things are annoying when you are writing them, and even more of a problem when you are reading the code, as you have to understand the intent from the messy internals that Go forces you to expose.


I frequently describe Go as "C, with memory safety and GC". I don't run into many that disagree.

Go is basically how Java was pre-generics. People forget that early Java was much like Go in some respects. Everything casted to object. Not really sure what the object is unless you wrote the code. Lack of common patterns and a rich set of libraries (Guava, Apache Commons).

Early Java was supposed to be safe C++. Converting C++ to Java is still trivial unless there's pointer craziness. Generally copy paste the C++ and change naming conventions.

I hate working on unfamiliar Go projects. Copy pasted code everywhere. And everyone builds their own abstractions because there aren't enough.

I don't understand Go's appeal. To me it feels clunky and stuck in the 90's. Designed by C programmers for C programmers.

Like C it has no generics, bad packaging, and is a pain in the ass to cross compile. Its only killer feature, IMO, is it's fiber threading model and that's quickly being copied by other languages, even ancient Java.


> Converting C++ to Java is still trivial unless there's pointer craziness. Generally copy paste the C++ and change naming conventions.

What? You've obviously never seen actual C++ code to make such a ridiculous statement.


1996 C++ (when Java appeared) being used in MFC, OWL, Powerplant and CSet++, alongside Smalltalk in the gang of four book, definitely.


Google C++ looks a lot like Java, maybe that's where they are coming from. But it's still quite a stretch!


As they say, "you can write FORTRAN in any language".


In fact, Go is not even memory safe despite the GC. It has data races, which are exploitable.

  - https://blog.stalkr.net/2015/04/golang-data-races-to-break-memory-safety.html
  - https://golang.org/doc/articles/race_detector#Introduction


> I hate working on unfamiliar Go projects. Copy pasted code everywhere. And everyone builds their own abstractions because there aren't enough.

Arguably, if the project requires lots of abstractions and copy-pasted code, then the project shouldn't be written in Go. Just because you can, technically speaking, write object-oriented code in Go does not mean that it is ergonomic to do so or a recommended use-case for the language. Projects that benefit from object-oriented design should stick to languages with first-class support for it.


I don't disagree with your conclusion but I don't see it following from the quoted premise. What was your thought chain to get from the quoted text to OO stuff?


There's a difference between using a struct to represent concepts like program configuration or grouped function parameters, and shoehorning OO-native design concepts like the decorator pattern that need inheritance into a language that doesn't support dynamic dispatch. Much of the bad Go code I see comes, yes, from overusing abstractions and lots of copy-pasting, but most of that comes from trying to fit square pegs like OO design into Go's round hole.


Still don't get the connection in the way you do. Maybe I'm so used to OO stuff that I just gloss over it.

> a language that doesn't support dynamic dispatch.

Go's interfaces do support this enough that I consider it worth at least mentioning.

https://en.wikipedia.org/wiki/Dynamic_dispatch#Go_and_Rust_i...


> Like C it has no generics, bad packaging, and is a pain in the ass to cross compile.

Go modules are pretty good packaging IMHO. What don't you like about them?

As for cross-compiling, are you joking? It's very easy and i literally cannot imagine it being easier. What's painful about it?


I would say Go has very poor packaging, but this is when comparing it with Java (maven), Rust etc. It's definitely much better than C.

The biggest reason why Go modules are a very poor packaging solution is the horrendous stupidity of tying them to source control. This makes it difficult to develop multiple modules in the same repo, it requires your source control system to know Go, it makes internal refactors a problem for all consumers of your code (e.g. if you are moving from github to gitlab, all code consuming your package will have to change their imports).

The versioning ideas of their module system, particularly around the v2+, have made it such a pain that there still isn't a single popular package that uses the proposed solution so far, not even Google ones like the Go protobuf bindings.


> The biggest reason why Go modules are a very poor packaging solution is the horrendous stupidity of tying them to source control

I think it's actually a great idea. It uses native things you use anyway, avoids an unnecessary third party ( a package repository like pypi), and adds in extra security ( remember those hijacked pypi credentials and pypi-only malicious packages that weren't in source control, so hard to verify?).

> This makes it difficult to develop multiple modules in the same repo

How so? You can justs use different folders and packages.

> requires your source control system to know Go

Not really.

> it makes internal refactors a problem for all consumers of your code

That's always the case - regardless of your package repository, if you change it, you have to update the new references everywhere. You can use go.mod to replace a reference ( e.g. use gitlab.com/me/library instead of github.com/legacy/library) without updating all the code. And then there's the goproxy protocol, which makes all of this optional.

I agree on v2 modules, that is very weird.


> I think it's actually a great idea. It uses native things you use anyway, avoids an unnecessary third party ( a package repository like pypi), and adds in extra security ( remember those hijacked pypi credentials and pypi-only malicious packages that weren't in source control, so hard to verify?).

Not really. You may be using Git internally, but if you want to consume a module that is developed in Perforce or Subversion, you must now also install P4 and/or Subversion and configure them so that they have access to the remote repo.

It doesn't avoid a third party, it actually makes you reliant on numerous third parties, the source hosting sites for every module you use (Github is a 3rd party between me and the developers of the Go language bindings for protobuf, for example).

Also, that is false extra security, as nothing prevents the code itself from being malicious or embedding malicious files, and this can be arranged to be served only when request through Go mod (based on headers, on the HTTPS requests that only Go mod makes etc.). Especially given that go supports shell execution in source files (//go:generate rm -rf ~/).

> How so? You can justs use different folders and packages.

If you have multiple modules in the same repo, you have to tag every "release" with multiple tags, one for each module (e.g. mod1/v1.0.123, mod2/v1.0.131) and you'll quickly run into problems (if you don't sync releases, you'll have absurd combinations of module versions whenever you sync your repo, so you won't be able to rely on `replace` clauses for example).

> > requires your source control system to know Go

> Not really.

It does, if you have a dependency like "github.com/user/proj", go mod will first make an HTTPS request to this URL and expect some specific responses telling it how to get the code.

> That's always the case - regardless of your package repository, if you change it, you have to update the new references everywhere.

If I publish my modules to Maven, I can switch from Perforce to Git without any change whatsoever for anyone consuming the Maven package. I can split or merge my repos internally in any way, but as long as I produce the same Maven package, my consumers don't need to care.

Replace directives and goproxy are bandaids for a silly problem to have.


> It's very easy and i literally cannot imagine it being easier.

`go build -target x.y` could be considered fractionally easier than `GOOS=x GOARCH=y go build`? But yeah, this is a nonsense claim by GP.


I don't think this article really overdramatizes things; if I had written "zomg Go is terrible look what I need to write to delete an item from a list!" – a sentiment I've seen expressed a few times over the years – then, yeah, sure.

But that's not really what the article says. As it mentions, I don't think these are insurmountable issues, and I still like and use Go: it's pretty much my go-to language these days. Overall, I think it did a lot of things right. But that doesn't mean it's perfect or can't be improved.


The curse of having landed on the HN front page. You write a little blog post with some random thoughts, somebody found it insightful and posts it to HN and if more people like it, it ends up on the front page. That exposure tends to attach a wide-ranging seriousness to this kind of blog post which may dramatically overinflate the point that it originally intended to make.


I used "dramatize" to describe the blog post (in such that the blog post was written) and then "overdramatize" at the end in reference to my own infinite list of annoyances with Go and the normal course of discussion on HN about proglangs.

The blog post does admit "Are these insurmountable problems? No." Learning it in 5 minutes certainly is a stretch as you point out, but I'd still say you can learn it in a weekend to a greater degree than any other language I use.

Though you'll notice my comment after the first sentences is a way to share my own thoughts on Go, an opportunity I'm never going to neglect.


I agree with that. Go can be frustrating if you expect the level of expressiveness of Python, Java, or Rust. But if you regard it as a better C, you can appreciate its ergonomic upgrades compared to C. What is unfortunate is that Go could be much more than just a better C, considering Google's potential and the decades of programming language research that were present at the inception of Go. By trying to simplify things too hard, and ignoring precedents, Go missed a huge opportunity cost. And it even failed to fulfill its original intent: it cannot be used in place of C, unlike Rust.


Agreed. I would love for Go to have stolen more pages from Rust's crusade for zero-cost abstractions and high level conveniences. I can appreciate ruthless conservatism in some places, but Go isn't actually ruthlessly conservative. It has quite a lot of weird things in its stdlib.


Could you please expand on why is Go not a replacement for Rust? Is that because of the lack of manual memory management?


What I wanted to say was that Go was not a replacement for C, compared to Rust which could have similar performance characteristics as C or C++. The lack of manual memory management is one thing, but there are cultural reasons too. For example, in Go it is very idiomatic to return an error value as a string, which cannot be even imagined of in the C, C++, or Rust ecosystem. That's because as Go is targetted toward backend-level programming nowadays (which was different at the beginning of Go, when Go was marketted as a systems language), some minor overhead is acceptable if the convenience it brings is worth it. Go is full of such compromises, which isn't exactly a bad thing, but unsuitable to the lowest level programming which begs for an extremist position regarding performance issues. The mentality of Go is good enough for conventional programming, but bad for systems programming. And I think such cultural differences stem from Go's simplicity first mindset.


Yes and more:

1. Lack of "manual memory management" can be taken as GC (though Rust is neither GC'd nor manual memory managed, it's declaratively memory managed) and a GC is unacceptably expensive for systems programming. Furthermore, until recently GC was also unacceptably bad for FFI. However stable Rust and Haskell with experimental features can nowadays GC across FFI without issue, but I'm still unaware of any other language that can.

2. Lack of "manual memory management" also means lacking direct memory access, etc. This is a problem in systems programming, though you do not need it for all systems programming.

3. Then there's green threads. Early on Rust had them, but they were removed when it was discovered it's theoretically impossible to implement them with acceptably low overhead for a C or even a serious C++ competitor. What's worse, you have to pay the overhead even if you don't use the feature. Nowadays, you can opt in by importing an ecosystem library that provides green threads but mostly people just use futures rather than green threads.

4. Interfaces: Even tinygo's documentation advises avoiding interfaces and tinygo isn't even a C/C++/Rust competitor, it's a micropython competitor.

5. ...


The obvious case is embedded programming. You can run Go via a custom compiler for microcontrollers: https://tinygo.org/.

But to their point: why incur that sort of overhead for a language that doesn't necessarily give you enough conveniences to be worth it? Or rather, Rust gives you more conveniences with very little runtime overhead.

It's not exactly damning to be unable to compete with C on a microcontroller with 2K RAM, they were just pointing out the end of my comparison with C/C++.


Go's lack of generics and metaprogramming can be extremely frustrating when you're working with services that copy data around from one struct to another, which ends up being a lot of them. You're left with reflection, code generation, or doing it field by field.

It'd be nice to have syntactic sugar for splatting structs together, or a library that let you do it in a typesafe and efficient way.

EDIT: I will defend the random map order, though. Golang maps were only predictable up until a set number of items, which caused hard-to-detect bugs where your small test case would be in order but your large real-life data would be out of order.


I certainly agree. I would love for Go to get to a place where I never have to copy and paste code again. Generics will get us part way there.

The next item on my wishlist are algebraic data types. In complex projects I've opted for Rust over Go just because of the abstraction power of being able to represent things like:

    Dest = Register (A | B | C)
    Src = Register (A | B | C) | Immediate value
    OpCode = Nop | Add(Dest, Src) | ...
In Go, I'm so tired of `interface{}`.

Edit: To respond to your edit about map iter order, that's related to my complaint of Go's lack of dev vs release build. Randomizing map iter order in dev builds is acceptable to me. Go makes you pay for that in prod.


I don't understand the jump from the lack of algebraic types, to using interface{}.

You can spell out all your types into explicit structs (where you can choose between a simple implementation with a little worse performance or to make the implementation a little more complex) and pass them around.

There are many complex code bases (kernels & databases come to mind) which use C (not C++), and they don't resort to passing void* around the "business logic" parts.

The idea that for a complex project you would choose a different language with so many different characteristics due to a minor detail about the type system (this is not exactly Haskell vs JS...). This kind of decision making would not pass in any reasonable organization...


> (this is not exactly Haskell vs JS...

No, it's more like Haskell vs C, considering the expressiveness of Rust’s vs Go’s type system.


I don't know Go. How can you emulate sum types on it? And how do you pattern match them?


It depends on what you mean exactly by "emulating sum types" and "pattern matching them".

An example is rolling your own discriminated union:

  type Avariant struct {
    a int
  }
  type Bvariant struct {
    b string
  }
  type SumChoice int
  const (
    SumA SumChoice = 0
    SumB SumChoice = 1
  )
  type Sum struct {
    type SumChoice
    A *Avariant
    B *Bvariant
  }

  sumA := Sum{choice: SumA, A: &{a: 7} }
  sumB := Sum{choice: SumB, B: &{b: "abc"} }

  func foo (x Sum) {
    switch(x.choice) {
      case SumA: {
        fmt.Printf("Value is %d", x.A.a)
      }
      case SumB: {        
        fmt.Printf("Value is \"%s\"", x.B.b)
      }
    }
  }
As usual with Go, it's ugly and verbose and error prone, but it just about works.

The previous poster was probably thinking of something similar but using interface{} (equivalent to Object or void*) instead of Sum, and then pattern matching using an explicit type switch:

  func foo (x interface{}) {
    switch actual := x.(type) {
      case AVariant: {
        fmt.Printf("Value is %d", actual.a)
      }
      case BVariant: {        
        fmt.Printf("Value is \"%s\"", actual.b)
      }
    }
  }
This is slightly less risky and has less ceremony, but it's also less clear which types foo() supports based on its signature. Since Go has only structural subtyping [0] for interfaces, you would have to add more boilerplate to use a marker interface instead.

[0] In Go, a type implements an interface if it has functions that match (name and signature) the interface functions. The name of the interface is actually irrelevant: any two interfaces with the same functions are equal. So if you declare an interface MyInterface with no methods, it is perfectly equivalent to the built-in `interface{}`: any type implements this interface, and any function which takes a MyInterface value can be called with any type in the program.


Thanks for the write up effort.

On the first approach, with "error prone" you mean the tag could be incorrect, right? (or even have an impossible variant with 0 or multiple variants set).


Yup. Also, there is no exhaustiveness checking (the constants we chose to define for SumChoice(0) and SumChoice(1) mean nothing to the compiler anyway - exhaustiveness checking would have you test any possible int, since SumChoice is simply an alias for int).


But are those two variants held in memory inside Sum or are they heap-allocated sitting out on some other cache line? Can one write a performant Sum type?


With pointers, they are in the heap, but at least there is no overhead. You could also declare them embedded (without the *s), but then the Sum struct would have a size that is a sum of all the variants' sizes, instead of a proper union which should have the max size (more or less).

  type Sum struct {
    type SumChoice
    A Avariant
    B Bvariant
  }
This is what I meant by saying that it depends on exactly what you mean by "sum types".


Got it. Although I’m not sure what “no overhead” means if the instances have to live way far away on the heap. That means you’ve got an alloc (including a mutex lock), then the value lives far away, then the garbage-collection overhead then the delete. When I think sum-type I think a flag plus storage for the maximum size and alignment type, and all of the above bookkeeping and cache misses go away.


Yes, you're right - I was thinking of "no space overhead", and even that is not entirely correct (you pay an extra pointer per variant size, which in this case would be some overhead over the embedded version).

Still, I think most people don't worry so much about efficient packing of sum types, and instead the safety and convenience factors are more important. Of course, YMMV based on exact usage.

I'm not in anyway claiming that Go supports sum types. Someone just asked how they may be emulated, and I don't think it should be surprising that emulation has an overhead.


> But I think a nice way to appreciate Go's conservative middleground is to go back to writing some C/C++ code for a while, like some Arduino projects. Coming from that direction, Go feels like a nice incremental improvement that doesn't try to do too much (with perhaps the exception of goroutines).

As someone that was writing Arduino like code in C++ in 1993 (MS-DOS), I fail to see that.


If in 2021 you think of “C/C++” as a single language, you are missing out on the last decade of C++.


C/C++ isn't a programming language, rather the abreviation for C and C++.

I am fully aware of the last decade of C++, including papers written by Bjarne Stroustrup and other C++ key figures, where they uses the hated C/C++ expression, that so many happen to have issues with.

Which I can gladly point you to.

Given that security is one of my areas, I always keep up to date with C and C++ standards, even if I don't use them every day.


I know the history, but I still think your comment was lumping them together unfairly in this context. With few exceptions, if you are writing `new` or `delete` you are doing C++ wrong in 2021 and that I would call C/C++ :-)


It wasn´t my comment, rather OP's quote.

As for not using new and delete, I kind of agree, then again the only C++ GUI worth using has it over the place.

I rather see all those school kids learning Arduino to use new and delete than malloc() error prone size calculations, or dive into template errors due to the misuse of smart pointers.


Hopefully concepts will save us from cryptic error messages.


> C/C++ isn't a programming language, rather the abreviation for C and C++.

Maybe the abbreviation should be `C & C++` then :-). But I'm not sure if infix precedences allow it to work.


> Giving maps a runtime penalty of random iteration in order just to punish devs who don't read docs is annoying

Can you explain what you mean by "runtime penalty?" Last I checked, the iteration is only random wrt its starting offset; after that, it proceeds linearly through the buckets. So you only generate one random integer per `range`, which seems acceptably cheap.


It still seems to be working as you described.

https://github.com/golang/go/blob/db8142fb8631df3ee56983cbc1...


But why would you be comparing to C? I've yet to find any scenario where Go is an option and e.g. OCaml isn't (unless the scenario is "I want to use a language whose creator called it a "systems language"").


Great summary of areas where Go could be improved for real world use. I’d want any of these improvements over delete at index in a slice (which I’ve never wanted).


Agree, agree, agree. I've bounced between Python, C++, and Rails in my career, and lately have been ramping up at a Go shop. Go is like C++ in the sense I can tell that eventually I'll get better at eyeballing a file, sensing the shape of the code, and zeroing in on the important bits.

Just like learning to read large C++ codebases, there's clearly a learning curve because a simple idea that would be a one-liner in a high level language is going to be 5-15 lines.

Go also makes the "learn to read" learning curve even steeper when folks pick up and propagate the core team's propensity for single letter variables. I feel like one of the understated benefits of ruby/python/similar languages being relatively readable is that folks are inclined to keep them readable. Whereas when you feel like you're one step away from specifying the register and how hot the welding torch should be to toggle the bits, you're somehow more likely to write `c.T.Client.Send(v)`.

IMO if we accept that most of our work is maintenance & modification of existing code, and most of the work in those tasks is understanding what's already been written, it follows immediately that code that's easy to read has high business value. I know it's too soon in my learning curve, but I'm really hoping I find the patterns that make Go easy to read soon. (and a little tangentially but the descriptive power of a variable name should be proportional to its scope - so imo anything exported or that I might see across multiple files is an awfully strong contender for a multi-word descriptive name.)


Learning code shapes and conventions as a skill is definitely an important approach for the pragmatic programmer. Ultimately though, isn't this a necessary consequence of just not having a high enough level of abstraction in our programming languages?

Here's my very opinionated view: in scenarios where maintainability and readability are the most important long-term characteristics of a program then the best programming language would be one which separated out the specification of a program's intent from its implementation.

Separating concerns is a design pattern that we've learnt is really important (remember the early days of PHP?). We currently encapsulate logic around reasons for that code to change because we recognize the value of that pattern. Extending that same design pattern to what we know about the long-term maintainability of code means that we should be abstracting intent from implementation to allow both to change independently. Compilers and virtual machines already do some optimizations to this effect at the moment but we could go way further in our programming languages themselves. I think this means that declarative programming is the logical evolution of software design (really hope someone has an intelligent counter argument here because I can't see any myself).

Go makes a great implementation language in this sense (Rust too) but it's bound to loose ground in the application space as soon as we find the right high-level declarative language to describe application software.


The Go team’s recommendation (from Effective Go) is exactly what you’re suggesting. One letter variables should only be used in short functions, a couple lines away from where they’re created. The further you get from where a variable is created the longer and more descriptive its name should be. Some people just take it too far or don’t read the whole guide I guess.


In my opinion, coming from Java,C# background and currently working on golang, here's my take.

1. Golang did right design in terms of OOP

2. They didnt complicate the language by accepting everything.

3. At the same time, they made simple things so complex. Simple logics to removing an element from slice/array and even iterating them without screwing up the data.

I have been programming for 10yrs and I find myself doing those mistakes without proper IDE/golint, while I can easily code in other languages without much help. I can concentrate on solving the business problem rather than fiddling around the language


“Everything should be made as simple as possible, but no simpler.” I’ve often felt Go went too far trying to make everything simple, to the point that it’s wrapped around and made things complex.


"Civilization advances by extending the number of important operations which we can perform without thinking of them." --Alfred North Whitehead

Go forces writing loop for removing an element from an array even if the programmer knows it's linear time. Ruby gambles that programmer isn't ignorant - or it's not critical - but allows the convenience every time.

Designing languages is hard.


> Go forces writing loop for removing an element from an array

It does not:

copy(a[i:], a[i+1:]) // Shift a[i+1:] left one index.

a[len(a)-1] = "" // Erase last element (write zero value).

a = a[:len(a)-1]


Having worked a lot with at least a dozen languages, this is definitely on the cumbersome side of the spectrum. And a sibling comment says it doesn't even do what it says on the tin. In what other language does it take two devs to remove an element from an array?


Your code removes by index. This can be done in one line in Go:

    list = append(list[:i], list[i+1:]...)
but to remove by value you need a loop.


This makes it very unclear what's going on. I had to test it to find that this specific use is special cased to not copy. And very surprisingly you don't even have to assign back to the same variable to get this no-copy behavior!

Edit: I didn't do my test quite right. It's not really special-cased. But it's still very surprising to see this happen:

Code:

    s1 := []int{1, 2, 3, 10, 11, 12}
    s2 := []int{4, 5, 6}
    s3 := s1[:2]
    s4 := append(s3[:2], s2...)
    fmt.Println(s1)
    fmt.Println(s4)
Output:

    [1 2 4 5 6 12]
    [1 2 4 5 6]


Yup.

Simple! But not easy. Go is absolutely filled with nuggets like this in my experience. False-simplicity is deeply ingrained in the standard library as well: https://fasterthanli.me/articles/i-want-off-mr-golangs-wild-...


This is a Go idiom, one of the "slice tricks" that you are expected to just know. In fairness every language has its non-obvious idioms.

It may or may not copy.


It’s crazy that the function you call to remove an element from a list is `append`.


If you come at it with the thought that you are creating two sublists (on each side, avoiding the index) you then need to merge them back together. That append reads to just recreate the list from the two parts.


When I see that, I have no idea if it allocates a new array or if it's in place. Which one is it? At least with a delete function it's clear.


In Go it may or may not allocate a new array; it depends on the array's capacity. This means that the resulting slice may or may not alias the original slice.

See this playground for an illustration: https://play.golang.org/p/mOpuGVj2ypG

That uses append to delete, and then modifies element 0 in the result. In the first call, only the new slice is modified. But in the second call, both slices are modified, since they share the same backing array.

I consider this to be one of the worst mistakes of Go, to design a highly multithreaded language in which variable aliasing is a dynamic property. I am convinced that many Go programs have latent races and bugs, invisible to the race detector, and they have not yet been tripped because they have been (un)lucky with the runtime's capacity decisions.


you are right that a loop isn't required, but the point that kind of index / array element juggling is a headache still stands


A lot of the things in Go that seem horrible seem simple if you are used to the slice syntax used in Python and other langs, which I am not.


removing an element from a slice is trivial if you don't care about order. Just swap the element you want to remove with the last element and truncate the slice. If you want to maintain order, shift all the elements following the one you want to remove left one and then truncate. The latter can be slow if the slice is large, but it's linear time.


OP isn't asking for a solution. OP is talking about not having to implement the solution themselves and having the language provide some shortcuts for doing what you just described.


shortcuts for trivial operations is how we got the leftpad disaster.


People need those shortcuts so they don't reimplement them over and over. Not having the leftpad as part of the language or standard library is what caused the problem. This is exactly the problem the article talks about.


>Not having the leftpad as part of the language or standard library is what caused the problem.

There are a few other comments in this tree making the same claim - is this what most people took away from the left-pad incident?

To me, the stdlib lacking left-pad is a fairly distant cause. You could make the same case for any popular library, but that road leads to a bloated stdlib, which forever will haunt us. The more relevant causes, in my book, are:

1. Many builds relied on pulling left-pad directly from NPM

2. NPM allowed left-pad to be unpublished

I like to compare this to the usual practice in the Java world:

1. The primary repository in use is the Maven Central Repository. Nobody can unpublish a library from it. This is specifically to avoid pulling dependencies out from under consumers of those dependencies.

2. Most Java shops run their own repository. This serves both to caches requests to the Maven Central Repository, and to provide a fail-safe in case something were to happen to Maven Central. Backing up dependencies is considered as important as backing up your own code. Sometimes vendoring is used as an alternative approach.


Leftpad was a result of making libraries (comically) small. If you have a bunch of convenience operations, you can put them all together into a single utility library (or even the standard library).


Leftpad was a result of the JS standard library being terrible and not providing built-ins for such simple, common things.

And Go is the same way...


If the leftpad functionality was part of JS by default it wouldn't have happened.


I'm a little surprised deleting an element from a list is this hard but that seems to be a case. That seems like an API oversight. You shouldn't be writing your own code for what is a fundamental operation.

As for leaking Goroutines, yep that's a real thing. I had colleagues working on a Go project where exactly this was exhausting memory and causing the process to be killed. It can happen really easily.

I view this as a fairly fundamental problem with GC languages. GC solves some problems of course but in doing so it creates other problems.

For request-servicing type problems like servicing HTTP requests, I actually think the PHP/Hack model is pretty much ideal: stateless core (so low initialization costs unlike, say, Python), cooperative multitasking (ie no threads per se) and just throwing everything away after you're done. But I digress...

What Go does have going for it is syntax. It's minimal, opinionated without being too opinionated (IMHO) and familiar to anyone who has ever used a C-style language.

I tend to view Go as a better Python.


Deleting an element from a slice is a O(n) operation. It better be explicit in the program so the programmer knows just how expensive it is. Either you know what you are doing, and then writing it down is trivial, or you don't know what you are doing, and you are creating an efficiency problem for the future. Other data structures have a delete operation because it is efficient on those.

And fully agree on the GC part. GC doesn't remove the work of resource management. It simplifies it vastly. This can some times lull people into thinking they can ignore resources altogether.


> Deleting an element from a slice is a O(n) operation. It better be explicit in the program so the programmer knows just how expensive it is.

The chances of deleting an element in an array being a bottle-neck are really low. I'd rather save my brain cycles for thinking about disk/network access and wait for profiling to show me the problem.


Agreed. And the fact that disk access might end up being a bottleneck is not a good argument for making disk access APIs less ergonomic. I say give the programmer the nicest set of tools possible and let them make informed decisions about what’s appropriate to use when.


Yeah, I'm on board with the idea that expensive or not-recommended operations should be more verbose to discourage them.

But you'll find other expensive operations (eg prepending an element to a slice) are much less verbose. At a certain point (of verbosity), you're just adding the potential for bugs.


But why does the strings package exist if it’s better to write out the for loop each time? Do you think there won’t be a slices package after Go gets generics?

The argument that writing out the loop is better for readability is just status quo bias. The real reason we don’t have standard functions for such things is a technical limitation that will probably be removed soon.


Another thing that drove me up the wall recently (semi-related to leaking goroutines) are libraries that doesn't use the context package or a done/quit channel and can get stuck in long-running operations that hug absurd amounts of CPU/memory.

As far as I'm aware, you simply cannot kill a goroutine from another goroutine, i.e. there is no terminate()/stop() construct, meaning you cannot implement a timeout on top of an existing library without actually modifying said library.


https://github.com/golang/go/wiki/SliceTricks I was reading through this, which was referenced from the article. This just seems insane. None of any of that seems intuitive.


Meanwhile, in the much more complicated Rust, all but four of those are one-liners[0]. The exceptions being:

* Expand. Rust doesn't provide an easy way to insert multiple values into the middle of a vector, so I had to do 2 lines: append, then rotate right a subslice starting at the desired insertion point by the length of the insertion.

* Shuffle. No RNG in Rust's stdlib, so I used Rand. I did re-implement the shuffle, but in reality I'd probably just use the shuffle function provided by Rand.

* In place dedup. Needed two lines: one to sort, then I could call dedup.

* Move to front. This is not a function Rust provides, so it's completely implemented. Needed 10 lines. This one needs to search for the item and move that if it exists. In the event it exists, I rotate it left onto the end of the vector, then rotate the entire vector right. Otherwise it inserts at the beginning.

[0] https://play.rust-lang.org/?version=stable&mode=debug&editio...


Expand does actually exist in the standard library, but it's a bit of a strange incantation...

    vec.splice(i..i, std::iter::repeat(0).take(length));
This replaces the values in range `i..i` (an empty range starting at i) with `length` copies of `0`.


That's not exactly the same. Splice returns an iterator, it doesn't modify the collection in place.


Yes it does exactly that. It returns an iterator over the removed items, but when that iterator is dropped, it modifies the collection. See https://play.rust-lang.org/?version=stable&mode=debug&editio...


Huh... I guess I should read the docs more carefully next time; I clearly misunderstood them.


And for move to front, why not `a[..i+1].rotate_right(1);`, rather than moving to the back and then shifting everything?


Uh... because I'd been up for 20 hours and by brain fixated on a weird way to do it?

Don't program when tired, kids.


Me too. Made me realise I picked the wrong language just for a performance boost. The Go patterns mentioned earlier are even worse.


FWIW, the generics proposal was accepted:

https://github.com/golang/go/issues/43651

This will make a generic "slices" package much more trivial to write.


Finally! I honestly think that any statically-typed language without some minimal facility for generics - even C macros will do - isn't fit for purpose. I used Java and C# before the generics were introduced and you can't pay me enough to go back to that.


I think the reason Go is usable compared to early Java is because it does have generics for the builtin types -- slices (growable arrays), maps, and channels -- and for the builtin functions that operate on these types. And you can get really far with just slices and maps (which double as sets). You just can't add your own generic types or functions.


Error checking/handling is still a pain. Maybe generics will make that easier? Result type when?


How does a Result type making error handling easier? More reliable, sure, I buy that. But I write a bit of Rust these days, and error handling is definitely not easier. It's actually kind of an enormous pain in the ass, and it certainly seems to involve more lines of code than it does in Go (not just in my code, but in all the libraries I've read).


The most common thing you want to do with an error is bubble it up. IME Result types strike the least-bad compromise between making that too intrusive (if err != nil clutter) and making it too subtle (exceptions i.e. magic invisible control flow).

(I don't think Rust is the best example of result types; it's missing a lot of tools for working with them because Rust doesn't have HKT and functions aren't quite first-class because of their lifetime checker, so instead of using ordinary functions you end up using ad-hoc macros or language special cases like ?. Since Go is garbage collected they wouldn't have that problem)


I've not had that problem. I would love to see the question mark operator in go because I think it is an elegant way to avoid a ton of boilerplate while still remaining a tad explicit about where errors can occur.


Easier in that I don't have to force my team to use a static analysis tool just so people don't silently ignore errors.

CI works, but I have to set it up. For every project. And fix anything that was not developed with golangci-lint.


Hasn't there been several error handling proposals already?


Yes, and all have been rejected thus far. Finding the right balance for those things seems to be hard.

See:

Problem statement: https://go.googlesource.com/proposal/+/master/design/go2draf...

Proposal one: https://go.googlesource.com/proposal/+/master/design/go2draf...

Proposal two: https://go.googlesource.com/proposal/+/master/design/32437-t...


I want compiled Python with strong types. All the batteries of Python included. And runs at C speed. Go feels like C and Python had a baby except the baby looks more like C and less like Python.


Have you looked at Nim? Python-like syntax, compiles to C, and a considerably sized stdlib (http, parsing, channels)

https://nim-lang.org/


Nim is an impressive undertaking, though I was sheepishly overwhelmed when I tried using it for a quick project and realized I might have to spend 30 more minutes reading the docs than writing cowboy code. It's quite a big language: https://nim-lang.org/docs/manual.html

Would love to take a more serious look later.


I can recommend a look at Crystal also, although Crystal is not even 1.0

Between the two I found I needed much less referencing with Crystal, and when I did, I found it more intuitive, well described and easier to remember and apply the same concepts in other places.

https://crystal-lang.org/reference/index.html


Many people just don’t understand the joy of working with something like python or ruby. Maybe they haven’t had an immersive experience with an “a ha!” moment. Coming from basic and similar languages to java, then later to jruby and ruby, and now back to java I have to say it depresses me how much more I enjoy some languages and technologies over others. I wish I could feel equally excited and happy to work in them.


100%, being productive is addicting and fun. Laboring through for-loops 80th time in a the day is something for sadist purists, not me.

As a non-expert at Go, I would have loved classic OOP + batteries included Python and remove the curly braces. Perfect.


I stopped missing Python once I found Scala. It may not quite be C speed, but it's fast enough (orders of magnitude faster than Python, good enough for doing casual linear algebra in). Standard library is probably smaller than Python's, but you don't need it because the JVM world has really good dependency management (Maven).


Kotlin pretty much did this for me. I'm no longer interested in writing Python or Go because Kotlin is better than both in every dimension that matters to me personally. i.e power/expressiveness, performance and runtime/ecosystem quality.


Heh yeah fair enough. I find Kotlin infuriating to work in (far more so than many objectively worse languages) because it's such a step backwards from Scala, but if I didn't know what I was missing then I'd probably feel the same way.


I actually used Scala before Kotlin. I get what you mean in Kotlin being less powerful than Scala but for me this has turned out to be a good thing in bigger projects. Kotlin provides enough power to get the job done without giving people too much rope to hang themselves with.


For me it's not about power, it's about consistency. Scala has a couple of general concepts that get applied to everything. Kotlin seems to have a new ad-hoc way of doing every different thing. E.g. my teammate just rewrote some code to use a Scala-style IO type rather than suspend functions because none of us could understand which coroutine context was being used where.


Mypy with python’s new static types is a fantastic replacement for “real” types. The types mostly don’t exist at runtime (except for some rare cases like how dataclass handles ClassVar variables) but they’ll take you very far during development time, especially paired with a language server like Microsoft’s Pylance.


It won't be ready for production use for a while, but https://github.com/python/mypy/tree/master/mypyc is being worked on.


You may enjoy working with Nim.

Its main drawback is the small number of native libraries available for it, but wrapping and using existing libraries from other languages is a walk in the park.

https://nim-lang.org/


Thanks, Nim looks extremely interesting. Anyone here used Nim in production?


Also, I am not sure what counts as "in production" but on literally a daily basis I use about 15 different command line utilities I wrote in Nim using cligen [1] (EDIT: Really I have written several dozen but only about 15..20 are useful every day). There are always compiler bugs/things to workaround (as with any language), but Nim has been pretty usable for years.

[1] https://github.com/c-blake/cligen


The Nim Forum [1] is written in Nim and has been used in prod for years. A commercial video game Reel Valley was written in Nim. Status [2] is doing Ethereum 2.0 stuff with it.

[1] https://forum.nim-lang.org/ [2] https://status.im/


This might be wrong but can't you use cython and then compile python to speed it up? Obviously won't be as fast as Go but seems like it can have speed ups


If you think of Cython as a "new but related" language rather than just a Py compiler then you can sprinkle enough C-like `cdef` code into your Cython so that, when compiled, your code can run much faster than Go..as fast as any C, really. (EDIT: Pypy is usually a better way to make unadorned Python run faster.)

That said Nim [1] provides nicer metaprogramming features than Cython and very nice overall ergonomics..I think more ergonomic than Python with, e.g. UFCS. Nim has almost all the safety/speed of Rust with better ergonomics than Python. What is not to love?

[1] https://nim-lang.org/


I’ll definitely check out nim. The best thing about python is the ecosystem and vast knowledge available. It makes it hard to move to something “better”


The Nim Forum and IRC channel are pretty helpful/rapid feedback. Maybe not as much as a canned StackOverflow answer for Python or an existing library to do exactly what you need, but the Nim ecosystem/userbase is growing all the time.


If you’re ok with s/python/ruby Crystal is great, LLVM compiled and has a couple of cool web frameworks coming up.

https://crystal-lang.org/


Swift could be interesting for you. Modern syntax, easy to use, performant.


I love go, but I think it is a straight up dangerous language. I believe this because:

- The tiny standard library and low learning curve make it quite easy to get started.

- Concurrency is a first class citizen, through goroutines and channels

- The performance, combined with the low barrier to entry, combined with its popularity make it very desirable for companies

In my experiences this means a lot of companies without deep concurrent programming experience begin to adopt it. In my experiences this means concurrent programming errors....literally EVERYWHERE. I've seen (and created these of course) in every company I've been at and in huge open source go projects.

I wish go would address this through first class tooling or type supported in the language like rust.

I have a hueristic based approach that I've had success with when writing and reviewing code, but writing a static analysis tool for it is a bit out of my experience:

https://medium.com/dm03514-tech-blog/golang-candidates-and-c...


Can you expand on the types of errors you're seeing? Go has a fairly powerful race detector you can run as part of your test suite that seems to catch everything I've thrown at it.


Me too! I generally think that the race detector is the best approach right now.

At a prior job we had a lot of code that wasn't structured in a way that made it easy to exercise using the race detector. This was combined with misunderstanding about what the race detector did and didn't do. For example there was one team that ran `-race` on non-concurrent code, and expected it to verify race conditions.


> Go is not an easy programming language. It is simple in many ways

This, exactly. It’s worth contrasting this with Python, which has a reputation for being simple but is actually extremely complex - but it is easy (at least for small programs).

However, I can’t help thinking that the two shouldn’t be in opposition. IMO it should be possible for a language to be both simple and easy to use.


Meet php.


You can't reply to someone's comment on internet about "...could pick up Go in 5-10 minutes..." that seriously.

But couple of points about simplicity author missed out:

- deleting from slice is expensive so should be explicit. True that lack of user generics prevents people to write own generic function for that, but that's not the only thing that generics will inevitably bring – are we still talking about complexity?

- Go is still the easiest language to pick up and understand due to smaller basis in the feature vector space. That's what Go authors mean by orthogonality of features. The search for the solution is way more straightforward in Go space – and that's what we call a cognitive load.


Agreed wholeheartedly. I professionally progammed in Java, PHP, and Go, alongside a few other languages for open-source/pet programms (Python, C, C++, ...) and finding a solution to a problem in Go is straightforward. When I had to write Java, I always had to decide which language feature I'd like to use first before getting down to the problem.


I really like Go, but I'd love something like JavaScript that had the performance of C^. If and when that happens there really wouldn't be any need for other programming languages IMHO.

Oh well - that being said Go is way simpler than C++18 that I've done a tiny bit of work in. I think the issue is that Go generally competes with Ruby/Python/JavaScript in certain scenarios and just seems way more complex in comparison.

If you compare Go with C++/Java/C# it seems more reasonable and "easy."

^ the good news is that a ton of money is being spent to improve Node performance - whether it'll approach Java/C++ speed is TBD, but the performance gap is shrinking year by year.


I find Java and C# to be significantly easier than Go. ("more reasonable" depends on your goals of course, though if I had to pick one language that makes it easy to do things in a reasonable way it would be Python.)

C# was one of the first languages I learned after HTML4 and Game Maker, no problem.

Java I learned later, no problem.

I also get C++ though I never used it enough to be really proficient.

And of course I know a bunch of other languages from awk to bash to javascript to php to ruby to sql to vba (not an exhaustive list).

But whenever someone makes something in Go, they seem to expect that the unique patterns and unusual operators for backgrounding tasks etc. to be intuitive to me and I should be able to pick up where they left off in a few minutes... it's not just not.


> C++18

(Out of curiosity, is this C++17 or C++20? I'd assume 17, and also I'm curious which aspects you find confusing.)

Several coworkers have remarked that it's just easier to reason about correctness of concurrent code in Go vs. C++, while not sacrificing performance as in, say, Python. (There's definitely a performance hit, per the use of a garbage collector.)

One thing that's always bothered me about Go is just the things that are missing from the standard library; I think that contributes significantly to it being "harder" to work with.

(A good number of these likely stem from the lack of generics, e.g. max(a, b int) int -- math.Max takes and returns floats.)

That said, the biggest thing I liked about Go from the first day I used it was the treatment of concurrency as a first-class citizen (goroutines, channels).


> but I'd love something like JavaScript that had the performance of C^

Java honestly is close. Data object creation is notably worse unless you're using Lombok or similar. Besides that, modern Java and JS syntax are similar. And Java doesn't have a horrible underlying object model based on prototype chains.

IMO Go seems easy because it's simple. I know Java and JS well and they're about the same complexity these days.

Knowing both I will never touch JS for backend again. Java is better suited for it. Strong typing is light-years easier to follow code paths and refactor. IDE's, debugging, profiling tools, memory usage, and performance are all better in Javaland. Real threading with proper shared memory exists. App size is smaller since Java bytecode is compact and distributed zipped.

My company stopped using JS on backend after the hype wore off. Our Java backends are easier to monitor and maintain.


I agree wrt to Java. Because Java has been around for so long it has a lot of emotional baggage that I think makes people reflexively dislike it. But, modern Java with any of the micro frameworks or something like Spring boot, plus jOOQ makes for a very easy and productive backend. And because Java has been around for so long, if you run into edge cases like parsing a weird file format, there is probably a library out there. Kotlin has a lot of same advantages, plus a more expressive syntax if that's what someone is after.

Go is also works for the backend, but you have to get into the mindset that you're probably going to write a lot of code. That's fine, but it's a shift from many other languages. As someone who has programmed for 20+ years, I like the single way to do most things in Go. There's also a bit of elegance coming full circle through many languages where a for loop is once again just a for loop.

And maybe I'm showing my age, but I also completely agree that backend languages need strong typing. The other option is significant test coverage, but we have all seen how that goes in practice. Code without strong typing always feels like it's meant to be written once and thrown away because it's so hard to read later. And, this style fits with how UIs often work where the life of a product will end up with many UIs on the same backend.


I was on the Java hate train till I was forced to use it. It became my favorite language. For being so old, Java has avoided feature creep and bad decisions that make old code painful to deal with.

Sometimes I work on 10+ year old areas of codebase and it's fine. Compared to dumpster fire of 10 year old JS/Python/C++ it's paradise.

I use lombok judiciously to avoid getters/setters, the cruftiest part of Java. Newer versions of Java are quite pleasant.

Java is extremely mature. I pull in a library and it "just works" 99% of the time. No worries about the version of Java is was built for or missing native dependencies. Practically every language falls short of the polished user experience in Java.

I don't like Kotlin because the non-Android experience is an afterthought and it's a captured language by Google and Jetbrains. Java has a lot more hands at the wheel.


How does Kotlin compare?


Kotlin is better on paper but IMO, only the better choice on Android. I always run into Kotlin tooling issues on "non-android" Java. Stuff like Maven plugins not working. Plus, IDE support is only good in IntelliJ and it's been that way for years so I don't expect it to change.

Kotlin fixes tons of issues in the frankensteined Java 8 of Android. You can use Java 15+ everywhere else and its adopting many features that made Kotlin worth using.

Google is pushing Kotlin so they have control over Android language like Apple does for iPhone. It's just a nice layer over Java in the end since 100% interop is required.


Java is a long way from Kotlin feature-wise and at the current rate of "innovation" we're more likely to have expired before Java catches up with Kotlin. I mean, come on, in the 25 years since 1.0 Java hasn't even fixed the requirement to escape metacharacters in a regex. The SAM lambdas of Java 8 are also a feeble compromise with FP at best. Kotlin has a much cleaner approach to FP, immutability and null safety.


There won’t be one ultimate language. The difference between low-level and high-level languages is significant. High-level languages allow you to not have to worry about intricacies of memory layout mostly, and thus even changing the data structures you use will not result in a refactor. While using low level languages, even ones like rust, you can change the allocation strategy/memory layout of your data structure, but you will need to refact on it. So it is a tradeoff between ease of code base change, and tight control on memory/resource usage.


I would try Kotlin.

Syntactically it's even more terse than JS and is as fast for most use cases as C. Main downsides are memory usage and needing to understand JVM limitations for latency sensitive codebases (TLDR: Use a modern JVM and ZGC or Shenandoah).


Note that with SubstrateVM/Graal native image, you get far lower memory usage and startup time equal to or even better than C.


The fact that Go supports channels, buffers and workers means it's very much tailored to parallel processing. I always thought of it as a soft of worker farm except it's running on a single machine. You can run it in a multi-node environments as well, of course. I find the parallelism feature of Go so powerful that it overshadows its other features. For server side there are already many really well designed systems that allow for parallelism, Apache, Nginx, Kubernetes, RabbitMq... For client side parallelism maybe useful in things like games but even then there are frameworks better suited to take advantage of parallelism offered by GPUs. I like Go for command line apps, it's a much faster alternative to Python.


There's so much wrong with the second example (it works, yes, but for all the wrong reasons) I don't even know where to start.

Using the buffer size of a channel to control number of concurrent jobs is just the wrong approach. It's so much easier and cleaner to just use the number of goroutines for that:

    const workers = 3
    const jobs = 20
    
    jobsChan := make(chan int)
    var wg sync.WaitGroup
    for i := 0; i < workers; i++ {
      wg.Add(1)
      go func() {
        defer wg.Done()
        for work := range jobsChan {
          time.Sleep(time.Second)
          fmt.Println(work)
        }
      }()
    }
    
    for i := 1; i <= jobs; i++ {
      jobsChan <- i
    }
    close(jobsChan)
    wg.Wait()
    fmt.Println("done")
One thing about channels in go is that the only time you want to use a buffered channel is the time that you know exactly how many writes (n) you'll have to the channel, while n is finite and reasonably small, so you create a channel with buffer size n to unblock all the writes. An example to that is that if you want to strictly enforce a timeout to a blocking call:

    const timeout = time.Millisecond*10
    ctx, cancel := context.WithTimeout(context.Background(), timeout)
    defer cancel()
    resultChan := make(chan resultType, 1)
    go func() {
      result, err := myBlockCall(ctx)
      resultChan <- resultType{result, err}
    }()
    select {
    case <- ctx.Done():
      return nil, ctx.Err()
    case r := <- resultChan:
      return r.result, r.err
    }
If you are using a buffered channel in other cases, you are likely doing it wrong.


I agree with the 2 example tasks, which are not "simple" to write.

The remove-by-value snippet is 8 lines, and it can be wrong in a subtle way: if list contains pointers, then we have a memory leak beyond the slice final length (pointer values still exists within capacity, and objects are not garbage collected).

I wrote several possible implementations here, none of which is as concise and as obviously correct as an hypothetical "list.remove(v)": https://programming-idioms.org/idiom/136/remove-all-occurren...


> But I also don’t think that Go is a language that you “could pick up in ~5-10 minutes”

I disagree. I use Go for our API backend because I can onboard junior developers and have them producing very fast. Because they are familiar with loops and C-syntax, this is very much a guaranteed "pick up in ~5-10 minutes"

The explicitness of Go makes it easy for them to get to work, and easier for me to understand what they are doing when it comes to code review

They won't ever have to use interface{}, nor concurrency, and slice manipulation won't take much time to lookup any "cheat sheet", or copy from another file


I hope "~5-10 minutes" is shorthand for a somewhat longer time periods.

Even so have you considered whether this approach has some downsides and risks?


Since we just work with mostly simple CRUD, there isn't many downsides. Also we use SQL queries for most of our logic, so SQL knowledge is the real barrier

I usually ask them to read gobyexample.com, but they can start working on projects on the first day.

CRUD project with Go makes it easy to add new endpoints and simple features, so we can focus on hiring frontend devs and smoothly turning them into "fullstack CRUD devs"

I am sure this can be achieved with other languages, but for our use case (software-house) Go just fits really well because it provides a low cognitive overhead for juniors and even interns to make sense of the project and contribute

It fits pretty nicely in an "assembly line" style development, but maybe the downside is that it is boring and makes devs want to leave because they feel like they're always hitting the mark and "mastered" development and want to climb to the next level. It seems reasonable for them, but the turnover is tiresome (around 6 months) for me


Thanks - interesting perspective.

I do think that we (or me!) hold ourselves to too high a standard in terms of mastering all aspects of a language, especially given some languages have got too big. It's refreshing to hear an approach that focuses on knowing just enough to get the job done!


I agree with the author that Go is not as easy as Python or Ruby. However, the first example given is bad.

Running delete on a slice is the same as trying to delete an element of an array. It's the wrong data structure if you need that type of access pattern. That's why it's hard. You can cut a tree down with a hammer, but it's going to take a while and you're going to have some blisters.

Go has built-in linked lists. https://golang.org/pkg/container/list/ and I will agree, they're not as easy to use as other languages due to the lack of generics in go, at present. Using the list.* package requires type conversion to evaluate and pull values out of the structure.

I think it's fair to say that Go needs to do a better job explaining the list package or possibly including it in the "Tour of Go" guide. That might help avoid these misconceptions in the future.


I feel like the majority of the time you just have arrays or slices of a manageable size, say <= 25 elements, and you just want to delete an element from it. You don't care about performance because it'll be fast regardless and surely not the bottleneck of your program.

In my experience with Go, a lot of basic functionality is missing and requires you to write your own utility functions. And some things you would expect like mySlice.append(elem) are instead written mySlice = append(mySlice, elem) which isn't as elegant. Plus you have to remember it's not just append(mySlice, elem)

If there's a handful of open source utility function libraries, that will split the Go developer base, kind of like how you used to have both jQuery, Prototype.js, underscore.js and others in Javascript. They should have just standardized all the common functionality you'd want and in an intuitive way.


If the array fits in the cache, deleting from the middle of an array and copying all the subsequent elements one to the left is actually faster than a linked list.

And most arrays fit in the cache.


I don't think the author was concerned with performance. I was providing the list.* type suggestion to make it easier to delete elements syntactically.

However, you're likely right for slices with len < 500 or so due to garbage collector churn on linked lists.


> Running delete on a slice is the same as trying to delete an element of an array. It's the wrong data structure if you need that type of access pattern.

If you keep deleting elements from arrays then maybe, but if you do so once in awhile the cache-friendliness and limited allocations overhead of an array will most likely come out ahead. Especially if you also want arbitrary access.

Removing an element from a linked list is cheap if you’re already at that element (assuming the node itself is exposed), but most operations are way costlier.


Delete on a not-too-long arrays is pretty cheap. Probably even better than iterating over a linked list, but of course it depends on the exact application.


I don't think that the claim has ever been that Go is especially easy to program in; it's about as good as any other mainstream language for expressing logic. It's that Go is simple enough that the language gets out of your way really quickly.

I think the article kind of got the idea midway through, at "I think most programmers can figure out what the above does even without prior Go experience". Yes. That's the idea.


Most of these will be solved with the recently approve generics.

Concurrency (less the pattern libs that generics will provide) feels generally hard. Although channel semantics in Go are very annoying (e.g what happens when you close a buffered channel). This is hard in most languages I’ve used. Anyone have an example of what the state of the art hard-to-shoot-yourself-in-the-foot concurrency looks like in another language?


Clojure's core/async and Elixir's actors.


Simple and easy are two words with no meaning. Why? Because simple and easy attest to common brain paths that do not exist. In computer science the divide between functional programming ease and simplicity as with the likes of Lisp and procedural languages as with like Python stand in stark contrast. Functional programmers believe everyone thinks like they do even though the vast majority of software is procedural and whose population do not find being lost in sets of parenthesis easy or simple. Easy and simple are meaningless because there is no uniform, blank slate learning path in the human brain. In fact, if one had to model every person having a unique learning path versus today's notion that what's easy from me is easy for thee then the unique model would surely realize far more efficacy in practice.


In the go trainings i have given i noticed that syntax really was never the issue, but some concepts were awfully hard for some participants to understand.

Also pointers are a thing people are afraid of for some reason.

On the other hand i notice that most go code i have written does not need any maintenance at all, which is neat.


I'd say that Go in its current form is easy but tedious. Concurrency has some ways to shoot yourself in the foot but that's pretty much par for the course. The good news is it's going to become a lot less tedious if a little bit less easy with the introduction of generics.


This is all just crazy to me. I cannot even remember the last time I dealt with array indices directly.


Go is not designed to be a scripting language. It is designed to have users think about edge cases and error handling so they are sure they are doing what they actually want to. Both the slice and goroutine patterns the author uses are pretty much standard patterns in Go, like making a factory function in a more OO language.

To echo others, when Go makes something cumbersome (like an O(n) operation) it’s usually to hint to users that they’re doing something inefficient.

But, agreed you can’t pick up Go in minutes. It’s best approached coming from a background of writing service binaries in C/C++. If you aren’t familiar with strong type systems and concurrency you’re going to have problems.


Statesments about ease of programming Go are relative.

Go is easy once you internalize its semantics. And this is the kicker of Go, it remains simple even after you have achieved a level of mastery of the language. This is not the case even for a language like Java. Certainly, C++ gurus will have your eyes bleed reading their code.

But this is not the case for Go, for the simple fact that Pike et al have engineered so that you can not do astronautics with Go, without enduring severe pain. So no one does it. Short of Go assembler code, any Go code out there is accessible to an exceptionally wide range of Go programmers. There is something to be said for that.


OP complains that GO official tutorial is all about syntax. So what are the ressources to learn GO specific mindset ? More generally, that was my concern back in the day with "The C programming language" which was only about how to write C code but not how to think in C. Which is quite different !

What are the best ressource online about how to think in a language (RUST/GO/C/C++/JAVA/JS/PYTHON/HASKELL/CAML...) ? Not only limited on about to syntaxically write a correct program !


Tutorials on source code structure and open-source projects.


Why gripe about how difficult it is to do concurrency in Go? It is difficult to do concurrency correctly in ANY mainstream language. Go offers more support for concurrency than most: goroutines, channels, non-blocking I/O, great networking and HTTP support, ... There is a book specifically about this ("Concurrency in Go" by Katherine Cox-Buday) that carefully explains how to do concurrency correctly in Go.

Go has become the language of cloud infrastructure:

https://hackernoon.com/go-has-indeed-become-the-language-of-...

And there are lots of developers who want to use it:

https://zoolatech.com/blog/why-use-golang-for-your-project-a...

Are there things we would want added to the language? Sure. Generics. Check out the progress:

https://thenewstack.io/this-week-in-programming-go-approves-...

If there are other things you want added, it's open source, and you can join the team working on it.

Don't lose sight of the fact that Go has done alot of things right, and is GREAT the way it is right now. The uptake in the developer community proves it's worth.


> "I’ve seen many people coming from Python or C♯ try to shoehorn concepts or patterns from those languages in Go. Common ones include using struct embedding as inheritance, panics as exceptions, “pseudo-dynamic programming” with interface{}, and so forth. It rarely ends well, if ever."

I've seen this so much. I did it too. You can almost watch people go through this process in places like r/golang.

We all start off coming from a different language and being used to that, and while Go takes about an hour to learn all the syntax, it's only then that the learning starts. We all then try to write code the way we've always written it, only to find that Go doesn't work like that. Then we bump into the "this standard lib logging library is so shit, let's write a better one" (for the more ambitious, this is a whole framework). Then we learn more, and begin to realise why the logging lib is like that, and why there's no need for some kind of middleware construct, and so on. I'm not sure where this journey ends, because I haven't finished it yet - I suspect my understanding of chans and goroutines is incomplete because I still think they're "a bit clunky".

Simple is hard. But good.


My favorite way to summarize this is "some people can use any language to write Java" (or C or Python or Haskell etc.).


To be proficient in any language / framework you need to understand its mechanics. It doesn't matter what language you use. Or how easy it seems. I've seen many junior devs shooting in their feet with Java (Spring) or Node. And certainly did it myself often enough.

After initial skepticism Go as become my personal go-to tool for backend, cli tools and throw away scripting stuff. At my workplace Go has replaced Spring and Node for standard business backend stuff as the standard stack.

This is *not* because Go is such a fabulous and easy language, but because of its overall characteristics:

- pretty impressive runtime performance and more importantly memory efficiency

- large and very useful standard lib

- useful tooling

- stable and mature

- encourages a package oriented design that enables large scale, however that can be achieved with many other languages as well. In other words: Building your app with packages as if they were microservices.


This article is built on a false premise: that people reach for Go because it is easy.

Despite it's flaws (and it definitely has flaws), Go has replaced Python as my go-to language, not because it is easy but because it is small.


In Go tutorials I almost never see anything about mutexes or how to be thread safe


Generally the answer is to stick everything into a channel. This doesn't fix race conditions, but it does make it easier to write code.

Want to query the db in parallel? Start n goroutines and write the results to a channel, then read from the channel n times. Want to limit your concurrency to m tasks? Start m goroutines reading tasks from a tasks channel and pushing results to a results channel, write n tasks to the tasks channel, read n results from the results channel.


I find the channels to be the most confusing thing in Go.

What happens when you write to a closed channel? What about a blocked one? What if you read from a closed one. Can you close a closed channel? Does that panic? What was the non blocking read? What about write?

Fuck that, I use the sync Mutex primitives and good old slices instead.


+1 to this. I'd say nearly half of the channel code I've encountered in "real code" has had severe flaws when things don't align perfectly. They don't have data races, in that the race detector will give it a thumbs-up, but they do have semantic races leading to deadlocks, misbehavior, dropped data, etc. They generally work fine when they're not under heavy load / contention, so they aren't noticed until they collapse.

Mutex code though? 90% correct, easily. It's often dramatically simpler to make correct, especially when more than one construct is needed, or nearly anywhere with error handling. (`defer` is quite nice for locks)


Cause you need not worry about the threading if you are using channels and go routines. The scheduler does it all f9r you - you just describe the data flow and the concurrency.


Sharing mutable state is still pretty risky; either immutable objects or byte channels (no pointers and no non-serialized objects) would have helped a lot.


After reading this article now I’m questioning my self should i continue learning Go as this is giving me second thoughts that if it’s going to be useful for me to learn go


I could write a "ugh, this is kinda annoying"-criticism of any language I've ever used in depth. I certainly didn't intend this to be a "Go sucks!" kind of post: as the author of this article, I still use (and like!) Go very much, and is pretty much the language I use by default for most problems.

And even if it turns out you don't really like Go: that's okay. The only way for yourself is to actually learn it, and you will be more knowledgable and experienced regardless.


It depends what "easy" means. Go is a language that has very few surprises.

As a beginner, it takes a while to figure out good goroutine design patterns. And you get bit by append to slice copies a few times. And it takes a bit of time to figure out how to structure code efficiently, and get over the fact that the code is not completely DRY.

I'm sure I forgot a few gotchas, but this list is still a miles shorter than all the popular languages out there.


> There are a number of patterns to limit the number of goroutines, and none of them are exactly easy. A simple example might be something like:

This is probably what the author was looking for https://pkg.go.dev/golang.org/x/sync/semaphore


Some things are done on purpose so that you consciously know how expensive of an operation you are doing. In languages like Python, it's easy to abuse convenient operations that do a ton of computation. Then you have to spent time with valgrind or something similar and figure out what the heck is causing the code to be so slow.


Yeah I agree with this. Go is awesome but pretty complicated. Channels still break my brain every time I use them.


Conversely, I think go can be pushed as an easy language. In large, whether a language is "easy" seems dominated by perception.

That is, most things that are "hard" have a reputation as being hard by non practitioners.

Which isn't to say that empirically it is clear some languages make some errors easier to make.


The idea behind https://github.com/adsharma/py2many is that you can still write:

    del a[i]
and have the go code auto generated. py2many doesn't do that yet, but you can teach it to.


Thank you, I’ve been looking for something more actively used than https://github.com/google/grumpy.


Go definitely not as simple and easy as many of newcomers think of :) The slices itself has many gotchas:

https://medium.com/@gotzmann/so-you-think-you-know-go-c5164b...


Easy to learn doesn't necessarily mean easy to use, which I gather is where the "go is easy" meme got started. Morse code is easy to learn but hard to use.


Thinking outside the box, maybe people who don’t like Go ought to try a different language. Give Perl or Ruby a whirl. No memory management, no types, no concurrency, nothing to think about, just write code. Learn bash and never worry about arrays again. I knew a woman who went straight from 6502 assembly to bash. All her bash programs had two variables, $x and $y, and she reused them throughout. What other language gives you that freedom?


I hope this will change with generics, and we will get

> slices.delete(slice, index)


I think Go can only be defined as "simple" in the New Jersey (worse is better) sense of simple, that is: "it is more important for the implementation to be simple than the interface."

A lot of things about Go are not simple. In my opinion a simple language is not about how simple it is to write a parser or a compiler for that language, but it's about how many different non-trivial and arbitrary pieces of information the developer has to memorize.

This is highly tied to the Principle of Least Astonishment[1] in language design: how many unexpected surprises does the programmer have to deal with?

With Go, you get quite a lot:

1. Go already has generic types. These are the magical maps, slices and channels. Everything else is not.

2. Even if you think #1 was also true for Arrays in Java 1.4 and no one was complaining, Go goes further: it already has generic functions like 'copy', 'len', 'min' and 'append'. Since you cannot properly describe the interface of a magic built-in function like 'append' using the Go language itself, this is not a standard library function, but should be viewed as an entirely new piece of custom syntax, like the print statement in Python 2.x.

3. Nil interfaces and interfaces with a nil pointer are not equal.

4. Multiple return values are a magical beast - they are not tuples and you cannot manipulate them in any useful way.

5. Channel axioms[2]. Possibly one of the more astonishing and painful aspects of Go.

5. Slices are mutable, unless you copy them. This can lead to some very surprising cases where a slice is passed down many layers below and then modified, breaking the caller.

6. Continuing the topic above, Go has neither clear data ownership rules (like Rust), clear documentation tradition on who owns the data passed to functions (like C/C++) nor a way to enforce immutability/constness (like C++, Rust or FP languages). This really pushes a lot of the cognitive overload to the developer.

7. Go modules are a lot better than what we had before, but are quite hard to deal with. The moment you need to move to v2 and above and start creating subdirectories they becomes rather confusing compared to what you would do in other package management system.

8. If a simple language is a language that allows you to write _simple programs_, and you follow Rich Hickey's classic definition of Simple[3], then Go is probably one of the LEAST simple languages available today.

tl;dr: I'm not saying other languages often compared to Go (like Rust or Java) don't have their own share of complexities, but I don't think Go should be viewed as a simple language in the broadest sense. It is a language with a rather simple implementation for our day and age (though Pascal was much simpler if we stretch this definition backwards).

[1] https://wiki.c2.com/?PrincipleOfLeastAstonishment [2] https://dave.cheney.net/2014/03/19/channel-axioms [3] https://www.infoq.com/presentations/Simple-Made-Easy/


The blog post misses the point. Of course writing idiomatic, gotcha-free, production ready code is never easy.

But Go itself is comparatively easy. By far the fastest I have ever onboarded new engineers with, even more so than Python (which is terrible but somehow has a good reputation at being easy and friendly).


What is horrible about Python wrt onboarding new engineers?

In my personal experience, I had an easier time getting onboarded on Python than Golang.


Everything that happens in between development and deployment.


We're comparing (linked-) lists with arrays and compiled languages with scripting languages now?

Why would you use an array/slice instead of a linked list, but then want to use it like a linked list?

If you need a linked list, use something like this: https://golang.org/pkg/container/list/




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

Search: