3 ms·
The Go vs Rust debate is really tiring. Beyond releasing around the same time, they're languages built for extremely different purposes. Instead, I'm going to
by 015a 4y ago
The Go vs Rust debate is really tiring. Beyond releasing around the same time, they're languages built for extremely different purposes.
Instead, I'm going to lament about a few things I think would take Go from an extremely good language, to one of the best languages out there:
* Context. Context-creep is really annoying; similar to async in JS, it becomes something that over time absorbs your codebase and dirties function signatures beyond recognition [1]. Unlike promises in JS; for surprisingly low benefit. I don't for sure know what a better solution looks like, but my initial thought: the language runtime should have globals like setContext() and getContext(), which can set and get a goroutine-local context. If you want to pass a context between goroutines, just do it like you'd do it today.
* Enums. I will die on this hill: proper enums (think typescript and rust) are one of the most powerful programming paradigms in typed languages. Go not having them is one of its largest gaps; and leads to really weird obviously-should-be-an-enum things that instead get shoved into a set of constants with a common prefix [2]. What do you lose out on? So. Much. Beyond the obvious [3]; one of the coolest features of enum-prioritizing languages like Rust is exhaustive switch/casing; this is a huge feature in application development, and that's what Go is great at; except for this gap.
* Pointer-to-interface shenanigans, and how that oftentimes interacts with custom error handling. This is the WILDEST part of Go, and I am convinced that even extremely seasoned Go devs run into this, know that it does happen, try to avoid it, but can't explain why it happens. "Oh yeah, uh, nil, when coalesced into an interface, is nil, but, uh, also isn't, uh, nil, uh, when it comes to, uh, checking if its nil" [4] WHAT. There's no excuse for this; this isn't a feature, this is a bug, end of story. Its a bug that, for some reason, has lasted years; in the same breath that defenders explain why it happens they also say "oh, but, Go is industrial scale, its for large engineering teams"; this is not industrial, this is brittle.
[1] https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/s3 https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/s3
[2] https://pkg.go.dev/net/http#pkg-constants https://pkg.go.dev/net/http#pkg-constants
[3] https://go.dev/play/p/mc-0m_fakfJ https://go.dev/play/p/mc-0m_fakfJ
[4] https://go.dev/play/p/lIEmO9tG4iQ https://go.dev/play/p/lIEmO9tG4iQ
- jcrites 4y agoI haven’t written a lot of Go code, but I suspect I’d be extremely annoyed by its approach to error handling (as a result that requires explicit handling). In the vast majority of all Java programs that I have written, there is typically a single try/catch block at the very top level (and occasionally in low level IO code) - and none of the intermediate layers of application logic need to concern themselves with error handling (exception handling) at all. This is as it should be. The needless ceremony required to check and handle errors on every function call sounds tedious and I can’t see how it contributes to clarity. Try/catch is a blessing. Rust achieves a similar degree of ergonomics with its `Result` type and `?` operator. You can propagate the failure case of functions that return a result easily with `f()?;`. While it does require ‘annotating’ these functions by declaring them to return a `Result`, unlike Java, that explicitness fits well into Rust, and is trivial to compose using `?`. Instead of multiple if/then or match blocks, you can write `let x = foo()?.bar()?.baz()?` or similar. This also works with Rust’s `Option` type, and can be implemented for arbitrary types (as I understand it) using the `Try` trait. Simple monadic programming without the complexity present in other functional languages. And I haven’t even touched on the benefits of Rust macros. TDoes Go have anything like Rust’s `dbg!`, or its statically-typed `println!`, `format!`, etc.?
- Andys 4y agore: Context, I actually like having things as explicit function signatures, because it is very simply & abundantly clear at a glance which functions take a context. This helps reduce magic or unexpected behaviour creeping in. Agreed about the Pointer/interface/error shenanigans though.
- bsaul 4y agoAgree with you on all points, but especially on enums. The most surprising thing to me, is that typed enums look super orthogonal to all the other features of the language, look quite « easy » to add and certainly won’t make the language more confusing (like generics for example) Yet having a compiler be able to tell you « this code isn’t handling all the cases » is really really valuable.. Edit: about the error interface nil casting, i had this problem just 2 days ago, fortunately the linter detected the problem (not the compiler) with a weird « this code is never reached » error. I would have never guessed this behavior otherwise.
- 015a 4y agoIts EXTREMELY weird behavior. Like, in that example I linked in [4] above; just parsing over the code what you should expect to happen is nothing all that interesting; it should just run. Instead, what happens is, the error conditional on line 33 is triggered, which triggers the panic on line 43, but that's not even the end of the insanity, because due to the underlying value being nil even though it clears an if != nil check the PANIC itself PANICS while it tries to render the error. This isn't okay! Like, the other stuff I complain about or ask for improvements is whatever, but this is something that if any Go devs are reading, legitimately needs to be fixed, even if it means a language breaking change. There's no logical or reasonable explanation as to why that conditional on line 33 passes. There's definitely a reason; I don't care what it is, its a bad one. Its a surprisingly common situation to run into once you start hitting medium-sized codebases and custom errors; where you have some module returning a custom error, some other module returning its own custom error or just an error, and you need to coalesce them back into just a plain-old-error. The SOP at my 1000 engineer big-tech 90%-go-shop is to literally never specify custom error types in a function's return signature. Its too unsafe; you know, that thing that's supposed to make your code more safe, providing more information to the type system, yup it makes your code less safe. Return the custom error (concretely), specify 'error' in the signature, and put in a comment that it can return that error type for eventual type coercion. How embarrassing is that for the language? Reminder, team: its 2023.
- pjmlp 4y agoEven Pascal like enums would be progress, why are we forced to do the const / iota dance favoured by Rob Pike instead of type A enum (A, B, C) Or something similar.
- 015a 4y agoDon't even get me started on iota. Ok I'm started. Iota is a solution in search of a problem; a solution which was designed to replace far better solutions (enums) worse, and ended up solving practically no problems. Its issue is really obvious once you think about it and what enums are oftentimes used for: data serialization. In the face of a constantly changing code-base, iota doesn't make any guarantees that some enum key will always be equal to the value iota increments out. Today, UserStatusDisabled is equal to 1, but tomorrow it could be equal to 2 if someone adds other iotas around the code; and detecting this statically is difficult, because there are (very, very few, but present) valid use-cases of iota, and its impact on the resulting value depends on the order the statements are evaluated. This makes it functionally useless in any situation where an enum's value is serialized external to the application; e.g. in JSON structures returned to clients, messages published to Kafka, or database schemas serialized to SQL, which is a pretty substantial number of places where enums in general are useful, and also a list of things that seem pretty damn core to what a "systems programming language" would be used to do! Additionally, an integer contains no context; so an enum like "UserStatusDisabled = "disabled"" is an awesome thing to serialize in JSON to a client; but "UserStatusDisabled = 2" is a horrible, horrible thing. Obviously, technologies like gRPC functionally do act like this; but the schemas are a shared contract. Go variables aren't shared, and they aren't a contract. Which is all to say that my company also has a static linter that rejects any usage of the iota keyword. Go is just littered with mistakes like this.