back
69 comments
I still haven't found another language like F# where it's so enjoyable to distill complexity down into expressively clean code. Every time I start with spaghetti in F# inevitably I can refactor across multiple passes and it naturally collapses into a singular easily-readable essence.
Scala 3 is similar. It's means of describing datastructures is more powerful than F#'s but also in many cases not quite as concise. On top of that, Scala does not support type providers (a real bummer IMO).

Other than that, it's quite similar and enjoyable, especially when it comes to refactoring.

How's the experience of developing in F# for .NET Core? When I look into .NET C# code, it relies heavily on dependency injection and magic imports. This seems to be completely the opposite of what you would want in a functional language.

Are there good solutions for this already provided? Is it a non-issue?

> This style of if err != nil error-checking is, laughably in my opinion, celebrated as an example of the “simplicity” of go programming.

Literally nobody ever celebrated having to write 3 lines to return error.

The "simplicity" part is "return error at point of happening, each function have error as last argument so you handle error here and now"

The inept syntax was never liked from the beginning, despise stubborness of Go authors to not provide better alternatives

Rust's Result<T,E> does wholly better job with same context, altho tendency for people to just .unwrap() with no context often puts it back into "bad error messages" territory

> Literally nobody ever celebrated having to write 3 lines to return error.

On Hackernews many people say that they like it and it's easy to understand. Maybe celebrate is a bit exaggerated, but I would say Golang is celebrated and the simpleness of errorhandling is often mentioned, so I can see where OP comes from.

> altho tendency for people to just .unwrap() with no context often puts it back into "bad error messages" territory

I use Clippy lints for that [0], and a tool called Cranky that makes it easy to use Clippy lints in a project instead of as a command line tool like Clippy normally does [1].

[0] https://rust-lang.github.io/rust-clippy/master/#unwrap_used

[1] https://github.com/ericseppanen/cargo-cranky

The problem with this is that _you've lost all the wrap messages_!

In a good golang codebase, you get human-readable stack-trace with the relevant variables printed inline on every error. It's super useful and every "Result" based error handling implementation totally whiffs on this.

But it doesn't have to. You _can_ get the best of both worlds if you take a moment to appreciate the benefits of the golang style and provide simple to use APIs to build context for every possible error.

Another aspect that I think is often not well understood when you get this "standard Monad error handling tutorial" is that the monadic approach is nicer precisely because you're not handling errors. The reason why the monadic code always looks cleaner isn't only that it's "monadic" but also because the "standard Monad error handling tutorial" deliberately doesn't handle any resulting errors. Monadic code like (sorry, I know Haskell syntax, not F#)

    user <- getUser
    message <- getMessage dbConn msgIdx
    renderMsg user message
looks so much cleaner, but if you actually need to unpack the result of getUser to figure out what the error is in order to do something specific based on an authentication error versus a database connectivity error versus a timeout error, the syntactic advantage over Go dissolves. I forget the exact Haskell syntax now but think something like

    user <- case getUser of 
             Error err ->
               case err of DBerror e -> ...
                           AuthError e -> ...
                           Timeout -> ...
                           default -> ...
             Result u -> return u
    message <- case getMessage dbConn msgIdx of 
             Error err -> ...
and so on, etc. Yes, it's great that the happy path looks that nice but monadic error handling ends up affording the same problems that lead people to complain about exception based handling. Most of my Go code would only be marginally improved by "better syntax", the bulk of it is the actual logic of handling the many and manifold problems that arise in network servers, which is most of what I use Go for.

Syntax is almost never my blocker. Can't quite say in good conscience "never". But it's not my biggest problem. My biggest problems are generally that I didn't realize that API could return a response like that, or what do you mean TLS is rejecting my certificate, or most of all, "Oh, guess I didn't understand the problem as well as I thought".

You don't have to use pattern matching syntax. You can just compose your get* functions with a custom function that adds whatever context to the error case. It can be made quite clean.
I think it's actually worse than exception-based error handling, as exceptions are typically integrated in the runtime in such a way that they produce a nice stack trace, which is very very often all the context you actually need (though sometimes parameter/variable values are important as well). The Result/Either/Error/[...] types have no such special support, so they often indeed just give you a single error type and no other context.

On the other hand, it's important to note that, except for maybe adding context and bubbling up, the vast majority of code in any code base will not actually handle errors, so the advantage of taking away the huge amounts of boilerplate when possible are still worth in my experience (as someone who has ~7 years of Java experience and have moved to Go for 3 years, all in a commercial setting with a large-ish team).

It's about separation of concerns. The happy path is one flow, and the error path is another.

The business logic is no longer mixed with the implementation details. You can look at the code and very quickly reason about the business logic. Yes the implementation details still exist but they can be reasoned about on their own.

exactly, the monad "erases" the error, at least in the "happy path", while go makes it first citizen. one is not better than the other.

I care a lot about errors, for logging, for metrics (how many connection errors did people from canada have last month?), for custom error handling (UX), for lower stack depth, etc... With tools like github copilot, I can now write extensive error handling code at the speed of my tab-key, which is a game-changer.

But if your team likes monads and doesn't care about errors (or you have other monads you can now lift into this with a single line of haskell), or you want to have a nice functional separation for easier unit testing and less coupled codebase, then the monadic approach makes more sense.

But that choice is one you can make even if you think in terms of monads when doing error handling.

I was thinking the same thing.

The errors are now returned without their context, and I've written enough F# to know that computational expressions aren't magic. Forgetting an exclamation mark circumvents the entire expression. Perhaps that's changed in recent years, but it was really annoying to find your code silently breaking due to a missing symbol some ~5 years ago.

As much as people love to hate on Go, it's the only language where I can read someone else's code line by line and not simultaneously have to read partly outdated language specs, no longer applicable SO answers or generally have to play internet detective to unravel magical incantations that sometimes fail at runtime.

Yes Go is verbose, yes it lacks XYZ. Junior, mid, senior, Go newb, Go expert, the code (for the most part) looks the same. How that's a bad thing I don't get.

Boring? Incredibly. Useful? Incredibly.

How would you manage to forget a `!` when writing F#? IIRC the code wouldn't type-check without it, because you'd be trying to use an expr of the monad type as if it was of the inner type.

It's been a few years since I wrote F# so I could be misremembering.

“Code looks the same” means your experts will forever be forced to wade through the same awful boilerplate as your beginners. There’s no payoff for learning more, and no way to improve their work (apart from code generation, which I strongly endorse here).
I like using the bind operator and avoiding computational expressions. Like Scott Wlaschin suggest.

But I've also found that sometimes traditional error handling makes more sense.

https://fsharpforfunandprofit.com/rop/

Yes, the messages! In Haskell, you would get the effect with a higher-level function that can add context, or something like <?> in Parsec, or maybe a custom function that calls catchError. In the continuation-passing style like you get in JS promises, you can do this with .catch(err=>...).
I might be missing something. Wouldn't the C# solution to keep the wrap messages be to add a MapError method? Something like:

    public ErrorChecked<T, E> MapError(Func<E, E> op) =>
        _value is not null
        ? Value(_value)
        : Error(op(_error!));
Used like

    var result =
        from thing1 in DoFirstThing().MapError(ex => new Exception("Failed doing first thing", ex))
        from thing2 in DoSecondThing(thing1).MapError(ex => new Exception("Failed doing second thing", ex))
        select thing2;
Which, maybe it's just me, but I think that looks cleaner than the Go code.
Yes, that is the solution; I'm complaining that it wasn't even mentioned as a possibility in the blog post.
Go's error handling is really easy to poke fun at. Is there a defense of littering your code with `if err != nil` branches?
In my experience, the code inside the `if err != nil` branch is often different. At least, when I write code.

It takes me longer to write Go code because of all the error handling stuff I'm putting in. Then it takes me less time to debug and fix run-time errors, because the error messages that my code generates are informative. I like this tradeoff for production code. This tradeoff is usually a bad tradeoff for throwaway scripts.

I have plenty of experience in other languages, including languages with exceptions and languages with enforced error checking (like Rust). My experience with these other languages has made Go feel a less clumsy.

For languages with exceptions, I often end up with code like this:

  SomeType aVar;
  try
  {
    aVar = SomeMethod();
  }
  catch (SomeError ex)
  {
    throw new OtherError("description", ex);
  }
For languages like Rust, I often end up with this:

  let aVar = match SomeFunction() {
    Ok(x) => x,
    Err(err) => return Err(OtherError::EnumValue(someContext, err)),
  };
(Yes, I know about map_err.)

And in Go, I end up with:

  aVar, err := SomeFunction()
  if err != nil {
    return nil, &Error{"some context", err}
  }
This is not meant as a statement about “the correct” way to write code, it’s just meant as an illustration of the kind of code I often end up with, in projects that I work on.

It depends on how you write code, what you're doing, and your approach to solving errors. Do note that you don’t HAVE to use the if err != nil approach in all of your Go code. It is not the only method for bubbling errors up. There’s a whole discussion to be had about when it is appropriate to panic()/recover() and why you would choose to panic() in Go, even though people say you’re “not supposed to do that”.

In my experience, the majority of the time, all you want to do with an error is to pass it on to the function above, while adding some amount of context to be able to later tell where it appeared in this function. Exceptions with stack traces (Java, C#, Python, Common Lisp - really anything except C++) give you both things for free: control flow stops, the function calling you gets the error, and information about where the error occurred is passed on.

At component boundaries you typically catch the error and perhaps convert it into a different type; and somewhere close to the user interface level you typically actually handle it in some meaningful way (retry operation, show error to user, cancel other work etc). Here by user interface I don't mean just GUI, but things like HTTP request handlers, terminal UI, RPC etc.

In most codebases that I've seen, error handling (as opposed to bubbling the error up with a bit of added context) is perhaps 1% of cases - whether in Java or Go or C. And in Java, this just means that 99% of cases will "handle" an error by simply calling a function, or perhaps the try-with-resources statement.

It would be still nice do "return if error" in one line say via some "macro" (I'd kill for Rust-like macros in go...)

    err!(err,"Connection to %s failed",serverAddr")
or even

    return ErrConnFailed{address,err} if err != nil 
instead of

    if err != nil {
        return "",nil,Struct{},fmt.Errorf("Connection to %s failed: %s",serverAddr,err)
    }
You can't even shorten it to one line because go fmt will "helpfully" expand it back to 3 lines...

I do think writing context (whether by error type or "just" text) with each error is the way to go vs "just throw it up the stack and hope for best" but Go syntax fails it in that quest.

> (Yes, I know about map_err.)

I am curious to hear more about why you prefer what you wrote to map_err, if you're willing to share. I have my own guess, but I'm always interested in things like this.

(For those that don't know, you could write the above code as something like:

  let aVar = SomeFunction().map_err(|err| OtherError::from(someContext, err))?;
or if it becomes too long,

  let aVar = SomeFunction()
      .map_err(|err| OtherError::from(someContext, err))?;
and in some cases you don't even need the |err| but since we're taking two arguments here you can't 'curry' it in a sense.)
The difference between those 3 examples is that the first two lean on the compiler for some nice rewards and the last one on the developer remembering to check the err variable.

Syntax wise it’s not much worse (until you start having multiple possible errors in a sequence when its ugliness scales linearly), but the problem is that you pay the 4-5 lines of boilerplate for zero rewarded guarantees such as “if the first thing failed the second can’t/won’t be attempted”. It feels like a lot of syntax cost and boilerplate boredom without much reward.

That is at the 'edge' of your code where you are calling some function and needing to translate to your code's error type. At all the intermediate layers of functions in languages with exceptions you write no code and just let it bubble up, while in Rust you just use `?` to send the error back up a level, while in Go you have to be verbosely explicit around every function call at every level.
I don't know why "if err != nil" is such an issue, as C programs have been doing the exact same thing for decades and it works just fine.
If we could add Monads to C to stop having to do that, we would. This would require so much revamping of the type system that it wouldn’t be C anymore, in a non-compatible way.

I’ve been writing C for at least 25 years, I am extremely comfortable with it and know most of its nooks and crannies, and I do it daily as my job. God I wish it had Monads just enough to stop cluttering my code with error checking boilerplate.

There are many things in computing and everywhere else that we have done for decades, before we got far along enough to be able to stop.

"works just fine"?

Like having a single global `errno` that you need to clear manually? Put this in a file `main.c` (or change the line opening `main.c` to a file known to exist):

  #include <errno.h>
  #include <stdio.h>
  #include <stdlib.h>
  
  int main() {
      FILE *fp;
      printf("%d\n", errno);
      fp = fopen("does-not-exist", "r");
      printf("%d, %p\n", errno, fp);
      fp = fopen("main.c", "r");
      printf("%d, %p\n", errno, fp);
      errno = 0;
      fp = fopen("main.c", "r");
      printf("%d, %p\n", errno, fp);
  }
The first `fopen` call sets `errno` to 2 (edit: really ENOENT, which happens to be 2). The second one should not have an error code but it's still 2 (ditto other edit). The third one demonstrates that the call does not, in fact, change `errno` since the file exists.
Dunno about "just fine" - I rarely use Go programs, but I found a bug in the one I work with the most which turned out to be precisely a failure to write "if err != nil". It just feels kind of embarrassing when we… already know how to prevent these problems.
The point about all these languages that came after C that have exhaustive pattern matching on a Result type is that there are better ways of doing things. Why have the user have to handle cases like this by remembering to put in an `if err != nil` when the computer can literally compute where it's not being used and yell at you or even add it for you?

Even in C it was not "just fine," it caused all sorts of bugs. But the bigger issue is that PL theory has improved since then, why be constrained to the same patterns as before?

It doesn't work fine if the check is omitted, which is easy to do, and if an error is extremely rare the missing check won't be noticed until much later.

For rare errors exceptions are an alternative, but something has to catch and handle the exception.

Ah, another “not a monad tutorial” monad tutorial.

This approach to teaching monads does seem like the more intuitive approach.

The main drawback is the same as teaching 1st and 2nd year university students about object-oriented design patterns:

The best patterns are motivated by the cost of not having them in complicated code. But you haven’t written enough complicated code yet to see the design patterns as solutions to any problems you have (had).

For this reason, learning about design before you ever got your hands dirty seems a lot like learning to swim on land. For monads, it surely seems like a lot of machinery just to implicitly keep track of state. Reaching an a-ha moment comes much later than reaching practical value from having them, which happens much later than writing purely functional code without monads. And how many people who takes a Haskell course are even already well-versed in functional languages without monads?

Sure, it is clean looking, but there is no actual error processing in this code. It is just "ok, it failed, not my problem, stop and go up the stack". If you have exceptions, that's the equivalent of not doing anything at all.

Sometimes, that's the thing to do, but overall, I don't think it is a good practice. Failures are as important at the "happy path". Connection failed? You really have to think about what to do. Use defaults or cached values instead? Panic? Print a helpful error message? Going up the stack and letting the caller deal with the problem is a possibility, a common one, but it has to be a deliberate choice. That's what I like about the go style of error processing: it is deliberate.

And indeed I find the criticized go code in the article to actually be better than the "clean" solution, because there are actual error messages. What you may end up with when you don't actually process errors are unhelpful messages like "unable to retrieve contact data: permission denied" because, for example, you couldn't read a configuration file because you don't have access to it. It happens when the developer just passed the system error up the stack, which the "clean" solution encourages.

Squinting, this is a very similar transformation to rewriting as a list comprehension, so "Not a Monad" is, technically speaking, not strictly true.

He knew what he was doing.

This is at the risk of courting GPT-fatigue, but this "what is a monad" question seems done to death. I'd ask here: For what is the following understanding of a monad insufficient?

> what is an example of a monad

"One example of a monad is the Maybe monad, which is often used to represent computations that may or may not have a result. The Maybe monad allows you to define computations that may or may not return a value, and provides methods for handling the absence of a value in a predictable and composable way. For example, you might use the Maybe monad to represent a computation that retrieves a user's profile from a database. If the user exists, the computation will return the user's profile; if the user does not exist, the computation will return nothing. The Maybe monad provides methods for handling both cases in a clean and declarative manner, without having to resort to explicit checks for null values or other error-prone techniques."

> what is an example of the maybe monad

...< some example code >

"In this example, the getUserProfile function uses the Maybe.fromValue method to wrap the user object that is returned from the database lookup in a Maybe monad. This allows us to chain methods such as map and orElse on the result of the getUserProfile function, without having to worry about null or undefined values. If the user exists, the computation will return the user's name in all uppercase letters. If the user does not exist, the orElse method will return the default value 'USER NOT FOUND' instead."

In this view, it's a function that returns a known but abstracted symbol of some underlying state. It sounds like the function true() is a monad in the sense that it returns or doesnt, but instead of TRUE, a generic monad could be something like isElephant, notHotdog, isSandwich, where the state represented by the monad can become an attribute (or a condition) of some other entity, depending on the order in which you evaluate the relationship between the monad and what it is an abstraction for.

I have either just won the "explain monads" quest, or failed quite publicly at it while writing something persuasive but misleading, but if I were to create an abstract state like isSandwich, could I be using a monad?

I don't think your explanation really makes it clear how list is also a monad, or how async is a monad. Edit: should have also mentioned IO here.

It also misses the point about what having support in your language for monads actually buys you - this is in fact what I've always missed: some examples of operations that apply to any monad, which would motivate why I want Monad to be a type in itself. The only example I ever grokked is Haskell's do notation, but that is not that compelling, being compiler magic and not something I could write myself too easily.

For me, the hardest part of understanding monads was convincing myself that the super-simple thing I just thought I'd understood was, in fact, correct, given the hype about how difficult they are to understand (and, indeed, a lot of the tutorials are weirdly-terrible)

Similar story for pointers, actually. And recursion.

"A monad is a monoid in the category of endofunctors, what's the problem?"
I am currently quite into category theory, and I've been thinking and writing quite a bit about what value it brings to me as a programmer. I think this article (and many articles) about category theory and the abstractions it "introduces" are missing an important point: abstraction is first a tool for thinking.

One thing often missing from "let's rewrite something with some design pattern" articles is that many different ways of writing something that on the surface seems like the same code are valid. If you are writing golang because that's what the team uses, you can be as extraordinary a functional programmer as you want, the `if err !=` style is going to be the best, simply because that's how your team communicates. If you are on a team of haskell programmers that all know how to instantiate a Monad typeclass, use Monad.

The value of knowing the concept of a monad is that mentally, you can think of both ways of writing the code as the same: chain things together repeatedly with the next step somehow being related to the previous step. When I write `if err !=`, which is my preferred way of doing this kind of error handling, I still think of it as a monad. If I want to refactor this method in terms of saying, passing in an object that counts the types of errors for the purposes of telemetry, I know that this is equivalent to lifting a state monad into it, at least abstractly. That I in practice then pass in a mutexed global object or actually use some liftM / monad transformer machinery doesn't change my thinking.

The way I think of abstraction now is that it has two directions.

One is seeing the abstractions, being able to boil down a concrete instance into a more abstract concept, by forgetting the details. By doing that, you then have the option to do some more interesting, powerful logic on it. You can use further concepts to maybe uncover new properties. You can shortcircuit complex refactors by mapping them to a simple transformation in this more abstract space.

The other direction is transforming your abstraction back into something concrete. You don't have to use a language that allows you to formulate things at the new abstraction level, although it's cool if you can. But you can totally write functors in C, in fact everybody does anyway. More importantly, when you do that transformation back from abstract-land to concrete-land, you can take a lot of shortcuts as a programmer. It's cool that you can do all kinds of fancy stuff with a pure function, but if I want to use a mutating function to encode my functor and it works in practice I can still think of it as a functor for most cases.

One concrete example I wrote about earlier today, which is the concept of a product in category theory. It's a very abstract idea, if you have these morphisms and these objects and you can do this and that then it is called a product. The usual approach is to then say "and that's what tuples and structs are" and moving on, but it falls way short of what I think this gives us. It personally allows me to see that if I have a function that parses a string into a date, it's fine for me to say, store a date as a string, because it's abstractly equivalent. If it sounds obvious, it's probably because that's an abstraction you have spent time already forming and internalizing, but probably didn't have precise mathematical language for it that now allows you to mine a huge swath of mathematical literature for further cool tricks.

https://write.as/mnmlmnl/the-value-of-category-theory-for-a-...

That's the same feeling I get after learning (a rather rudimentary but practical level of) category theory. How you end up looking at anything from a single function up to high level architecture and the same patterns emerge - that it's all about the maps between things, not really the specifics of the things themselves.
Why does the article use a syntax for type parameters in Go that is different from the actual syntax?
I can't find your email address to get it touch with you
The article is very good.

However, presenting this stuff in those languages makes it so obtuse.

There's a reason this is usually explained in Haskell or in regular mathematics. There's SO much noise otherwise.

3.4.1 is basically the definition of a (not "the") monad. But look at it! Still so much noise.

Personally, the only explanation I would need is this:

- Often, you need some state to be mutated as a matter of course while you are doing something in steps that you actually care about. But you don't want that state to be in-your-face.

- Therefore, create an abstraction "monad" that hides the state mangling, giving you the illusion that it's not there and the state just mangles itself (compare OOP).

- Then, have an operator to sequence state mangling operations. I'd just call that operator ";" from the C operator of the same name and similar sequencing effect.

- Also, unfortunately, you need a constructor to make a state mangling operation from a non-state-mangling-thing you otherwise have. This is necessary and the only reason you have to be aware of any of this stuff instead of your language being auto-monadifying (the latter you do not want in general for other reasons).

For example when you do writes to the console, it naturally depends on the order of evaluation what your console will look like in the end. But if ";" was not sequencing as it is (in C, too!), your compiler would be free to reorder your write commands however it damn feels like. So the semicolon is an active "thing" and not weird syntactic noise like C programmers like to pretend it is (and then they muddled the waters using it also for things that are not sequencing at all).

So how far does it extend? Well, for an IO monad, as long as you do IO operations, you have to be mindful not to reorder operands of the ";", or even evaluate the right-hand operand first (that would be bad--usually prevented by the right-hand operator being a function (of the respective state) still).

So a monad is a programmable semicolon together with the obvious laws you'd want for algebraic manipulation. That's all.

Now the only remaining question is how do you chain multiple things that need semicolons?

One possible and very popular way to do that is:

    write "hello" ;\_ ->
    write "world" ;\_ ->
    lift 42
that makes an operation that would write "helloworld" and then return 42 to the caller ("lift" being the monad constructor here--like Return in the article).

The "\" marks a lambda. The "->" separates the parameter from the body. Note that each body goes from the right of the respective "->" to the very end (after lift 42).

The names of the formal parameters here are "_" as tradition for "don't care"--but there's no special meaning of "_" at all for the calculus.

More interesting example:

    write "What is your name? " ;\_ ->
    read ;\name ->
    lift name
Or, equivalently (using one of the monad laws in order to simplify it):

    write "What is your name? " ;\_ ->
    read
This is kinda annoying to write, so you can introduce "do" notation (as a simple syntax-rules macro; or like "error_checked" in the article) so it's (very slightly) nicer to write. But of course that obtuses how it works.

The ";" is just the name of a regular function that you can define (per monad) and it will do stuff. Same for the "lift".

But neat that it works in other formerly-imperative languages now and actually looks halfway decent.

Definitions of ";" can be many and it depends on what you are trying to solve, but an easy-to-understand one is this ("a" and "b" are the two operands passed to ";"):

  \a -> \b ->
      \state ->
          let (result, new_state) = (a state) in
          b result new_state
  (to compare: the article has the semicolon abort if new_state != ok)
and the "lift" can be:

  \a ->
     (\state -> (a, state))
and "write"'s signature would be

  \a -> \state -> ...

  the result would be a pair of the value and the new state.
and "read"'s signature would be

  \state -> ...

  the result would be a pair of the value and the new state.
Seems like the author took two articles and stuffed them in the blender. Half the article is this somewhat interesting piece exploring functional programming techniques for error handling, and the other half of the article is a half-baked rant complaining about Go.

The half-baked rant complaining about Go really drags down the rest of the article.

> This style of if err != nil error-checking is, laughably in my opinion, celebrated as an example of the “simplicity” of go programming.

> it actually turns out to be an example of egregious type abuse

I don’t know how the author is indenting to come across, but these kind of comments in an article strike me as some kind of juvenile, playground mockery of Go. Getting a bit sick of seeing that kind of stuff on Hacker News.

I know some people really, really hate Go. There are some very passionate and enthusiastic Go haters on Hacker News. Trying to apply functional programming techniques in Go makes about as much sense as trying to use a hammer to drive a screw. You can talk about how much better screws are than nails all day long, and you can point at nails and talk about how they don’t have any threads, but that doesn’t make a good argument for saying hammers are bad tools.

> Trying to apply functional programming techniques in Go makes about as much sense as trying to use a hammer to drive a screw.

Shit, half the point of Go is that it makes doing things other than The Go Way painful enough that even devs on your team who are really really enthusiastic about <insert trendy but non-Go-like thing here> will think twice about trying to get their current pet paradigm or method into the codebase.

After a career of watching Javascript shift violently with the prevailing winds among the Cool Kids over and over since the 90s, this doesn't seem like a crazy aim for a language to have.

He is contrasting two idioms. I don't think this is a criticism of Go at all. Go is limited on purpose and the advantages of that are well known.