One year after switching from Java to Go
493 comments
·February 18, 2025time4tea
unscaled
I think we can sum it this way. The blog post writer's intuition was correct: if you write two equivalent Go and and JVM programs, the Go program would use less heap memory and have faster startup times. What they are incorrect about is the extent of these claims.
It is obvious that most of the memory and startup overhead in their software comes from Spring, rather than the JVM. The JVM is probably not an ideal platform for writing an Kubernetes infra tool like operator or a sidecar (mostly due to heap size and more a complex binary packaging story), but using Spring Framework for writing these kind of tool is a bigger problem. If they just wrote their initial tool in Kotlin without using Spring framework at all, it would be much faster. They could even have kept Dependency Injection with a lightweight framework like Koin or Dagger (even a reflection-based framework like Guice performs worlds better than Spring).
I would probably still prefer Rust for writing most infrastructure projects nowadays. GraalVM Native Images has similar memory footprint to Go and solve the slightly troublesome JAR deployment story, but with the complex license terms for the enterprise edition (where most of the performance optimizations are), I don't feel safe using it. I also prefer Rust's concurrency model and I find cargo to be a far more pleasant experience than either Maven or Gradle, but in the end of the day, you will not see serious issues if you use Kotlin in a sane way, and you can even save on some Go boilerplate.
AYBABTME
Rust has its uses but why would you write infra code in Rust when Go is used for most of it, and is just much more ergonomic and fast to work with. The iteration times with Rust are quite detrimental. On the other hand, most of k8s' ecosystem is in Go.
I don't like commenting in language-war territory things but I found your comment surprising. "Rust or JVM" for infra isn't a dichotomy I would expect.
sn9
Discord had a pretty famous transition from Go to Rust [0].
[0] https://discord.com/blog/why-discord-is-switching-from-go-to...
packetlost
Eh, I don't think iteration times in Rust are nearly as bad as you think. During a realistic development loop compilation is <1s in most situations thanks to incremental compilation and tests are fast unless you have synthetic slowdowns such as sleeps or IO (which would effect both languages equally).
indemnity
Funnily enough the initial version of Kubernetes was written in Java! But refactored into Go early on https://archive.fosdem.org/2019/schedule/event/kubernetesclu...
smusamashah
At work we work on a Java code base 20 years old and it is written like C. No dependency injection or other Java Web development like shenanigans. Almost every lib etc has been built in-house. It runs an MMO, it's fast. Its just way more productive/faster to work and implement something in the Java codebase than a C++ codebase that we have.
Someone shared a Job posting which asked for "no java experience". It was funny.
watt
If you write Java just like you would write Go - meaning, no reflection shenanigans, use Jigsaw to cut out unneeded JVM stuff, or use GraalVM and compile natively (which you will be able to, because you did not use any of JVM reflection magic), Java will absolutely be competitive.
wvh
In the end it all boils down to higher-level logic compiling down to machine code that has to run. A language's culture and philosophical choices are tied to the language itself as much as syntax or compiler, so maybe the largest difference between languages like Java and Go is really the whole developer attitude and aesthetic choices.
theshrike79
It's kinda understandable. I've seen "Java coders" write Python for example.
The first thing they do is create a class and maybe even a Factory or Interface.
You can see instantly where their experience is from and it's hard to unlearn.
kuschku
Java developers may write verbose python, but that doesn't compare to the crimes actual python devs commit.
Currently trying to modernize a python project that doesn't use modules, just executable python files that import each other with custom sys.path hackery, which is also used for globals, no type annotations, GLib used for everything including math and string to int parsing.
skapadia
That is funny, because I was a Java developer for many years, then Scala for a few years, and these days I mainly write Python, but the last thing I go for is creating a class. That's generally only when a set of functions in a single responsibility module need to share / mutate some state, and it's more self-documenting than passing around dictionaries.
procaryote
Yep.
Java is really good. Java developer culture is awful.
If you instead of spring boot just pick a few dependencies you really need, you don't throw the whole Design Patterns book at it just because you can, and you don't try to make everything changeable without recompiling or redeploying, it's pretty nice to work with
tombert
I have said that for awhile: the worst thing about Java is Java developers.
I have some issues with the Java language (though Java 21? Actually pretty ok!), but there's no question that there's a lot of great stuff in regards to libraries in Java land.
A lot of the stuff that's just built into the JDK is already very good, for example. NIO can be a bit hard to work with, but is generally very good, fast, and reliable. A lot of the concurrency abstractions (e.g. BlockingQueues) are really pleasant to work with for most concurrent programs, the different types of mutexes/locks give access to most of the patterns you want, and the thread-safe collections like ConcurrentHashMaps are very boring, in that they work pretty much exactly as I want them to.
If we extend to third party libraries, it's even better. Vert.x and Disruptor, for example, are downright excellent tools for wrangling concurrency.
The issue is that it feels like a lot of Java developers are stuck in 1999; it can be like pulling teeth to even use NIO, which isn't exactly "new" at this point. When I wrote some stuff using BlockingQueues instead of throwing `synchronized` everywhere, people acted like I was grabbing this was some exotic code from a distant land that had never been tried before, When I imported Vert.x into my project because I needed to run a lot of non-blocking concurrent tasks, it required a lot of justification for using it instead of using threads everywhere.
p2detar
Agree with everything that you wrote. The saddest thing is the job market. It would not be a lie to say the 90% of the jobs list Spring/Boot as a requirement. It's like a framework equals the whole language. This cannot be a good sign. I'm thankful enough to have around 2 decades of Java experience without touching anything named Spring and boy I can tell you I have been having lots of fun with Java. Using Vert.x right now and I like it a lot.
pjmlp
That is actually enterprise culture, using Go won't make a difference in the enterprise space.
Ever looked into Kubernetes source code?
monksy
I think Go adds to the enterprise culture and it does so on a global scale. Go is all about a consistent code style and a reduction of language complexity where your hands are tied on how you write code.
throw-the-towel
At a former job, my coworkers successfully wrote Java in Python.
mooreds
At $CURJOB, we have built a pretty good business on an app built in Java. We don't use Spring, but instead a lightweight MVC of our own creation[0]. It is open source but I don't know if anyone else is using it :) .
I once asked the founders why they chose java; it was because it was 2015ish and they had an existing product in java and they knew it. Which makes sense to me. I think there's a lot of path dependence to language choice.
I also think the domain matters. For database based webapps, Java can be great. Lots of tooling and knowledge around it. And modern java is pretty friendly to write. Plus, if you are interacting with something over an API and deploying it via a container, who really cares what it is written in?
For kubernetes operators? Seems like a natural fit for golang. Anything kube really. I had a friend who ran a k8s consultancy for a while and said that they'd prototype stuff in python because it was easy, then implement in golang because that was what was consistent with the rest of the ecosystem.
divan
> Java is really good. Java developer culture is awful.
First we shape our tools, then our tools shape us.
arccy
the Old Ones shape the tools, the tools shape the rest of us
cies
Like the article I hear Spring Boot here mentioned again. I also really hate the annotation culture. This is big in Spring Boot, and more common in Java since it is so damn verbose.
It is not inherent in Java though, and the Kotlin "developer culture" seems to be much more annotation averse (as we all should be).
whizzter
Annotations were adopted to cut down on the massive amounts of XML that was used to configure everything beforehand, still a lot was loosely wired for "extensibility" and people have learned that it was 99% overengineering so that's cut down these days in various ways.
peterashford
I think this is why most criticism of Java sounds weird to me. I've used Java since 1.3-ish and I've never used a web framework or an annotation and I think I've used a factory once (I think there's one in Swing and I recall thinking it was a stupid design choice).
gf000
You do realize that Java is objectively less verbose than Go? Even on a vanilla language to vanilla language basis, but let alone against something like Spring Boot that does almost everything for you in a typical CRUD application.
Annotations are declerative shorthands. How is a trivial spring boot endpoint with methods with a single @GET line above them denoting the endpoint verbose?
What about a single SQL query in an annotation above an interface method's name? Will your whole implementation of connect to db, execute query, iterate over the resulting rows, and convert them to some native object/struct shorter than.. 2 lines?
null
okr
Spring boot is just a tool. People seem to forget and take for granted the knowledge that is hidden there. Sure you can wire everything together yourself, but who does that? It would be such a waste of time. I rather wait these 5 seconds for a long running service, tbh.
renegade-otter
https://notes.ericjiang.com/posts/751
"PayPal and Wal-Mart have also had high-profile switches to Node.js. Of course, they’re comparing two completely different things to make Node.js look better. In these too-good-to-be-true stories, they’re switching from a gigantic enterprisey codebase to a Node.js app written from scratch. Is there any question that it wouldn’t have been faster? They could have switched to pretty much any anything and gotten a performance gain.
In LinkedIn’s case, they had proxies running on Mongrel with a concurrency of 1. It’s like switching from using one finger to type on a QWERTY keyboard to using ten fingers on a Dvorak keyboard and giving all the credit to Dvorak for a better keyboard layout."
P.S.: Google became so awful that a search for even the direct title of the article does not land it in the top results - just references to it.
okeuro49
> But a lot of people use it with spring/spring boot, a technology designed to work around the complexities of a particular type of middleware...
No, people use it because we don't want to reinvent the wheel.
Spring is well documented and Spring Boot gives you a set of dependencies that all work together.
Then you don't have to spend time messing around with things like OAuth and authentication, you can just write the application.
anthropodie
> Then you don't have to spend time messing around with things like OAuth and authentication, you can just write the application.
It sounds good but in reality people end up spending time messing around with config files and annotations.
gf000
Like, what other option is there? There is either a proper, battle-tested solution which requires some configuration so that it works as you want, or you start from scratch and create something specifically for your own usecase.
In the latter case, it may actually mean a significant amount of development orders of magnitude more than looking up how to configure stuff, constant maintainance, etc.
mike_hearn
Maybe? Depends on the framework. I've been using some Micronaut lately and it's a Spring-inspired framework where a lot of stuff Spring does at runtime is done up front at compile time.
The result is apps start really fast, can be compiled to a standalone native binary with GraalVM, use little memory, and errors that would once have resulted in a complex exception at startup now yield reasonable compiler errors instead (it has compiler plugins to make this work well).
I can't say I've spent much time messing with annotations or config files in this project. Certainly, what little time has been spent on the framework is more than saved by what it does.
javanonymous
> It sounds good but in reality people end up spending time messing around with config files and annotations.
I use Spring Boot at my day job and write mostly web services. I don't spend time messing around with config files and annotations. When I create a service class, I annotate it with @Service, and that is mostly what I need.
Example:
@Service
public record ItemsService(ItemsRepository repo) {
public void doStuff(String country) {
var items = repo.findByCountry(country);
// do stuff with items
}
}
Later versions of Spring Boot has reduced a lot of the annotations necessary, like @Inject if you use constructors etc. There are of course other annotations and configurations, but 90% of what I do is similar to the example I gave above. Things may have changed since last you used it, but the amount of "magic" and annotations is often much less than what is posted in these types of discussions.whstl
I don't see how writing "one more OAuth client" or "one more login thing" using Spring Boot is not reinventing the wheel in itself.
If you really care about "not reinventing the wheel" there are ready-made paid solutions such as Auth0 and Cognito, plus self-hostable open-source options like Keycloak, Authelia, and Dex.
Also, Spring Boot itself uses third-party libraries for OAuth and Authentication, like Ninbus, which people can drop-in in their non-Spring Java apps.
smrtinsert
Don't know why this is down voted its absolutely true. When our tech leadership reviewed the numbers we changed auth design without touching the application code base really. Saved a ton of work considering we have many microservices.
Spring rocks.
optician_owl
> Spring Boot gives you a set of dependencies that all work together.
But spring boot deps is infamous meme.
> Then you don't have to spend time messing around with things like OAuth and authentication
Yeah. The funny thing is reality is quite complicated and spring supports a lot of (almost) documented cases. But 99% javaspring developers do not care. I met quite a lot of experienced devs and only 2 of them know how to optimize application start or which errors Kafka wrapper would not retry and so on. Half of the non-default situations are solved via reinventing the wheel because of a lack of understanding of nuances. I can't say people are dumb, many of those devs are smart. I tend to say that ultra-framework kills people's expertise and in the long term hardly saves resources.
okeuro49
> I tend to say that ultra-framework kills people's expertise and in the long term hardly saves resources.
You can use as much of Spring or as little as you want. Don't want Hibernate? Use JDBC template.
I have noticed that people who don't use a framework, just end up inventing their own bespoke framework, which unlike Spring, is not documented and has no help available online.
cies
I found String (Boot) a horror to work with. Annotation based devt with super weird errors. The great thing was that most errors where run into by many before me so could be solved by a simple web search/ Stack Overflow article.
To me being able to CTRL-click my way into the libraries is very important. Overuse of annotations (a.k.a. magic) breaks that. It is what monkey patching is for Ruby. The beginning of the downfall IHMO of an otherwise great language.
Luckily Kotlin's culture avoids this.
dominicrose
I tried truffleruby for the adventofcode challenges. Almost everything ran faster with normal ruby. I got a stack too deep error in truffleruby that I didn't get in normal ruby. I don't recall having more problems with memory with one or the other. There was one case where truffleruby was really useful though.
Thus I'm with you that it's not a systems programming language. It seems good for processes that stay ON, like servers.
But then when compared with PHP, during development you don't need to worry about the server, it just takes the most recent version of your code. Surely hot-reload can be a solution with java, but in practice it's a complication, especially if you're part of a team/project where there is no hot-reload support.
Twirrim
The JVM doesn't attempt to compile a method until a method has reached 10,000 executions. So it's extremely likely your code ran the entire time in interpreted mode. Even if a method did get flagged as hot, you've then got the time taken to do the actual compilation to native code to contend with.
If you're trying to write systems code and want to use the JVM, GraalVM can do full ahead-of-time compilation and will get you almost instant start up with all methods natively compiled.
HumanOstrich
I don't follow. Seems like the issues you had were due to truffleruby and how it implements ruby's internals on the JVM.
That's not a Java/Kotlin/JVM issue.
gf000
Why would you expect any speedup for programs that run for a split of a second?
boreas7878
Yep, he is comparing the most bloated enterprise framework to lean Go apps. This article is so wrong in confusing Java with their particular monster project. You can definitively AoT-compile in Java, there are modern lightweight frameworks like Javalin, and I think the language itself now allows to write web services without any library.
725686
"cargo cult" really? Have you ever built maintained big a Java codebase? Spring is a marvel that sprang out of the experience of very, very talented people. It can be misused, of course, like any other technology, but it is a huge productivity booster.
cies
You are joking right?
"Spring is a marvel that sprang out of the experience of very, very talented people." --> sounds very culty to me.
And yes I maintain both Spring and non-Spring JVM projects.
bryancoxwell
> But there are obviously work around solutions in the Go ecosystem. It uses the Context ctx, which we pass around functions in order to juggle data around in the application.
Man. This works. The context API allows/enables it. But I’d really recommend against passing data to functions via context. The biggest selling point of Go to me is that I can usually just look at anyone’s code and know what it’s doing, but this breaks down when data is hidden inside a context. Dependency injection is entirely possible without using the context package at all, interfaces are great for it.
MrDarcy
I hit this point in tfa and had the same comment. Please don’t pass things around in a Comtext. Maybe stash a slog logger in there, but that’s about it.
I made the switch to Go a few years ago. For those who are on a similar journey as the author, or the author himself, I suggest spending time with the Go standard library and tools written by Rob Pike and Russ Cox to get a handle on idiomatic Go.
It’s clear the author still thinks in Java, not go. Saying Context ctx for example instead of ctx context.Context. Also DI, which is arguably not necessary at all in Go given how elegantly interfaces work.
I spent quite a lot of time using wire for DI in go only to really study the code it was generating and realizing it truly is code I would normally just write myself.
Edit:
Regarding stack traces, it turns out you don’t need them. I strongly suggest a top level error handler in Go combined with a custom error struct that records the file and line the error was first seen in your code. Then wrap the error as many times as you want to annotate additional lines as the error is handled up to the top level, but only that first point in our own code is what actually matters nearly all of the time.
andreasmetsala
> Also DI, which is arguably not necessary at all in Go given how elegantly interfaces work. > I spent quite a lot of time using wire for DI in go only to really study the code it was generating and realizing it truly is code I would normally just write myself.
DI is the idea that you should create your dependencies outside of the module / class / function that uses it and pass it in. This makes it easy to swap implementations.
DI does not require any framework and I would argue you can’t write modular code without it. Most likely you are doing DI even in your manually written code.
RussianCow
If you're doing "dependency injection" by just passing arguments to functions/modules, you're not really doing dependency injection—you're doing "dependencies" without the "injection" part. I'm not saying that DI necessitates a ton of magic, but you need at least a small framework for specifying dependencies and injecting them into your modules dynamically.
mukunda_johnson
I prefer stack traces in errors. It's gives so much more automatically so you don't have to worry about manual annotation. Stack traces and debug logs are the way to go. I like to use panics for exceptional conditions just for the convenient escape with the stack trace.
ignoramous
> I like to use panics for exceptional conditions just for the convenient escape with the stack trace.
One can debug.PrintStack(), instead.
nesarkvechnep
I’m yet to see a former Java developer who uses the idioms of the language they currently use. They all just write Java in a different language.
vram22
That is a function of the developer, not of the language, i.e. f(dev), not f(lang) ;)
Replace Java with star and that statement still holds true (for some people).
Hence the statement that you can write FORTRAN in any language.
https://blog.codinghorror.com/you-can-write-fortran-in-any-l...
mavelikara
This isn’t anything special about Java. A determined programmer can write Fortran in any language.
saturn_vk
> Also DI, which is arguably not necessary at all in Go given how elegantly interfaces work.
DI is necessary in every language that doesn't rely solely on global singletons. Passing dependencies as arguments to a function is DI.
What may not be necessary, are IOC containers automatically create objects and satisfy their dependencies.
unscaled
Too many people confuse the concept of DI with a DI framework. You don't even need a DI framework to write straightforward programs in Java. After all, Java also has interfaces!
One of the reason people needed a DI framework in Java is crazy "enterprise" configurability requirements and Java EE-based standards that required you to implement a class with a default no-argument constructor. If you're using a web framework like Jooby, Http4k, Ktor or Vert.x, you do not need a DI framework (source: we've written many modern Kotlin applications without a DI framework and we've had zero issues with that).
Of course, all of our non-toy Go applications are using dependency injection as well. Unless the code reviewer messes up, we won't let anyone configure behavior through globals and singletons.
arnath
This comment is about a very minor part of what you said, but isn’t the whole point of a DI framework to write code you’d have written anyway to save you time?
MrDarcy
I was writing code similar to how the popular int13 kubelogin kubectl plugin works, which also uses wire for DI and is organized as a clean architecture repo. In that particular case I found both the clean architecture and the wire DI to add more layers of abstraction, which took more time to comprehend, write, and maintain than jettisoning both and doing it with idiomatic Go.
bcrosby95
DI frameworks save you from writing trivial code, and it masks dependency insanity. This is why I don't use it even in Java. If the codebase gets to the point where a DI framework is really useful then you've fucked yourself over.
nine_k
The exact stack trace may not be very necessary, but tracing the chain of calls, especially async, can be hugely helpful in troubleshooting, performance tracking, etc.
In Node, I remember wrapping Promisesromises into objects that had a stack for pushing messages onto them, so that the creator of a Promise could mark the calling site, and creating another Promise within that promise would pick up the chain of call site names and append to it, etc. Logging that chain when a Promise fails proved to be very useful.
lelanthran
> In Node, I remember wrapping Promisesromises into objects that had a stack for pushing messages onto them, so that the creator of a Promise could mark the calling site, and creating another Promise within that promise would pick up the chain of call site names and append to it, etc. Logging that chain when a Promise fails proved to be very useful.
Sounds complicated. I don't use Node much (nor recently, for that matter), but when I write f/end JS I capture the chain of function calls by creating a new exception (or using whatever exception was thrown), and sending the `.stack` field to a globally-scope function which can then do whatever it wants with it.
Will that not work in Node?
eximius
> spent quite a lot of time using wire for DI in go only to really study the code it was generating and realizing it truly is code I would normally just write myself.
Yes, but the point is 1) you don't have to write it yourself and 2) it does 'the right thing' even if your junior dev straight out of BS CS wouldn't know how to write it.
There are, of course, caveats and not all frameworks are created equal and any tool can be misused. But I find DI very useful. Personally I'd recommend Uber's Fx framework and underlying dig library.
jbreckmckye
> Regarding stack traces, it turns out you don’t need them. I strongly suggest a top level error handler in Go combined with a custom error struct that records the file and line the error was first seen in your code. Then wrap the error as many times as you want
So instead of a stacktrace, you are - tracing the stack? Am I understanding correctly?
Because it just sounds like a manual version of stacktraces
Capricorn2481
> Because it just sounds like a manual version of stacktraces
Because it is. I don't understand it either.
cflewis
FWIW the internal Google style guide says to not pass anything via Context unless you _really_ know what you're doing and why. Things that make sense: security tokens and tracing. Things that don't make sense: almost everything else.
throwaway2037
> internal Google style guide
Can we read this somewhere?konart
I'm not sure about Google style guild but Go's context package docs state:
"Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions."
Using context as a some sort of a mule is an antipattern.
Saser
https://google.github.io/styleguide/go/ is the public version of it. Having read both I'd say the differences are small. The main thing missing, though, are the GoTip episodes that haven't been made public, and which are excellent.
khana
[dead]
Ferret7446
Context is just thread local storage aka dynamic scoping aka global variables.
It is useful for some things, particularly middleware that needs to cross API boundaries.
throwaway2037
> The biggest selling point of Go to me is that I can usually just look at anyone’s code and know what it’s doing
This is not possible in Java or C#? Both of those languages are so simple to grasp.lelanthran
> This is not possible in Java or C#? Both of those languages are so simple to grasp.
Not really, no.
Apart from the cognitive burden of knowing the test framework in use, the DI tooling in use, the Mocking framework in use, plus all the annotations that each framework may want/need, almost none of which applies to the Go projects I've seen, the languages themselves have drifted away from simplicity.
I used to program in C# but left it for a long time and only recently looked at it again. C# is approaching C++ levels of syntax indecipherability.
CharlieDigital
Kinda disagree on C#
It's actually converging with JS and TS
Small repo: https://github.com/CharlieDigital/js-ts-csharp
Edit: but I don't want to dismiss either because C# does cover a large gamut of use cases (desktop, 3D/gaming, COM interop, etc.) and it is true that in some use cases, you may see a wider range of keyword soup compared to just building web APIs (where I think it really shines; this is a .NET web API in 4 lines: https://imgur.com/a/simple-net-api-with-route-param-validati...).
zigzag312
> C# is approaching C++ levels of syntax indecipherability.
Can you post an example of such new syntax? In my experience C#'s syntax is getting cleaner.
gf000
C# tends to accumulate a lot of new features, but Java has a bog-standard syntax and even new features are very carefully added and easily guessable even if you haven't seen them before (e.g. switch expressions vs statements).
There is absolutely nothing more readable in Go than in Java, it's just classis familiarity (referencing the simple vs easy talk), I would even wager that too little expressivity will actively hinder code understanding.
(It's much easier to understand the intent of a complex stream API "pipe" than 4 nested for loops with ifs, with a bunch of dumb if errs pretending to be error handling).
Also, you are comparing a complex framework that solves 70% of your problem domain (at the price of a bit of a learning curve) with a vanilla Go project that only calls libraries. The exact same is more than possible with Java, it just doesn't make much sense in case of CRUD apps because better ways exist.
spicyusername
Gotta disagree for C#.
It definitely has more language features than Go, but by no means is indecipherable.
I find C# syntax to be quite crisp.
zdragnar
The languages aren't, strictly speaking, so much the problem as are the massive frameworks configured via distant files with lots of DI and reflection magic involved.
Go has some large frameworks, but the community frequently suggests they aren't needed and that the standard library is enough.
Cthulhu_
Exactly; if you ask about for example a test assertion or mocking library like in Java, you get told to not use it. These libraries often add a DSL of sorts, meaning you have to relearn them.
Granted, Go's testing library / setup is also a bit of a jump from regular Go code (especially things like table tests). But the assertions are just `if`s.
demi56
So the community influences the language, not the language that influence the community that explains everything
nprateem
Try understanding what spring boot is doing. Annotation soup. It's practically impossible for a beginner to Spring to debug.
mattgreenrocks
IntelliJ seems able to locate all potential implementations at injection points just fine.
gf000
I mean, would you be able to pilot an airplane at first try? Does it mean that it is badly designed?
Frameworks reverse who calls who, it is your code that will be called at certain points, not the other way around. Obviously, one has to learn a bit about the framework before touching one, blindly copy-pasting from stackoverflow/chatgpt is not software develolment.
rapsey
Programs written in those languages tend to have many layers of abstractions, on top of abstraction heavy frameworks.
roncesvalles
IoC DI in Go is a massive antipattern and absolutely should not be done. Do NOT write Java/.NET style controllers in Go i.e. initializing an instance of a "controller" type with some instances of a "dependency" such as a store.
Just use the dependent package directly. Initialize the package once using init() or Init(). Rely on the built-in package dependency resolver system in Go, which will catch cyclic dependencies, call init() in topo order, and other such things.
Test using monkey-patching. Stop using interfaces just to be able to swap a real thing with a mock implementation. These are all symptoms of writing Java in Go.
someothherguyy
So, write python in go instead, gotcha.
roncesvalles
The idiomatic way to write Go is as naively as possible after you fully understand how it works. Otherwise it'll just feel like Java with shitty ergonomics.
If you're ever writing Go and wish you had real classes instead of this deconstructed "mess" with struct types, methods, and interfaces, you're writing Go totally wrong.
stpedgwdgfhgdd
I don't get the part on package. Often you want to use structs. (Instance vs static)
How would you construct an instance in a test that needs mock implementations?
roncesvalles
Such an instance would just be a variable somewhere that would be initialized in some init() call when the program starts. Every user of that instance will use it directly (as opposed to storing a reference to it locally DI style).
the_gipsy
> Test using monkey-patching.
Can you elaborate? What library do you use?
3uler
And yet Uber wrote fx[1] to support DI in their golang services. It’s clearly a useful pattern when working on large services.
65a
I really cannot say Uber's use of Go is particularly idiomatic to me, having started writing Go more than a decade ago now. It just strikes me as overwrought, and I've worked on big services.
Cthulhu_
Just because a known company uses it doesn't mean it's authoritative; there's likewise functional programming libraries built by big companies, but FP should also be avoided in Go because it's not a functional language and not optimized for it.
roncesvalles
I disagree with the premise of that whole project. It shouldn't exist.
akoboldfrying
> Test using monkey-patching.
TIL Go has monkey-patching.
But since it has monkey-patching, how is it statically typed? Or does Go monkey-patching amount to creating an instance of a defined-on-the-fly subclass?
The latter would be interesting, because Java lets you do this too -- conveniently "new up" an instance of a subclass that you define inline of a given class or interface (this used to be the easiest way to define callbacks, before lambdas came along), so this testing strategy is available to Java too, but seems not to be preferred.
Separate question: IIUC, monkey-patching is convenient if your test code directly needs to create a TestX instead of a real X, but what if you need the test double "deeper down" -- that is, you need an X that creates a Y that creates a TestZ instead of a real Z?
tpm
Java doesn't really enable monkey-patching in the style of Javascript, Perl etc. AFAIK, or am I missing something? That you can create anonymous subclasses during runtime is different to e.g. editing methods of existing objects/classes during runtime.
nine_k
Implicit shared state? Exactly the thing to enjoy in a highly concurrent environment!
At least I hope that a context can be immutable throughout. I only see one variable in the documentation of the context package. Extending it with mutable fields, and mutating them to pass data between functions, would be something I'd never approve in a code review.
cyberax
The context itself is immutable, it's essentially a linked list (or actually a tree). If you need to "mutate" it, you create a new segment that links to a previous segment.
rendaw
How are interfaces a replacement or improvement to context? Is it just that they're type safe?
philipwhiuk
As a Java developer...
If the entire problem domain space is written in a language it's dumb not to follow suit. Libraries that solve problems reduce your work to your own specific issues, rather than 'building an apple pie from scratch'.
Java is good right now because most problems have libraries to do what you want. Most formats have APIs.
It's not perfect in any area - the start-up time is a bit lame, you have to write 'anti-Java' to really get close to native performance. But it's quick to build in, the toolchain is solid, the dependency framework works better than all the alternatives. It's a 95% language that's been made development friendly.
(Golang somehow added versioning late and is 'Git+' at best, NPM unpublished stuff, C++ is hell, etc. Rust crates just doesn't have much but seems to have been built properly).
But if you're working in a new space (crypto, AI, cloud) then you should definitely look at what the state-of-the-art libraries are written in.
And you should think real hard before you implement your app in anything else. Because there will be a real, long term, painful penalty unless you get VERY lucky and the entire ecosystem pivots to your language.
pylua
I’m also a long time java spring developer. I started writing a game recently and was really surprised about how bad the performance can be when you run it in a tight game loop.
The startup time is also a real problem, as you really want to be able to scale up pods quickly.
That said, it’s good enough right now. You can make it work at scale, and it’s worth the cost trade off of trying to do it more efficiently in a different language.
I would be curious to see how a rust microservice would compare in my companies infrastructure. How much cloud saving could we squeeze?
marginalia_nu
Writing high performance Java is definitely a bit of a dark science, a lot of the performance isn't just the code loop you're looking at, but memory allocations matter quite a lot as well. Complex hierarchies of large long lived objects can absolutely tank your performance.
There can also be a lot of performance to be gained by going off heap. I'd be probably be looking at an ECS design around MemorySegments, rather than modelling the game state with Java objects. Though, to be fair, this is how you'd write a game engine in C++ as well.
conwaytwitty
Spring Boot startup time is indeed a problem, especially when scaling horizontally on low cpu nodes.
If your environment allows for burst cpu usage until ready to accept traffic, you can start up really fast as spring does so much reflection magic during startup that can't be done during compilation "trivially". You can include hints for runtime configuration from a build, but it doesn't do much to help in really low cpu envs.
Then you can of course just do native images, but you lose some of the spring "magic", and might be annoying to refactor towards.
ivan_gammel
This actually makes me wonder if it is possible to preserve post-startup state and then restore it as a way to mitigate long computational stage during startup. I bet it is, maybe we could just serialize the application context and restore it.
pylua
I’ll have to look into burst cpu usage. Thanks
gf000
Could you expand on "how bad the performance can be" part?
If you are doing graphics, it is entirely more likely that you do something dumb there - there are many pitfalls.
Also, unless you are doing something very CPU-heavy, there won't be any noticeable difference as web servers are doing IO predominantly. Maybe slightly less RAM usage (but you could also just decrease the heapsize to tradeoff a bit of CPU-time for memory, if it were to make sense).
pylua
In my game loop I was using optional. When I profiled it the use of optional was one of the slowest points that could be optimized using null checks and ifs.
There were a lot of other slow areas as well, not where you would expect it.
rootlocus
Have you tried AoT compilation with graalvm?
pylua
No, the product has a lot of aop and I figure it would be difficult to make that work.
znpy
> The startup time is also a real problem, as you really want to be able to scale up pods quickly.
I was learning a bit of spring last week and a spring boot web application, generated via the web interface boots in like 800msec:
...
Initializing Spring embedded WebApplicationContext
Root WebApplicationContext: initialization completed in 339 ms
Tomcat started on port 8080 (http) with context path '/'
Started DemoCourseApplication in 0.746 seconds (process running for 1.012)
...
Reusing my experience from other technologies... I'd say the issue might be in whatever you're doing in your initialization and/or how much stuff you're loading.Looks like the core spring is decently fast, to me.
conwaytwitty
depending on the size of the app, this can go from a few seconds locally, to 60 seconds when running on 1cpu nodes
there's just so much being done during startup it requires some burst cpu
cmrdporcupine
Most "microservice" backend type stuff is I/O bound, not CPU bound. You're likely not going to win much.
Maybe get away with lower memory instances though.
rahen
Cold latency is an issue with microservices. If you need to use Java, you'll likely end up using frameworks like Spring or Quarkus, which somewhat diminish the advantages of using the JVM and Java as a language.
At that point, you might as well start with Go from the beginning.
tugberkk
why the downvote on this? is it wrong?
throwaway2037
> I would be curious to see how a rust microservice would compare in my companies infrastructure. How much cloud saving could we squeeze?
So, replace cheap Java developers for expensive Rust devs to squeeze out some savings? It makes no sense to me.tonyhart7
well if the argument is cheaper, I can argue that JS/python dev would be more cheaper since there are more pool of talent
jillesvangurp
I made the switch to Kotlin around 2018; after having been using Java since 1995. Java is a choice these days, not a necessity. Kotlin is a drop in replacement for Java in 100% of it's core use cases. No exceptions that I know of. Doesn't matter whether you do Android or Spring Boot. It kind of does it all. And it's actually really good at dealing with creaky old Java APIs. Extension functions (one of the party tricks modern Java hasn't yet picked up from Kotlin) are amazing for adapting old code to be a bit less tedious to deal with.
You don't really lose anything (APIs, tools, etc.); but you gain a lot. I still use Spring. But I've now used it longer with Kotlin than with Java.
And the nice thing with Kotlin is that it is gaining momentum as its own ecosystem. I'm increasingly biased to not touching old school Java libraries. Those lock me into the JVM and I like having the option of running things in wasm, in the browser or on mobile. Our frontend is kotlin-js and it runs a lot of the same code we run in our Spring Boot server. Kotlin multi platform is nice. I've published several libraries on Github that compile for platforms that I don't even have access to. Supposedly my kt-search client (for opensearch and elasticsearch) should work on an Apple watch. I've not gotten around to testing that just yet and I don't see why you'd want that. But it definitely builds for it. I had one person some time ago trying it out on IOS; same thing (I'm an Android user).
Ecosystems are important. But it's also important to not get too hung up on them. I say that as somebody that made the bet on Java when there was essentially no such thing as a Java ecosystem. It was all new. Kind of slow and wonky. And there were a lot of things that didn't quite work right. But it had lots of people working on fixing all of those things. And that's why it's so dominant now. People are more important than the status quo.
Sometimes you just have to cut loose from the past and not go with something safe but very boring like Delphi, Visual Basic, Perl, and several other things that were quite popular at the time and didn't quite make it. They're still around and there's a half decent library ecosystem even probably. But let's just say none of those things are obvious things to pick for somebody doing something new that was born this century.
Go as an ecosystem is definitely large enough at this point that I would label it as low risk. Same with Rust. Neither is going anywhere and there are plenty of well motivated people working on keeping all that going. Same with Java. All valid reasons for using any of those. But nothing is set in stone in this industry. A lot of things people were using in the nineties did not age well. And it will be the same in another 30 years. Most of those old people that will never change retire at some point. Java projects are pretty depressing to work on these days for me. Lots of grumpy old people my age. I've had a few of those in the last few years. Not my idea of fun. The language is one thing but the people haven't aged well.
philipwhiuk
Most of my new back-end code is actually Scala these days which I have a very love hate relationship with.
Kotlin. I don't have much experience with. I know it's fairly lightweight syntactically.
I'm unconvinced by cross-compile to web stuff so far. Scala.js looked very ugly.
jillesvangurp
I never had much experience with scala.js. The nice thing with kotlin-js is that it's just kotlin. Thanks to Android, it's actually a good fit for UI development and a lot of the same libraries work in a browser as well. For example, we use koin for dependency injection, kotlinx serialization, ktor-client, coroutines, and a bunch of other stuff. We also use some npms. Stuff like maplibre and tailwind for example.
As for the compiled code. I spend about as much time looking at generated js as I do examining jvm byte code (none in case you were wondering). It's a compilation target. Yes it's ugly. But who cares? It actually goes through a mininification/uglification step as part of the webpack build. So, yes, it's going to be ugly.
throwaway2037
Yeah, language ecosystems get no love here. Part of me dreads Java, but the developer experience is mostly worse elsewhere.
pjmlp
This looks like one of the typical "we switched from A to B, whithout actually mastering A, so B is alright" kind of posts.
Just on the monitoring part, Go has nothing even close to VisualVM, Flight Recorder, JRebel, VM agents, JMX.
No mention of AOT compilers, JIT caches, and so forth.
zipy124
I like to think of these problems as having skill ceilings and skill floors like in video games. For large companies often the skill floor is more important than the skill ceiling. If it is easier to start in B than A, even though after years of experience A can be much better, it might still be a better choice to use B.
When you have lots of junior devs, you are more concerned with this upstart time, and I think in this case for example, Java makes it very easy to write poor code, or use poor packages. Yes a master in Java can take advantage of the extremely mature ecosystem with tools like those you have mentioned, but for those who are not experienced like you, they may get better productivity out of what you see as the less powerfull tool.
ivan_gammel
I do have experience of managing junior teams and I think Java is actually great for them. You only need one senior person to kick off the project and put down the rails - a relatively small effort if you use frameworks instead of bespoke solutions. The team just needs to follow the lead. It is extremely cost efficient and good enough for business in terms of quality/security/performance.
zipy124
Another re-phrasing of my comment is that perhaps using Go they do not require the strong senior to kick it off.
Otherwise yes there is always the argument of "they're just using it wrong" etc...
ultimately: ¯\_(ツ)_/¯
unscaled
I think most of the tools on the second line are not highly applicable to a Kubernetes operator, or otherwise would just use the "Cloud Native way of doing things".
Something like JRebel would generally be a big no-no for Kubernetes, where the container image of the operator is expected be immutable (at least in terms of the code running in there). It may or may not be okay for developing an operator, but that's certainly venturing into a bit of an unknown territory.
VM agents and JMX are usually replaced by explicitly adding Open Telemetry support and/or a Prometheus endpoint. It's certainly more boilerplate and manual work, but so many more things are explicit in Go, that's just something you've got to buy into. If you're using Rust or Typescript this could be a lot more automated but that's not the case for Go.
Advanced profiling is probably something you'd have to give up, but I doubt a Spring shop would be deeply into profiling. The low hanging fruit is to just throw away spring and get an immediate 200%-400% performance boost.
But the JVM is still a more mature platform. Throughput is better on the JVM than on Go and some things are very troublesome in Go (basically anything that requires code generation).
My personal "cool tool" that I can run on the JVM but doesn't have a mature equivalent in Go is PIT[1] for mutation testing. But not everybody will be using mutation testing, just like not everybody will be using JRebel or JMX. And it doesn't mean everybody should. If you're a Spring a shop the thing you'll be probably missing most in Go would be things like Dependency Injection, Java-like ORMs, reflection magic and Spring Configuration Server.
linkdd
And it's fine. Why continue using something you don't master?
Yes you could try to master it, but it takes years. If another tool compensates your lack of mastery, why not use it?
WJW
To me the implication is that the culture at this company won't allow this team to master Go either, and in a few years there will be a post describing how they moved from Go to another language.
Many people like to write about how Golang is so simple, but the drawback of that simplicity is that many features of other languages are either covered by additional dependencies or by inflating code size. It's just as possible for Go projects to devolve into big balls of mud as for any other language.
ReflectedImage
Golang's standard library looks pretty complete to me and they will probably master it in a month.
btreecat
This is why I don't buy the FP hype
lenkite
There are a lot of leanings to master for Go as well which they may have not discovered.
As an example, the Go runtime does not honor container resource limits. You would think this would be one of the very first fundamental features supported out of the box by an advertised "cloud native" language.
demi56
> As an example, the Go runtime does not honor container resource limits
That’s no longer true for Go 1.19+
nprateem
Because like any shortsighted decision it can come back to bite you later.
kitd
pprof data via HTTP is part of the Go stdlib and is pretty close, good enough for 95% of cases. One could argue you need those tools less in a Go codebase anyway ;)
AOT is mentioned and JIT is not relevant.
pjmlp
pprof isn't not even close to what Visual VM and Flight Recorder are capable of, hence why then there are stuff like open telemetry integration, which require active coding to provide sensible data.
This is the only mention of AOT
> Java's JIT (Just-In-Time) compiler and Go's AOT (Ahead-Of-Time) compiler are definitely two very different approaches and therefore hard to compare.
Zero content on all the options since Excelsior JET and other commercial alternatives came to be, or the free beer alternatives available nowadays.
JIT caches and plain old JIT aren't the same.
JIT caches are similar in concept to AOT with PGO, with the difference that instead of doing that once for the lifetime of the executable with data from test runs, it gets updated after each production execution.
null
xxs
Pretty much. There are lots of post in that form, it makes for a good self tapping on the back and few blog posts - without any questions answered.
OTOH - if that's what makes run the business, I'd not argue.
IshKebab
I mean, they don't mention monitoring... so so why would they need to master it? How would mastering monitoring tools help them with the problem that Java startup time is orders of magnitude slower than with Go? Especially without having to do any extra configuration work.
Ygg2
I'm not sure who said it, but the quote went something like this: Programmers are at fault for the current status in Computer Science, rather than working on expanding their knowledge they keep chasing the latest fad.
If someone knows which cankerous Computer Scientist said this, I'd appreciate a reply.
owlstuffing
Going back to first principles, nominal typing is what I miss most with Go. I get the utility of structs + interfaces + structural typing, but most of the time there is more benefit in declaring that a type nominally implements an interface when that is the intention. Code is far easier to read and understand that way, both for developers and tooling.
I suppose exclusively structural typing would be more acceptable if Go supported _real_ interface composition, like Scala with traits or true delegation via the manifold project[1] for Java. But that's missing as well e.g., does not inherently fix the Self problem, etc.
Considering Go's initial goal, which was IIRC a better systems language, then yeah, sure it's an improved C. But now that Go is routinely compared with Java/Kotlin and friends, I personally don't see it, particularly wrt the type system, to be taken seriously as a Java contender. Shrug.
1. https://github.com/manifold-systems/manifold/blob/master/man...
dagss
Are you aware of the trick of "var _ foo.RequiredInterface = myType{}" to make the compiler enforce that a struct implements a given interface?
Is what you seek a nicer syntax for this or does what you speak of bring something more feature wise?
At least IntelliJ IDEs will always make it clear what interfaces all your structs implement.
owlstuffing
>Are you aware of the trick
Yes, and while it uses the compiler to ensure a type implements an interface, it's still a trick that exposes a large hole in the language... and begs for it to be filled. Most importantly, it still doesn't make the code much easier to read and understand.
>At least IntelliJ IDEs will always make it clear (or less foggy)
Indeed. Go benefits hugely from IntelliJ, or to be specific the IJ plugin API.
_ph_
While having to declare the interfaces a type implements can increase the readability, it comes with a huge drawback: you are limited to implementing the interfaces considered when writing the code. In my eyes it is a fascinating and important feature that you can add interfaces and all existing types automatically implement them, if they, by their methods signature, implement them. You don't have to edit their type declarations to do that.
owlstuffing
>it comes with a huge drawback: you are limited to implementing the interfaces considered when writing the code
Huge drawback? Quite the opposite. Unintentional implementation is a terrible trade for readability and type-safety, hence the trick the prior commenter mentioned.
In fact, structural typing is best when limited to the far less used case where an interface is designed to reflect functions from existing types. It's useful here, otherwise it's a hinderance for the primary case where a type _intends_ to implement an interface, which is what nominal typing is for.
Go was designed as a systems language, and the choice for structural typing served that goal in terms of performance enhancements. But used as a general purpose language, as the trick above (and others) demonstrate, Go's structural typing is less suitable compared with nominal typing.
null
throwaway2037
> But now that Go is routinely compared with Java/Kotlin and friends
Do you think Go is "moving up" (away from systems prog) or is Java "moving down"?owlstuffing
> Do you think Go is "moving up" (away from systems prog) or is Java "moving down"?
I haven't been keeping score, but I've read other articles like this one claiming Go as an enterprise language alternative to Java. Not much concerning Java as a systems language, but that makes good sense. I see Go v. C/Rust, not Go v. Java/Kotlin. Just my take.
heluser
I long for a deep article about the same topic. The real, core difference between Java and Go for backend is declarative vs imperative coding styles.
This one, as typical for such articles, repeats typical secondary talking points and even makes similar mistakes. For example it conflates the concept of DI with specifics of implementation in some frameworks.
Yes there are older Java frameworks that do runtime magic. But both new Java apps and well designed Go services use compile time dependency injection as a way of achieving dependency inversion.
jayceedenton
Which of these languages is declarative? Aren't they both imperative?
CharlieDigital
Maybe Java when using decorators?
heluser
Java 8+ is basically a declarative language. They even officially started departing from Object Oriented Programming towards Data Oriented Programming ( article by their chief architect https://www.infoq.com/articles/data-oriented-programming-jav... ). Unfortunately, most of the comparison articles come from people who still code POJOs with setters, use for loops and overall rely on mutable and unsafe code.
And using Pike’s own words “go is unapologetically imperative”.
idoubtit
> Java 8+ is basically a declarative language.
That's a bold claim!
The article you link to does not contain the word "declarative". It simply states that modern Java allows more of Data-Oriented Programming, meaning a emphasis on pure data structures (records) and more expressive types (algebraic types). It doesn't say much about the code that deals with this data, which is of course procedural.
Apart from SQL which is not generic, I've toyed with two declarative languages, and I can't see much similarities with Java. https://en.wikipedia.org/wiki/List_of_programming_languages_...
golly_ned
Write that article then. Don’t badmouth the writer’s firsthand (evidently not secondary) experience over the course of a year since they didn’t express your idiosyncratic view that Java is a Declarative Language, which is very different from a data-centric language, which itself is a term from the clojure/scala enthusiasm about functional programming around Java 8.
blindriver
I've been using Go for a while now. The biggest headache is error handling. I don't care what the "experts" say, having exception handling is so, so, so much cleaner in terms of error handling. Checking for err is simply bad, and trickling errors back up the call stack is one of the dumbest experiences in programming I've endured in multi-decades. If they are willing to add generics, they should add exception handling as well.
BytesAndGears
Maybe go just isn’t for you? It really doesn’t need every feature of other languages. The error handling is ideal for me, better than any other language. You are always explicit, with every function call, about “what could happen if this fails?”
Maybe passing it up the stack is the best way to handle it, but also maybe it’s better to handle it somewhere in the middle.
The thing that always happens with exceptions in API projects I’ve worked on, is that exceptions can come from any level of the stack, then by default it skips everything in the middle, and the controller has default handlers for what to do in case of an exception.
If there are exceptions you didn’t know existed because of some library or just complex code with dozens of possible exceptions? They still end up being handled in your controller. You need to know exactly what exceptions could happen at every level of the stack, and how to handle it, otherwise everything just short circuits.
With the go errors, you only need to know “did this function call work? If not, then what?”
hedora
Exceptions are a terrible idea.
However, I strongly prefer rust error handling to Go.
go:
(res, err) := foo()
if err != nil
return err
(res, err) := bar(res)
if err != …
Equivalent rust: let res = bar(foo)?)?;
I think go should add the ? sigil or something equivalently terse.Ignoring all the extra keystrokes, I write “if err == nil” about 1% of the time, and then spend 30 minutes debugging it. That typo is not possible in idiomatic rust.
johnisgood
I don't want ")?)?;" in Go. I prefer readable syntax rather than symbol soup.
In Rust, there are cases where I have seen at least 8 consecutive symbols. Not a fan of that.
qaq
99% of people who never written go will know what the go version does
briankelly
This would drive me nuts to write as a Scala dev, but I can see merit to the philosophy. Go basically lowers the ceiling to raise the floor. Meanwhile Scala can let you glimpse the heavens but has no problem showing you the deepest, darkest pits of hell.
wwweston
> Exceptions are a terrible idea.
I hear people say this frequently, but don't ever hear people actually state the case.
My experience is that particularly for web back-end exceptions have been incredibly useful, but I'm interested in the counter view.
searealist
equivalent exceptions:
let res = bar(foo());
(I think you meant: let res = bar(foo()?)?;bmurphy1976
To each their own. I'm not going to claim to be an expert, but as somebody who's been coding since the 80s it was a breath of fresh air to see Go do what I wanted languages to do all long instead of ramming exceptions down my throat. I have problems with Go (examples: slice behavior and nil type interfaces) but error handling is not one of them.
CharlieDigital
What challenge did you run into with exception handling?
I'm curious because I've never felt it being onerous nor felt like there was much friction. Perhaps because I've primarily built web applications and web APIs, it's very common to simply let the exception bubble up to global middleware and handle it at a single point (log, wrap/transform). Then most of the code doesn't really care about exceptions.
The only case where I might add explicit exception handling probably falls into a handful of use cases when there is a desire to a) retry, b) log some local data at the site of failure, c) perform earlier transform before rethrowing up, d) some cleanup, e) discard/ignore it because the exception doesn't matter.
hedora
Exceptions are fine if you never catch them. So is calling abort(). (Which is the Unix way to do what you described.)
If you need to handle errors, you quickly get into extremely complicated control flow that you now have to test:
// all functions can throw, return nil or not.
// All require cleanup.
try {
a = f();
b = a.g();
} catch(e) {
c = h();
} finally {
if a cleanup_a() // can throw
if b cleanup_b() // null check doesn’t suffice…
if c cleanup_c()
}
Try mapping all the paths through that mess. It’s 6 lines of code. 4 paths can get into the catch block. 8 can get to finally. Finally multiplies that out by some annoying factor.ratorx
In Go, b) is really common. Most of my code will annotate a lower error with the context of the operation that was happening. You’ll ideally see errors at the top level like: “failed to process item ‘foo’: unable to open user database at ‘/some/path’: file does not exist” as an example.
Here, the lowest level IO error (which could be quite unhelpful, because at best it can tell you the name of the file, but not WHY it’s being opened) is wrapped with the exact type of the file being opened (a user database) and why the database is being opened (some part of processing ‘foo’, could even generate better error message here).
Although this is a bit of work (but in the grand scheme of things, not that much), it generates much better debugging info than a stack trace in a lot of situations, especially for non-transient errors because you can annotate things with method arguments.
I think the common complaint of ‘if err != nil { return err }’ is generally not the case because well-written Go will usually prepend context to why the operation was being performed.
kgeist
Having exceptions means that every line in your function is a potential exit point, often without you being aware of it. This can lead to bugs when a non-atomic operation is abruptly terminated, and you might not realize it just by glancing at the code.
When we were rewriting some code from PHP to Go, I remember that simply thinking about "what to do with err" led me to realize we had a ticking time bomb - one that could explode in production. We had never realized that a certain line of PHP code could potentially throw an exception, and letting it bubble up the stack would have resulted in data corruption. With Go's explicit error handling, this issue became immediately obvious.
Mawr
How can you "let the exception bubble up" when you don't know what "the exception" is nor where it's going to be thrown? The agency you imply here does not exist.
All you can do is what you've described - catch all exceptions at the top level and log them. It's a valid strategy so long as you don't mind your service going down at 3am because someone didn't realize that the call to Foo() on line 5593 could in fact throw exception Bar.
Explicit error handling would make it obvious that Foo() can error, allowing the programmer to do whatever's appropriate, say implement a retry loop. Said programmer would then be sound asleep at 3am instead of playing whack-the-exception.
ted_dunning
Well, my favorite nightmare with exceptions was code that was littered with
throw NoopException()
You tell me what that did.
khana
[dead]
rollulus
I guess I won’t be adding new information to what your “experts” said as well, but hey. I love Go’s error handling. Syntactic sugar like in Rust could’ve made things a bit nicer to they eye, but apart from that: being forced to think about each error path leads in my view to better code. Compared to a fat try/catch and fingers crossed that all goes well in it.
codr7
Agreed, every time I jump back into Go I'm at first relieved at how nimble it feels; but it never takes long to remember what a pita error handling is or the kludges you need to write to do basic collection transformation pipelines (compared to Java/Streams, C#/LINQ, C++/std etc).
throwaway2037
> or the kludges you need to write to do basic collection transformation pipelines (compared to Java/Streams, C#/LINQ, C++/std etc).
Why hasn't anyone written a good open source for this problem, now that Go has generics?metaltyphoon
It seems like the std library has slowly been adding generic iterators methods. Maybe one day?
codr7
Don't know, every time I try to do anything beyond trivial using Go generics I run into some kind of issue. They haven't been around that long, it takes time for ideas to mature.
ed_blackburn
The real win for this team isn’t just switching from Java to Go. It’s breaking free from the heavyweight framework ecosystem that the JVM all but forces on you.
It’s not that the JVM is bad or that Go is a silver bullet, but Go does act as a forcing function, pushing teams to write simpler, more efficient code without layers of boilerplate, indirection, and unnecessary IO.
You can still do inversion of control without an IoC container—instantiation works just fine! Look at Go’s HTTP middleware pattern with structural typing and first-class functions. No config files, no annotation magic, just composition, testability, and code small enough to hold in your head.
anthropodie
> It’s breaking free from the heavyweight framework ecosystem that the JVM all but forces on you.
> No config files, no annotation magic, just composition, testability, and code small enough to hold in your head.
This. When I look at code I should just be able to follow it and know what's happening. The whole annotation magic and config files makes it hard to understand the flow of things.
thecupisblue
I wouldn't see this as the win for the team. The real win here would be:
* Understanding where the issues and bottlenecks are coming from * Understanding how it came to these issues * Figuring out how to resolve the issues without changing the whole tech stack
That way, the team would end up with a deeper understanding of their organisational issues leading to it, understanding of their code, cost of services, cost of the ecosystem and cost of computation. They would have been able to apply this knowledge to other projects in the company running the same stack.
The way this is written is basically sweeping the problems under the carpet with "tech slow, new tech fast" solution, while simultaneously letting the team keep doing whatever it is they were doing that made this slow. This is nothing but a surface level rewrite without understanding the real cause of the pain.
never_inline
> heavyweight ecosystem
Try micronaut once. It does DI and even ORM mapping (with micronaut data jdbc) at compile time and avoids most reflection.
hyperpape
> the JVM all but forces on you
I don't think you're wrong, but you're making this sound like it an issue with the JVM. It's certainly not. I guess it could be an issue with the Java language, but I don't really think so.
Mostly it's an issue with the Java ecosystem.
chamomeal
Yeah sounds more like a java issue to me. Clojure is usually beautifully terse and straight-to-business with very little boilerplate, and it usually runs on the jvm.
But still agree with the other commenter on the benefits of go. I’m not a huge fan of go, but it’s definitely encourages grug brain style (https://grugbrain.dev/) which has many benefits.
In fact, the only time I disagree with grug style is when I’m really close to the perfect generics for a TS function with lots of coupled inputs. Always so close…
aqueueaqueue
IoC isn't even DI. IoC is saying "I need to talk to a service that handles these account operations I care about" rather than "I need a MySql connection, a coupla s3 buckets and a folder on disk"
DI can support both the "I need" and "I orchistrate" patterns.
Obviously modulo leaky abstractions! You might want to known if that account code is in L1 cache or Timbuktu.
chamomeal
So is DI usually referring to automatic framework-wiring DI? Cause I constantly pass dependencies in as arguments and call it DI.
aqueueaqueue
DI can be manual DI - passing dependencies. I think the hallmark is lack of "new" in your classes (or probably lack of specific imports in a non OO idiomatic language like JS)
IoC is more about design than dependency resolution mechanisms.
gf000
> pushing teams to write simpler, more efficient code without layers of boilerplate, indirection, and unnecessary IO
Ergo, more code, much more development time, more chance for bugs, for questionable benefits (there are an endless number of web applications running on Django. If python is fast enough to tell the OS what IO to do, then surely enough Java with two indirect calls (which you will likely end up doing in your hand-written implementation as well) will be more than adequate.
Also, I don't buy that more code is easier to hold in one's head. An annotation that literally declaratively says what that thing is is much easier to grasp and maintain.
MarkMarine
I work with a brownfield go monorepo with a couple different styles and lesser known “frameworks” in it. It sucks to work in, one of the previous devs was a huge fan of clean code, so every function with more than a couple inputs has its own struct, pointers are used everywhere just by default (and basically never nil checked) and the AI generated tests are so plentiful that changing a small thing quickly explodes into thousands of lines in of changes.
Because it’s go and not really following a framework or pattern, the LLMs just can’t get the style right, so everything is brute force. Know what is easy to build with an LLM? A spring boot app. You can work on the hard logic while your little automated friend works on all the boiler plate and wiring.
rollulus
Disasters can be created in any language, right? Has little to do with Go.
matsemann
True, but most of the arguments against java in this thread are similarly about bad dev practices and not the language itself.
MarkMarine
Exactly.
epolanski
Very ignorant about Go, is dependency injection not a thing there?
E.g. I like declaring interfaces in other languages that model some service (e.g. the database), focus on the API, and then be able to provide different implementations that may use completely different technologies behind them.
I know that some people don't see the point, but I'll make an example. At my primary client the database layer is abstracted and provided at runtime (or compile time, depends) with the repository pattern. How's that useful? Developers can use a filesystem-based database when working, and don't need to setup docker images that provide databases, anything they do is saved on their machine's filesystem. E2Es can run against an in-memory database, which makes it very simple to parallelise tons of different tests as there are no race conditions. And yes, the saved formats are the same and can be imported in the other databases, which is very useful for debugging (you can dump the prod database data you need and run it against your implementation).
There's many other situations where this is useful, one of the core products I work on interfaces with different printing systems. Developers don't have printers at home and my clients have different printing networks. Again, abstracting all of it through DI makes for a very sane and scalable experience.
I don't see how can this be achieved as easily without DI. It's a very nice tool for many use cases.
dgunay
DI in its most fundamental form is common in Go via interfaces. People just declare that arguments to their functions must implement some interface, and the caller provides something that satisfies it.
"DI" in the sense of having a more complex purpose-built DI container is less common. They exist, but I don't see them used a lot.
If your application code has a ton of dependencies, the direct injection/low magic style favored by Go can get kind of boilerplate-y. I'm not going to comment on which style is better.
CharlieDigital
.NET's DI has constructor injection so I suppose this is similar; you just write your constructor saying "I need an `IDataProvider`" and it can come from DI or direct.
Question I have for Go though. Without DI, what is the idiomatic way of supplying one of n implementations? An if-else block? Like if I had 5 possible implementations (AzureBlobStore, AwsBlobStore, GcpBlobStore, CFBlobStore, FSBlobStore) and I need to supply 10 dependent services with access to `IBlobStore` what would that code look like?
In .NET, it would look like:
// Resolve the concreteImpl in DI
IBlobStore concreteImpl = switch (config.BlobStoreType) {
case "Azure": new AzureBlobStore();
case "Aws": new AwsBlobStore();
default: new FSBlobStore();
};
// Register
services.AddSingleton<IBlobStore>(concreteImpl)
services.AddScoped<Svc1>();
services.AddScoped<Svc2>();
// Services just use primary constructor and receive impl
public class Svc1(IBlobStore store) { }
public class Svc2(IBlobStore store) { }
// But I can also manually supply (e.g. unit testing)
var mockBlobStore = new MockBlobStore();
var svc2 = new Svc2(mockBlobStore);
dgunay
Yeah it basically is just an if-else or switch block.
type BlobStore interface { /* methods */ }
// later, when initializing stuff
var foo BlobStore
switch {
case isAzure:
foo = AzureBlobStore{}
default:
foo = FSBlobStore{}
}
svc1 := NewSvc1(foo)
// Constructors are just functions and are totally
// convention/arbitrary. If the fields are public you
// can instantiate the struct directly.
svc2 := Svc2{ BlobStore: foo }
alexjplant
> Very ignorant about Go, is dependency injection not a thing there?
It is but there isn't a heavy emphasis on a DI framework per se. At its core DI is "inversion of control" insofar as an object constructor receives what it needs to do its job as opposed to the constructor instantiating said requirements itself. In my experience Golang devs don't use of a bunch of meta-programming via tags/annotations/decorators to tell a third-party framework what reflect-y magic it has to do at runtime; they typically just abstract everything behind an interface and pass instances of what you need to a New function that instantiates the consumer object. I personally prefer this to Spring-type frameworks because it's more verbose and checked at compile time. This means one can easily follow what's going on instead of having to dig through a bunch of nested stack traces riddled with Aspect4J esoterica to find out why some magical Maven dependency suddenly broke your entire app for no reason at all.
Then again the last time I had to bootstrap a complex Spring project was in 2014. Since then I've used Spring Boot for simple 3-tier microservices and it worked fine... it's likely improved since then but I still wouldn't use it for anything greenfield after the loop it threw me for.
bob1029
DI at its most fundamental interpretation is the act of passing (injecting) arguments (dependencies) to functions.
Nothing stopping you from doing function-level "injection" of generic or interface type arguments. In this context, function could include the ctor.
MrDarcy
DI is generally not an explicit thing in Go. As a sibling comment mentioned, there is wire but it simply generates code, it is not a runtime DI framework like Spring has. This is a feature. The code wire generates is the same code you’d normally write, which is what makes it unnecessary for many projects.
Go has interfaces natively which are implemented in a different way. We never need to say X implements I like we do in other languages. X either satisfies the interface or it doesn’t and the compiler and editor catch this fact early.
So, you’d simply declare an interface to define the API you described in Go, no need for DI or anything else.
tymscar
Totally agree with you, but one point against doing what you mentioned is that you want to replicate your prod environment as much as possible in CI, to be able to catch bugs
epolanski
From my experience this has been mostly painless, and yes, in CI you can obviously run against environments using the same stack as prod.
It may help that I'm writing plain old boring ecommerces or internal tools (scrapers, chatbots, backoffices, warehouse management, delivery systems, etc) and not something more complex and low-level like databases or rendering engines.
prein
Interesting idea, but I would worry that developers wouldn’t catch things like slow queries or other performance issues that crop up when using an actual database.
gt0
DI exists in Go, but it's not ubiquitous like it is in the C# or Java worlds.
Last time I used Go, I used "wire" for DI and was pretty happy with it.
cflewis
I worked on Wire right at the beginning, happy to answer any questions about it.
badrequest
No questions, but a heartfelt thank you. I've used wire for years and plan to continue doing so. :)
gowk
Given that the development of Wire seems to have slowed down, do you still recommend using Wire for dependency injection in Go projects? How has your perspective on its design philosophy evolved over time?
deepsun
Seems like they wrote slow memory hogging software and didn't make any attempt at optimizing it.
E.g. there's no mention of AOT compilation for Java. No hints at what actually consumed 2GB of RAM.
Greenfield projects are always more fun.
TheCycoONE
We run mostly Java apps with a few Go apps. What I miss with Go, maybe just because I'm not as familiar and don't know where to look, is all the runtime analysis that's built in. Thread dumps, heap dumps, and even flight recorder profiling is all built in to the JVM so it works with all apps everywhere. When a Go app suddenly slows down it's very difficult to determine why unless the app was coded to provide the right metrics.
burch45
I actually much prefer go ‘s runtime tooling. Pprof has everything I need built in; heap, cpu, blocking, mutex contention. And don’t need additional tools to visualize the collected data. https://pkg.go.dev/net/http/pprof@go1.24.0
cmrdporcupine
I haven't done Java fulltime in almost 15 years, but I still haven't seen anything out there that is as good as JMX was, out of the box. For just getting decent metrics / observability without rolling in frameworks. Just part of the runtime.
The jvm is a pretty insane beast. It will do usage based recompilation, escape analysis for memory, so non heap allocation is super fast, has great memory safety... But a lot of people use it with spring/spring boot, a technology designed to work around the complexities of a particular type of middleware software in the late 90s and early 2000s. It's cargo cult programming of the highest order. In the OP, the author is comparing apples with oranges, with a bit of misunderstanding that java/jvm means spring boot, and while that is true for a lot of people and certainly a lot of stuff on the internet implies that 'this is the way', it's not required. Startup times of ~100ms are absolutely standard for a big program, similarly unit tests taking 1ms. I prefer to write kotlin rather than java, as it's a nicer language ,IMHO, but still those bytecodes run on Jvm and same stuff applies.
Edit: im not advocating writing 'ls' in java, and I would also agree that java uses more memory for small programs, so its not a systems programming language probably.
Just use new() it's pretty fast.