42 comments

[ 0.28 ms ] story [ 76.3 ms ] thread
Safari doesn't show the t's. Why?? Chrome does.
I commend Go for popularizing the channel-based concurrency, since I do think that that is a very elegant way of structuring stuff (especially compared to mutexes), but I have to admit that I don’t have a lot of fun writing Go.

I agree with a lot of the sentiment of this post; the weirdness with “tuples”, the weirdness of the error types, and etc. It’s not a “bad” language, just one that I don’t enjoy using.

Historically to get something similar to Go-style concurrency I would use Clojure with core.async, but more recently I have started using Rust and Tokio.

I wouldn't call tuples or error types weird, they're just poorly designed under the pretense of keeping the language (and first of all the compiler) simple.

It's not Go who popularized channel based concurrency, it's Erlang and Elixir.

Lack of enums are the main point for me.

The error story is not ideal but less bad than that most of the time, as you can downcast to access extra error data. Still, harder than it needs to be.

Overall, I've grown to like using the language even despite its warts.

I find the error story a horror one, mainly because it's quite easy to omit an error check when the err variable is reused which leads to weird crashes and garbage data, same happens when the error is explicitly ignored.
Go is a pleasure to use. The stdlib is one of the most complete, while keeping the keyword count low. LLMs understand it very well, project size stays low, line count stays low (if err nil included), doesn't need a bunch of scaffolded boilerplate in the project directory, and it compiles very quickly for a ton of OS and architectures. Very seldom do I ever need to go outside of the stdlib.

Is it perfect for everything? no. Is it the fastest compiled language out there? no. But, it'll do most things very well, and for me that's good enough. I choose go because when I need to make something, it steps aside and lets me build, and for that I have great respect and appreciation for it.

I will be defering all detractors and negative comments ;)

> The stdlib is one of the most complete

Coming from Java, I find the standard lib very small. A case in point is the collections library: arrays/slices, map, list, ring, and heap -- that's about as minimal as you can get.

If you delve, pun intended, into the stdlib you'll find out it's a pleasure to run away from it.
As someone who's been back into Go a couple of times (oscillating between Python, "C-ish" languages and the like), the ONE thing I hate about Go is error handling. The rest I can live with just fine.
One of the things I wish more people talked about isn't just the language or the syntax, but the ecosystem. Programming isn't just typing, it's dealing with dependencies and trying to wire everything up so you can have tests, benchmarks, code-generation and build scripts all working together well.

When I use modern languages like Go or Rust I don't have to deal with all the stuff added to other languages over the past 20 years like unicode, unit testing, linting, or concurrency.

I use Go where the team knows Java, Ruby or TypeScript but needs performance with low memory overhead. All the normal stuff is right there in the stdlib like JSON parsing, ECC / RSA encryption, or Image generation. You can write a working REST API with zero dependencies. Not to mention so far all Go programs I've ever seen still compile fine unlike those Python or Ruby projects where everything is broken because it's been 8mo.

However, I'd pick Rust when the team isn't scared of learning to program for real.

This is such a big deal and I wish more people talked about it in these types of blog posts.

I used to be a Python programmer and there were two things that destroyed every project;

- managing Python dependencies

- inability to reason about the input and output types for functions and inability to enforce it ; in Python any function can accept any input value of any type and can return any type of value of any type.

These issues are not too bad if it's a small project and you're the sole developer. But as projects get larger and require multiple developers, it turns into a mess quickly.

Go solved all these issues. Makes deployment so much easier. In all the projects I've done I estimate that more than half have zero dependencies outside of the standard library. And unlike Python, you don't have to "install" Go or it's libraries on the server you plan to run your program on. Fully static self contained executable binary with zero external files needed is amazing, and the fact that you can cross compile for any OS+ CPU arch out of the box on any supported system is a miracle.

The issues described by the original post seem like small potatoes compared to the benefits I've gotten by shifting from Python over to Go

10 years ago we started out with Python. We switched to Go probably 8 years back. I think my little startup would have utterly failed without Go. Thx google :)

But yes Enums are so much nicer in Kotlin vs Go. That's true, it doesn't impact productivity much, but he has a point.

(comment deleted)
Author is still early in their exploration and has some definite mistakes in here. Probably the biggest one is around the error handling, thinking that the only way to interact with an error is through the error interface itself. That is intended as a baseline interaction, a fallback for when nothing else is appropriate, such as just slamming an error into a log. If you want to interact with specific errors, you should use the various functions in the errors package [1] to check for specific types, and then use those specific types in whatever they support. Go error support is quite good; you can return an error object that says "The user was not found, the file the user was supposed to be found in was not found, and while trying to log this the log failed to accept the write" as various error types composed together, and any consumer of the error using the errors package can pick out the various bits of the error they understand using those tools without having to understand the error binding them together or the components it doesn't care about . You should never use string manipulation to check errors unless you have no choice, and whatever library left you with no choice should have an issue filed against it. It doesn't come up often, but it does sometimes come up; most recently I had the AWS SDK emitting an error I could only use string functions on, but I think they've since fixed it.

I don't like the term "enums" because of the overloading between simple integers that indicate something (the older, more traditional meaning) and using them to mean "sum types" when we have the perfectly sensible term "sum type" already, that doesn't conflict with the older meaning. If you want sum types, a better approach is to combine the sort of code structure defined here: https://appliedgo.net/spotlight/sum-types-in-go/ with a linter to enforce it https://github.com/alecthomas/go-check-sumtype , which is even better used as a component of golangci-lint: https://golangci-lint.run/

I'd also add my own warnings about reaching for sum types when they aren't necessary, in a language where they are not first class citizens: https://jerf.org/iri/post/2960/ but at the same time I'd also underline that I do use them when appropriate in my Go code, so it's not a warning to never use them. It's more a warning that sum types are not somehow Platonically ideal. They're tools, subject to cost/benefit analysis just like anything else.

[1]: https://pkg.go.dev/errors

I don't buy that example about errors at the end at all. The problem the author is trying to solve is to write code that calls a foreign/fixed-API function that is known to return a particular subtype of Error, and wants to extract structured information from it. But you can't, because the type returned is just the outer Error type that only has a string.

Obviously, though, the only way this happens is if you know a priori what actually failed, because otherwise it might be some other error type. And you don't, obviously, because it's an error! If your behavior is deterministic then just return whatever you want in your own API. If it's not, you need to parse/extract/inspect the error.

Basically it's entirely contrived. This never happens, and to the extent it does it's a terrible bug where you have code making assumptions about runtime error state without fully inspecting that state.

The second failure is that Go does in fact have runtime type inspection facilities ("type assertions" is the particular jargon) and if you want you can absolutely "cast" that error into a derived type to get the data out. So it's not even a problem in the language as it exists.

I'd encourage the author to spend more time learning Go. They've come to incorrect conclusions -- especially regarding errors. Read more of the stdlib to see how powerful they can be, e.g. net.OpError: https://cs.opensource.google/go/go/+/refs/tags/go1.25.5:src/...

  > The user now has an interface value error that the only thing
  > they can do is access the string representation of ... The only
  > resort the consumer of this library has is to parse the string
  > value of this error for useful information.
This shows a lack of understanding about the `error` interface, `errors.Is`, `errors.As`, error wrapping etc.

Personally, I think Go errors are fantastic, just the right sprinkling of structure on top of values. And panic/recover for truly exceptional circumstances.

> e.g. net.OpError

I have my own complain against Rust's error handlings, but I can't bring myself to praise what Go has been doing.

One problem with the error interface is that you can't know exactly which error type will be returned, and the Go authors may add new error types form time to time. `net.OpError` itself is a great example for that, which is a newer type than net.Error.

This style of error signalling is best used when the error handling is binary, either "No error, continue" or "Errored out, abort", but not branched out path like "If encountered error A, do this. If encountered error B, do that. Otherwise, abort".

Rust's error handling encourages such branched out handlings by default, but if you want to so the same in Go, there will be a nightmarish manual type discovery and tracking operation waiting for you.

So for me, I rather use a dedicated signal code to tell which error branch I should do next rather than relying on the error, i.e:

    if next, err := func(); err != nil { // If errored, abort it
        return err
    } else if next == condition1 {
        return op1()
    } else if next == condition2 {
        return op2()
    } else {
        panic("unsupported condition")
    }
I've only written a handful of production projects in Go so my experience isn't very deep, but I find the syntax to be the ugliest of any PL. The mixed capitalizing based on function privacy, to me, is awful (among other things, i.e. personally I loathe curly brace initialization/definition). I'm sure you get used to it? It doesn't help that it just feels very hacked together, from the wonky generics, the lack of useful types like tuples and enums, the bolted on module system, to the annoying error handling, to say the least.

That said, compile times are great, the concurrency is dead simple, it's performant, and it's still easy to be really productive in it so it's not like I'd never consider it. Many other languages have many of the same issues, anyway.

His example criticizing errors in `rootInfo` func is silly. There is utterly no need to do a `strings.HasSuffix(err.Error(), "not a directory")`.
> difficulty of writing if err != nil

Literally the simplest way to deal with errors (cognitively and character wise). Since AI autocomplete entered the scene, typing this repetitive (for a reason) pattern became not a problem at all (I'm not even talking about post Claude Code era)

> The only resort the consumer of this library has is to parse the string value of this error for useful information.

Well, no. See for wrap/unwrap functionality https://go.dev/blog/go1.13-errors

> In Go, errors are values. They just aren’t particularly useful values.

In his example author could easily use his `progError` type instead.

Gosh, why it's so tempting to write a post about bad language instead of just reading docs or article about idiomatic usage?

Correct me if I'm wrong, but the Error interface means you can display the error as a string, but you don't have to? Like the err.Error() is idiomatic if you just want to display something, but you could also use the error itself

https://pkg.go.dev/errors#As

here's an example that uses a field from your blog example error type: https://go.dev/play/p/SoVrnfXfzZy

Also "In Go, errors are values. They just aren’t particularly useful values"

...sometimes user mistakes are due to the language being too complicated, maybe for no benefit, but I don't think that's the case here. It's a very good thing that you can just slap .Error() on any error to print it quickly, and not too crazy complex to say an error can be any normal type that you can use, as long as it also can print Error()

Cracked at "I need to contribute my own beating so this horse is really dead"
I think I know why Go ended up without good enum support.

(Disclaimer, formerly worked at Google and used proto/grpc/go there and now in my own startup in github.com/accretional/collector which tries to address this problem with a type registry and fully reflective API. Not privy to the full history, just reasoning.)

Proto is designed so that messages can be deserialized into older/previous proto definitions by clients even if the server is responding with messages of a more recent version. Field numbers Re what let you to start serializing new fields (add a new field with an unused/the next number) or safely stop setting fields in proto responses (reserve a field) without risking older clients misinterpreting the data as belonging to some existing field they know about. This requires you to encode the field numbers alongside the field data in the proto wire format.

Two major problems: nothing in proto itself enforces that field numbers are assigned sequentially, because there is no single source of truth for the proto schema (you can still have one of your own, but it’s not a “thing” in proto). Also, the whole point of field numbers is that they can be selectively missing/reserved/ignored and allow you to deserialize messages without special handling for version changes in your code at runtime.

So, field numbers aren’t a dense, easily enumerable range of numbers, they’re basically tags that can be any number between 1 and 536,870,911 except for the reserved 19,000-19,999. This smells like serious tech debt/ a design flaw that completely closes the door for even fixing this at Google or anywhere else, because it’s arbitrarily in the middle of the range of field numbers and a leaked implementation detail from the internals. You couldn’t build your own dense field number management/sequential enforcement system on top of proto without ripping that part out, but your existing proto usage relies on that part and changing it would break existing clients because you’re removing field numbers, which is the whole fucking point of proto, and makes it difficult to roll out even if you did fix it yourself.

So, representing union/enumerable types in proto is impossible. For proto enums to have forward compatibility, they have to handle adding new enum values over time, or need to remove and reserve old ones. So, proto enums end up being basically just field numbers. That’s exactly what you see in Golang enums and I don’t think it’s a coincidence: Google has no good way to serialize/deserialize/operate on enumerable enums or union types anywhere they use proto/grpc. Golang inherits this “enums” implementation from protobuf because it’s the context in which it was created.

An interesting theory, however I rather suspect it is basically because Limbo had a similar concept for pseudo-enums.

I allowed (e.g.) a syntax like:

    M0, M1, M2, M3, M4: con (1<<iota);
That was taken from one of the Limbo papers.
One of the few languages I've known that didn't provide any standard features for libraries (until the sixth major revision), Scheme, has been the worse for it.
Error handling in Go is actually very nice. You do not have unhandled errors, not possible unless you really want to not handle the error. Now I even use it similar in Python, amazing how many errors I did not handle at all. But what got me into using Go is that there is no libc dependency, just system calls, static binary, that is just amazing. I can compile Go compiler in like few minutes. Rust I cannot even compile after 24h, I use rust-bin in Gentoo,luckily that exists.
Another tired error-handling take. It ain't pretty but it works.
(comment deleted)
Another languages that just “gets” concurrent right (imo) is erlang/elixir. I’ve done elixir for the last 3 years off and on.

Can someone with experience in both Go and Elixir compare the two? I’m sure I can have GPT whip up a comparison and see the syntax diffeeences, but I’m curious what the real experience “in the trench” is like.

Another comment already mentioned that the OP don't understand how to work with errors in Go.

As with any language, Go has its own philosophy and you have to first understand why it was created and what it brings to the table. To summarize it in one phrase "Less is more"

I think this post should be a mandatory reading for everybody learning Go. If this resonates with you it's possible you're gonna like the language:

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

Not shilling for Go here but there are a few misconceptions in this blog post. In addition to the others mentioned:

> All of these examples involve assigning to a constant a value known at compile time but none of them will work

Maps are not known at compile time. Hash functions are randomized based on a seed only known at execution time. The hashed value of "HELLO" is actually different each time the program runs. Even if the hash function weren't random, the runtime has to allocate buckets for map values on the heap, which involves calling the OS to get memory addresses for those buckets, etc.

In Go, `const` means "the compiler can completely evaluate this expression and store the final bytes in the executable," which has the effect of making them non-reassignable, but protection from reassignment is not an actual feature of the language the way it is in Java and C++ (goes back to the maintainers wanting to keep it simple).