* Go is "simple" (the language has a "small surface area", if you will)
* Go has a broad and deep standard library that is generally well-documented
* Go comes with a reasonably good general-purpose build tool (it builds, formats code for you, runs tests, etc.)
Some cons:
* Go's structural subtyping is a pretty terrible approach to handling the problem it's meant to solve. It's not quite as bad as a runtime failure when a 'type' doesn't implement a method (since that would be caught at compile time in Go), but it leads to what I consider an abusive over-reliance on ad-hoc interfaces that just get in the way of understanding the code (runtime or test code)
* Go's error handling method is inefficient and generally awful. I'm not talking about the verbosity (it's a relatively small issue, in my opinion), I'm talking about that in Go errors are just strings with a gross, inefficient library and system for adding context
* Go's generics implementation is, to put it mildly, inadequate; doing anything mildly interesting with it will be a cumbersome chore
* Go's handling of dependencies...leaves something to be desired
Can you give specifics? I think publishing module versions as entire subtrees is very verbose and can be cumbersome but otherwise enjoy everything about Go modules. I find most peoples complaints are that its not like <npm|pip|maven>
I don't like the proxy system at all. It is not, in fact, easy to set up a private proxy that "just works." Also, it is quite trivial to have "sync" issues with the go.mod and go.sum files that manage/"lock" dependencies. `go mod tidy`, `clean -modcache`, etc., are required far too often. And it has the same problem with dependencies-of-dependencies that python does. It leads to bloat and sometimes inconsistent behaviors in applications.
I haven't ran into that first hand. Is there a dependency management tool that prevents this? Cargo? (I don't have much experience with Rust yet FWIW)
* Compiling to a single binary is awesome
* Rich library ecosystem at this point
* Backwards compatibility is a priority so you can be pretty sure code you wrote yesterday will work tomorrow
More Cons:
* Unused variables and imports are a compilation error which is INCREDIBLY annoying during development (number one frustration with the language)
* If you work with a team or a legacy project, you will almost certainly encounter panics from a nil dereference at some point (or in my experience basically all production bugs were the result of one)
* If you work with a legacy project, early lack of generics encouraged copy/paste spaghetti piles
* Due to early lack of real generics, many popular libraries (such as ent) used codegen to make generic behavior possible. Generated code balloons PRs (unless you take care to quarantine them to their own commit and share commit ranges for diffs), and gets in the way of understanding code you care about.
It's easily solved with:
_ = unused_variable
_ "unused_import"
Sure, it's annoying, but not in an incredible way :) Before I knew about this trick, I used to temporarily delete or comment out all related code, which is indeed incredibly annoying.I actually like it, from the architectural angle. Say, I have an entity (class, service etc.) which does only one thing X and it does it well. For a certain scenario, it wants also to do Y, and it wants to delegate it to a different entity, because it's not its responsibility. It doesn't really care how it's done and who will do it, it wants to just delegate it. Why should it know or care if there's an existing interface in some package? That's an implementation detail. The consumer specifies what it wants by declaring an ad hoc interface close to its own definition, and as a result, there's no explicit dependency on a different package. Sure, there will be duplication if several entities in different packages want similar interfaces but, as they say, duplication is cheaper than the wrong abstraction.
>in Go errors are just strings with a gross, inefficient library and system for adding context
Not quite true: errors are interfaces, there's a common pattern to construct an error from a string (because most of the time, that's all you need), but no one stops you from using other ways to construct an error. What is inefficient about it? It's no different from constructing an exception in Java/C# etc.
>Go's handling of dependencies...leaves something to be desired
Can you elaborate?
Go's errors package is riddled with `reflect` and other inefficient code constructs. Printing an error (e.g. to stdout or the log) is fine. But if you want to actually do anything with the error in the code (e.g. to discriminate/branch based on the error), you have to resort to bloated types that implement a poor interface or else rely on string inspection. In either case, you are using the underlying errors package. In small scale applications it's fine, but when you're doing anything at even modest scale and your application encounters errors, it's going to introduce measurable performance degradation.
Can you give an example? I don't follow. I generally don't like abstractions for the sake of abstractions. I use Go's interfaces for a very specific reason: when I want dynamic dispatch, but with nice static typing guarantees. Interfaces in languages without structural typing force you to design a rigid, unflexible hierarchy/ontology well in advance, which only gets in your way when requirements change.
>is "contained" (i.e. not overly used)
Can you show why "containment" is necessary and why "overuse" is a bad thing?
And believe me: once you pass the first slope of learning (borrowing, lifecycles...), everything clicks just right and writing code becomes a very delightful experience, similar to what you do with [pick a trendy dynamic language of your choice].
And I think it's safe to say that Rust in 2023 can be considered mature and non-niche.
If you'd like sheer enthusiasm to help you make your mind, I recommend this guy's YT series on Rust:
https://www.youtube.com/watch?v=Q3AhzHq8ogs&list=PLZaoyhMXgB...