It’s faster than Node or Python, with a better type system than either. It’s got a much easier learning curve than Rust. It has a good stdlib and tooling. Simple syntax with usually only one way to do things. Error handling has its problems but I still prefer it over Node, where a catch clause might receive just about anything as an “error”.
Am I missing a language that does this too or more? I’m not a Go fanatic at all, mostly written Node for backends in my career, but I’ve been exploring Go lately.
I feel like I could write this same paragraph about Java or C#.
There are languages with fewer warts, but they're usually more complicated (e.g. Rust), because most of Go's problems are caused by its creators' fixation with simplicity at all costs.
Anyway, assuming you're talking about TypeScript, I'm surprised to hear that you prefer Go's type system to TypeScript's. There are definitely cases where you can get carried away with TypeScript types, but due to that expressiveness I find it much more productive than Go's type system (and I'd make the same argument for Rust vs. Go).
In C#, for example, there are multiple ways, but you should generally be using the modern approach of async/Task, which is trivial to learn and used exclusively in examples for years.
Regarding Typescript, I actually am a big fan of it, and I almost never write vanilla JS anymore. I feel my team uses it well and work out the kinks with code review. My primary complaint, though, is that I cannot trust any other team to do the same, and TS supports escape hatches to bypass or lie about typing.
I work on a project with a codebase shared by several other teams. Just this week I have been frustrated numerous times by explicit type assertions of variables to something they are not (`foo as Bar`). In those cases it’s worse than vanilla JS because it misleads.
https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals...
Given Python's substantial improvements recently, I would put it far ahead of the structural typing done in Go, personally.
When you write JavaScript (or TypeScript that gets transpiled), it's not as easy to assume the target is Node (V8). It could be Bun (JavaScriptCore), Deno, a browser, etc.
For NodeJS development, you would typically write it in Typescript - which has a very good type system.
Personally I have also written serverside C# code, which is a very nice experience these days. C# is a big language these days though.
Give me an apples to oranges comparison. With routing, cookies, authN/authz, SQL injection, cross site scripting protection, etc.
At the larger codebase go company I worked at, the general conclusion was: Go is a worse Java. The company should've just used Java in the end.
That’s the selling point for me. If I’m coming to a legacy code as that no one working wrote, I pray it is go because then it just keeps working through upgrading the compiler and generally the libraries used.
Python, for a number of years at this point, has had structural (!) pattern matching with unpacking, type-checking baked in, with exhaustiveness checking (depending on the type checker you use). And all that works at "type-check time".
It can also facilitate type-state programming through class methods.
Libraries like Pydantic are fantastic in their combination of ergonomics and type safety.
The prime missing piece is sum types, which need language-level support to work well.
Go is simplistic in comparison.
Even AST-based macro systems have tricky problems like nontermination and variable capture. It can be tough to debug why your compiler is stuck in an infinite macro expansion loop. Macro systems that solve these problems, like the R⁵RS syntax-rules system, have other drawbacks like very complex implementations and limited expressive power.
And often there's no easy way to look at the code after it's been through the macro processor, which makes bugs in the generated code introduced by buggy macros hard to track down.
By contrast, if your code generator hangs in an infinite loop, you can debug it the same way you normally debug your programs; it doesn't suffer from tricky bugs due to variable capture; and it's easy to look at its output.
If I hit a point where I need to do that in pretty much any other language, I'll cast about for some way to avoid doing it for a while (to include finding a different dependency to replace that one) because it's almost always gonna be a time-suck and may end up yielding nothing useful at all without totally unreasonable amounts of time spent on it, so I may burn time trying and then just have to abandon the effort.
In go, I unhesitatingly hop right in and find what I need fast, just about every time.
It's the polar opposite of something like Javascript (or Typescript—it doesn't avoid this problem) where you can have three libraries and all three both read like a totally different language from the one you're writing, and also like totally different languages from one another. Ugh. This one was initially written during the "everything should be a HOF" trend and ties itself in knots to avoid ever treating the objects it's implicitly instantiating all over the place as objects... this one uses "class" liberally... this one extensively leans on the particular features of prototypal inheritance, OMG, kill me now... this one imports Lodash, sigh, here we go... et cetera.
Go (and lots of other languages...) wreck it on dependency management and deployment, though. :-/ As the saying goes, "it was easier to invent Docker than fix Python's tooling".
I didn't really use it much until the last few years. It was limited and annoyiongly verbose. Now it's great, you don't even have to do things like explicitly notate covariant/contravariant types, and a lot of what used to be clumsy annotation with imports from typing is now just specified with normal Python.
And best of all, more and more libraries are viewing type support as a huge priority, so there's usually no more having to download type mocks and annotation packages and worry about keeping them in sync. There are some libraries that do annoying things like adding " | None" after all their return types to allow themselves to be sloppy, but at least they are sort of calling out to you that they could be sloppy instead of letting it surprise you.
It's now good and easy enough that it saves me time to use type annotations even for small scripts, as the time it saves from quickly catching typos or messing up a return type.
Like you said, Pydantic is often the magic that makes it really useful. It is just easy enough and adds enough value that it's worth not lugging around data in dicts or tuples.
My main gripe with Go's typing has always been that I think the structural typing of its interfaces is convenient but really it's convenient in the same way that duck typing is. In the same way that a hunter with a duck call is the same as a duck with duck typing, a F16 and a VCR are both things that have an ejection feature.
Agree on node/TS error handling. It’s super whack
What's important is how good primitives you have access to. Java has platform and virtual threads now (the latter simplifying a lot of cases where reactive stuff was prevalent before) with proper concurrent data structures.
AST macros are complicated, yeah, and I agree that any half-decent macro system needs a convenient way to dump generated code and so forth. I'm not saying any macro system is better than codegen. But a decent macro system will give you hygenic tools to modify an AST, whereas codegen really forces you to hack something together. I'll grant that there's some worse-is-better charm to codegen, but I don't think that saves it from being ultimately worse.
Writing a compiler is not "worse is better" and does not force you to hack something [fragile] together. Therefore, your argument is wrong.
Frankly, an official Go codegen library would solve pretty much all my complaints, but the only difference between that and a macro system is compiler integration.
type HorsePlay interface {
Neigh()
}
type SomeClass[T HorsePlay] struct {
players []T
}
func (sc SomeClass[T]) NeighAll() {
for _, p := range sc.players {
p.Neigh()
}
}
type Horse struct{}
func (h Horse) Neigh() {
fmt.Println("neigh!")
}
func main() {
sc := SomeClass[Horse]{players: []Horse{{}, {}, {}}}
sc.NeighAll()
}