Go may win out in the early days of a program for resource usage and perceived complexity but by the time your team has cooked up some frankenstien of an application framework and tried to standardize logging, metrics, etc you would have been better off in JVM land from the start. At least then you can pick battle-tested and supported frameworks.
Python, Ruby, JS/TS -> Go is a weird one though. I can understand why when people make that transition they think Go is the best thing since sliced bread. It's by far one of the easiest languages to learn, it's miles faster than what they are coming from and the tooling is way less shit than those interpreted languages.
However all of those languages represent a very very low bar. I think if the same people could be convinced to try Kotlin (or even modern Java) they would be a) surprised how easy it is these days and b) finally understand that it's ecosystem and tooling that really matters at the end of the day.
Though I have to say Go existing is by and large a good thing even if I don't like it. It helps get people off things like Typescript which are absolute garbage-tier onto something half-reasonable. When they grow out of Go they generally end up somewhere better, just takes a few years.
My personal preference for such tools is Rust but it's admittedly a harder sell.
Comment above is mostly addressing the server software use case with larger teams and 100KLOC+ (either one or many programs).
I don't think anyone was using Java much in the CLI niche. It was dominated mostly by Python and Ruby (and Perl before that) before Go came along. I do think Go represents an improvement on that status quo.
Kubernetes, Docker, Terraform and their respective ecosystems really aren't what one would call a smaller codebase.
Go offers a nice balance between performance and platform agnosticism, features like parallelism without it being too complex to write. Manual memory management would have been a useless obstacle wasting developer time for little real gain. Fighting the language to have parallel execution, easy crosscompile, etc. why?
As for HCL, I'm on the opinion that a DSL is better. It's even in the name, Infra as Code, not with code. You want your infrastructure declarations to be easily readable and supportable. Anyone who has used Terraform can read any HCL; your Python code doing the same could be terrible and unreadable by anyone not already on your team. Too much flexibility for something as critical can end badly, imho.
In any case, there's cdktf now, you can mix and match terraform with cdktf.
Disclaimer: I work at Hashicorp, opinions my own and have stayed the same since before joining
As far as I know, it's not hard to ship a JVM runner with the installer of your program, so this should be a non-issue.
You can trivially ship a self-contained JVM that is completely isolated within a single directory.
> Go offers a nice balance between performance and platform agnosticism
I don't disagree with this. It's too bad the language itself has major problems (which I won't get into here).
> I'm on the opinion that a DSL is better
The two aren't mutually exclusive. What I want is a DSL embedded in a full programming language, like chef's dsl in ruby.
> You want your infrastructure declarations to be easily readable and supportable.
Absolutely.
> Anyone who has used Terraform can read any HCL; your Python code doing the same could be terrible and unreadable by anyone not already on your team
It's possible to write terrible and unreadable HCL too. I know because I've written some.
> In any case, there's cdktf now, you can mix and match terraform with cdktf.
Unfortunately, I haven't had a chance to look too deeply into it yet, although I definitely want to.
I've seen some atrocious Ruby for Chef, so I'm not sure I'd agree here. The advantage of a DSL is that it limits how far you can go. In the case of HCL pre-v2, it had some limitations around dynamism, but now that's out of the way IMHO it strikes a decent balance.
You can write hardcore HCL, but IMHO it's still way easier to parse by a human that bad Python or JavaScript.
As for CDKTF, I'd recommend you do. You can mix and match like having a project with CDKTF in e.g. Python or Go use regular TF HCL modules, but also vice versa, defining a module in TypeScript and consuming it from regular Terraform. So you can have the extra flexibility ( with all of it's advantages and risks) on a per module basis, not all or nothing.
It's still around, there is a lot of existing J2EE codebases but it's successor - Quarkus has done away with the vast majority of the config mess. I can't speak to how well it manages that transition though as I was never a big J2EE guy.
The other old offender, Spring, developed a new convention over configuration distribution called Spring Boot which requires minimal configuration and has pretty much displaced the old way of doing things entirely. It's been around for years as the default option at this point.
The way I see Java is that it moves slowly but tends to happily assimilate the best ideas from other languages, runtimes and frameworks and over time sheds it's warts (Java 8 era sun.internal.*, old Date/Time APIs, etc).
Not everyone seems to have an up to date opinion of Java because the last time they touched it was J2EE days or worse yet in university.
A lot of things about Go work out well in practice when there's a human watching it that can just slaughter the process if it starts going off the rails, and the trivial distribution and excellent startup performance make it almost perfect for CLIs. Small code works fairly well, and the sometimes-quite-yolo stdlib gets you to "works on my machine" for 99% of your needs quickly and that's often good enough for ad-hoc tooling (and when it isn't: just delete that non-UTF-8-named file, sheesh).
For large stuff? No please. The type system is far too weak, and the runtime has too many sharp corners. About the only thing I like about it for this is that runtime reflection is very tightly restricted, which prevents some of the worst insanity you see in Java.
There are no stdlib filter/map/reduce yet, and generics are new enough that most people are still writing the same for-range loops over and over. Lambdas don’t capture loop variable values.
x, y := f() sometimes assigns and sometimes shadows an outer scope.
defer isn’t block scoped, you have to simulate the code to figure out which calls are stacked at this point. Hopefully they’re added deterministically, but you can keep deferring stuff in a loop if you want.
(This is aside from the many ways Scala and Kotlin are far more readable than Java.)
How do you easily ignore the majority of the screen? Is there an editor plugin that just doesn’t display those ifs?
Here's one example that might make it clearer: https://commandcenter.blogspot.com/2017/12/error-handling-in....
If the only thing you're doing is saying `if err != nil { return nil, err }` anyway, which is what empirically the majority of go's error handling code does, yes, you should have a better way of representing that, whether it's a "?", ASSIGN_OR_RETURN, or exceptions, which take a single character, a single line, or a no characters at all, as opposed to golang's 3 lines of visual noise!
Saying that "you can use custom error types to give finer grained error information" is great and all, but I learned that in my CS101 class, in Java, in like 2012. It's not fancy or better or good, except compared to C. Its a worse way of doing a thing that Java and Python (and...) have been doing for decades.
// Bad, most of the time.
result, err := foo(x)
if err != nil {
return err
}
But to do this: // Better, most of the time.
result, err := foo(x)
if err != nil {
return fmt.Errorf("performing foo on x = %v: %s", x, err)
}
This isn't noise. This is meaningful logic. That is what exception mechanisms seem to discourage, as they are invisible (unless you wrap every call to everything that throws into a try{}, but then it's not better than an if{} is it?), and this is what the ? operator in some languages discourages as well, because it adds no human- or machine-readable context.There must be syntax that indicates what to do when an error occurs, and that syntax must support addition of information about the context in which the error has occurred. And why not have the most basic conditional take the role of that syntax?
Also, bubbling up is the sane default, even if you are writing a fast script/trying out some code it won’t clutter anything and you will be notified to handle the problems once you want to write robust code. While in Go you will just put empty if errs and will forget about the whole thing, swallowing errors which are arguably the worst thing.
try { let res = somethingThatMightFail(); catch(err) { // log the error or something return null; }
// res is good now... but it's out of scope
Whereas in C++ you'd just write
ASSIGN_OR_RETURN(auto res, somethingThatMightFail())
And it will bubble your error status just like you wanted, only takes 1 line of code, and keeps all the error information (assuming you're using absl::Status).
I haven't really used Go, but it sounds like the worst of both worlds.
> even if you are writing a fast script/trying out some code it won’t clutter anything and you will be notified to handle the problems once you want to write robust code
How are you going to go back and find every callsite that might potentially fail at a later date? There's no easy way to even tell if somethingThatMightFail might fail if it doesn't return something like a "StatusOr" or "Option" or something. I think Java has some tooling to force exception handling. Not sure about Go.
While Java’s exceptions are not without problems (too deep inheritance trees don’t make sense on exception types, lambdas not operate too well with this feature, etc), I think it is a really good direction not utilized to the full by any mainstream language.
I've seen people that do that and it sounds nice until you end up with
Failed foo on x: failed bar on x: failed doing baz on x: failed to do quux".
You've created a worse stacktrace! I now have to search for a string and hope I can guess where the %s (or v or q) was instead of just grabbing the line number.
Or I do none of that, pass errors around directly, and just use the stacktrace.
The issue with your claim is that we only care about errors in two places, where we create them (like where the file was unable to be read) and where we handle them (when they are either swallowed bc the behavior was expected or passed to the user).
You don't care about them in all the intervening code, and exceptions handle that better.
> And why not have the most basic conditional take the role of that syntax?
Because it bloats your code and you end up with in practice more lines spent on verbose, cookie cutter error returning than actual logic!
Like, you don't need additional human readable context except in very particular cases. Go proves this in practice as almost everyone just returns the unmodified error most of the time, and it works great. Except for the annoying syntax.
Because having approx. 3/4 of your code devoted to error handling and error propagation is both hard to write and hard to read.
IMO rust's approach is close to ideal, in the common case where you just propagate up, the syntax is succinct, but still explicit, and when you handle it, the logic is close to the callsite, and is just working with a normal value. Useful stack traces are still a bit of a problem, but there is work being done on that.
And that's not to mention how in go you always return both the error and regular values, even though you usually only use one.
I agree about defer and the walrus operator though. Both can be quite confusing at times, and I think that Go with block-scoped defer and no walrus would be a more consistent language.
2) No try/catch. Inexcusable in my opinion.
3) The syntax for declaring maps/arrays looks gross. Again, being different for the sake of it.
4) No while loops. The for loop syntax is not too bad, but I think this was again a choice made out of trying to simplify things when not a single person ever complained that "Java/C has too many types of loops".
I could go on, but in general my opinion is that Go deviated from some good established norms for no good reason, and this makes it much harder to adopt. It also isn't fun when you have a code base with e.g. Go and Java or Go and C# mixed in. The mental overhead of having an entirely different syntax to reason about is frustrating. And for what benefit? At least list comprehensions in Python are nice to read. I can't think of anything that is nice to read in Go.
You won't like Rust either then. Though its just a matter of time till Rust gets try-catch too. It's slowly moving towards that step by step. Folks have already made crates https://docs.rs/try-catch/latest/try_catch/
For 2 you can see it that way. Or you are glad there are no exceptions and prefer the explicitness of error handling in Go. So in the end it's also subjective I guess.
I remember when I first looked at Go ~7-8 years ago I also didn't like the syntax. However it grew on me and now I like it. Opinions change. These days I try to not instantly judge something on my initial feelings.
The reality is that go is a very hard language to master and to avoid footgun. But it is an hard truth that few people are willing to accept.
What makes go powerful also make it weak.
In general the complexity need to live somewhere and in go it can only be the code itself as the language is too simple.
As for concurrency, that topic is hard in pretty much any mainstream language. Maybe with the exception of Rust, but Rust isn't exactly a langauge that is not “hard to master”, and even Rust doesn't protect you from all forms of concurrency issues. Merely most of them, heh.
The introduction of generics should make it easier to wrap these lower level concurrency primitives into higher level safe constructs without needing code generation or runtime type casting via interface{}.
Most devs also struggle to structure go programs in a way that makes them easily testable, but that’s true in a lot of languages.
> The introduction of generics should make it easier to wrap these lower level concurrency primitives into higher level safe constructs without needing code generation or runtime type casting via interface{}.
That process has actually already begun with Go 1.19's sync/atomic.Pointer[T]. See https://pkg.go.dev/sync/atomic@master#Pointer. Things like easier non-blocking send and receive or easier fan-in and fan-out channels should probably follow suit.
My experience especially relative to Java is the opposite - between httptest and sqlmock and the ease of setting up listeners in a separate goroutine, it's easy even for new Go developers to create new, realistic request-to-response functional (behavioral / Detroit-style / whatever they're called today) tests.
On the other hand, getting our long-time Java devs to hook up a reasonable wiremock test is like pulling teeth. They much prefer piles of redundant unit tests with a few Spring-injected "integration tests" against H2 and an in-process gRPC "endpoint" or whatever, that of course therefore aren't testing much integration at all.
Excuse me but Java? This is as subjective as it can be.
All java code be lie `public static void SomeReallyLongNameDescribingEverthing42 implements SomeOtherEvenLongerName`
For me it's 10 times harder to follow than Go's `if err` blocks (which I actually do appreciate most of the time).
CamelCase makes it even worse. Short CamelCase is quite okay, but the longer it gets, the harder it on my eyes.
But even with short naming it still like 2-3 words just to name a function. wtf.
1. snake_case - best of the best. words are separated, easy to read even if the words are not in english or latin
2. ShrtCmlCase - worse than snake. Pros: quite short, does not take whole monitor width. Cons: requires additional thinking and some sort of in-team guidelines. Very opinionated.
3. JavaCamelCase - worse than ShrtCmlCase. Pros: you can put in and describe anything in a single name of a class\variable. Cons: you spend too much attention on reading rather than analysing the program.
I can understand why Java uses long names though. Its oop and enterprise nature is to "blame". This kind of naming is unavoidable when you are building a giant monolith with hundreds or thousands of classes, all kind of inheritance nuances and trying to have all names actually mean and describe something.
As I said earlier - all are very subjective topics.
There isn't a ton about Java's syntax to be super opinionated on, compared to Go, simply due to how unique Go's syntax is.
My main problem with Java code is that it's too hard to read due to endless variables\classes etc names and such. It's almost like reading some Oracle's documentation rather than reading code.
Golang date formatting on the other hand... I still mad an whoever is responsible for this.
I then rewrote the backend in Go, which gave a 90% performance increase.
It could now run all the streams on any normal pc.
When Java 19 lands you can have best of both worlds. Virtual Threads are equivalent to goroutines but you have access to better languages like Java, Kotlin and Clojure.
Valhalla has also produced an amount of language machinery that results in most of the same issues as Go. (E.g. "is this function receiving an interface or concrete value?" becomes "do I have a class, value class, or primitive class?") The performance ripples of requiring value objects to be immutable also remains to be seen.
I think by Java 21 they will be rock solid though which isn't really that far away in Java timescales.
Immutability is trivial to optimize away.
(That said, C# and Java are no performance slouches and I expect they’d all be roughly comparable in the end if the Go code didn’t suck so much.)
Go isn't fast, it's decent but nothing special for AOT native code. (atleast the standard gc compiler, llgo, gccgo etc could end up being a different story). It can show good overall performance in I/O bound workloads because of native M:N green threading though. I imagine even that is no longer a point in Go's favor when Java's Virtual Threads land and become widespread however.
Go's only real interesting runtime feature was their emphasis on having a very low latency GC. This was somewhat special for a few years but between improvements made to .NET and Java's new GCs (Shanandoah and ZGC) that gap has been eliminated while these runtimes still provide better throughput on top of their now low latency.
There really isn't a good performance or resource usage reason to pick Go over C#/Java anymore, all of its advantages have been eroded.
Garbage collection accounted for over 95% of the run-time. It was a perfect pathological case where it continually thrashed below/above a GC threshold, without hitting the point where it would make more room. Adding just a couple kilobytes of ballast[1] pushed it over the limit to where it reliably took about 200ms. (and then I rewrote the test to stop allocating so much junk)
Go's GC is mostly pretty good in practice, but yeah. It's definitely not something you can rely on blindly.
Also I wish they'd stop claiming that its pause times are tiny - the stop-the-world pauses are indeed consistently very short, but it hardly matters when it can pause goroutines for far, far longer[2]. And e.g. when you're allocating, your goroutine may be paused to "help" the GC, which can take a substantial amount of time (as happened in that^ test), which frequently leads to very large tail-latency. Most people don't care about stop-the-world times, they care about tail-latency, and Go is just leaning on people's misunderstanding that one is the cause of the other. It can be, but it's not the only source.
[1]: https://blog.twitch.tv/en/2019/04/10/go-memory-ballast-how-i...
[2]: http://big-elephants.com/2018-09/unexpected-gc-pauses/ among other sources of individual goroutine pauses.
I think Go's GC is solidly "good enough", and much better than many languages, so I broadly like it. Coupled with built-in tracing that shows GC activity and it's in a pretty good position. It's more that all GCs have pathological behavior somewhere, and no amount of distracting hand-waving changes that.
All three use load-barriers which result in more work done in application threads as the pressure on the GC increases.
That said these GCs are substantially better than the Go GC so you are talking about much higher allocation throughput before running into these issues in practice vs Go.
At Google's size this means nothing. The protobuf and gRPC developers don't even always align well, let alone with an entire language ecosystem.
By the same token, C#'s excellent gRPC performance is the result of MS dumping a chunk of resources into gRPC/PB optimization most recently. For Google it's cheaper to buy 2x as many nodes as it is to make Go gRPC 2x faster.
That is definitely not true if Google was using Go gRPC at any reasonable scale.
A more accurate conclusion to draw from it is MSFT is using C# gRPC at scale and Google isn't using Go gRPC at scale. Which in itself makes a lot more sense because C# is the bread and butter language at MSFT for most large network services and Go isn't at Google, it barely has any penetration compared to Java and C++ (which predictably have much much faster gRPC and PB implementations).