Hacker News new | ask | show | jobs
by cybrexalpha 670 days ago
I've been coding in Go for over five years. I like Go, but I don't love it. It's never my first choice, although I don't advocate for rewrites just to move away from it.

The tooling is a mess. Go modules still feel like a 'first pass' implementation that never got finished. There's no consistency in formatting or imports (even though Go claims there is). Generics are a good step but are still very primitive (no generics on interfaces, no types as a first-class object).

It still feels very unfinished as an ecosystem. I hope it'll get better as the Go team mature things, like iterating on generics. But I can't see Go modules continuing without a fundamental rewrite.

3 comments

Not sure what you mean by "no generics on interfaces"? https://go.dev/play/p/jGINeUt1JTE

Also echoing not sure what you mean by "no consistency in formatting or imports". It is increasingly difficult to use Go without gofmt getting run, since I have to imagine fewer and fewer people nowadays are using an editor that has neither custom support for their language nor LSP integration, and integration with gopls automatically runs goimports on save.

> Not sure what you mean by "no generics on interfaces"?

I didn't word this very well. You can have a generic interface, and functions on that interface can refer to generic types. But you can't have a generic method on an interface that uses a different generic type. For example you can't have:

``` type YieldThing[T any] interface { Yield() T DoOperation[U any](U) } ```

I figured based on your comment you meant something by it.

In which case I'd point out that it goes beyond interfaces and Go just doesn't permit that in general, for those who are playing along. All generics are always fully instantiated. You can have a

    type Holder[T any, U any] struct {
        Val1 T
        Val2 U
    }
but you can never have anything like a variable of type Holder[int], with a type-currying effect or something where the U bit is still unbound, or a bare variable of type "Holder" without any further parameters. All types within the language are always fully instantiated after compile.
Yeah I get it. I've found workarounds in the past. But it's always involved some friction.

In general, Go's generics are a huge step forward though. So I'm not that annoyed about it.

There are good reasons for not allowing generic methods:

https://go.googlesource.com/proposal/+/refs/heads/master/des...

Realistically the reasons are this:

> So while parameterized methods seem clearly useful at first glance, we would have to decide what they mean and how to implement that.

They just didn't want to decide what they mean or how to implement them.

Right, but that's because it is not obvious what they should mean or how to implement them. In particular, it seems that intolerable complications to the linker would be required. Contrast that with, say, a proposal to add a more concise syntax to Go for anonymous functions. Everyone can see exactly what that would mean and how it would be implemented.

I'm not saying that Go should never add generic methods to the language. But it's at least reasonable to hold off from doing so until these issues are clarified.

There is a concise explanation of the central problem in a comment in the issue thread:

> The crux of the problem is that with a parametric method, the generated code for the body depends on the type-parameter (e.g. addition on integers requires different CPU instructions than addition of floating point numbers). So for a generic method call to succeed, you either need to know all combinations of type-arguments when compiling the body (so that you can generate all bodies in advance), or you need to know all possible concrete types that a dynamic call could get dispatched to, when compiling the call (so you can generate the bodies on demand). The example from the design doc breaks that: The method call is via an interface, so it can dispatch to many concrete types and the concrete type is passed as an interface, so the compiler doesn't know the call sites.

Rust allows generic methods on traits but doesn't allow these traits to be instantiated as trait objects. Perhaps Go could do something similar. https://docs.rs/erased-generic-trait/latest/erased_generic_t.... But it's far from obvious to me that this would be a good idea for Go, and I'm glad the Go team haven't rushed into anything here.

What do you mean by no consistency in formatting? go fmt is a solid formatter that does its job
Go fmt is pretty good, but it's not ideal. My biggest gripe is imports. Go fmt will just sort imports alphabetically in lists that aren't separated by a blank line. Goimports will separate out core from 3rd party imports, unless you run it with the local flag then it'll add a third block of "local" imports.

But this spread means that it's not consistent across projects which style is preferred.

Some examples, based on cursory looking at big Go codebases: - Kubernetes, one of the biggest public-facing Go projects, uses the 3-block style https://github.com/kubernetes/kubernetes/blob/master/pkg/con... - TIDB uses 2-block style https://github.com/pingcap/tidb/blob/master/pkg/ddl/placemen... - MinIO uses 2-block https://github.com/minio/minio/blob/master/internal/grid/con...

In all of those cases if you make a change and just run 'go fmt' it very well could inject any new imports in the first block, which would be wrong and you wouldn't know until project CI picks it up.

It sounds to me like there are some people who don't follow the style, rather than there not being a consistent style.
Which of those options do you view as 'the style'? One block, two blocks (core library and others), or three (core library, 3rd party, same-source-tree).
The first rule of any style guide should be "adhere to the conventions of surrounding code." So the answer to your question is it depends on what the convention in the file (and surrounding files) has already chosen. If there is only one grouping and you're adding enough imports that you have to choose between making two or three, then I would say use two because that is what the canonical & normative style guide recommends [0], but if you're working in an organization that has chosen to use goimports or has alternative guidance in its org-specific style guide then go ahead and make it three groups.

[0]: https://google.github.io/styleguide/go/decisions#import-grou...

Try gofumpt, it’s my default.
After using rustfmt, I feel gofmt is not up to snuff. My main gripe with it is there are multiple formattings that are valid: there is not one true format. E.g. gofmt does not enforce line length which makes diverging styles possible, like for function declarations.
Funnily, 1.5 decades after Golang popularized formatters, in 2024 it is the only language that I work in that requires me to think about formatting. Mostly line length, but super annoying.
Not sure what you're comparing to, but Go modules are probably the best dependency management system in any language.
It does a lot well. For example it correctly pins dependencies by hash in go.sum. It's by no means the worst dependency management system I've ever used.

IMO the biggest miss with Go modules is conflating the identity of a dependency with how you get it. This means that renaming a repo not only breaks the module itself (as you self-import other modules in the same source tree using the full path), but all of your dependencies. I've seen repos be renamed from github.com/foo/proj to github.com/bar/proj as part of organisational reshuffles, and then there's a big warning somewhere that says "never make a github.com/foo/proj repo or it'll break GitHub's automatic forwarding for renamed repos, and you'll break every package that depends on us."

There are workarounds like using replace directives. But that makes an even worse situation where you can read a source file and assume a dependency is at github.com/foo/proj but actually it's elsewhere. But ultimately a real fix involves touching every single file that imports your dependency. If Go modules left the way of pulling a dependency in go.mod alone it wouldn't.

You should use a Go modules proxy to solve this, and a custom import path. But by the time most orgs realise they need this it's too late and adopting one would be a huge change. So you end up with a patchwork of import issues.

> But ultimately a real fix involves touching every single file that imports your dependency.

Why is that a problem?

For an internal-only dependency it's possible. But if you've got a lot of active branches, or long-lived feature branches, it'll create chaos in merge conflicts. Even worse if you've got multiple supported versions of a product on release branches (e.g., `main-v1.0`, `main-v1.1`, `main-v1.2`, and `main` itself for the yet-to-be-released `v1.3`) you either make backports awful (by only changing the import path on `main`) or have to change even more things (by changing the import path on the release branches too).

It's effectively impossible for pubic-facing dependencies. Imagine if https://github.com/sirupsen/logrus wanted to change their Go modules import path, for example to move to another git hosting provider. (Logrus is great by the way, I'm only 'picking' on it as a popular Go library that's used everywhere.) GitHub tells me that almost 200,000 Go projects depend on it (https://github.com/sirupsen/logrus/network/dependents), so all of them would need to change every source file they do logging in (probably most of them) in order to handle that.

GitHub seems like it's going to be eternal for now, but when the industry moves on in 10 years time every single Go project is going to break. This would be a problem for any source dependency management solution of course, it's not like any of the others are immune to this issue. But because Go has you encode the Git path in every source file you import it into, the level of change to fix it is an order of magnitude higher.

> GitHub seems like it's going to be eternal for now, but when the industry moves on in 10 years time every single Go project is going to break.

This isn't correct. The Go module proxy stores all of the module content so it's still available even if the original source is removed.

Between conflating imports and URLs, the weird go.mod syntax, and the nonsense that are GOPROXY, GOPRIVATE and such, yes, it’s a mess.

Honestly, aside from left-pad stuff, even npm is much better. I personally find cargo to be the best one I’ve tried yet. Feature flags, declarative, easy overrides, easy private registry, easy mixing and matching of public/private repos through git, etc. And like go it properly handles multiple versions of the same dep being used, but the compiler will help you when it happens.

Dealing with private repositories does suck, I'll give you that.
Exactly. What is he even comparing go modules with? npm and pip? The dependency disaster management which keeps breaking unless I create a new virtual environment for every project?