Ask HN: What do you like/dislike about Golang?

42 points by nassimsoftware ↗ HN
What I like:

- Ecosystem. Very easy to install and use libraries.

- Can compile programs to single executables easily.

- Easy to work with (Very productive) while also running fast.

What I disliked:

- The syntax but I'm starting to like it. (It seems that Go's syntax is an acquired taste for me)

110 comments

[ 3.9 ms ] story [ 187 ms ] thread
Not too many complaints. I like it much better than Java backend.

Go and Typescript are now my go to for everything I think.

curious if you prefer Typescript or Go for backend development. My friend group told me they prefer Typescript because of shared code with client and ease of hiring.
I have a nodejs+typescript backend stuck in a limbo state because of the whole esm modules fiasco. I have outdated npm modules I can't upgrade because they are ESM only, and I have old modules that will likely never update.

It's to the point where I will only be using golang for backends moving forward.

I’ve heard the shared code argument before, but in practice the only shared code I’ve really ever seen used is shared type definitions. Now shared type definitions are huge, but they’re a solved problem using JSON schema, GraphQL, or some other interface definition language that don’t necessarily require the same language in the front end and backend.
The quality of the standard library, especially net, time, and to a lesser extent, strings.

The tooling, including the built-in testing package and test tool, and go doc. Also vet and fmt, if for no other reason than it precludes most arguments over braces and tabs vs spaces. I don't have cause to use it much, but pprof is a dream.

I like the idea of the go module system, and the fact that dependency management is part of the language ecosystem. It's still a bit clunky, especially if you end up, like I did, stuck with a system that has lots of libraries with poorly-thought out dependency relationships.

interfaces, especially single-method ones. But also the fact that your code doesn't have to declare it's implementing an interface, and has no dependencies on the interface declaration itself.

Love: - Ecosystem - Clean code - Single executable file

Dislike: - Pointers -- maybe I am still getting the hang of it. But I feel like pointers throw off a lot of beginner programmers. Would love some practical advice here

> pointers

Stick with pass-by-value and non-pointer receivers. Use pointers only if you have to.

I worked with one team who deliberately chose pointer receivers for everything. Their reasoning? The compiler can't know if the receiver for the call you are making will be nil at run time, so it doesn't complain.

Yes, they literally chose to subject themselves to runtime panics to silence the compiler.

For me Go is unique in that it can produce a fully static binary that can fetch from a HTTPS endpoint. It also supports many architectures. On top of that the package system and it's just way more convenient to use than other languages. Easy to learn, easy to put somewhere in practice.

Not every program has to be 100% safe and delivering 2M req/s. Sometimes you just want to build a program once and ship it.

What other language has that? Go isn't perfect though. It's startup sequence is long, the API towards C (and other languages) is quite bad, and if you build a fibonacci program and compare it with C it's 3x slower despite requiring no allocations whatsoever.

Static linking predates dynamic linking by several decades, hardly unique.
Wait, what prior programming language is compiled, can be statically linked, and has an https implementation in it's standard library?
fwsgonzo never said anything about it being in the standard library.
Well that's not on him man. You are the one calling him out for being unaware of static linking, when you don't even seem to understand the full set of pros he listed in his first sentience.
You're not replying to the same person. In a separate comment, I asked for a clarification on that specific part of their comment. The other pros, they're not part of what I asked about.

In the comment I replied to you, I'm just stating that you're bringing up something that's not in discussion.

Sorry for the misattribution, my mistake. Nevertheless, the fact remains that https is a part of the standard library and one of the elements that the op finds unique about Go. It is unambiguously part of the conversation, and the ostensibly negative comment that focuses on static linking is missing the point.

But I didn't even make the assumption that I was right on that understanding. I simply asked for more detail. Which was so that either I would learn something new, or the parent would.

> https is a part of the standard library and one of the elements that the op finds unique about Go.

Where do they say that it being part of the standard library is unique? I don't even see the words "standard library" mentioned.

EDIT: Look, this back and forth seems kind of pointless. This started with a statement about Go being unique for being able to be statically linked and talk HTTPS. If a language can be linked to compiled libraries, static or not, it's surely able to at least use libcurl, so I and the other commenter you replied to took it as Go is unique for being able to link statically. I assumed they meant, ignoring C/C++, static linking is rare, which I found interesting. I found other languages and asked for clarification. That's all.

I just addressed this question more directly on your other comment.

I do not agree that it is incumbent upon a speaker to anticipate the listeners knowledge, so I do not think it is a reasonable expectation that every qualifier be included. It's simply not practical. But I do think this is an interesting conversation you bring up.

The convenience of building network clients in Go is heavily implied. Strictly you can say that assembly code can make standalone binary HTTPS endpoints, but that would be a bit silly.

To be sure you might write such a program in assembly, but you’d be doing it for fun or aesthetics. No engineering manager would commission such a thing.

Though of course this is inviting the trading maniacs to tell stories of hyper optimized clients…

Ok, but it's not either Go or assembly. There's other languages. I listed 6 in a close-by comment. I wonder if Go's much more convenient at network clients than, say, Haskell or Common Lisp.
HTTP doesn’t need to be in stdlib. Java and Python each had one but everyone long since switched to third-party reimplementations with better APIs. And HTTP will probably be replaced over the next twenty years, like FTP and Sun RPC and CORBA before it.
> For me Go is unique in that it can produce a fully static binary that can fetch from a HTTPS endpoint.

What do you mean? C, C++, D, Rust, Haskell, and Common Lisp seem to be capable of static linking. (Admittedly Common Lisp seems to require a fork of SBCL that was written about last year, but still)

> that can fetch from a HTTPS endpoint.

Seems like a lot of replies are glossing over that clause. How many of those other languages can do that in a fully static binary that runs on (almost) any system that can read and launch that executable?

It's glossed over because it's trivial, isn't it? Just statically link libcurl through a binding. And at least Haskell also has Haskell-implemented HTTP and TLS libraries. There's even options.
Then why not assume it is the static linking that is trivial? Isolated each item is trivial in some context, its the exclusive set that is non-trivial, or at least uncommon.

The fact https is included in the standard library means that you can give a new user a hello world tutorial that includes producing a web server. It's a huge boon to productivity in a programming language to have a default path for such libraries.

I also work in C++, and it is infuriating the amount of time that must be spent sorting out the correct libraries for all the various aspects of an application one is not inclined to write themselves. People who don't fully grok the Go ecosystem overlook this cost when they claim that you can do the same thing in some other language. What they are missing in the subtext is the fundamental quality of life improvement.

> Isolated each item is trivial in some context, its the exclusive set that is non-trivial, or at least uncommon.

I'm not just taking it in isolation for no reason. If you have static linking, you basically have HTTPS by consequence.

> The fact https is included in the standard library means that you can give a new user a hello world tutorial that includes producing a web server. It's a huge boon to productivity in a programming language to have a default path for such libraries.

I think you're taking our discussion as if it were about if the language is cool or not. I never argued against that.

> I'm not just taking it in isolation for no reason. If you have static linking, you basically have HTTPS by consequence.

Can you clarify what you mean by this because to me there is literally nothing about static linking that implies https as a consequence. The point about Go is centrally not that it uniquely has access to an https library.

The point is that it is included. This may not at first appear all that noteworthy, but this is a substantive quality of life improvement. The standard library not only provides a vary large set of common functionality it is packaged with the distribution, works on all the platforms supported by Go without user intervention, and is bound in lock step to the release version of the compiler.

In Go that quote is literally true. If you do nothing on a new computer but download Go you can immediately build a statically linked application that can fetch from an https endpoint. There are no implied additional steps.

I don't believe this can be said of the other languages. I know that C, C++ and Rust do not include https in their standard libraries, so while they can be made to compile statically and use a library that provides https functionality, those are additional steps that must be taken by the developer, and it is the responsibility of the developer to choose the correct source and version of the https library to use. This will also include understanding and setting any additional compiler flags that the library may require, setting any optional defines or other library configuration settings and making the appropriate changes for every platform they wish to build for.

Go requires none of this.

(comment deleted)
1. Static linking. I'm astonished by how many languages expect the end user to install dependencies/runtime themselves, and something about it feels very impure.

2. The "go" keyword (and the whole goroutine system underneath it). Threads are clunky, somewhat unportable and generally unscalable. Goroutines just feel right.

3. Ability to use multiple major versions of a library. Diamond dependencies are an unsolved problem in many languages I've worked with, but with Go you don't ever even think about them.

4. Implicit interfaces. Having to write "implements X" just to be able to use a type always felt wrong. Implicit interfaces just make sense.

5. The gopher mascot. I just love that little guy.

1. Static linking. I agree with this, coming from JS to learning C, and having difficult time compiling things.

3. Implicit interfaces. I'm in love with this idea when I learned of it. however it wasnt obvious that this should be a thing. if you learned interface from other languages first

Good list. Regarding (4) Implicit interfaces -- to me, it's implicit interfaces that feel wrong :) Something having a method with a particular signature doesn't seem to me to be any kind of promise that it's intended to be used for the interface that expects that method.

'implements X', is a clear statement that yes, this method is made just for that specific interface.

But this is just academic. I've never actually hit a situation yet where something just happens to implement an interface by chance, and does something completely unexpected.

(comment deleted)
Here's what I do:

``` type Doer interface { Do() error }

type CanDoer struct{}

var _ Doer = &CanDoer{} // This ```

If CanDoer does not implement Doer, it will fail to compile, and you can quickly see what you need to do to fix.

I think the question is more in lines of "How do I know that CanDoer does what I expect of Doer to do, semantically?". In which case, it's impossible to know for sure.

For example, I could create a type with a `Read([]byte) (int, error)` method that doesn't conform to the `io.Reader` expected behavior (e.g. reads from the byte array and returns an int if byte array contains a numeric string), yet I'd be able to pass it as an argument to a function that accepts `io.Reader` - the type system won't stop me, but the behavior will be broken.

However, in practice, writing erroneous implementations of common interfaces is uncommon. Naming conventions are really important in Go, so the only way to abuse type system in this way is to abuse the naming conventions, which is already considered bad practice in any sane programming language.

Yep, this is exactly what I had in mind.
How do you ensure that a language with explicit interfaces implements what the expected behavior is?
It is a kind of honour system, the developer has explicitly stated, yes I want to comply to interface XYZ, instead of doing it by accident and due to copy-paste error passing the wrong one, ensuring a couple of debugging hours tracking down where the error lies in code that compiles without any issues.

Typical debugging problem in dynamic languages.

> the developer has explicitly stated, yes I want to comply to interface XYZ, instead of doing it by accident

To be honest, it's pretty hard to implement an interface by accident. You'd have to specifically implement methods with specific names and type signatures.

If method names + type signatures don't intuitively specify how should implementation behave, then it's either a bad interface or a trivial one.

This is, IMO, clear indication of a lack in the language
What I like:

- Easy to deploy

- Short learning curve

- goroutines

What I don't like:

- c bindings are not the best

- Error handling code gets old, but at least it's always consistent and never magical

Like: 1. Expansive Stdlib: it’s great to know there’s a working library I can rely on that will have long term investment for most things 2. Compile times are fast 3. Readable libraries. It’s pretty easy to folllow go library code Dislike: 1. Interface{} and gotos. They are easily avoidable in production code, but they open up some weaknesses in working with third parties. 2. Error handling. It should be a compile error not to handle a Retuned error without an _.
It's a very practical language, and I like its affinity for static binaries.

A downside is Google's approach to handling modules: https://drewdevault.com/2021/08/06/goproxy-breaks-go.html

This seems to only be a complaint about a particular (granted, default) proxy for downloading modules, and no criticism of the actual module system (which does not depend on a proxy) nor the idea of a proxy (which is pretty easy to run, especially when compared to most other languages' package repositories).

As to the criticism, if a public module isn't playing well with the default proxy, it's a good sign I don't want to use it to begin with.

> about a particular (granted, default) proxy

Yeah, but defaults matter. That's one of Go's big selling points, usually.

> As to the criticism, if a public module isn't playing well with the default proxy, it's a good sign I don't want to use it to begin with.

The concern raised here is more that things only work because the proxy masks problems. Also alluded to is that said default proxy is kind of awful to its upstreams, unnecessarily pulling things and burning bandwidth.

> The concern raised here is more that things only work because the proxy masks problems.

But making the modules available in the face of upstream breakage is a point of the proxy. I can still build when GitHub is down. I can still get the version of the source I expected when upstream rebases or retags. Those are reasonable defaults. And with one env var I can also turn that off and check that the upstream is still as I expect.

> Also alluded to is that said default proxy is kind of awful to its upstreams

This is debatable; sr.ht seems to want to host a lot of go repos but not have people pull a lot of go repos. I'm not sure that's a case worth catering too.

As someone who likes async/await/coroutines, I really miss them in Go. Channels are the GOTO of synchronisation, they lead to confusing spaghetti code because there is no structure.

Go is really nice to read (once you get used to the fact that half the lines of code in any function are for error handling) unless it uses a lot of channels. Then you need to take great care to understand every <-.

> As someone who likes async/await/coroutines, I really miss them in Go

Interesting. Most complain about languages with async/await and say they want goroutines. What is it about async/await that you prefer over Go's approach?

Not OP, but I am also in the camp of "like goroutines, not a fan of channels". Low cost threads are great - but channels can represent so many intermediate states, especially when error handling is involved. I wish there was a simpler default for coordinating goroutines

(Do we add an error field to the struct we send through a channel? Do we close the channel on error, or only on success? Do we make a separate channel to send errors on - and if we close that one it means for sure that there were no errors? If we select on the two dependent channels, do we have to make sure that they are in different sequential selects, so that we don't go out-of-order or deadlock?)

Async/await allows you to be much more explicit about dependencies. I don’t care what thread executes something, but I do care that e.g. these three network requests happen in parallel. With async/await I can explicitly use an ‘await Promise.all(a, b, c)’ where Go would need a new channel and goroutines, etc. all being implicit, so it’s hard for the reader to know what the intent is, and it’s error prone, who knows if you properly clean everything up.
This is a weird example as Promise.all and Promise.race are what are pretty easy to write in Go:

Promise.all is trivial:

    return <-a, <-b, <-c
Promise.race is a little longer:

    select {
        case x := <-a: return x
        case x := <-b: return x
        case x := <-c: return x
    }
What Go doesn't do very well compared to promises is composing; composing channels, especially with error returns, is usually tricky. But async/await are also bad at this. Passing around the actual promise/futures objects is the best way, which is for some reason discouraged today in favor of await/async noise in most cases.
Like: get things done mentality, green threads, duck typing, default formatter.

Dislike: still no easy way to dynamically create mocks -maybe some brave soul will try to solve it after generics :)- I don't like code generation or manual mocking, we have a large codebase, 1000+ file and so much noise in the repo because of generated mocks. And yes we are fan of unit tests and aiming 100% coverage on business logic.

What I like,

It is a nice language had it been released in the mid-90's, following the footsteps of Oberon and Limbo.

What I dislike,

Being designed a decade later ignoring everything that happened in mainstream computing since Oberon and Limbo came to be, then adopting features that weren't properly backed in from the get go.

Can you elaborate more on what was ignored?

For one, I think it's mind blowing that they didn't have a good system for dependency management in place from the get-go. Things are pretty good now with go modules but it's like they never even heard of languages like Python and the unholy mess that package management was (and still is to some extent) with Python.

Not mind-blowing at all. It's originally a language designed by Google, for Google, and Google famously uses a monorepo for most of their code. If you have a monorepo, package management is a non-issue.
The whole generics drama.

The const/type declaration dance to this day, to declare enumerations, even Algol could do better.

Error handling based on comparing strings, to find out the real cause.

> Error handling based on comparing strings, to find out the real cause.

I'm not a fan of Go, at all, but credit where due: they did make this slightly better with error wrapping (https://pkg.go.dev/errors#Is). Of course, this doesn't work unless errors are wrapped, instead of blindly concatenated, forcing you to do string matching more often than not. :)

Like: standard library and readability. The "If it ain't broke, don't fix it" philosophy that I miss from the earlier Python days.

Dislike: awkward error handling and testing / mocking

Pro:

- Community. Pragmatic, high signal to noise ratio in online resources, little magic, sympathetic to the needs of production operations. Consider for example httptrace [0]. This kind of introspection isn't even normally possible in many languages and their library ecosystems. In Go world, people care about this stuff.

- Standard library. High quality and flexible packages for stuff like web servers and clients, TLS certificate manipulation, SQL, various data encodings, etc.

- AST package, strong first-party support for manipulating Go source files.

- Table driven testing norm is very cool in certain circumstances.

Con:

- Verbosity. Something like making an HTTP request is ~7 different operations, each with their own error branches.

- The tedium of meeting coverage mandates. Having to code-generate tons of mocks and type out very elaborate test tables for what feels more like busywork than genuine confidence-building. Partly this is my employer's fault for having high coverage standards though.

- Despite its apparent simplicity, there are significant footguns lying around slices, loop variable captures, channel deadlocks, etc.

- The Go community's solution to a lot of language shortcomings (verbosity, type system, etc) is code generation. For example, we have an internal framework for parallel task graph execution, which is much safer and cleaner than manually managing channels and goroutines. But this kind of thing cannot be expressed as a regular library like it would be in other languages.

- Despite the prevalance of code generation, Go does not quite have a macro system, leading to third-party build systems like Bazel in companies that do a lot of code generation with their Go. This then leads to rough edges with the other tools' handling of "de-materialized" generated code and the need for stuff like GOPACKAGESDRIVER for proper editor support. Editor and debugger support for generated code, the way things stand, can be rough.

- Certain syntax rules such as trailing commas and unused variables/imports (especially when commenting stuff out temporarily to debug something!) can get on my nerves.

[0] https://pkg.go.dev/net/http/httptrace@go1.19.3

Below are my current feelings having worked with it as my primary language over the last 16 months. My previous 7 years of development focused on Java, C++, Python, and JavaScript.

Like:

- Standard Tooling: formatting coverage, dependencies, BIN install, versioning, vendoring all done in Go CLI

- Opinionated, minimal design: often I feel like there are fewer ways to do things in Go than in other languages.

- Readability: fewer operators and minimal language design make it feel easier to ramp up and read most go programs

Dislike:

- Hard to master: some elements are very unintuitive coming from other languages (interfaces). I feel like a lot of content on go.dev is out of date (Effective Go). The Google Style guide is helping to light a path and when they publish Go Tips I think it will get better.

- Too Minimal: in some cases it feels like Go went too far in not building language features (no set, use map[$TYPE]bool instead or map[$TYPE]struct{} for more efficiency but less readable imo)

Like:

- Ecosystem

- Static linking

- goroutines

- small and light runtime

- fast compilation speed

Dislike:

- missing shorthand lambda functions, making functional programming very painful

- typed nils

- billion dollar mistake (lack of non-nullable types)

- implicit/automatic copying on value types. Value types as a whole need to be re-thought.

- too much error handling boilerplate

- unused variables are compiler errors

- struct init not exhaustive

I forgot about starting with nil and ending up with an interface where every method will immediately panic, yet doesn’t equal nil because it’s carrying a type around. Reminds me of the HHGG box containing “no tea: just like the tea professional hitchhikers don’t carry!”
Having to use reflection and the interface{} (now 'any') escape hatch to see what fields are in an unknown JSON object "the pragmatic way"
Like:

- Compiles to a static binary that's relatively small compared to something like .NET AOT.

- Structural typing.

- No semicolons.

Dislike:

- Lack of exceptions. I can understand wanting to avoid exceptions for safety critical software like avionics, but for most application programming, requiring manual error handling for every function call is cumbersome and unnecessary.

- Too verbose / lacking features. For example, there's not a short syntax for lambda expressions, like the => operator in C# or JavaScript.

- Public / private access modifiers at the package level rather than the class level (there are no classes).

- Language development is driven primarily by Google, who has a history of killing products.

Like: The package and build system is amazing and just works. Simple, yet intuitive and powerful with the introduction of modules. It's hard to quantify the number of hours I've saved over other tools hunting down build/linker/dependency management issues.

Dislike: Error handling feels extremely verbose. I like the design choice to return errors explicitly, but it feels like there needs to be a return-if-error syntactic sugar introduced to avoid the endless if err != nil ... return err peppered everywhere.

Having worked with Go from the days of vendoring, to Glide, to Dep, it is really amazing to see Go turn its greatest weakness into one of its greatest strengths and most-loved features. It was worth the wait. Here’s hoping the rest of my gripes follow a similar arc.
> Like: The package and build system is amazing and just works.

I don't have much experience with go, but that's not at all my experience. I had the compiler panic on me for daring to run "go get", as described here: https://github.com/golang/go/issues/47979

This issue has now been closed, but there are several others open with the same symptoms. The response by a golang maintainer was:

> It seems likely that there is still a bug here, but the code is complex enough that we're unlikely to find it by just reading through the code.

Anecdotally, i've tried to fork the imported module to remove some dependencies i was not using, and failed miserably, as explained here: https://github.com/ooni/probe-cli/pull/986#issuecomment-1327...

I have experience with C/Rust import systems and i can say with 100% confidence that golang's module system is far from amazing.

Like:

-The go AST package was a pleasure to use

Dislike:

- Boilerplate error handling

- Excessive verbosity

- Multiple return not being of a type

- Duck Typed interfaces and the empty interface

- Go generate that makes verbosity worse

- Case-sensitive namespacing that leads to hunts for all the

instances

- Namespacing in modules that leads very large files

- Shadowing rules that let you shoot yourself in the

- Rules around =, := and var

- Slice tricks and for loops try to encompass iteration instead of something like list comprehensions

- Nullability and default values

- Short variable names to the point of being meaningless even in the standard library

- The go community's seeming insistence on a singular or idiomatic way to do things

- The standard library is missing basics

Edit: Added some formatting.

The simplicity is nice. It's one of the more readable languages. I particularly like that reflection has sensible limitations, so you can't produce the ridiculous frameworks that appear in Java and friends.

I wish channels and goroutines were more strict, so that you could have some basic assurance no race conditions can be present.

Defer is very convenient, which is a problem, as it is a source of bugs. It's usually used to ensure file-like objects get closed, but generally those return important errors when closed. A simple "defer file.Close()" will silently ignore the errors generated.

Likes:

- Big standard lib, including SQL base classes, allowing very consistent DB code. No 6 different ORMs and slightly different DB code for every DB type. Ahem Rust.

- Cross-compiling is easy and consistent

Dislikes:

- Error handling. Done to death, but it direly needs some syntax sugar like Rust's `?` operator. Seems like 3/4 of any function that does anything significant is `if err` branches.

- Nullabilty and the way references work feels like a footgun. Rust feels more clear about what's happening when. Not sure there's much to be done about it now though.

Without adding another comment about syntax or language specifics, I just like that you can get shit done and stay sane. I have fewer bugs in Go because it's easy to read and because adding unit tests is easy. Deployment is simple because it compiles to a single binary. I don't need to mess with libraries too much because the standard library is almost all I could have asked for. I can show code to other devs, and they understand it even if they don't know the language.

What I don't like is that Rust is also very intriguing to me :D