Why Go and Not Rust?
kristoff.it
kristoff.it
Certain things in Go are not easy (not simple), because Go has few language features (is simple).
Go has definitely a lower barrier to entry and is very productive for certain size and complexity of a project. However, as you start pushing beyond "scale" Go is designed for, Go becomes less simple to use. To appreciate things like generics, non-nullable types, no shared mutable state, you have to be working on a problem that requires these things, and only then these extra features pay off and make development and maintenance easier.
I wonder if that is really true, some really complex systems are written in Go and it does seem to scale well with that. Take Kubernetes for example.
Too true, I touch on that in a post I made here as well. In Go I often found myself having to reuse logic in functions that, in Rust, could be hidden behind iterators or custom iterator implementations.
While the Rust version is definitely more complex if you look at the implementation of the iterator; in practice you are just using the iterator - and that ends up meaning the problem you're solving keeps locality, and is easier to reason about. In my experience, at least.
The Rust iterator is a pretty simple abstraction in my opinion. It's essentially a lazy list. You see an iterator used, you know what you're getting. You see the map function used, you know it's the same as a for loop over all the elements. A fold/reduce is just a for loop over all elements with a result variable that gets returned. I really don't think using iterators is any more complex than a for loop, and with a for loop, you actually have to look through it to identify the pattern used and find out what it does. I'd say having to manually check for patterns is more work, but I couldn't say which one is more complex; maybe neither is.
I do it in my libraries when warranted.
The pattern is:
v := NewIteratedValue()
for v.Next() {
item := v.Item
// process item
}
if v.Err() != nil {
// process error
}
A variation of this pattern exists in standard library and third part libraries.I know that you meant implementing some sort of iteration protocol natively supported by the language, so that you can write:
for v { ... }
vs: for v.Next() { }
But you ended up making much stronger and untrue claim that one can't write re-usable iteration logic in Go.Your iterator has to be specific to a set of types — "v.Item" in your example is always of some specific type. Using an untyped interface{} would be worse than any alternative because every single use would have to use type switches and casting; you lose out of compile-type type safety.
Because of this, even if you have a local iteration system, it isn't composable beyond your package and its types. Given this (simpler) interface:
type Iterator interface {
Next() (Foo, error)
}
...then I can write a map function: func Map(it Iterator, mapper func(Foo) Foo)) Iterator
...but I cannot write a generic one that works with anything other than Foo. And if I need this: func MapFooToBar(it Iterator, mapper func(Foo) Bar)) Iterator
...then I have to write it just for that purpose.That's why there's no "iterator library" for Go, like there is with Rust.
Not at all. Are you telling me that `v.Next()` can be written in a reusable way for different data structures? Nonsense.
`v.Next()` will have a single, bound return type. So at that point you're re-implementing iterators for anything you want to iterate on.
Furthermore, lets say I want to `v.Next().Map()`. How can I write a Map function in a reusable way? The entirety of your iterator chain would have to be custom implemented for the one data structure you're using. Or you throw your entire type information away and use `interface{}`.
With Go, your limit of reusability is hand writing the entire iterator implementation per type. Which is hardly reusable in my view. I touched on this when I mentioned creating separate functions for these patterns. That's all you're doing here - creating custom methods to make something look like, but not behave like an actual iterator. No Map support, no Filter, no .. anything. All you made was a loop, and a hand written one.
I'm not trying to be overly negative here. However saying Go has an iterator pattern is imo strongly misleading.
The difference is in speed of producing such code, amount of bugs introduced, ability to understand, modify, and evolve the code, etc.
What would you call a language that forces you to write / use a preprocessor to introduce higher-level features?
Low-level. More specifically, low-level for the domain where you're working. C is admittedly low-level because it had to be minimal even for 1970 and close to the metal. Other languages usually enjoy less drastic design constraints.
I let you research when it was released.
You can can implement this generically in Go using interface{} types and runtime type checking, but then you have runtime type checking failures.
A java/c++-esque "generics" implementation would be able to type-check at compile time.
Go has the most useful containers built in. Most code I write doesn't need custom containers, even in languages with templates (I write far more C++ and Java than Go) -- so I find that I don't miss them much when I spend time in Gopherspace.
I suppose it's not sorted or concurrent-safe?
`map[T]struct{}` is only a type, it's not an implementation. You still need a handful of methods, that might be named inconsistently in each implementation by different developers. I will be happy when I never have to think about this again
Set storage is not really the concern here, it's the set-theoretic operations which make sets useful (generally speaking, there are cases where all you need is the set being a set e.g. deduplication).
So I wouldn't be surprised that GP found several different implementations of union, intersection, difference, symmetric difference, subset, superset, …
That means for every conceivable input type and output type combination you have to have a new MAP or REDUCE function to handle it.
All Modern languages except golang and elm only require one implementation of MAP that can handle all type combinations.
It's called parametric polymorphism.
I seem to remember an article being written a while ago that found Java's type system, with the addition of generics, was unsound. Maybe that's what you're trying to say.
You can do exactly the same thing in Golang. Just use interface{} everywhere and add few casts where needed. That is Java generics.
That's not true, Java generics are not just syntactic sugar.
> They don't add anything substantial.
They add parametric polymorphism and type safety.
> You can use Object (or another bound type) everywhere you're using generic type.
Only if you go out of your way to do so, and ignore warnings. This has less to do with how generics work in Java and more to do with the fact that Object can be explicitly cast to another type and vice versa.
> You can do exactly the same thing in Golang. Just use interface{} everywhere and add few casts where needed.
No, it's completely different.
Go makes you implement max and min in all your codebases.
Traits is a way to solve it. But the problem is that in Go types are "too open". If you wanna constrain it more traits is not an elegant solution.
Generics is better in this case.
BTW: This is how is in Rust. Rust have traits but you fill the rest with generics.
I made a mini-go database command liner that I call from rust (so I get access to drivers that are not yet available in rust). Is much more boilerplate from some stuff that in the rust side is just Value<T>.
And type errors that Go have that rust don't.
I think that Rust & Go sharing some(most?) design principles (about how model types) make easier to see what each side make harder...
Generally it makes more sense to describe an interface that can be satisfied by different types, and writing logic to handle that interface instead. Go may not have traditional generics but the interface pattern is certainly a kind of generic programming that can be used to achieve some powerful results. Because structs can satisfy many interfaces and interfaces can be satisfied with any kind of struct (if you want an in language example check out the io.Writer interface and the many functions that use it) you can end up with highly reusable code despite not having traditional generics.
One example is to look at how Go handles the Image package vs other languages with generics. https://golang.org/pkg/image/
In Go, I believe you need to give up type safety to write these implementations, by using Interface{}
You can get away without them in many cases, but if you are working on stuff that you want to reuse, generics are the way to go. In combination with Rust’s traits this is especially cool.
Then I ran unit tests, and sometimes a weird error cropped up. Turns out I made an error in an append: instead of concatenating two arrays, I added the second array as the last element of the first. That was a time consuming bummer.
This problem could have been prevented by two approaches: copying every function for every type, or generics.
Go is a nice language, which makes some tasks easy to implement, almost fool proof, but it still lacks in other areas.
Enumeration types in rust can be the usual ones you find in other languages:
enum UserStatus {
Anonymous,
Registered,
Confirmed,
}
But their variants are not limited to names, they can be other things[1]: enum UserStatus {
Anonymous,
Registered{ email: Email, date: Date }, // a struct
Confirmed(date), // a tuple
}
Enum types can also be made generic. For example the Date type could be a parameter in the example above: enum UserStatus<T> {
Anonymous,
Registered{ email: Email, date: T }, // a struct
Confirmed(T), // a tuple
}
Which brings us to the Option type[2] in rust. It is an Enum with a generic type attribute, defined like this: enum Option<T>
{
Some(T), // a tuple with a single item of type T
None,
}
In rust, when a function says it returns an `i32`, it will always return an `i32`. It never returns `i32 or null`.If you want not return an `i32` sometimes, you must specify exactly what else it can be. It is often convenient to return an Option<i32> instead. Then it can return `Some(i32)` or `None`. The difference with the "implicit null" from before is that the compiler will force you to "deconstruct the option safely", in compile time. You will never get a "Null Pointer Exception" in run time because of this.
For me, this is huge, and one of the reasons I like Rust more than Go.
[1]: https://doc.rust-lang.org/rust-by-example/custom_types/enum.... [2]: https://doc.rust-lang.org/std/option/
That said, I wanted to talk about Go for enterprise development, and I did explicitly made the point that some abstractions are in my opinion detrimental to the reality of enterprise software, and I suspect generics might be one of them. It's a big topic and I honestly don't know the definitive answer, but I just wanted to point that out. I do agree that non-nullable types are good and a big hole in Go's repertoire.
It seems to me like the final section has a lot of bad things to say about the methodlogy of a go developer. Huge, bloated toolchains required to make progress. Code volume rather than appropriate abstraction. To me, your MS Word vs LaTeX graph sure seems like it's damning "easy" languages like Golang in favor of other languages with more powerful abstractions.
You also assert that Golang is easier to learn than other languages. I don't think you've really got a leg to stand on there. It's "easy" for folks doing service development because the arbitrary decisions made by Golang were made from the perspective of someone experienced at writing web services. If you already know C and Python a bit, Golang cherrypicks a lot of good stuff, but if you don't (or you're not writing a bunch of web services that essentially do nothing but punt to C frameworks and validate strings in request headers) then Go's going to struggle, and folks have pointed this out.
It's pretty surprising to me that you write about how Generics are the death of enterprise software when so much quite-usable software uses generics. Why do yo simply get to forget the existence of the huge body of Java work and instead assert it's bad? Android uses generics in many cases where appropriate and it's one of the better UI kids available these days.
Even the Actor methodology you're citing as part of Golang as good is actually a fairly sophisticated abstraction with lots of implications for the runtime and execution order. Why is that specific programming abstraction given a pass because of its benefits, but writing a generic linked list is going to be the death of your programming organization?
You also defend the structuring of many enterprise groups even as you suggest that they lack training, refuse to pay technical debt, and place unreasonable burdens on developers. You seem to have just accepted this and said Golang helps you be more complicit in this mode of operation that you also seem to suggest is somewhat bad. Why would we want to pander to a methodology that asks junior developers to proceed without training and places focus on process and hyper-specialized domain experts rather than clear communication, sound architecture and sustainable velocity?
I'm quite confused how you can hold both these opinions at the same time. It seems like they're contradictory.
Go is easier to learn than C# (I'll omit Java in this comment as I have more experience with the former), but that doesn't make Go an "easy language". The name itself implies that mastery doesn't come immediately.
The powerful abstractions that you mention, including generics, are indeed good things, but pardon me but I suspect you've never seen how badly and easily they can be abused in certain environments. You've probably seen the Factory<Factory<NaturalNumbersFacade>> joke somewhere, real live enterprise software is sometime like this, but unironically.
> Generics are the death of enterprise software
That's an hyperbole, I've never made such strong statement.
>Even the Actor methodology you're citing as part of Golang as good is actually a fairly sophisticated abstraction with lots of implications for the runtime and execution order. Why is that specific programming abstraction given a pass because of its benefits, but writing a generic linked list is going to be the death of your programming organization?
Goroutines and channels are simpler than the threaded multiplexing that C# does with its stackless coroutines. My argument is mainly related to the many ways you can mess up async/await in C# vs the 2-3 ways you can deadlock in Go. I link to a talk, an image and a blog post related to the subject.
> You also defend the structuring of many enterprise groups even as you suggest that they lack training, refuse to pay technical debt, and place unreasonable burdens on developers.
I don't defend nor encourage this, but that's what happens in real life. It's the result of many factors at play, some of which I described in the post, some of which even I don't understand. If I did, I would be way more rich :)
You can refute my assessment of reality, but "being complicit" is not exactly appropriate when 80% of the enterprise consultancy jobs out there are like this.
> Why would we want to pander to a methodology that asks junior developers to proceed without training and places focus on process and hyper-specialized domain experts rather than clear communication, sound architecture and sustainable velocity?
You can't change how things work until you understand why the work the way they do (or at least you can't reliably change the world unless you do). Refusing the current state of things is step 0 of N, and I've learned in my experience that change in big systems comes incrementally, you can't "distrupt all the things", so Go is (in my opinion) a step in the right direction.
I would recommend you talk with somebody you know that has this type of work experience, they will be able to convey to you how these things work better than I can, probably.
Isn't that more of a criticism of enterprise software than generics? If they can write ugly generic code, they sure as hell can write ugly Go code as well. They're just different different programming styles, and while some like one others prefer the other. Even when you code in ASM, you'll probably end up writing generic code down the line. You'll just be managing it manually rather than have a compiler, preprocessor, or templating engine do it for you. It's a preference, and I say to each their own. Also, the whole moralizing holier-than-thou simplicity and anti-abstraction talk is getting kind of annoying, but then again, there's lots of moralizing people in the Rust camp who are just as annoying.
Absolutely yes! My whole argument is restricted to (how I experienced) enterprise development.
People complain that Rustaceans are insistent to the point of being obnoxious about Rust. But I insist that Gophers turn post-hoc rationalization into an art form. Things are bad because Go didn't do them, if they were good Go would have.
I'm using "easy" in the sense of "easy" vs "simple". I think that the modern "commonly in-use" parts of C# are about the same size as Golang, to my sense of scale.
> , but pardon me but I suspect you've never seen how badly and easily they can be abused in certain environments. You've probably seen the Factory<Factory<NaturalNumbersFacade>> joke somewhere, real live enterprise software is sometime like this, but unironically.
Pardon me if I think it's profoundly disingenuous for you to conflate factory patterns (which can exist in literally any type system and language) with Generics, and let me again positively beg for pardoning if I come across thinking you don't really understand the Golang argument for generics if this is your go-to example.
> That's an hyperbole, I've never made such strong statement.
Perhaps, but you have lumped generics in with a group of features and said, "The Go community regards as anti-patterns many abstractions regularly employed by Java / C#" and essentially intimdated that Generics are almost never good.
Further, your rhetoric carefully partitions everyone else's abstractions as risky anti-patterns while below you carefully rationalize Go's abstractions as in fact good. Your argument there essentially boils down to taste and fear.
> Goroutines and channels are simpler than the threaded multiplexing that C# does with its stackless coroutines. My argument is mainly related to the many ways you can mess up async/await in C# vs the 2-3 ways you can deadlock in Go. I link to a talk, an image and a blog post related to the subject.
No, they're not universally so. Firstly, actors and threads define equivalent systems [0], they simply have different tradeoffs within that space. There are some constructs where actors are easier (e.g., when a process maps well to an individual loop consuming a mailbox) and some where they simply are not (e.g., when spinning over shared memory and hoping to pull out a copy to another space). What's more, there's an awful lot of progress on the shared space model by attacking the memory coherency problem.
It's quite possible to build systems that are as resistant to deadlock as Golang using threaded models. They're also amenable to static analysis. There's also a very large and useful body of research on using structures that are unopinionated about the order that they receive updates in (CRDTs are a good place to start here), making the strict linearization of actors unnecessary and even a performance bottleneck sometimes.
You're either unaware of it, or you're uninterested. I don't know, but if it is the former then you should probably keep up with what's going on there.
> I don't defend nor encourage this, but that's what happens in real life. It's the result of many factors at play, some of which I described in the post, some of which even I don't understand. If I did, I would be way more rich :)
You are encouraging it though. You're saying we should use tools that are designed to accommodate it. That's literally baking this mode of operation into our automation at a fundamental level. And as you've implied, once there it often takes monumental effort to get it dislodged.
> You can't change how things work until you understand why the work the way they do
That's my line. The way it works is people suggesting that there is no other way it could work, and then baking these assumptions deeply into their corporate structure.
> I would recommend you talk with somebody you know that has this type of work experience, they will be able to convey to you how these things work better than I can, probably.
Hi. I'm Dave. I'm a SRM at Google right now but I've also been a Director for Capital One, worked in software at numerous companies including Microsoft and about a dozen startups in technical and advisory capacities, and founded (and sold) my own startup. I've been in management in one capacity or another for nearly a decade.
And a lot of my time spent when I'm not working directly on projects is advocating for developers to be more empowered, receive more training, and have the power to actually set and run a sustainable pace, even if that means a slow start to burn away the technical debt in place.
But thanks, I'll keep that in mind.
[0]: "On the Duality of Operating System Structures", by Lauer & Needham http://web.cecs.pdx.edu/~walpole/class/cs533/papers/duality7...
I suspect the larger factor may simply be familiarity. It is very rare to see a balanced piece about some programming language - let alone one about two programming languages - where the author has an equal and substantial amount of experience with both and is really able to compare them on their merits.
Async and await in C# may be wonderful to some, but they are horribly broken to others.
I disagree they're broken.
C# 7.1 introduced async main. No reason to call Task.Result in console programs.
Even before that feature arrived, visual studio debugger could resolve that literally in a minute. Press F5, reproduce deadlock, you'll see exactly what's locked and why.
I'm sorry, this is a terrible argument. That's definitely not a common usage of Generics.
The common enterprise OOP idiom that is mocked is NaturalNumbersFacadeFactoryFactory, which use no Generics at all, leading to a large number of classes.
Ironically, if one used generics to replace factories like you did in your example, you would be able to replace hundreds or thousands of Factory or FactoryFactory classes in an enterprise application with a single Factory<T> class. Of course, that would probably be pointless. There's a reason multiple factories exist in a program, even though there are ways to replace them with something simpler.
I get it that Go programmers don't want the language to be complex, but Generics themselves don't have to be complex. They solve a lot of problems in a simple way and are MUCH easier to use than ad-hoc generics made using interface{} + Reflection.
It is very common indeed in the three large Enterprise Java houses I've worked in over the past 15 years. FizzBuzzEnterpriseEdition is the reality in all three of those places. Not only is it reality, it is requirement.
You're just making my point for me.
new TypeReference<FooResponseCollectionResource<MemberUpdate>>(){})
I like generics, but I don't love this sort of thing and it's not even a mis-use of thembut then you loose type checking and some people don't like to loose type checking...
The problem in those cases always the abuse of often obsolete OOP patterns and misnamed classes.
using NiceType = TypeReference<FooResponseCollectionResource<MemberUpdate>>
// ....
new NiceType ()Of course you can mix generics with terribly named classes and OOP patterns, but that's hardly a problem with generics themselves.
We could say it's good for Web and Services Enterprises (probably) but for a different kind of an enterprise it can vary, depending on their business model and requirements. As the significance of it's features can change.
Tldr; All languages have a place(IMHO), Go is a simpler language with reasonable value in the current industry for various reasons.
and I would argue there's idea that simplicity of the instrument/tool is not a problem in itself and we shouldn't lose it in pursuit of 'ease'
Go was designed at Google to be used internally at Google, so frankly it's hard to imagine commonly pushing past the "scale" Go was designed for.
For me, scale means more micro-services. For another person it means a bigger monolith.
It's not scale about number of users or data, it's complexity scale.
Last enterprise app i worked on had (provided) something like 10.000 different api calls (and yes mostly because of bad/wrong initial design decisions), and over 50k different possible queries that it could run against database. (my job was to "make database go faster")
Just running unit tests took longer than 6hrs (of course most unit test went out and connected to the database, because why not \s).
It all could be replaced with dozen or so 10k to 30k lines go projects each, that would be a lot easier to maintain and to scale out.
I know Go and Haskell equally well, and I get just as much shit done in Haskell and don't have to be a copy-paste machine sometimes. There are many benefits of Haskell and like-minded languages besides being "beautiful."
Feels like if the Go designers had the sense to include parametric polymorphism and sums (aka proven features from the 70's whose main "downside" is being weird to Go's target audience), it would be a great middleground.
Missing polymorphism is a valid complaint but it hasn’t had the impact I thought it would.
Meanwhile, some of the programs I had written in Haskell I’ve rewritten in Go, e.g. due to problems with the use of finalizers (causing crashes!) in Haskell. You can chalk this up to poor library design but it simply isn’t a problem in Golang, which seems to avoid the finalizers more than other languages.
I’m going to continue using both Go and Haskell, I’m happy with both.
I've never run into finalizer issues (or finalizers at all really) in Haskell but I'm sure they exist. What libraries in particular used finalizers that you had issues with? In general though I find resource management much easier in Haskell thanks to bracket, ResourceT, and the (imo very good!) exception handling stuff. Async exceptions in particular are a surprisingly nice feature when combined with threading!
The biggest place lack of parametric polymorphism hurts is in concurrency. I have to hand-roll so much concurrency in Go and I don't really have a good option for abstracting over various patterns.
> I'm going to continue using both Go and Haskell, I'm happy with both.
Yeah same here. I'm glad to have them both in my toolkit.
None were academia & most were commercial enterprises (that most people have heard of)
That's not true, expect in a pedantic way. Google was "designed at Google to be used internally at Google" only in that:
(a) a few people at Google, on their own, designed a language (mostly based on an older Plan 9 language some had helped built) - not at the request of Google execs, nor as an explicit company-mandated project to create a language to solve Google's problems. It was almost on of these "20% spare time" things.
(b) these people added the features that they thought, as far as they were concerned that would be nice for programming Google style stuff. Those were based on their ad-hoc (and quite idiosyncratic) intuition and personal experience, and not some special research into programming at scale, or from involving the company at large at it.
Go was designed at Google, but not "from Google", if this makes sense. E.g. a few googler's building a language on their own initiative and among other work is not the same as Google, i.e. some higher-ups, saying "we need a language of our own that's a match for our developers' challenges" and the company devoting resources for this.
Google has 2 language projects they really put money on, Javascript as V8 and Dart. They built a top notch specialist team, spent lots of money for promotion and branding, built an IDE and developer tools for both, etc.
Apple has had Obj-C and now Swift like that, MS has C#, etc. Those are language projects with a big weight of the companies behind them.
Golang was not like that, but, according to all official accounts and recollections, a grassroots project, by a small team:
"Robert Griesemer, Rob Pike and Ken Thompson started sketching the goals for a new language on the white board on September 21, 2007. Within a few days the goals had settled into a plan to do something and a fair idea of what it would be. Design continued part-time in parallel with unrelated work. By January 2008, Ken had started work on a compiler with which to explore ideas; it generated C code as its output. By mid-year the language had become a full-time project and had settled enough to attempt a production compiler. In May 2008, Ian Taylor independently started on a GCC front end for Go using the draft specification. Russ Cox joined in late 2008 and helped move the language and libraries from prototype to reality."
It was never officially intended to be "Google's development language" or had a major push from Google. Since then, and after the first few years, Google seems to have devoted more money and time to Go, and several Google internal projects have adopted it, but Google is fine with C++, Java, and Python as well.
In particular, language designers typically have concrete problems in mind, even if they're inventing a general-purpose language. It's enough to say that Go's designers expected to use their new language at Google, so it needed to work well in Google's environment and solve some problems that weren't currently being solved well in that environment. If it didn't work at Google, then they wouldn't have succeeded at their original goal.
While it's not the most popular language at Google, Go is officially supported for server-side projects (communicating via RPC with many other internal servers), and has been for some time. That's a high bar that few languages meet, and about as official as it gets. If it didn't succeed internally then the Go team probably wouldn't have had consistent management support and stable funding over many years.
The other languages you mention (Dart and JavaScript) are considered client-side only within Google so they mostly don't compete with Go. For example, Node is supported for developer tools and external users, not because Google runs its own servers using Node. (Or at least that was true when I left.)
I've been working at Google for 8 years and have yet to touch a Go codebase in production.
It's just not used that frequently.
But I'm not going to dispute that it's the most-used server-side language, because I've seen the statistics.
Yes.
But it's another thing to have a company's the management, say "we want a team to create a language to solve programming at our scale" and throw full resources and money at it (at C#/Java/V8/Swift scale) and have it adopted by mandate for further development,
(as is often implied),
and another thing to have an independent group of a few devs at a company to sit on their own, and say "you know what would be interesting to try to build? A language to handle what we see as Google scale problems", and then getting some more resources, and seeing some at the company adopting it, as just one more language used along with several other company-approved languages for greenfield stuff...
If anything the trend is towards slowly adding acceptable languages, but the bar is pretty high.
Like Google scale?
That's what Go is designed for.
Google's major Go projects appear to be gVisor, Android GPGPU debugger, Fucshia's TCP/IP stack and volume management tools and the download server as it was done by the Go team.
https://commandcenter.blogspot.com/2012/06/less-is-exponenti...
"In this talk we explore the devastating effects using object oriented antipatterns in go while building programs in a monorepo. ... Unknown to most, Kubernetes was originally written in Java. If you have ever looked at the source code, or vendored a library you probably have already noticed a fair amount of factory patterns and singletons littered throughout the code base. "
So basically writing Java in Go lead to clusterfuck codebase.
Most of the codebase problems in Kube are:
1. we depended on half the Go ecosystem at one point (docker, grpc, etcd, a few others) which is hard to do with dependencies in go (few standardized libraries)
2. performance of serialization mattered and JSON and protobuf were still raw at the time
I don't think Kubernetes is any worse than any other large (3M+ LOC), relatively young codebase I've seen on average.
They are not scalable for Google scale and are not maintainable at google scale.
That's why Google C++ guidelines basically disallow everything that is problematic in C++ and enforce a style guide to
> Avoid surprising or dangerous constructs. C++ has features that are more surprising or dangerous than one might think at a glance
> Avoid constructs that our average C++ programmer would find tricky or hard to maintain. C++ has features that may not be generally appropriate because of the complexity they introduce to the code
> Be mindful of our scale. With a codebase of 100+ million lines and thousands of engineers, some mistakes and simplifications for one engineer can become costly for many.
I was replying to
> However, as you start pushing beyond "scale" Go is designed for
Go is designed for Google scale, I don't know anybody pushing beyond Google scale right now.
Anyway, I'm a nice person, you'll find your answer below
Enjoy
----------------
most of those projects are legacy software
did you expect search to be written in a language invented 10 years in the future?
Go is slowly replacing C++ to write the tooling.
C++ is simply not scalable enough for day by day use, Linus knew that 15 years ago.
Even Firefox is replacing it for its engine, it must mean something.
It can be done in C++ doesn't mean it should anymore.
Java is there because it runs in ads systems, you don't replace your core product overnight, just like in some banks you still find Cobol.
Java is the new Cobol.
Fuchsia is written mainly in C and Dart.
You wanna write an Haiku style microkernel?
C++ is fine, it's just a few thousands line of code.
Wanna write millions lines of code in a maintainable way at Google scale?
You don't start the project in C++ today, unless you're a crazy person.
Even ES5 Javascript is more maintainable than C++.
And easy as close by and accessible i.e. npm i latest-framework might be easy but not simple
edit: The "Limits" slide (go to 12:30 in the vid) is one that I really internalized early on. And looking at it again years later, the principles from that slide absolutely guide my app development:
- We can only hope to make reliable those things we can understand
- We can only consider a few things at a time
- Intertwined things must be considered together
- Complexity undermines understanding
This is not some advanced feature. I would think this should be the default in any language.
My point isn't that this is wrong, but just to suggest that people's mental models incorporate the idea that programming languages actually move pretty slowly. There's a constant froth of undergrowth in the forest, but it takes a long time to establish a tree, and a long time for it to be supplanted by another.
Moving 10,000 bricks from point A to B is likely simple or easy, but it takes time. If not careful a client can think “oh it’s just moving 10K bricks 5 feet over, they should automagically be able to do this since John said it was “simple” ”. What I intended to impart was that I know how to do it, no solutioning needed, we have the skills and resources to get it done, and we can probably get moving on this quickly. Now I say something more like … “Moving 10,000 bricks from A to B is straightforward for my team, when do you want us to get started”.
One thing that gophers say a lot is the count of Go's keywords is minimal. This is true literally, only 25 keywords as of Go 1.13. But it does not reduce the complexity of writing code, it just hides them.
For example, Go does not have the `new` keyword, but it has `make` function, which is so powerful that one of my friends actually thought `make` was a keyword of Go.
Having converted some large Go programs to Rust recently I now have the perspective that some programs, while perfectly fit for Go, are miserable to write without Generics. I had a program which did some pretty minor work, but it did so with a lot of varying data structures and writing it in Go was less than pleasant. No, Go might handle this better with generics. However I imagine this will only move the bar. Certain problems are just going to be friendlier (though, more complex!) to solve with a more advanced typing system.
I do look forward to the day that Go can get basic iterator behavior via Generics (if ever). Some of the smallest things in Rust were the biggest sighs of relief for me. Converting a slice of slices from one type to another is just a PITA in Go (in my opinion), and iterators make a world of difference for short, easy to read and comprehend implementations.
In my experience, Go's biggest fault is code bloat that "simplicity" offers. A simple goal could turn into a multi function implementation in Go that frankly shouldn't have to be. Then code locality gets worse over time, and suddenly simple doesn't feel simple. Rust's larger complexity (iterators and the like) improve code locality, and thusly simplicity, in my experience.
It's a game of tradeoffs. They're both great languages, and they both have their places in my company.
I would say that the complexity of the overall language is a one time hurdle. Once you get past that and once Rust has more mature libraries. Which language in your opinion is the better one?
I can see a future where Rust and Go overshadow Python but that future isn't near. Python still has superior libraries for many use cases, a great community, easy to learn syntax, and little training requirement (most people know it or can become proficient in a very short time). Moreover with the type annotations and linting tools it's becoming a lot easier to write a large and maintainable Python codebase.
So as for the author's pretext (writing some tool in Go), personally my view is "why not?" not "why not Rust?". If someone had written that same tool in Rust I wouldn't be saying "why not Go?". Whatever floats your boat.
But here is the one part where I disagree with the author:
> Go is unapologetically simple
It is not as simple as it seems, just like every GC language out there. I find Go advocates in particular seem to dismissive of any GC complexities or downsides. Maybe because many of them just don't have the background in dealing with this from years of Java.
I worked on a team at Google that wrote and maintained a project using Go. Note that I never wrote anything for this project so my experience was second hand but close second hand.
One thing I remember was the Go binary blowing up on memory limits (10GB+) in production. Some debugging found there to be millions of Go channels (IIRC) that needed to be cleaned up. For whatever reason the GC didn't clean them up, possibly because of a non-obvious dangling reference, possibly not.
Anything can have bugs obviously. It's just a myth that GC is a silver bullet is my point.
There was a guy who worked on the Go team (David Crawshaw) who'd stop by every now and again. Occasionally I'd get into debates with him about Go and GC. It's from these that I established my general observations of Go pundits:
1. Full GC pauses and GC in general are totally not a problem in Go.
2. If they were, they're totally going to be fixed in Go 1.N+1 where N = the current version of Go.
(At the time, the argument David made was that Go's STW GC pauses were sub-millisecond so totally not a problem).
AFAIK, by "systems programming" he meant "non-user-facing programming", not "kernel or embedded programming". So, basically, servers, batch programs and other os utilities.
If it was possible to write whole operating systems in garbage-collected LISP in the 80s, then it surely is possible to use a GC'ed language for systems programming thirty years later.
The highest-performance gc systems (OCaml?) might very well be associated with functional programming languages, but the more relatable system for a lot of people might be the Oberon system, which did influence Go. I'm pretty certain Oberon did not rely on any hand-coded assembly to build a functioning workstation. (although, on the topic of microcode: the newest Wirth Oberon incarnation does have the student program an FPGA to run the system, as an exercise...)
Right now, state of the art for real time GC is that it's a huge trade-off between throughput and determinism. Like orders of magnitude lower throughput to be able to make guarantees that you'd expect out of a desktop system.
What's your objection to having the compiler emit "hand coded assembly?" It's not clear to me what axe is being ground here. (that's true for me in a big picture way here too. There's a very strong argument against gc languages being made here and I'm not sure if you're saying Oberon doesn't count as a gc language, or what.)
> Like orders of magnitude lower throughput to be able to make guarantees that you'd expect out of a desktop system.
Do you have a current citation for that?
When you need to start designing your hardware around your computer language you know there's a problem going on.
That's also why these dedicated machines tended to disappear right as instruction caches became more standard; an I$ solves the same problem in a way more general way. As long as the hot path to your interpreter fits in I$, it's six of one, half dozen of another.
Caching doesn't really help with this. Even when everything is nicely in I$ and L1, it costs cycles to do the type checking. What helps is compiler techniques: type inference to eliminate some of the checks. But hardware can basically bury the cost of the checks; you can do them all the time on all operands.
Lisp Machine operating systems were written in the mid 70s - developed for a new breed of computers: single user workstations with graphical user interfaces.
They were developed at Xerox PARC and at the MIT AI Lab.
> They even started making dedicated hardware interpreters of LISP to try and get around this.
The machines were built to get around slow Lisp systems on time-shared computers with tiny memory. They wanted to have dedicated machines with memory exclusive memory for that one user.
A bunch of stuff was then invented for those systems, including better automatic memory management like generational garbage collectors.
Around 1980 (!) such machines were extremely expensive and still had tiny hardware: around 1 MB RAM and approaching 1 VAX MIPS of speed. Mid/end 80s they had 20 MB RAM and 5 MIPS...
No wonder: an entirely new class of systems was developed on the slow hardware of the time.
That's a misconception; the actual technology of that type provides an instruction set architecture (which isn't Lisp), to which Lisp is compiled.
As someone else hinted, your rant loses everyone familiar with the history of computing right here.
"There were bugs in Go at one time" is not much of an argument against it either.
Check this out: http://www.projectoberon.com/
A full system (not just a language, but also custom CPU on FPGA, OS with GUI, applications, etc) where the language is garbage collected. The system has 1MB of RAM (by default). Note that the garbage collector is implemented entirely in the language itself.
The Oberon language was also a big inspiration for Go.
Maybe tuning for latency is the appropriate trade-off given Go's high level of control over allocation and data layout?
It should be straightforward (not easy, because compilers aren't easy) for somebody to write another implementation, tuned for another use case - I can't imagine it wouldn't be easier than doing it for Java.
To rephrase the question more precisely: can you explain why OpenJDK, which has a multitude of garbage collector implementations tuned for both throughput and latency, performs worse on both throughput and latency than Go on this benchmark which has a garbage collector that, according to the original statement, is "tuned for latency above all else"?
Perhaps OpenJDK is problematic in other ways (the author of the benchmarks suspects it's JIT induced, even though that's attempted to be controlled for), or perhaps this test depends less on the GC for some reason. Or maybe typical Go programs don't require as many allocations making allocation time much less important making tuning for latency the correct engineering decision. Would HotSpot do significantly better as is claimed? That's the sort of interesting technical discussion I'd like to have, instead of pedantry.
I guess being a long time part of Java eco-system, kind of gets tiring of having outsiders (in general, not referring to you) always mixing up Java with what comes with their PC, as if C would be defined by GCC.
As for the actual question, naturally having value types helps reduce GC pressure. Which on Java's case could be helped by trying out Graal or other JVMs that do better job at escape analysis than Hotspot. Alternatively, although it kind of is cheating, using the language extensions from either Azul or IBM for value types.
In any case, when inline classes (aka value types) arrive, Java can easily do the same as Go here.
JIT and de-optimizations play a role certainly, and are to blame for some performance impact, which can be further improved if a JVM like J9 gets used, given that it allows for PGO across runs.
Finally, while Hotspot has good defaults, tuning all the knobs is a science, even with help of J/Rockit and VisualVM, which opens the door for performance consulting.
The JVMs I mentioned, are targeted at soft real time deployment scenarios, as such they have APIs for low level control of memory management, while also supporting AOT with PGO compilation, thus allowing for low level fine tuning out of reach for the regular Java developers (pure Java SE implementations).
Since most of the explanations that you gave involve things like controlling value types and data layout, it sounds to me like you might agree with the statement that in languages with better control over allocation and data layout, tuning a garbage collector for latency over throughput can be a good idea because your time spent allocating is less important. Is that fair?
Some previous discussion https://news.ycombinator.com/item?id=17551012
Because those graphs measure overall throughput and latency in an I/O setting, not GC throughput and latency specifically. There are many other confounding factors. In particular, the application in question has been tuned to perform as few allocations as possible, so GC throughput will naturally not show up as much as in other apps!
Thanks to its generational GC and TLABs, allocation in HotSpot is like 5 instructions.
> Maybe tuning for latency is the appropriate trade-off given Go's high level of control over allocation and data layout?
Go doesn't give you control over allocation in a meaningful way.
Allocation being fast isn't very important if you don't spend a significant portion of your time allocating. Isn't it possible that some languages (maybe even Go) by the nature of their semantics spend significantly less time allocating than other languages, and so tuning a garbage collector for latency is the appropriate decision?
Why do you seem to believe that the Go application has been tuned more to avoid allocations than the Java application? If it hadn't been, then you would have to agree that tuning the garbage collector for latency is better because you can invest some effort into your application to have it perform better on both throughput and latency.
Or is the argument that because "[throughput] is typically what applications want", this application is not a good example of most applications (according to what measure of "most")?
The most salient difference between Go's GC and HotSpot's GC is that the latter is generational. There is no convincing reason I've seen for Go not to have a generational GC, which would dramatically improve throughput by enabling bump allocation in the TLAB. The tiny amount of latency that this could add is by no means worth the cost of making allocations an order of magnitude slower. Allocation in HotSpot is five instructions.
Who defines when a construct is “unnatural”? Can you show examples of “unnatural” patterns in the provided benchmark programs supporting your hypothesis that you have to write unnaturally to control allocations? If not, why are you making those claims?
Why is throughput of allocations important for “most” applications? How much time is typically spent on allocations in those programs? What is a typical application? What about typical applications in Go specifically since that’s the only language that Go’s garbage collector matters for?
Also, why did you bring up the point about generational collectors? It seems like a red herring. This discussion has been about tuning for latency, and how in this benchmark, Go’s implementation did better on both throughput and latency than any OpenJDK collector under any tuning, and that tuning for latency is possibly a sound engineering decision.
I’ve also noticed that you’ve made no explicit attempt to answer my more difficult questions, and if you continue to do so I will no longer assume you are acting in good faith.
There's plenty of empirical evidence—a wealth of papers dating back to the 80s—that generational GC provides better throughput than non-generational GC on most applications. I'd be extremely surprised if a properly-implemented generational GC with bump allocation in the nursery wouldn't improve the performance of Go's GC by trading off a small amount of latency for increased throughput. The reason why you won't see a benchmark like that for Go is that nobody has implemented such a collector for Go.
There’s no way I can believe you are acting in good faith. It’s obvious to me now that you’ve just been trolling for years every time you discuss the topic of garbage collectors. I’m no longer going to engage.
Back in the 90s, it was a running joke that if you were going backpacking, you should take along a 3' length of fiber optic cable. "If you get lost, just bury the fiber optic cable and ask the backhoe operator for a ride back to town."
In at least three programming-related communities I'm in, I have made a quip about "I think I'm gonna replace the 3' length of fiber optic cable in my backpacking gear with a short post about Go's garbage collector", and people have filled in "so if I get lost, I can just take the post out, and then ask pcwalton for a ride back to town".
I can't say whether or not the argument is made in good faith, but man, there sure is a lot of it.
I've been a part of systems that process million of events a second and never allocated. These will beat most C++ systems I've seen, and Go doesn't even start a chance. Java and HotSpot can do some amazing things (esp around inlining), but you do have to do them a little differently (and carefully), but at least you can. My experience with Go is that I never had the same level of control, and I don't think it is possible.
This whole "you can't control your allocations in Go" thing is a strawman. You don't have absolute control because of escape analysis, that's true. But there's a pretty wide gap between "oh I'll just move this allocation point out of a loop" and "oh I wrote a bunch of unnatural code". It's pretty idiomatic to manage allocations in Go, just look at all the posts about using pprof.
Besides, the spectrum of "clarity" (or whatever) to performance is present no matter what language you're using. Ex: ripgrep takes on more complexity so it can search a big buffer of text instead of just a line [1]. I wouldn't call that "unnatural" at all, just systems programming.
Here's just one example: The Go compiler judges that parameters to indirect calls such as interface method calls as escaping. So in Go if you don't want to allocate you have no choice other than to avoid interfaces. But Go heavily encourages the use of interfaces, especially in the standard library.
In C, C++, and Rust, on the other hand, you can use indirect calls without allocating, because the language guarantees the escaping behavior. This is a significant difference.
1. optimizing allocations away in serialization
2. optimizing allocations away in api or biz logic in critical paths around that
3. algorithmic improvements on certain naive code paths
4. blocking
... distant gap
5. everything else
Serialization is its own can of worms in Go, and many places we tried to optimize came down to trying to avoid a) stupid (don't use pointers to naive objects) and b) obvious (use value types) and then hit a wall where there weren't many cheap wins.
I probably can count on one hand the number of hot paths which were interface related, so while I've occasionally been annoyed at the forced allocation moving into an interface, it's rarely actual something I've gotten a win from.
That's just one particular experience, but the AMAZING integration with pprof from very early days has saved me far more time in improving perf than other Go annoyances has cost (relative to my experience in Java 2005-2011).
Well, they explored generational GC (after admitting that their uber-ultimate GC that they marketed as future-proof until 2025 could be better): https://blog.golang.org/ismmkeynote
> It isn't that the generational hypothesis isn't true for Go, it's just that the young objects live and die young on the stack. The result is that generational collection is much less effective than you might find in other managed runtime languages.
About the GC pauses, check out the latency graphs at https://github.com/ixy-languages/ixy-languages (being careful not to mix up the Javascript and Go lines). Go handily beat every other garbage collected language in latency, and is in the same ballpark as the two non-GC languages (Rust and C). The peak tail latency times are measured in the hundreds of microseconds even at the highest loads tested, so I think the pundits may have a point.
The handler and all of the scope it can reach stick around forever even though they should have been replaced or removed.
Also, there are ways to solve your throughput problems if you have control over your allocations, but there are not ways to solve your latency problems. Indeed, even if you don't have control of your allocations, you can often run multiple copies of your program to increase your throughput, but multiple copies will not help your latency.
Also, tuning your latency does in fact help increase throughput, because if your process spends a significant amount of time in allocations, its latency suffers. It's not a zero-sum knob between the two, especially in the presence of humans that care about tuning the performance of their applications.
> Also, tuning your latency does in fact help increase throughput, because if your process spends a significant amount of time in allocations, its latency suffers.
I assume you meant to write "throughput" in that last sentence. That's not how throughput is defined. Throughput isn't "how long does it take to allocate", though that influences throughput. It measures how much time is spent in memory management in total during some workload. Optimizing for latency over throughput means you are choosing to spend more time in GC.
Maybe you mean something different by "control over allocation"? Go does allow that, for example "The Journey of Go's Garbage Collector" talk has a good summary,
https://blog.golang.org/ismmkeynote
Specifically the sections about value-oriented programming are exactly that: Go allows the developer to avoid a lot of allocation by embedding structs, passing interior pointers, etc. Compared to Java or C#, it can have a much smaller number of allocations as a result.
I can create a struct and take a ref to an interior field of that struct in C#, can I not? And if I wanted to badly enough, I could use unsafe code and take a pointer to the third byte of an interior field of that struct.
To elaborate, for example, indirect calls cause parameters to those calls to be judged escaping and unconditionally allocated on the heap. Go style encourages frequent use of interfaces such as io.Reader. So in order to interoperate with common Go code, such as that of the standard library, you will be allocating a lot.
> Compared to Java or C#, it can have a much smaller number of allocations as a result.
C# also allows you to embed structs within other structs and pass interior pointers. Java HotSpot has escape analysis as well, though it's less important for that JVM (and other JVMs), since HotSpot has a generational GC with fast bump allocation in the nursery.
> https://blog.golang.org/ismmkeynote
As I've mentioned before, I also have a problem with the conclusion of this talk: that generational garbage collection isn't the right thing for Go. The problem is that nobody has tested generational GC with the biggest practical benefit of it: bump allocation in the nursery. I'm not surprised that generational GC is a loss without that benefit.
That said, being able to work on the stack does influence the design of the garbage collector. Throughput becomes less important because you can choose to allocate less, and pause time is more important because latency is harder to optimize. Latency is affected by your whole stack and can’t be meaningfully reduced by just adding more machines in the same way that throughput can be increased.
But your choose one of the few examples where there is no pause at all, because go's GC didn't even tick once!
Go could have a 10 seconds stop the world, it would still perform the same way here, that's why I say this examplei is a poor one for the point you're making (which is valid, even though one could argue than 10ms pauses are better than 30% of the CPU usage being the GC running (actual production usage on a Go service at my former work)).
As for moving from C# to Rust, you can learn to program with the Span<> APIs and get the non-GC perf. of Rust for the most part and still keep GC and other C# niceties (like the whole toolchain and ecosystem...) in much less time than a wholesale move to Rust. As as for message-passing/agent/channel programming, you can certainly do that in C# if you like, and though there is certainly a lot a foot-shooting that can be done with async and thread contexts, in general the Task<> system is a joy to use compare to almost anything else out there, not only for async code, but for concurrent code.
Don't get me wrong, I always thought that C++ was a "worst of all worlds" language and appreciate moving to Rust from there, but for most user-facing applications that tend to have complex object lifetimes, I just can't see why you'd want to deal with RIAA when modern GC's are so darn good.
Go is interesting and I really wonder how it'd be doing if someone went all in on the multi-return syntax sugar (pretty awesome stuff), the prohibition (mostly) on exceptions, channels and trivial threading and a bunch of other nicely packaged features while giving some ground on Generics. All languages have their warts, but I think this one is particularly easy to address - though it may take a major version bump and introduce some BC breaks I feel like those could be minimized, the biggest cost would be library incompatibility.
Go doesn't do that though. There is some magic for builtins (e.g. variable-arity MRV) but as usual mere mortals need not apply.
C++ templates are a purely-functional Lisplike language. There's nothing strange or absurd about it; it's a very vanilla way to approach string rewriting systems.
(It's ugly to read, yes, but then all Lisplikes are too.)
That sounds cool. An obvious question is, why not a lisp? But generally do you find f# works well on dotnet mixed with c#?
That guy saying that you can easily avoid GC in C# is also not realistic or doesn't know what he's talking about - even language expressions can lead to object allocation in C# - you really need to know what you are doing to avoid the landmines - it's very clear the language wasn't designed for this - if you need a lot of code like that.
But very little code actually looks like that in reality. A lot of code looks like that by accident.
Even in C++ I generally prefer to use handles over pointers for cases like that because graphs are just plain hard to get right, and if you use handles you can get a nice error message when something goes wrong instead of a segfault.
Regarding C#, I said for the most part. I stick by my assertion that (a) GC is quite efficient (esp.w/gen0 objects generated by your "expressions") and far from "lame" and (b) working with value types and Span<> (like Rust Slices) together can significantly reduce GC pressure to the point where it's acceptable, assuming there was actually an issue to begin with. Doing this in C# on hot-paths is certainly much less effort than moving to Rust wholesale, and you haven't thrown out the GC baby with the bath-water.
All the things that you do to make Rust efficient - allocate on the stack instead of the heap when you can, pre-allocate when size is known, use static/nested lifetimes, use slices to owned structures, etc., you can do in C#, but you don't have to worry about it until it actually becomes an issue.
If you're in the kernel or embedded system or the middle of a game rendering loop, then sure, Rust's compiler guarantees make this style programming easier if that's the way you want to go - and Rust has macros and other features that C# lacks. (Although the .Net JITTer is going to generate type-specialized methods and inline them, etc., w/a Rust macro you know up-front exactly what is being generated, which is nice.)
I've kept track of .NET progress since then and read about stuff like Span and ValueTask, ASP.NET Core team perf did a good job leveraging those for optimisations (eg. their optimised JSON parser) in such scenarios I agree C# with low level stuff sprinkled in is a good choice
But if your problem domain requires avoiding GC throughout and having better understanding on what the abstractions will compile to pick a language thats designed for that. It's like when I had to review some Java 6 code which tried to work around the fact that Java doesn't have value types with byte arrays - it was just soo bad compared to even C equivalent it was better to rewrite and go trough JNA.
However, I usually pick Rust for performance-critical code with quite a bit of concurrency, so the problems involved are inherently tricky. I'm gaining more and more intuition about ownership and rustc-friendly software design, and so I hope I'll be able to better understand if my struggle is due more to limitations of the compiler and Rust's semantics or the limitations of my skills.
For comparison, I've written ~6000 SLOC of Rust in my lifetime, so I do have some experience but I'm definitely not an expert.
First having neither a build cache, or binary dependencies, means that C++ wins on the "make world" build, because naturally all my third party libraries are already compiled.
Then there is incremental compilation, incremental linking, pre-compiled headers and modules to help with the rest.
GUI code then becomes a fest of Rc<RefCell<>> in event handlers, or using arrays with vector clocks workaround as shown on Catherine's talk.
There's no cognitive load required to code in C++. You just don't know the language.
There's no easy tutorials or learning materials for it, but it's not hard.
> You just don't know the language.
Nobody knows the language haha. If anyone tells you they know C++, they're absolutely wrong. This has been, IME, the easiest way to know if a candidate is over-stating their resume -- if they say they "know" C++.
No.
> full of edge cases,
Definitely no.
> it blows my mind every time I pick it up.
I'm tempted to say "programming is not for you", but I'll be charitable and just point out that you've never learned the actual language to make statements like that.
> If anyone tells you they know C++, they're absolutely wrong.
That's a load of crapola. It's impossible to "know C++" in the sense of knowing the ISO standard for C++; but that is also true of any other language with a real standard.
Languages without standards, of course, are worse in every way.
> No.
Most defenses I've seen of C++ boils down to something along the lines of "you're using it wrong (tm)".
I've seen enough C++ to take the side of "the design is probably wonky if it's that easy to do the wrong thing."
Maybe, but that wasn't what I said. The problem is that lots of people know C, but very, very few people know C++.
A big part of the problem is that both languages share one compiler, and people come from C thinking that C++ is just an upgrade with some features bolted on.
It's not. It's a completely different language, and if you approach it as "I'll learn a bit of C and then throw in some C++ features" you're setting yourself up for a world of hurt.
The hurt goes away if you forget C, start with a clean slate and learn C++ as a new language.
False.
std::string x = y;
There's no way to mess this up.Again, learn the language. There is no such thing as "C/C++". 99% of the problems stem from the fact that people don't understand that C and C++ are completely different languages, despite sharing one compiler.
Learn C++ as itself and your problems go away.
> Lastly `const` what does it do where and why and should everyone ever use it...
`const` is a contract that means "I will not modify this object here". Nothing confusing or complex about it, unless you're trying to shoehorn this concept into C semantics somehow.
#[get("/hello/<name>/<age>")]
fn hello(name: String, age: u8) -> String {
format!("Hello, {} year old named {}!", age, name)
}is more complex than express + Node. With added bonus that you get validation for free
require 'sinatra'
get %r{/hello/([^/]+)/(\d+)} do |name, age|
"Hello, #{age} year old named #{name}!"
end require 'grape'
params do
requires :age, type: Integer
requires :name, type: String
end
get "/hello/:name/:age" do
"Hello, #{params[:age]} year old named #{params[:name]}!"
end
In any case, my beef is more with the macro than with the function annotation, which is rather spiffy. The macro hides the fact that string manipulation in Rust definitely requires a little bit of manual reading. And I feel that as soon as your app becomes more complex Rust will just start getting more in the way. And to an experienced Rust dev that might not be a big deal, because an experienced dev knows how to work with strings or which memory management strategy so it won't bog them down. But if you're just working on something and you quickly want to whip out a service that tells people what age they are, I'd definitely go for a quick Ruby or Go service.My day job is ~half c++ and ~half c#, and I couldn't possibly disagree more.
The first problem is IDisposable and event handlers. Because c# doesn't have anything like weak pointers, event handlers require that you manually dispose of half your objects. In c, there's a simple rule: you always dispose of your objects. In c++, there's a simple rule: the destructor of your data structure/smart pointer always disposes of your objects. In c#, the rule isn't simple. Half the time, you have nontrivial destructor doing nontrivial things, the other half the time you can leave it up to the GC. But it isn't necessarily immediately clear which is which.
The second is `using`. Again, you must necessarily mix the semantics of non-deterministic GC cleanup and deterministic RAII cleanup. Which is worse than having a simple rule that always works.
https://docs.microsoft.com/en-us/dotnet/api/system.weakrefer...
https://docs.microsoft.com/en-us/dotnet/api/system.weakrefer...
https://docs.microsoft.com/en-us/dotnet/api/system.runtime.c...
> event handlers require that you manually dispose of half your objects.
You probably have software design issue on your day job. C# events can be great, but they're not a silver bullet.
For loosely coupled application wide events, event aggregator pattern works better, see IEventAggregator from Caliburn.Micro for an example.
If the two sides of the event handlers need strong coupling for a good reason, an interface or abstract class for the consumer works better, see ExpressionVisitor from System.Linq.Expressions for an example.
https://docs.microsoft.com/en-us/dotnet/api/system.runtime.i...
This and the the thread of the other day regarding having to use C for what C# is capable of, apparently many don't look on their toolboxes.
Go is an OK language, not a great one. The real advantage is that you have the libraries that Google uses for their own web server side stuff. Those have been pounded on by billions of transactions, and that the special cases have been handled. There's one well-tested library for each major web service related function.
Rust's libraries don't have that volume of use behind them. Look up "http" in the Rust libraries. You find "Note that this crate is still early on in its lifecycle so the support libraries that integrate with the http crate are a work in progress!"[1] (Rust enthusiasts may comment with a complicated excuse for why this isn't a problem, if they like. That's what the official document says.)
It's also not an "official document"; that's just a package that exists. It's not run by the Rust project.
A nice thing about Rust is that there is so much safety is built into the language that even random libraries tend to work quite reliably.
This is probably the most succinct and accurate description of Go. It's fairly decent and reasonably reliable at small/medium scale (which is why it is popular among the microservices crowd), but has no outstanding features.
IMO by far the biggest reason it became popular is that it is backed by Google (which makes it a "safe" choice). If it weren't, most people would not have heard of it today.
I think it's important to define which "scale" you're talking about here; Go is deployed at a larger scale in the sense of number of deployments, but both are deployed in production inside the largest tech companies in the world. For example, Rust is now at the core of all of AWS Lambda (and Fargate).
That being said, I do think that "decent and reasonable at small/medium scale" is an attempt at damning through faint praise, and is certainly not how I'd characterize Go in any sense.
For instance: "Today, most Dropbox infrastructure is written in Go." Or: "Today Go is at the heart of CloudFlare's services". Or: "Rend is a high-performance proxy written in Go with Netflix use cases [ed: all internal memcache] as the primary driver for development". Or: "The search infrastructure on [Soundcloud] Next is driven by Elastic Search, but managed and interfaced with the rest of SoundCloud almost exclusively through Go services." Or: "How We Built Uber Engineering’s Highest Query per Second Service Using Go.". Or: "Handling five billion sessions a day – in real time [at Twitter]".
I don't see where there's room for a "that said" here.
Both languages are obviously capable of scaling.
> Both languages are obviously capable of scaling.
This is clearly true, and I agree fully. I don't really want to argue "is this deployment really larger than that deployment", as it's kind of silly. The point is that both have demonstrated the ability to scale to the largest workloads, and so knocking either one of Rust or Go on this axis doesn't make sense.
It's my impression that you may need to have worked with functional languages and have an understanding of type systems before you can be efficiently productive in Rust, but for those who reach that I think they can be much more productive in Rust than Go,based on only my personal experience. One of Rust's biggest and best features, and its biggest barrier to grokability, is the Rust Borrow Checker.
I think Go is a very good language, has awesome concurrency designs (I love Go channels), and is more accessible to more developers, but it also doesn't seem as flexible. I think the OP hit the nail on the head with the idea that Go is designed for Enterprise, where a sea or journeyman engineers are working under a small number of senior eningeers. I think Rust OTOH is designed for senior engineers, but is usable by less experienced engineers if they are mentored properly, i.e. 1:1 or at most 1:3.
Also, I like the tooling better in Rust than Go. Rust tools are easier for me to install, and the cargo build tool is not part of the language, but there is a standard build tool, so I get to have my cake and eat it too. Meaning I can customized the build tool to my needs, and have different customizations for different projects. That can get out of hand, which is why so many hated Gradle. OTOH, the level of customization is why many shops with senior engineers loved Gradle (e.g., Netflix).
Examples of build tool install:
* Go: $GO111MODULE=on go get golang.org/x/tools/cmd/stress
* Rust: $cargo install cargo-stress
One thing I think both languages will need to watch out for is the P3 problem (Package Proliferation Pachyderm), where there becomes an ocean of packages, some too trivial to really be an effective package (e.g., a package with a single functor to add two numbers). I've seen this with Node.js, though the community has noticed it and is working to rectify the situation. This, like so many problems with language adoption and evolution, is a policy/community problem and not a technical problem. Both Go and Rust have great communities, so hopefully we can avoid P3 in the future.
Lean toolchain, lean build times, lean formatting (can you imagine the amount of time wasted on IDE config, commit syntax, and formatting style debates ?) so that large groups can just go to work.
ps: I'd love to work in rust. As you said, it seems very potent at making very expressive yet very efficient code.
I was up to my elbows in troubleshooting (when you don't know how to solve the problem, describe it more clearly), so I didn't have time to dig into the history, but I got a chuckle out of that.
Mm, not really. Go is a less sophisticated Java / C#, in the same way that a bicycle is a less sophisticated motorcycle. There's situations to use both, and there's nothing with either, but sometimes you want or need to travel hundreds of kilometers in a day and a bicycle isn't going to cut that for most people.
User-space network driver? The benchmarks group Go closer to Rust with Java / C# way behind: https://github.com/ixy-languages/ixy-languages
This would be a very complex comparison and simple analogies do us a disservice. I think the author's blanket statement that Go is faster than Java / C# also seems too broad.
1. C/Rust
2. Go/C#
3. Java/OCaml/Haskell
4. JavaScript/Swift
5. Python
And depending on how you draw the picture, Go might not even be in the bandwidth picture.
That's a very generous reading of those benchmarks. What you said is true in the latency benchmarks, but in the throughput benchmarks, C# _beats_ Go at high packet rates and is much closer to Go than Go is to Rust at low to medium packet rates.
The Java/C# vs Go equivalent being causing your project to run overbudget and a be a trainwreck of bugs due to overengineering and runaway complexity.
It's just an overhyped, subpar language.
Java is a language and computing platform from the 90s and it shows. It really needs to just stop and the mess around licensing only serves to help kill it.
I also wouldn't tout the concurrency of Java over Golang - having the concurrency model baked into the language and runtime means a clear advantage for Go and one which simplifies the communication among different agents.
Please learn some basics.
Concise is not a word I'd use to describe golang. I can't count how many times I've come across code that would be 1 or 2 lines in Java compared to 15 lines or more in golang. It's quite ironic given the unsubstantiated claims around golang. Lots cruft with "if err != nil" littered everywhere is barely scratching the surface.
Java is getting a green thread implementation by means of project Loom. However, the JVM already handles very high concurrency systems in production by using libraries like Akka, Vertx, and Reactor. It is already used by major corporations to handle extremely high load systems. It's already proven itself over decades.
I don't know what you mean by Java being a computing platform from the 90s. If anything, golang is at a similar level as Java when it was first released (no generics), except worse (error prone error handling, poor design of interfaces, and many bad and unsubstantiated design decisions). Not to mention the JVM having state of the art GC, as well as performing optimizations way beyond what golang is capable of.
OpenJDK is fully open source, no licensing there. Not only that, but Oracle has open sourced previously closed source projects pertaining to the JVM and tooling around it.
The main reason for golang is the hype behind it. Proof is that some of the same authors worked on its predecessor a long time ago, and nothing ever came out of it because they didn't have the Google brand behind them. People today follow hype without substantiation.
> I have used both in production
likewise, and I have found the opposite stance for Java/JVM based applications.
Terrible deployment story, terrible amount of tweaking and JVM "hacks".
> Java is getting a green thread implementation by means of project Loom
Well done to Java. It's getting features already commonplace in Golang. Bolting on Akka, Vertx... yeah, enjoy your frankensteins monster lol.
> no generics
Generics _arent_ critical for a language, exceptions are a mess and as for the JVM being state of the art... yeah. In the 90s it was.
> The main reason for golang is the hype behind it
I just fundamentally disagree - if that was truly the case, I'd have adopted Java back when it was hyped and relevant and would've moved onto Rust by now from Golang which never happened.
A lot of people these days are building fat/uber jars. All it takes to run your code is `java -jar foo.jar`. Can't get much simpler than that.
Tweaking is a plus that golang doesn't offer. The JVM runs an extremely wide range of workloads, and gives you the ability to tune it accordingly, e.g. whether you care more about throughput vs latency. Compared to golang where this is extremely limited.
It's not surprising that a huge number of data processing workloads run on the JVM (regardless of implementation language).
> Bolting on Akka, Vertx... yeah, enjoy your frankensteins monster lol.
That's not an argument. These frameworks are mature and battle tested, not to mention built on sound principles (e.g. Akka is similar to Erlang's actor model, which includes supervisor capability and remoting - nothing like this exists in golang).
> Generics _arent_ critical for a language, exceptions are a mess
They're not critical in the same way "functions/procedures" are not critical, but you're going to end up with a messy code base when it comes to reality. Again, the number of times I've seen what amounts to map/filter calls littering the code base, making it more difficult to read (not to mention error prone) is too many to count. Generics fix this. Funnily enough, it's golang that chose to disregard best practices from the 70s.
> and as for the JVM being state of the art... yeah. In the 90s it was.
Tell that to Google, FB, Apple, Amazon, and many more who run their critical infrastructure on the JVM. The introspection and monitoring it provides is literally second to none, not to mention performance, tunability, hot-swapping, rich ecosystem, and many more.
> I just fundamentally disagree
You can, but the fact remains that some of the same authors worked on a golang predecessor which never went anywhere, precisely because it didn't have the Google name behind it.
I can keep going on and on, but to cut it short; yes I would write a high performance router, reverse proxy, or a very simple micro-service in Go. But I would never recommend anyone to write a high business complexity service in Go. It's a matter of time before somebody writes a transpiler that would take these short comings of Go, and fixes them (Like Kotlin, Nim etc.).
The Go community has a lot of good ideas on designing interaction through interfaces that I really hope won't get lost in Go 2.
That said, yes, casting interface{} manually over and over is stupid, let's hope we can get the best of both worlds.
1. the built-in tooling (cross-platform building, profiling, formatting, test coverage, etc)
2. the standard library. I've written services that have convoluted TLS certificate handling, encryption and REST calls and never had to look elsewhere.
The language itself is good, though 2 gripes for me:
1. x.Y. At first glance, it isn't obvious whether this is function Y in package x, or method Y on object x. Package::function would be better IMHO.
2. The ':=' assignment with inference operator has sometimes led to unintended variable shadowing.
edit: another one comes to mind. The usual ... generics, pretty pls?
BTW, the author claims Go is faster than Java. Is this true, because it didn't used to be?
I think for certain use cases C# or Java will be faster. THere was a post here the other day about a network driver written in C, Rust, C#, Go, Java and a few others and The C# one was faster then Go. Can't remember if Java was faster or not.
As pointed out over here (https://news.ycombinator.com/item?id=20984503), Compared to C#, Go was faster (higher throughput) for smaller batch sizes, and it always had significantly lower latency.
.NET Core is particularly good these days, so I'm sure there are times when C# is definitely faster than Go, but the network driver isn't a great example to support that.
The throughput graph doesn’t show C# pulling ahead significantly. Significant is how far behind Java is from Go and C# in that benchmark.
Also be sure you’re not looking at JavaScript on the throughout graph, since it is colored very similarly to Go.
FWIW:
Back in ~1998, I wrote a VRML browser that was faster (FPS and event loop) than Sony's. Benchmarks showed Sony's was then the fastest (publicly available). They used 'C'. I used Java (JDK 1.2). We both used same OpenGL stack.
Same kind of deal as a network stack. Most of FPS was due the graphics card and use of OpenGL. Java's JNI added a minor performance penalty.
As for the all the other stuff, like reading files, parsing, user interface, I just figured Sony's VRML team sucked (worse than me).
Update: Others made the point about latencies.
And the package thing gets annoying because it's natural to call a package that deal with foos.. the `foos` package.. but that's also the natural name somewhere else for a slice of foos.
Fwiw, Rust attempts, by design, to not be batteries included, as this would tie Rust to specific architectures, or operating systems – or even require Rust code to run on an operating system, which doesn't have to be the case.
So, yeah, as a Rust dev, I pretty much always rely upon external crates. But hey, that's what they're here for :)
For random numbers, there is the rand crate, described here in the Rust Cookbook:
https://rust-lang-nursery.github.io/rust-cookbook/algorithms...
It makes no sense to call a third party, and maybe a broken one, when you want to generate a random number.
The worst part about rust is its standard library and thats not a secret.
Take a look at the golang standard library.
Is this going to be an issue in a decade or two? http://pyfound.blogspot.com/2019/05/amber-brown-batteries-in...
If you want to remove stdlib packages, I guess you would need Go to ship a tool that automatically rewrites sources to point those package imports to a standard "golang/x" location (or something) that provides the same package.
Or alternatively, some kind of indirection via the go.mod file.
I've been bitten by this once or twice? It is helpful for when declare and assign multiple return values.
eg:
g, ctx := errgroup.WithContext(ctx)
there is of course a shadowed variable above. In practice it hasn't really been an issue for me, but I can see the foot gun.The thing I like most about Go is the resulting output is a lot easier to follow. Maybe that's just because I am more familiar with Go than Java. Java seems to have endless abstractions that are not intuitive to me
It doesn't always lead to a bug, but it's always frustrating. Even when restricting the problem to a single (non-error) return value, there are 6 combinations to consider:
* What do you do with the non-error return value? (none/assign/create)
* Is it the first time you've checked an error? (yes/no)
none, yes | err := f()
none, no | err = f()
assign, yes | var err error; r, err = f()
assign, no | r, err = f()
create, yes | r, err := f()
create, no | r, err := f()
The shadowing case comes up infrequently enough to surprise you when it does. You've been trained by the other examples to change `=` to `:=` when you see a certain error, and you don't always get a warning about the shadow this creates.In general, the trend of making things easier for the compiler rather than the developer is a thing that annoys me about go, you're right to point that out
I very recently ran an evaluation that included load testing feature identical business focused microservices written in both Java and Go. These are IO bound processes. The results are that, in this context, Go is on par with Java when it comes to performance (i.e. throughput and latency).
http://glennengstrand.info/software/architecture/microservic...
Not for any meaningful deployments. The amounts of optimizations done by HotSpot completely dominate anything that golang does (which isn't much). In golang, not even function parameters are optimized to be passed in registers, let alone aggressive interface devirtualization, etc.
If your identity is tied to being a 'Xer' you are going to have a hard time working with 'Y' even if it is the best solution in this case.
Don't identify as a Xer or Yer, but as someone solving problems and creating value.
One place where Java and C# win, and sometimes C++, is the availability of excellent libraries. Rust doesn't have as many and not as fleshed out either. Go has the same problem though.
Diesel. If a developer came to me and wanted to write something in Rust over Go, but didn't know Rust well and wanted to use Diesel, I'd warn them not to. The errors you can get from Diesel are insanely unhelpful and quite advanced in the type system.
With that said, I don't think Go really has a comparison to Diesel either.. so perhaps this is an unfair warning. If instead you use raw SQL with Diesel, suddenly it becomes just as friendly as Go's SQLX, and yet again Rust becomes largely as simple as Go.
Rust is in my view surprisingly simple. If you choose advanced features you usually understand them. It seems to me that using libraries are the biggest threat to making Rust feel insanely complex; ie Diesel's type system.
As an aside, I hope one day Diesel can achieve errors akin to TQL (which I found recently), as TQL really nails the error reporting from what I've seen, at least.
Is not bad.
I think for RDBMS Rust lack the Python DBAPI interface. Too much reimplementation at the low level (ie: I adapt postgres and sqlite and frankly, both drivers are dissimilar is unnecessary ways).
BTW, I love how the Rust compiler uses all CPU threads. It's fast on my 8 year old Ubuntu desktop with 6 cores / 12 hyperthreads.
Rust forces you to think about memory use and ownership ahead of time, but after some short practice it really gets out of the way and does not become a problem, while avoiding bad code nasties like interlinked singletons, cyclical references or random shared_ptr held forever. Additionally the explicit memory model makes interacting with C (and often C++) code much more straightforward.
Source: had to wrangle that performance issue on Android both in Java and C++. Had to help JVM not stall forever in IntelliJ.
For many applications I've had to speed up, the performance issues were either localized with low hanging fruit, or were architectural, where layers of abstraction led to translations between impedance mismatches throughout the whole application -- transforming an image 4 times between different coordinate spaces before drawing it on the screen does not make for a fast application.
A language change isn't going to solve that. GC pauses have almost never been an issue: It's irrelevant for servers. For UI, most code just doesn't end up allocating that much in the UI thread. Yeah, for things like games that do a lot of work and need to update at 120fps, GC can be a problem. I generally don't write that kind of code.
I'd really rather have a GC for 90% of all code that I ship.
For web style apps with moderate to heavy client complexity (Kube APIs, client libraries) I have appreciated about being able to start with the GC, when we hit a scale cliff go and remove a 5-10 stupid allocation patterns (inlining, value types, removing pointers or replacing with internal index references so the collector ignores them), get another 10x scale from that, then go back and do architectural improvements, get another 10x, then trim some fat around the edges and get another 5x.
You definitely hit a wall at some point (comparing the best that you can do in Go with protobuf / JSON relative to say Serde) where the remaining 2-10x efficiency is not achievable. That is where I'd be most interested in generic like constructs in Go where I can type and define down. It's hard to beat the clarity and brevity of Serde - no one in Go has done it at anywhere near the performance.
It's similar in C#. Once you have some experience writing latency-sensitive .NET code like games or realtime multimedia, GC works quite well in practice because you learn how to write code in a way that doesn't stress the GC too much. People who don't care about latency are still able to get their stuff done.
> Would be cool if Rust could cover that gap.
To really cover most of that gap, writing Rust would need to be as easy as writing Python, since that's one of the main selling points of Python. But Rust's massive benefit, compile-time memory management, is definitely not as easy to write as code that uses GC.
GC has overhead that you sometimes can't tolerate, but it's just simpler to write. If you have a graph of objects, it's easy to do in Python or C# or JavaScript. In Rust it's more complicated, because it's hard to do that at compile time!
The huge benefits of Rust are directly relevant for systems code, but for most Python code it's probably not the best tool.
Wie bitte??? My wig has been snatched.
I may be biased since I've been using it for almost 6 years, but you mostly don't need to worry about lifetime annotations which IMO is the really foreign part if you are already comfortable with functional languages, especially since the compiler is unbelievably useful.
Many well-known Rust crates also account for the majority of the best all-around libraries I've ever personally used in any language, and you can actually use them without a PHD in build systems thanks to Cargo.
Re-usability is superb thanks to traits (composition over inheritance) and generics, even if still immature domains tend to be overly generic for the end-user (HTTP networking for instance, though it's getting better fast).
The first question is "What does Python do better than Rust?" Libraries/frameworks is the big one--there is no excellent Django-equivalent or Jupyter notebook-equivalent, for example (maybe there never will be by virtue of the language differences, but it is what it is). Python helper scripts generally ship in .py form so you can fix bugs in crappy Python scripts (if people start shipping Rust auxiliary scripts that's going to be a step backwards). Verbosity--okay, I'll concede that if I want things to be mostly GC, I'm writing a lot of extra wording in Rust.
And when I reverse the question: "What does Rust do better than Python?" The answer is a whole lot. Strong typing is a big one--because of Rust doing okay type inference I don't wind up with types repeated very often--it feels more like Python. Performance, sure, but I rarely care. Tooling seems to be a bit better--and Rust seems to be targeting to be a first class citizen on Windows. And, of course, concurrency--Python should have just sucked it up, taken the performance hit in 3.0 to remove the GIL and then optimized performance back over 7 versions (Good concurrency is becoming a standard in languages--I suspect Python is eventually going to concede this or start losing mindshare--and the pain would have been best suffered back at 3.0).
Yes, there are issues. If you have a doubly-linked anything in Rust, you're about to have a bad time (or not, if you REALLY need double-linkage--it's time to quit being pedantic, to break out "unsafe" and to encapsulate things). If you're writing a library, your lifetime annotations look like 300 baud line noise.
I really learn a new language about every 10 years or so--I have to feel a significant upside without a lot of downside in order to move (my last big change was Perl->Python in about 1996--my history of languages that I knew cold is assembly->C(with a C++ excursion for a bit--I got better)->Perl->Python). At this point, for one-off things that I'm writing a quick 100 lines, I'm starting to reach for Rust more and more and Python less and less.
Like the article suggests and was stated numerous times, Rust and Go are not competing languages, they have different use cases. Rust is a good choice if you want to write a rock solid and secure library to replace a C++ library, or instead of writing it in C++ in first place. Go is a good language if you want to write a web service and want to rely on many existing libraries to cut down development time.
Both are great languages if you know them intimately as an expert and are deeply familiar with the common frameworks. Almost every language is great in that case.
However, for my use cases Rust would be a bad choice. Relying on GC is not only perfectly fine for my performance requirements, it also makes life much easier.
Personally, I think that both Rust and Go are a step back when we're talking about pure language features. They are both needlessly and artificially restrictive in comparison to good old languages like Ada and CL. But in the end, the frameworks and 3rd party libraries are more important anyway.
His answer was that for larger companies, server costs are orders of magnitude cheaper than payroll. It's often much more valuable for a company to improve their development process than server performance. So high-productivity (and ease of hiring) technologies like Node and Go can end up being a really good business tradeoff in terms of actual dollars, even if they result in double or triple the server costs.
C# is a great language and has a ton of modern features. It's also quite a bit faster than the author is giving it credit for. In fact there was a network stack written in it that was posted here last week that had it beating Go by an order of magnitude.
I don't really agree with the statement that 'Rust is a better C++' either. It very much can take the place of C in many applications also.
(2) Both Rust and C++ can be used to write most / all of the code that can be written in C, at a zero runtime cost. Their difference is in ergonomics, safety features, ecosystem maturity, etc.
C++ has lots of bells and whistles, some of which Rust doesn't have (classes, to name one), but those aren't necessarily more important bells and whistles.
Also, the Rust infrastructure is light-years ahead of linking in libraries in C/C++. Compile times are higher, but not horribly so.
Wouldn't it be fair to say that reducing memory unsafety as much as possible without hurting performance is a good goal to have when making a replacement for C? Assuming that's the case, it's relevant that C++ doesn't eliminate it as much as Rust when comparing the two languages as C replacements.
Sorry, but you've got this backwards. From a user's perspective, cargo seems like a much nicer build system than the more-or-less manual build systems of make et al. But that's because cargo is opinionated and inflexible: it really wants to be the primary driver of the build system, and it also doesn't quite support all of the things you can do with custom linking.
As a result, when you have large projects composed of multiple libraries, your choices are either to drive rustc manually and skip cargo altogether, or to create a single macro crate for cargo to link all the rust code as a sublibrary for linking into your main project code. You end up with something like this: https://dxr.mozilla.org/mozilla-central/source/toolkit/libra... to describe all of the Rust libraries you have in your project.
Seems different. But it doesn't seem bad.
Granted, I've not worked on a project of that size, but it seems to me that if it works for 'normal' sized projects quite well and works as well as anything else for large projects (as in, nothing works perfectly for large projects, they all need tweaking), then it is ahead of the alternatives.
I don't know what all of the trade-offs are, so I don't know where the current situation sits in this space. https://gist.github.com/rylev/0e3c3895dcb40b6a1c1cf8c427c01b... is the minutes of a session between the scary-custom-build-system people discussing (among other things) the pain points of cargo with their build systems.
No tool is perfect, but the team seems pretty dedicated to improving things.
The other "feature" that C++ has in this space is that it has a bad reputation as far as the C stalwarts are concerned, whereas Rust has not gained that reputation.
This is not what happend, Go was faster until batch size was > 16 then C# took over, but still C# latency was 2-3x higher than Go in every benchmarks.
Also C# / Java had to use C code where the Go driver was pure Go.
https://github.com/ixy-languages/ixy-languages/raw/master/im...
https://github.com/ixy-languages/ixy-languages/raw/master/im...
At 20Mpp/sec the C# latency was no longer on the graph:
https://github.com/ixy-languages/ixy-languages/raw/master/im...
BUT
Is easier to make faster/memory-efficient app in Rust or Go than in .NET.
The "defaults" of a language matter a lot. And what it give for "free" impacts a lot. Specially for the code made by less skilled OR skilled-but-tired developers.
Rust indeed always pushes you towards efficiency :)
And I bet the .NET GC is much better than Go.
But what I was trying to say is that is very common in .NET to build layers of layers of abstraction. .NET is still largely used by a lot of "enterprisey" developers with bad or non-existence training.
With Go you get a much simpler deal: You have structs (like POCOS in C#) and you pass them. Sometimes, you put traits.
Is alike what is preached for good F#/C# code, but .NET still carry the inertia.
However, if you get into .NET with modern runtime (aka: around 4.5+) and idioms then I think the results will be very good.
BUT, regarding what I feel to be a false equivalency - Is Go truly right for the exact kind of Enterprise Mush that the Author is describing? Go was invented at Google to solve Google problems is the claim in TFA. And yet, Google is famous for having a very high bar for engineering hiring with a strong focus on CS prowess.
So, is the kind of code mush that gets created at Enterprises who put production code written by one-week-of-pluralsight Junior devs headed by analyst-preached-cargo-culting managers - the same as - the Software produced at Google? If not, does Go still fit that former zeitgeist?
I don't ultimately know, I think some enterprises are doomed to fail regardless of the technology they end up choosing.
In some ways their inevitable doom is capitalism's greatest gift to society, but I've worked in a shop where we had both Go and C# at the same time, and I would have quit way earlier if I had to work on the C# code full-time. Both green-field projects btw.
As such, you look at it, you see what it's doing, not how clever the programmer was. And you can feel free to dive in and edit.
If I am choosing a language for a personal project, or for a small team of developers I'm really confident in, I'm probably going to choose something which is more fun to write. But if I had to recommend a tool for a large shop where long-term maintainability regardless of the team makeup is more important than programmer happiness, Go seems like a great option.
I think the reason is: Go code has a low level of abstraction-construction and a high level of explicitly writing the algorithm. You can see through the language, which is "boring" and does not distract your attention, to what it is trying to achieve, which is interesting.
It's just that in some cases I do like to work with a bit more expressive and low-level tools than Go has on offer, so it's not my first choice. But that's 100% personal preference.
Incidentally, for this reason I’ve always enjoyed coding in C because in domains where I’ve used it (most embedded code) the required feature set is quite limited, so C’s slow pace of development isn’t a problem. On the other hand when working with Go server code at a startup, I was deeply frustrated because I just wanted a much more abstract language to rapidly write features in.
Fun-to-write code isn't always that great to read; and your code will probably be read more times than it was written. I get it - I wrote Perl applications in a previous lifetime and I had fun doing it (Perl's text manipulation is unparalleled): until it was time to grok something a teammate wrote. I appreciate that Go is 'boring'
Maybe in time I'll get advanced enough where I'll miss features in other languages that other people argue about. But for the moment I'm having fun making useful programs in Go.
NB: When I was writing C++, we had clang-format setup to do the same for the C++ code. This advantage is in no way unique to go.
Just like any programming language and probably most of all Go. The fanaticism around it is just next level.
And yet Go programmers are often happy.
If the programming language you use is your primary source of happiness, you might want to reflect a bit on what happiness means to you. :)
Every mention of Rust on HN feels like a public relations exercise where one is force fed an opinion until they learn to like it.
The Go community's actually quite relaxed, all things considered, which makes them look more self-confident and less desperate.
I have actually never seen anything like what you mentioned when it comes to Rust. I think I have actually lost ~50-70 karma here on HN for constructively calling out some flaws and things I don't like about Go. Hasn't happened with any other languages though.
I think the cult mentality with Go is a special kind.
Btw. I'm Go developer myself and I do like the language, but I just don't get the "jihadist" attitude that seems to be far too common here on HN.
> Go has great concurrency support, but Rust has provably-correct concurrency.
I believe it's incorrect to say Rust has provably-correct concurrency. It prevents data-races, but not race conditions: https://doc.rust-lang.org/nomicon/races.html.
For me it is a perfect fit for writing automation, services and command line tools. It is stable, it is simple to use/write/read, it is easy to deploy, it has great IDE support and tooling, it just makes sense in the enterprise, as author himself notes.
I like Rust too, but compared to Go it has the feel of a research project. I think Rust has great future, I definitely see using it, but for now I will stick with CL/TS/Go.
If you do get to use it for work, how does it scale (programmer/code-wise, not efficiency-wise) for "real" projects?
Folks at Grammarly have a writeup about how they are using it, it might have more relevant information for you[1].
I would definitely consider using it again, but proper buy-in takes time, and I have simply been moving too fast since then.
I am still not a big fan of Clojure myself, but it is definitely seems easier to sell than CL. In my humble opinion, if you are already a convert the benefits of going from Clojure to CL will not be as great as the ones for going from Java to Clojure.
[0] https://news.ycombinator.com/item?id=13979002
[1] http://tech.grammarly.com/blog/posts/Running-Lisp-in-Product...
The reason I ask is that Clojure is an easy-ish sell largely because there's a near guarantee that you will never be blocked due to lack of libraries, since you can mooch off of anything in the Java ecosystem (similar arguments can be made for F#). I hate Java, but it has been around a long time and is extremely popular, and as a result there is a library for virtually anything for it. As far as I know, there is no such guarantee for CL...unless I'm mistaken (which wouldn't surprise me).
When using it for work, did you ever get stuck because of a lack of libraries (e.g. JSON parsing, protobufs, socketing stuff, threadsafe collections)?
Nope, I did find some to be less well documented than ideal, but pretty much everything had tests and/or examples making up for it. Have a look at Quicklisp[0] and explore yourself.
CL has many implementations[1], including the one targeting the JVM, so you can make use of Java ecosystem from it too.
CL has been around a long, long time. First edition of CLTL[2] was released in 1984! and at that point Lisp had already been in heavy use for almost three decades. The standard has not been altered since 1994, and likely never will, but due to its nature innovation continues in the individual implementations and in the community, and there is a well established culture of portability libraries.
[0] https://www.quicklisp.org/beta/
[1] https://common-lisp.net/implementations - Not an exhaustive list
The is more often untrue, than it is true. There are times when the low latency GC is beneficial though.
Of course “fast” is relative, but I would reserve it for languages that are nearly as fast as competitors in their segment. There are too many languages very similar to Go’s ergonomics that are much faster for us to meaningfully call Go “fast”.
That said, I don’t really think that’s a problem. I think people who use Go are often just happy it’s faster than Python, and that’s okay.
My personal dislike of Go comes simply from their unapologetic[1] embrace of default nullable pointers (the “billion dollar mistake”[2]): There is very strong theoretical (and practical) ground supporting the approach of Rust/Zig/Swift/etc’s (to use algebraic data types instead) as objectively better (yielding inherently more reliable results with virtually no ergonomic compromise[3]).
In other words, in the 21st century, we know how to design statically typed languages that guarantee the impossibility of null dereference exceptions (not counting bugs in external libraries from other languages). And we can do this without any runtime performance or code ergonomics compromise!
Therefore there are no good excuses anymore for any statically typed language in the 21st century to not provide this extremely beneficial guarantee.
[1] There are no plans to fix this, ever: I’ve seen entire articles written by members of the Go team not just defending “all pointers are nillable”, but encouraging this as an idiomatic Go style of coding.
[2] https://www.infoq.com/presentations/Null-References-The-Bill...
[3] The ergonomic difficulties of Rust come from the borrow checker, not from their use of algebraic data types to replace nullable pointers.
I also agree on the problem with null pointers with is really ridiculous. Another complaint that I have about Go is how for some reason Google decided to implement many web protocols in the standard library, but never really cared to get websockets right, to the point that they just sedn you to a third-party library from their own documentation. That + QUIC/HTTP3 make me want to take my tinfoil hat out from the drawer.
Though all dynamic allocation happens by resizing predefined arrays.
It feels like a real step backwards using languages without parametric enums. My litmus test when learning a new language involves porting across some plain text operational transform code. The go code came out about 40% larger than the rust and swift implementations for this reason. It was also much uglier and harder to read. Like, those extra lines were pure overhead.
Eg this rust code is beautiful: https://github.com/josephg/textot.rs/blob/bb14b4b483e7dace67...
And that’s prettier and about as performant as this C implementation of the same function (I think I somehow lost the Go code - but it wasn’t much better than this): https://github.com/ottypes/libot/blob/902470a22d3a99d9b776ce...
The equivalent javascript code (my go-to language!) is larger than the equivalent swift / rust code and, last time I checked, about 8x slower. Most of the gap in readability is this one beautiful feature!
In my experience C# is a great language by nature, but riddled with foot guns, bloated libraries that love doing runtime reflection and other organizational problems that typically surround the development process.
This is definitely an appropriate answer for the author to give. He wrote a small tool, and most folks writing a small tool would just run with what is already know by coder.
Because I wanted speed, Rust became my choice. But that zero cost abstraction, does not extend to the programmer. The cost is in the productivity. Although productivity in Rust is likely better than most older languages, C in particular.
I also like Starlark, very weak language in terms of features. That is its strength imho.
For the sake of context. Rust is my eleventh language, but I haven’t written much software, and most of it was ten years ago.
[1] https://old.reddit.com/r/programming/comments/d50u9g/why_go_...
Disclaimer: I’ve had a “few” beers so I might talking with my ass, but damn...
Rust is not meant to replace every single language on Earth. Rust is, as you mention, a better C++, with great predictability, great safety and great concurrency. That's a feature set that's critical for some classes of applications, but absolutely secondary for others.
It's a good thing we have more than one language to pick from :)
Small correction, Rust provides a barrier against data access race conditions. It does not prevent deadlocks, cache hits/misses, work distribution bottlenecks, or any number of other concurrency issues.
[1]: One of the most dangerous types of coder there is is the brilliant one, who has several times the working memory capacity of a normal person, and who can write 1000-line "functions" without breaking a sweat, and builds entire codebases out of such things. (Sometimes, perhaps the core loop of a program will be a bit of a monster, but the whole program isn't like that.) The cost of salvaging such things can be disturbingly close to the cost of rewriting such things, and indeed, the process for either may not be all that different.
The code base has a history of non go programmers doing work on it. It's not pretty but it works and is maintainable. IMHO that is impressive. I'm sure a few senior go developers would be all over the code base to improve it. But as it is; it kind of does what it is supposed to without much fuss. Having looked at but not really used Rust, I'm pretty sure there's no way I can be productive in that language on a similar timeline. Unreadable rust is a thing if you are not familiar with all it's syntax, macros, type voodoo, and pointer juggling.
That being said, go does look kind of ugly and verbose too me. I've never seen so many "if err != nil" statements before. The code I was working on is littered with it. Error handling is going on all over the place along with a lot of conditional logic around that which is a bit of a code smell. I also noticed a poor man's version of OO where you have structs and then a bunch of functions operating on that struct as the first parameter. I think I'd prefer the real thing. It seems variables are mutable by default, which seems a bit backward these days. So, there's also a lot not to like.
I think this is generally true, but in the interest of pedantry, I feel like I have to point out that Go does have "traditional" locks and mutexes and whatnot, if you're feeling masochistic and don't want to use channels for some reason [0].
For me, I'm a part-time solo "services" developer using Rust. I've built some terrible projects that I hate peeking into to add features and maintain. But, ultimately that's gotten better and the web-services story is maturing (Actix is somewhat unstable but easy to work with, and async/await will finally hit stable in a few months). I also write non-idiomatic code quasi-functional code that works for me but others would scoff at or at least find hard to maintain. Rust, to me, is an enjoyable language to use and has great tooling (debugging being a major exception). I've become productive in it after a heavy upfront cost. And, although I haven't quantified this exactly, its speed saves me money considering my relatively small single server.
I think, as a sole-developer, the largest issue with using Rust for services is the package support. Python, Go and JS all have mature Redis packages, for instance. Rust's works, but it has some peculiarities particularly with connection management and some Redis commands. It's enough where I could get everything working with enough time. But I don't have all the time in the world. It's a similar story for lots of packages, even in Actix web. You often have to dig into the source code to figure things out. I'm trialing a "microservice"-like architecture solely because it's easier to do some things in other languages. Hopefully widespread WASM support hits soon but I'm a long-time single-server guy now trying to figure out Kubernetes because I find Rust enjoyable.
[1] https://github.com/kristoff-it/zig-heyredis (far from complete)
This claim however is somewhat misleading: "Rust is a C++, not a C". It's clearly not either of them - it's a new language. However, it's a good replacement for both C and C++. May be the above idea means, that those familiar with C++ concepts can find Rust easier to understand - that's true, it uses major ideas from it like scope based resource management (aka RAII) and so on. But it uses other ideas as well.
No language is perfect in absolute sense, and each has its own trade offs. But the above gives a rough framework to answer "why Go and not Rust" or "why Rust and not Go" in some particular situation.
There is always going to be some overlap area, where both are adequate enough, so any answer to such questions would be moot there.
https://old.reddit.com/r/programming/comments/d50u9g/why_go_...
Some things were still missing though, if I remember correctly something like handling memory allocation errors was an issue, which is important for embedded systems and similar scenarios. I suppose there was some plan to address that.
I'm thinking Algol based syntax, expressive type system (sum types/generics/structurally typed structs), and optional mutability like Rust, but without the mental overhead of memory management.
I would describe the difference between Rust and Kotlin as: Rust offers control over interior mutability and guarantees that there is only ever one mutable reference at a time—Kotlin offers exterior mutability control, not all that dissimilar from final in Java or const in C/C++, but it is better.
> Too bad that outside of iOS development it's still unusable.
Sub-optimal for sure, but not unusable. It's being used in production on the server in several instances.
It's not based on an ML-ish type system, but the amazing metaprogramming capabilities allowed std.variant.visit (sum type matching) to be implemented in normal library code:
https://dlang.org/blog/2018/03/29/std-variant-is-everything-...
immutability is not default too… but on the other hand, there's @nogc and betterC mode, purity annotations, contract programming, C++ (!) interop, an option to use the "C-ish" linking model (to build with e.g. Meson instead of Dub, install dynamic libs system-wide, and so one), and again, just next level metaprogramming.
You can do anything with Rust procedural macros, but they give you a token stream and you drag in the rust parser as a dependency and operate on the raw AST. That's hard and "special". With D metaprogramming you can, for example, just `static foreach` over the list of the current class's members directly inline where you want it.
It should be
As long as the tool gets the job done, I don't see the issue.
That's not to say some tools aren't better suited to a task than others, or that there isn't room for improvement, but, unless things are manifestly mismatched I don't see why it wouldn't be a satisfactory answer, especially for a "hey i wrote this cool thing" project.
Boxed types? What a crazy idea that an int can be null. Just fixed a bunch of bugs related to that. String can be null? Just say no. golang's value types are much less error prone. You can go out of your way and pass a pointer to a string, but having to always worry about whether something is null in Java is a PITA.
The concurrency story in Java is also horrible. So much effort is required to achieve simple things. You're always having to make trade offs with the heavy weight threading model. Thread pools? What is this, the 90s?
With golang, I never once messed with the gc. Not once. We have a gc related discussion about once a week with Java.
And the deployment story. So simple with golang. So much extra work for nothing with Java. Just give me an executable I can run. How hard is that?
But I think it will get there, eventually.
It is OK to love and propmote a language. GO is terrific and we have been waiting for it for a long time.
But so is Rust.
If you want to say GO is better choice than Rust ("Why GO and not Rust" is the title after all) listing featurs of Rust ("...Go doesn’t want unused variables or imports...") is not a convincing way to proceed.
Horses for courses and we should have stopped language wars in 1995!
And don't let detractors get you down. No matter what you do, or how successful you are, there will always be critics. Ignore them.
Still, the vast majority of critical and infrastructure code at Google remains written in C++ and Java. Languages with superior performance, ecosystems, and that have actually proven to work in large scale programming (as opposed to hand wavy incorrect claims about being that).
golang is more or less a hobby project with a lot of bad design decisions that went too far.
In my case, I sometimes ask it to learn about particular real or perceived shortcomings of Rust, which may be fixable.
If I believe that Rust should address the problem, and someone chose not to use Rust (which is different from choosing to use Go!) I am curious as to what obstacle the person hit.
We should tame the zealous members of our community, but don't cluster everyone into the same bucket :) Some of us are just trying to build a great ecosystem and that requires us to find blindspots and notice hard-to-observe-from-inside problems.
Strongly agree, that's how I concluded the blog post :)
:)
I'd like to offer constructive criticism, but really, there is nothing to salvage here.
- Go is much easier to learn than Java or C#.
- The Go community regards as anti-patterns many abstractions regularly employed by Java / C#.
- With Go, it’s easier as a junior developer to be more productive, and harder as a mid-level developer to introduce brittle abstractions that will cause problems down the line.
Huge red flags all.
Rust is very clearly an ML derivative, not (just) a better C++
Yes. Go's "do not communicate by sharing memory; instead, share memory by communicating" thing [1] is advice, not a language requirement. Go's memory model is similar to C's. Channels are completely optional; you can share pointers and protect stuff with mutexes. You can skip the mutexes and have data races, too, unfortunately. At least Go has good tooling for finding these kinds of races dynamically. [2]
Rust is similar, except that it prevents data races (as well as other types of memory errors) in safe Rust code, barring compiler bugs. I absolutely love this property, and it can save you from very difficult-to-debug production problems. It has a significant upfront cost in terms of "fighting the borrow checker", adding extra annotations to your code, etc., so it's definitely not worth it for every developer in every situation.
It's kind of a put-off.
Broadly speaking, in any system, there is necessary complexity (imposed by the problem domain) and unncessary complexity (imposed by the tools, the developers, the customer's interpretation of The Matrix, etc).
Go aims to minimise the latter, which is a worthy goal: I understand very personally how easily it is to add unnecessary complexity while trying to 'contain' the necessary complexity...
However, languages also provide useful and usable means to tame the necessary complexity. It's a tradeoff.
These days I try to write C# like Go: no IoC, composition over inheritance, specificity over genericity, etc. But enforcing those kinds of limitations at the language layer places a hard cap on the abstractions available later on.
To some extent, again, that's a good thing: far too many abstraction astronaut libraries in DotNet-land, spawned by someone's pet project getting overgeneralised. Such things need to be better considered and better contained within the developer group using them, unless they're extremely well-designed which is rare.
But if you remove the clean way to do something, people will do it anyway and it will be a mess. Quite probably in an 'edge' codebase, in a system where the senior devs have enough to deal with already, and the clique of devs looking after that particular edge develop their own little dialect of the language. (I don't care how much you think 'any dev can modify anything' in your code, if you've got more than ~10 devs you have 'preferred' people for certain areas, and some newbies will pick up some bits preferentially.)
Or you could give them a tool with higher-order concepts, and they'd be able to work within its idioms.
Restricting the available idioms just forces people to create new ones. You get doubleplus ungood code from that in fairly short order. Idioms get generated when the culture doesn't already have them, and the culture fragments according to the idioms its subcultures generate if they can't crosspollinate effectively.
Don't try to solve a social problem with technology. Solve it with education, discussion and code review instead. If you don't have time for that, you don't have time to fix the mess resulting from the technical solution either.
That said, the smaller the service the less likely that it's going to need to deal with higher-order abstractions. If you really are 'just' gluing together services in interesting ways (note that the filesystem is a service of sorts, and Git LFS is written in Go) and you can confine your code to solving specific problems, you probably don't need the higher order abstractions anyway.
In which case any other language could probably do the job too, to be honest, but they'd make it easier to sprawl... which brings us back to a language which puts an 'awkward' cap on making things sprawl.
Remember that it's very easy for a project to grow beyond its initial scope, and choice of language can encourage, enable, smother or doom that, and any of those outcomes can be good depending on your outlook...
Maybe because trendy languages aren't a panacea.
There's always going to be pain. I think we just assume the other pain will be more tolerable.
Elixir doesn't need to "cross compile" because its only compile target is BEAM. That's like saying "Java doesn't cross-compile".
As many of you know, the Go community has been having a knock-down drag-out argument over whether/how generics should be added to the language. I joked a bit about this, but I am serious in my contention that much of the value proposition I see of Go over Java would be wiped away if a feature that complex gets bolted onto it.
Go doesn't have "shoot yourself in the foot" pointers. For that, you need pointer arithmetic, and Go doesn't have that. (Outside of the "unsafe" library.) interface{} may be an analog of void∗, but it lacks the failure in C where casting something to void∗ loses all type information; in Go, if you put an "string" into an interface{}, the runtime still knows it's a "string" and you can't put it into any sort of strongly typed variable except a string. The runtime/virtual machine Go implements knows the types of all values at all times.
I really wish the Go designers had called their pointers "references". Considered across the entire landscape of what various programming languages call "pointers" and "references", there's substantial overlap but I think Go's pointers are closer to the center of the "references" cluster than the "pointers" one.
Especially for "big scope", I place a lot of value on leveraging Rust's more extensive type system and abstractions to model the "complex domains".
I would also bet that services written in Rust would be more robust than those initially written in Go. Initially standing it up is, in the end, such a small part of your long-term productivity, and Rust really shines in all the other parts.
Seems to me that Go is more compelling than Rust for enterprise software development because many businesses do a bad job of looking at their development costs over a longer time frame, thus valuing short learning curve over high reliability.
So really, it seems like the author does not have a profound familiarity with Rust. Which is fine, but not a great basis for confidently expressing semi-universal truths on your blog.
For me, the biggest hit to productivity are long compile times. One of my projects is ~80k loc C++ app (SumatraPDF) and I pretty much abandoned working on it because the long compilation times are killing me.
Go is good in that regard. My medium sized projects compile pretty much instantly.
I hear Rust is not doing well there.
The second productivity boost is GC (Garbage Collector). I don't want to manually track every allocation (C++) or think about how to contort the code to a form that Rust will be happy about.
Long term productivity is very much why I use Go.
In addition to GC and fast compile times, there's now a very rich ecosystem of libraries for almost everything you might need.
The language has been stable in past 10 years. I don't need to fix the code every major release because language changed (Swift) and I don't need to learn a large number of new things because of significant new addition (Rust).
And for what I use it (backend servers, cmd-line apps) Go is more than fast enough (which can't be said about out languages that have similar productivity, like Python, Ruby, Node).
If there's a C++ competitor I'll be willing to look at, it'll be JAI (when it's released).
Never claimed this. I compared the Go toolchain with other toolchains I happened to encounter in enterprise development.
> I place a lot of value on leveraging Rust's more extensive type system and abstractions to model the "complex domains". I would also bet that services written in Rust would be more robust than those initially written in Go.
In my experience it's not easy to get there in an environment like the one I described in my post, and any non-trivial abstraction carries a relative risk to later become a problem. The problem is that it doesn't matter how "sound" the abstraction is, the product owner will come in and change things in surprising ways. So at that point investing in sophisticated mechanisms becomes a form of premature optimization. That's the reason you can't afford microservices if the business doesn't have a well-defined structure.
>Seems to me that Go is more compelling than Rust for enterprise software development because many businesses do a bad job of looking at their development costs over a longer time frame, thus valuing short learning curve over high reliability.
For some business, "high reliability" does have less value than a shorter time-to-market, and in enterprise software development providing value to the business is the entirety of your job, not building cool tech. That's why reconciliation procedures exist even now that we have tons of tools to ensure transactional/eventual consistency.