502 comments

[ 2.4 ms ] story [ 377 ms ] thread
After dealing with constant build issues between Java and Typescript and Node and Python: I love go so much. The package management alone makes it worth it.
After dealing with constant build issues with Go, I hate Go so much. The fact that they've conflated "source code" with "consumable library" means that you need a special case in your CI to publish new versions of a library in Go vs. every other language that builds and publishes an executable, and any tooling that pulls from a private repository has to hack `git config` rather than authenticating like you would any other artifact repository.

And that's before we even get onto the "v2" nonsense[0], because apparently it's unidiomatic to continue developing your packages after you publish them. Actually.....given that this language arose at Google, I may be onto something...

[0] https://go.dev/blog/v2-go-modules

Btw js/ts, python, and golang, which one do you find more productive with?
Hard to pick between Python and TypeScript - they have different strengths. TypeScript's type system is more useful (you'd hope, given the name!), but Python "just works" more often for simple use-cases IME (though I have well over double the lifetime experience with Python, so that might be a me-factor rather than a language factor).
Baffling. If you're seriously saying package management for python "just works" and for go "always breaks"... I guess your either a troll, or you've never written production software. And I'm not talking some clever Jupiter notebooks used for some internal auditing or whatnot, I'm talking about customer facing software used by _at least_ 100 people. I'm 12 years in the game and Python has always caused me nothing but pain, while Go hasnt ever cost me a second of afterthought, working in multiple environments after nothing more than git clone. But, if you like making "virtual environments", a horrible shim gimmick which wasnt even supported by the language itself originally, be my guest!
Agreed! python easily has the worst package management out of all of them!
No, that wasn't what I was saying. I was asked a tangential question about which language I preferred; and considering the language _itself_ rather than the ecosystem, I find that Python is the one that typically "just works" - that is, the barrier between thought/design and expression is smallest.

I've repeatedly heard terrible things about Python's packaging, and I have to believe that they're true, even if I've never experienced them myself. And, yeah, you've guessed partially correctly - my own 14 years of professional experience have been primarily with Java and TypeScript, with Python being my language of choice for _personal_ projects. So - yes, I never have written production Python.

To once again be clear, I'm not making any claims that Python's packaging is better than Go's. I'm making two separate and unrelated claims (because the latter was prompted by a tangential question):

* For someone currently building development tooling for a polyglot company, Go's dependency-management system requires more special-casing (both for publication and for consumption) than the others combined.

* For me personally, when translating thoughts/algorithms/designs into code (without considering publication), Python is the language in which I can do so fastest and most intuitively. Never having published a package with it, I've never had to engage with that side of things - I believe folks who tell me that it sucks, but from the consumers' side `source .venv/bin/activate; pip install -f requirements.txt` has always works flawlessly for me.

I’m confused by this. Go doesn’t have a “publish a library” step, whereas every other language does. Is “noop” the special case you are referring to? And why are you comparing publishing a library in Go to publishing executables in other languages?

> unidiomatic to continue developing your packages after you publish them

Are you talking about publishing breaking changes without bumping the major version? Because that seems … like a bad idea in any language?

> Is “noop” the special case you are referring to?

"Well, yes, but actually no". It's _not_ a noop - to publish a new version of a library in GoLang requires tagging a commit in the source code repo. This, in turn, is tricky because there's no way to review a version bump during PR - unlike, say, JavaScript where a PR makes a change to `package.json`'s `version` field (which will be used to determine the version of the resultant built library), there is no in-code representation of what the version "will be when merged" of a change being reviewed. We've resorted to hacking-in a `version.txt` file which is updated and read by our version bump automation.

But that's not all! When making a _major_ version bump in a library, you also need to change the `module` line in your own `go.mod` to have a trailing `/v<n>` (e.g. https://github.com/Masterminds/semver/blob/master/go.mod#L1). Another special case - every other language's generic version bump automation logic is "update <file location> to hold the version", Go's is "IF bump is major, THEN update go.mod to hold the major version".

This is separate from the awkwardness that comes from "publishing" via source code vs. publishing to a binary repository, where the difficulty arises from `go mod download` needing to authenticate to GitHub, whereas every other language's build tools authenticate to the location where built libraries are stored (Artifactory etc.)

So, yeah, in fairness there are actually two separate annoyances here that I mistakenly represented as the same - "recording version in tags makes it impossible to review the version-bump _of_ a change-in-review" and "publishing source code directly, rather than built libraries, means pulling dependencies from private repositories requires messing with authentication". Forgive me - there are just so many friction points in GoLang's development process, it's hard to keep them straight.

> why are you comparing publishing a library in Go to publishing executables in other languages?

Eh - I guess my terminology was off there. I considered and avoided using the word "binary" instead of "executable" because idk if every language uses a binary format to represent the result of building their libraries. The distinction I was trying to draw was between languages that have a transformation process (building/compiling/assembling/whatever) which turns "source code" into "a <thing> that other code can depend on", and GoLang which...doesn't do that. To be explicit - yes, in all cases I'm talking about building and publishing the consumable representation _of_ a library repository.

> Are you talking about publishing breaking changes without bumping the major version?

No, I'm not. I agree that breaking changes need to be accompanied by a Major Version increment. I'm talking about how cumbersome it is in GoLang _to_ bump the major version that you depend on of a dependency library. You don't just have to update the dependency in your go.mod to `github.com/foo/bar v2.0.0`. Because the import path _includes_ the version, you need to update all the `import` statements throughout your code to use the new `github.com/foo/bar/v2` path. Pointless busywork - even worse than the endless `if err != nil` statements which can be defended on the tenuous grounds that they prompt an author to think about how to handle their errors - this import path change is _truly_ useless as a method to catch errors because, if there is a syntax-breaking change in the depended-upon code, that will be caught at build-time _anyway_.

An update to the major version of a library that you depend on is a Big Change, and should be treated with caution. I&#...

I’ve been out of the Java scene for a really long time, but will be coming back to it soon. I’m curious - these performance issues described here, are they inherit to how Java itself? Is it baggage from Spring/Boot? Are there ways to get more bang for the buck with some careful choices in a system like this?

The closest I’ve done to Java recently is C#, which I think may have similar challenges, but overall didn’t seem quite as bad if you avoided lots of framework extras. It also wasn’t something I was digging into deeply though, so perhaps I’m mistaken.

Spring by far - Spring is effectively a build tool running at run time (scanning, enhance, code generation, etc.) - most of it is just startup as JVM does a decent job at optimizing the cruft. In most cases the boot times don't matter, though - at least for most people, esp. when it comes to production. (it's mostly developers time wasted)

I have some personal experience optimizing Spring to record previous runs and effectively cache resolution and code generation, etc. for massive boot latency improvements. Never got around contributing it back, though (not a real spring user)

> most of it is just startup

Yep, I have some Spring code in AWS ECS and it hits 100% CPU usage on start-up before dropping back to 1.5% when idling (this is with 1 vCPU I think).

But yeah I remember reading one of the Spring devs say that some (a lot?) of the runtime reflection could be done at compile time but isn't.

> Spring devs say that some (a lot?) of the runtime reflection

It's a lot more than reflection, if it'd have been reflection alone - it'd be markedly better. (and yes, lots and lots can be optimized). Spring effectively:

  scans the classpath for resources - that includes jars, file system
  loads every single class matching the scanned directories as a byte array
  parses it in java (not by JVM) to check what annotations it has (it doesn't load  the classes actually)
  builds dependency tree
  enhances the previously loaded byte arrays, i.e. generates different byte code
  loads the newly enhanced classes and create instances (usually through standard reflection)
  makes calls like PostInit (life cycle)
  in some cases it uses the standard java reflection to set fields/call methods; in lots of cases java reflection is generating (and loading) a new class (byte code) to carry the process
all the steps above can be recorded on run time (or be a step in the build process) and let the JVM just load the classes organically.

as for the 100%, spring initialization is mostly single threaded - so likely you dont have many cpus dedicated to the java process. (or you meant just a single core 100%)

Ah okay - yeah I'm not across the internals at all.

And yeah I assume it's a single core, but definitely a heavy start.

This makes recycling nodes, be it ECS or Kubernetes nodes, that run tons of Spring apps, a huge pain.
You can write clean beautiful Java and make it perform beautifully if you're careful with what you pull in for dependencies... but then you're not really using Java to its full potential as an ecosystem for basically building massive line of business apps and services that you can spin up everything from junior to offshore to 30 years of experience developers. The tradeoff for this is performance and it's a thing a lot of companies are willing to trade off on.

Things get messy the moment you involve large frameworks, reflection, ten different approaches to parallelism etc. There's also the problem of code base evolution in Java. Code before JDK-8 feels different than code after it and there's a lot of Pre-JDK 8 code out there in the wild.

And that's just it at the end of the day... it's a cultural difference in the ecosystems.

My experience with very simple Jersey web apps is ~1.5 seconds to start up. Much less than his reported ~8 seconds with Spring Boot, but still not in the 100 ms range he reports with Go. I assume one second or so is about as low as you can go with a mainstream Java framework without AOT, though I'd be happy to be corrected.
I worked on an app in kotlin a while back, and am currently working in a dotnet app. We can run our entire unit test suite faster than the JVM started up on that project.

Also, 8 seconds is quick in my experience for Java - I’ve seen more like 15-30

The JVM starts in milliseconds

Probably you're loading many thousands of classes...

It certainly tells me it does. i can hit 'run unit test', sit back in my chair and zone out for 5 seconds, then come back and read that the test took 22ms.
Yeah that’s been my experience too.
haha well your test did take 22ms, but starting the JVM and loading all the classes and all your dependency injection stuff probably took 5s. You can test it yourself with a tiny hello world, it's pretty fast to start.
But that's our point - you're saying java is fast (and it is ripping fast once it gets going) and the startup is fast, unless it's not.
Well no, startup is fast, period. You're probably just giving the class loader a ton of work on startup? I would start with checking that I think.

It could also be when he's hitting "run test" it's actually "compile and run"...

> Well no, startup is fast, period. You're probably just giving the class loader a ton of work on startup? I would start with checking that I think.

Ok, maybe I misspoke about the "JVM" startup. But the time between `mvn test` and it actually logging the first line of user code was in the region of 15 seconds. Every java project I've worked on has had startup time issues once they're bigger than a toy project.

Saying "the JVm startup is fine, it's just the class loader that's slow" reinforces the point of "it's slow to startup"

> It could also be when he's hitting "run test" it's actually "compile and run"...

This would be true in C# too, and in go. Both of those tools have much quicker compilation times IME than java, which is just a nice plus.

That's mostly because nobody cares enough to optimize it, but if you split code into modules so your number of classes, including dependencies, is smaller, then your tests will start quickly. Usually it's dependencies and DI that's the cause of this I think.

Aren't Go's compile times about the same as Java's? Of course it depends on what tooling you use to package the jars.

BTW, I wonder if any test frameworks are using the hot reloading JVMs yet? Would also solve this problem.
Well I could switch to a programming language that isn't so slowed down by ahead-of-time compilation. Maybe a JIT language?
Thanks for bringing up some painful memories. Now I'm working in C and tests take about 100ms end-to-end (multicore, no frameworks). Sometimes I re-run them for no reason just to enjoy how fast everything is.
Can confirm with Vert.x. I used to have a JRebel subscription for code hot swapping in another project but in my current Vert.x thing I simply don’t need it. It’s just fast.
Also, you don't have to continuously restart the server for every change. Can hot swap code. And for tests there are solutions to keep the jvm running between the code->test->fix cycle. But I've never felt the need for those, running a single test is often plenty fast as one doesn't need to instantiate the whole app, only the dependencies needed for the test. So plenty fast.
You can do AOT compilation with GraalVM to reduce both startup times and memory usage, but then you don't have the JIT.
Having done a lot of Java and Go, Go has much better mechanical sympathy between the language, libraries, and vm than Java does. The JIT GC in Java are marvels of engineering, but they have to be.

As an example, in Java, everything is a pointer, so pointer chasing all the time, which is not good for cpu cache, etc. In Go, there is first class support for composition.

The other main adjustment, if coming from Java, is reduced cognitive overhead. It usually only takes a week or two for an experienced Java dev to be reasonably effective in Go, but it takes a few months to break the mental habits of overthinking everything.

There's nothing forcing you to write EE style code in Java though, or depend on frameworks written in that style.
> As an example, in Java, everything is a pointer, so pointer chasing all the time, which is not good for cpu cache, etc

Strictly speaking that's not true. It's everything is a pointer in theory to make it easier to reason with and JIT / JVM optimizing in the background.

There are primitive types and there are lots of tricks in the JVM e.g. escape analysis that places objects on the heap/stack etc.

Despite all the advances in JIT, I've literally never seen it correctly optimize a HashMap<Integer> (happy to be proven wrong). Hopefully the renewed focus on value types can finally bring some sanity.
In C#, all struct generics are monomorphized and struct-based abstractions are zero-cost :)
In my experience, if you write Java like you'd write to, you'd probably get similar performance. Maybe AOT performs a bit better, maybe it doesn't because it disables live optimizations.

Java doesn't lend itself well to writing Go style code, though. You quickly end up writing very "Java" Java code. That has some advantages (code deduplication, less boilerplate) but also downsides (allocation overhead, performance impacts).

In terms of Java vs C#, I don't think you'll notice too many new issues. If you opt into using heavy frameworks like Spring Boot to solve annoying problems for you, you'll see RAM usage increase massively, but it'll also take care of a lot of annoying grunt work for you. Startup times overall are pretty similar. The biggest downside in my experience is the lack of nullable types (@Nullable annotations are a poor substitute) and some syntax improvements that are new to Java but have been part of stable C# for many years.

As long as you stick with small libraries and avoid writing too much ObjectInjectorFactoryProducerResolver style code, performance of the JVM is fine.

All of that goes as long as you're able to use modern toolkits. If you're stuck in the world of Enterprise Java with Oracle Java 8, you'll notice old Java's shortcomings a lot more.

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.

Which of these languages is declarative? Aren't they both imperative?
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”.

> 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_...

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.
Has anybody spotted a similar story of switching from C# to go?

As someone who is very fond of C#, I'm definitely curious what I'm missing out on.

Probably if you are fond of C# you wont like Go. I've always found C# incredibly verbose and full of all sorts of syntax sugar (that they add to yearly) making code everywhere look different, just enough to add a little cognitive overhead... Go on the otherhand, is purposefully opinionated and forces one more or less to write Go in a certain way.
It’s mostly the other way around. Go is a strictly worse, caveman experience after C#. In it’s best moments, Go is a sidegrade at most.
Go look at httpclient for .NET core 4.8

I mean, just the star count on GitHub is enough to show what the developer community thinks.

There is no such thing as “.NET Core 4.8”. There is no such thing as HttpClient’s GitHub repository either - HttpClient was never a separate project and was introduced into the standard library 12.5 years ago as the replacement to then aging WebRequest which had been around since version 1.1.

If you’re interested, .NET’s source code is hosted here: https://github.com/dotnet with the main repositories being runtime, roslyn, fsharp and sdk.

I've used C# years ago, then some Go for simple web services and CLIs and now I'm back at C# for those applications. You're missing out nothing if you're already familiar with C#. Go's biggest advantage is that it's much simper to learn. The whole experience feels much more lightweight and straight forward. Very easy to navigate the ecosystem.

Other than that, modern C# and .NET has the edge over Go almost everywhere. Good examples are obviously LINQ, null safety, extension methods, the type system in general, on the .NET side Generic Host (.NET standard solution for DI, config and logging for all kind of apps), Minimal API, EF Core, and performance. Memory usage for AOT is also very low, Go might have the edge here.

Currently learning ASP.NET Core, seems very well put together with quality first-party libraries. I miss Kotlin, but I suspect ASP.NET Core and Blazor SSR makes up for that.

Particularly excited by the fact that Blazor SSR lets you write server-side components. So I can have a statically typed language that lets me think about the UI in terms of components, and a DI container on hand for when I want to break business logic out into services. Love the flexibility all of those things coming together affords. EF Core seems really promising for persistence so far, though I need to play with it more.

Playing with their Identity framework last night I was able to get up and running despite being new to the ecosystem. Very good sign, most of the time integrating auth as a newcomer to a lot of languages ends up being way more than 2 hours worth of work.

The startup time comparisons may not seem like a big deal but having an app take several seconds just to start up can burn you really bad in incidents where you want to roll new application versions. And yes, it is possible to engineer around it but I think a better question to ask is why these apps take so goddamn long to start in the first place. There should be some kind of compile or runtime flag to speed this up.
There are a lot of applications where the startup time of several seconds does not matter at all. More likely, for most applications it does not matter. Of course, if you are FAANG, it does, but you should not optimize for that in the beginning.
I have never worked for Faang and have seen this be an issue in every single company Ive worked for. Every one. You don’t need millions of pods. Even a fleet of a few hundred (which isn’t crazy for most small to medium businesses) will cause you much pain if you don’t handle this properly.
Agree. Go is a joy to use. Java is okay but struggles with scaling unless you have the money to burn for this.

To scale up servers in Java requires spinning up highly priced servers with lots of RAM, which that is a lot of money. Using a low spec server is cheaper but you will get more JVM crashes and have to waste time doing more JVM tuning to prevent them. This is not even talking about having lots of 'microservices' to scale which that also means more money spent per month.

Using Go just reduces all of that and saves lots of money and is extremely efficient enough to rely on and it directly compiles to a single binary for deployment.

From a cost and efficiency perspective, Java is just not the answer even though it is a sound language, unlike JavaScript or TypeScript. Given a choice I'd rather use Java or even Kotlin or JS/TS. But the cost reduction, performance gains and onboarding experience with Golang is hard to beat for backend.

Would stay really far away from JavaScript or TypeScript for anything backend.

> Would stay really far away from JavaScript or TypeScript for anything backend.

IDK, TypeScript types are extremely ergonomic, and if you're not overzealous makes everything very readable, and given many things are I/O bound anyway, it doesn't matter much.

Big problems for APIs though because TS types disappear at runtime.

So now you need to add in Zod or Valibot as well to validate your types. If you have view models and DTOs, then you add more Zod. Might as well use a statically typed language?

Working with OpenAPI is much easier when you already have a static type system.

How about database transactions? Ran into an interesting issue this week. In a .NET world, every ORM can participate in an ambient transaction because everyone uses System.Transactions. Not the case in Node because there's no such thing.

Would not build backends in TS except for true microservices. The ergonomics of a Nest.js vs .NET Web API are night and day.

> How about database transactions? Ran into an interesting issue this week. In a .NET world, every ORM can participate in an ambient transaction because everyone uses System.Transactions. Not the case in Node because there's no such thing.

I mean you could say the same thing about TypeScript - if your entire stack is TypeScript then there's no need for the type validation either.

Even if your entire stack is TS, you still need validation because the submitted data from an external interface (API call) might not be valid.

You can't just accept any JSON payload and expect it to be valid.

In Java or .NET, it will fail at serialization when the runtime tries to map it to a static type (automatic). This is not the case at runtime with JS (because at runtime, it's no longer TS). Thus you need a Zod or a Valibot to ensure the incoming payload is actually valid by describing the valid types (even if your whole stack is TS at dev time because schema mismatches are a runtime problem).

.NETs System.Text.Json does the mapping and validation using either reflection or AOT generated serializers transparently and automatically because it has static types.

Yes .NET's built in SDK is larger than Node.JS's. I'm not sure what your point is exactly. As you stated just use a validation library if that's necessary. It generally is not though, you can cast the entire output to a type in TypeScript, of course this isn't validation, but it's generally good enough.

Of course if you’re writing a full stack app you can share everything, validating the front and backend with the same code. Clearly that would be a strange thing to hold against .net, similar to the lack of built in validation in typescript.

You double up your work. What you really wanted all along are static types.
not necessarily, and in plenty of programming languages static types do not give you validation for free, and in the case of .net you still have to call something else. it's no different than just using zod, other than the fact that it's built in.

if you're using typescript in the front end, seems double to even bother to introduce .net, when typescript and node is fine for the backend too.

    > [amazingamazing 31 minutes ago] I mean you could say the same thing about TypeScript - if your entire stack is TypeScript then there's no need for the type validation either.
I think that if you're here trying to make the case that if your whole stack is TS, you don't need to validate your incoming data, then you probably haven't built a system of consequence where the data actually matters and none of this discussion really matters; you have no context. Go ahead and try to `curl` some nonsense data to any of your TS endpoints and see what happens at runtime without validation.
I'm not really sure what your point is. you can add validation in node.js, just like you can with .net.

also, with curl you'd use isomorphic type safety, or just know what you're expecting and hard code the types per inputs, trivial with typescript.

anyway, I'm done here since this conversation is going in circles.

I think CharlieDigital's point is that a bad payload will fail right at the serialisation boundary in case of .NET. We know the problem right there. Now we only need to fix the bad payload.

For TypeScript with only types and without validation, a bad payload gets through, and there is no telling where in the workflow it will explode. This could waste more time and developer resources in debugging.

Again this isn’t an inherent property of .net, you have to add validation. There are plenty of ways to do this in node so the point is moot.
Here is a .NET web API

    var builder = WebApplication.CreateBuilder(args);
    var app = builder.Build();

    app.MapGet("/{userId}", (int userId) => userId);
    app.Run();
See `int userId`? If I call this API with `http://localhost:5123/asdf`, I will get an error because the types don't match. If I call this with `http://localhost:5123/1234` it will work fine. The same would be true if I required a class `User` here. The router is able to introspect the type at runtime to determine if the types match; both basic types like this as well as complex types using the built-in serializer. It is built in.

I've put it into a short clip for you: https://imgur.com/a/WNbGUQD

I’m not sure why you’re so obsessed with this. You can do the same thing with any validation library in nodejs. Your exact example is possible by integrating any validation library of your choosing into a nest js route pipe. In particular primitive type validation is built into nestjs anyway

The fact that it’s built in is neat but not really important. Most people are not making thousands of toy apps. If the necessary they will integrate and move on.

There are more compelling reasons to use .net than this.

The issue is that this sort of validation boilerplate shouldn't have to be written. The framework should be able to figure it out from the HTTP handler declaration. I suspect this is why FastAPI got so popular in the Python world.

IMO, a lot of the JS world seems mentally fixated on express.js-levels of abstraction still. Anything more is viewed as "magic" and viewed as suspect because it requires learning.

The irony is that the "learning" just gets applied elsewhere; there are certain foundational building blocks that I think every language and platform needs once you start building "serious" applications.
I guess you just dont know the domain well enough to understand the kind of issues strong typing and compiled software is solving.
Please do tell, what typescript, node and a few dependencies cannot do here.
Provide strong typing, low memory footprint and good performance at scale. See the topic of discussion for more details.
Yes, I agree, but what you're saying isn't really relevant in an I/O bound scenario. In any case, I wouldn't write a K8s operator in node.js, yes. Go is the best for that.
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.

Probably it only uses 100mb of heap but they didn't check or tune the memory manager at all
(comment deleted)
> 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.

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.

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?
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.
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.
To be fair, traditional Java EE apps often required a DI framework, because you couldn't control the main entry point of the program, and the entry point to your code was a class with a default no-argument constructor.

This is still insanity, but the insanity comes from Java EE rather than the apps themselves.

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.
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.

> 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?

I believe it does. Creating a new Error-instance to learn what the current stack is a bit hacky, but it can be useful, even when there is no error. The code can reflect on who is calling it.

Sometimes a function or method is called very many times so trying to log them all is useless. But at the same time it can be the case that there are multiple callers of the said function. Then looking at a stack we can log just the case of some specific call-chain calling that function.

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.
This isn’t anything special about Java. A determined programmer can write Fortran in any language.
> Regarding stack traces, it turns out you don’t need them.

goes on to suggest rolling your own buggy, slow, informally specified implementation of half of a stack trace printer

> 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.

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.
Design patterns are independent of the implementation technology.

OO and virtual functions can be implemented in C by looking up function pointers in a table.

Reference counting can be done by manually incrementing and decrementing references.

At the end of the day everything is compiled to assembly and the CPU doesn't care what ideology was in the programmer's head, except however much a given paradigm abstracts too far away from the underlying machine.

But that "framework" could be a simple factory function.
No, that's wrong.

DI requires that the deps come from outside, not that it's dynamically created. The opposite is that dependencies are created inside the unit. DI is about which part of the code owns the dependency. With DI it's some parent component, without DI it's the component itself.

DI with magic can simplify the management of component lifecycles, but it's entirely possible to do it without.

No, you can pass (inject) the necessary dependencies to the constructor of an object at creation time, and then simply use that object instance everywhere. The only thing frameworks do is "solve" the dependency graph and instantiate stuff in the correct order.

This is also the most common/preferred way Spring et alia implements their "framework-aided" DI, so that you can write unit tests easily without bootstrapping a Spring context (and it is just well-designed vanilla Java code).

"Injection" in "dependency injection" simply refers to getting your dependencies from outside instead of building them yourself.

Let's take the case of an object which represents a simple CRUD web service that needs to talk to a database to get some data. Here is what it looks like without dependency injection:

  func NewService(databaseHostname, databaseLoginSecret string) (WebService, error) {
    databaseConn, err := databse.NewConn(databaseHostname, databaseLoginSecret)
    if err != nil {
        return WebService{}, fmt.Errorf("Failed to create database conn for WebService: %w", err)
    }
    return WebService{
        databaseConn: databaseConn
    }, nil
  }
And here is what it looks like with dependncy injection:

  func NewService(databaseConn DatabaseConn) WebService {
    return WebService{
        databaseConn: databaseConn
    }
  }
This is the only concept: don't build your own dependencies, get them from outside.

Ideally then there is a place in your application, possibly in `main()`, where all of the base services are initialized, in the required order, and references are passed between them as needed. This can include "factory" style objects if some of these need to be initialized on-demand.

> 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.

How does Go avoid getting bogged down by middleware-oriented programing in practice? I think most large-scale programming in organizations tend to converge on that because it sort of works.

Is it that people who like this kind of stuff write microservices for deployment on Kubernetes clusters?

Go itself doesn't do anything about that, nor does Java or JS dictate anything about using middleware.

I can't speak for the ecosystem / developers though; there are plenty of examples where e.g. HTTP request handlers are wrapped in several onion layers of middleware itself. But, there's also an emphasis on keeping things simple and lightweight.

What kind of middleware are you thinking of when you mention things getting bogged down?

> Regarding stack traces, it turns out you don’t need them

Then you go on to explain how to recreate them by hand.

> Maybe stash a slog logger in there, but that’s about it.

Please don't do this either. Read the stuff you want to log as additional attributes in your slog handler from the context, which you ultimately pass to `slog.*Context`

> 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.

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.

> One of the reason people needed a DI framework in Java is crazy "enterprise" configurability requirements a

No, it's so that you can have something else manage the lifetime and disposal of your services instead of doing this yourself. You don't have to be writing crazy enterprisey code to have the need for this.

I agree DI is simple, but 100% disagree that you can achieve this through a hand-rolled library without sinking a ton of wasted time.

> Then wrap the error as many times as you want to annotate additional lines as the error is handled up to the top level

I'd add that this is a last resort; errors should be handled and resolved as close to where they occur as possible. Having them bubble up to a central error handler implies you don't really want to do anything with it.

I would argue that in the majority of the cases that's the only reasonable behavior. I do agree that errors should be handled as close as possible to where they occured, but not any closer -- e.g. there is not much you can do within a library's functions if an unexpected error occured inside. It should be handled at whatever business code happens to call it. And in the worst case, they should bubble up to a central handler, that either outputs an 500 error, or an error dialog.

Exceptions pretty much make this the sane/default behavior, with rust-like ADTs with syntax sugar being close.

> 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

> Because it just sounds like a manual version of stacktraces

Because it is. I don't understand it either.

> 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.

This is like saying "I won't use source generators because it generates code I would normally write myself"

Like, yea no shit. THAT'S the point.

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.

    > internal Google style guide
Can we read this somewhere?
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.
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."

https://pkg.go.dev/context

Using context as a some sort of a mule is an antipattern.

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.

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.

    > 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.
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.

So the community influences the language, not the language that influence the community that explains everything
Well, it's a bit of both. Java is of course very influential, and because it uses a framework for web applications, many languages/communities started imitating that (Ruby/Rails, PHP/Laravel, JS/[Framework_of_the_day] etc.). Then Go came along with its back-to-basics approach and its standard library which is "batteries included" but definitely not a framework, and for some this is a breath of fresh air, while for others it's apparently unbearably alien and backwards...
I'm fairly sure that most of these frameworks are not direct imitations of what Java did, and there was a back-and-fort co-evolution where standard CRUD web applications can be written in a very very productive way (for the small price of learning a framework).

Sure, reinventing the wheel is fun, but if I can finish a whole project while the equivalent go code can finally return "Hello world", then I'm not sure it's a worthwhile tradeoff. Java is not anemic when it comes to its standard library, people moved to frameworks because a good deal of CRUD apps have to solve some of the same problems. Don't forget, the only silver bullet is reusing existing code.

I would say the leaders(could be the creators) of the language plays the most important role in its infancy the decisions they make defines what decisions the community are gonna make and the was what Go made right, Java been a business oriented decisions made by the community will also be business oriented
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.

> 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.

> 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.

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.

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...).

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.

Programs written in those languages tend to have many layers of abstractions, on top of abstraction heavy frameworks.
Try understanding what spring boot is doing. Annotation soup. It's practically impossible for a beginner to Spring to debug.
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.

IntelliJ seems able to locate all potential implementations at injection points just fine.
How are interfaces a replacement or improvement to context? Is it just that they're type safe?
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.

And yet Uber wrote fx[1] to support DI in their golang services. It’s clearly a useful pattern when working on large services.

[1]: https://github.com/uber-go/fx

I disagree with the premise of that whole project. It shouldn't exist.
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.
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.
So, write python in go instead, gotcha.
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.

Sounds like a no true Scotsman fallacy.
That is certainly one way to write Go, but I wouldn't call it "idiomatic" when evil the Google Best Practices guide recommends against it[1][2]. I'll wager you'll probably find it in most other Go style guides. This is how the Go standard library itself works. While you can use http.Get() if you want to use the default HTTP client for simple apps, the library provides you the http.Client struct, where you can also override the Transport (RoundTripper interface) and Jar (CookieJar interfaces). The transport interface lets you further override the Dialer and Proxy function.

[1] https://google.github.io/styleguide/go/best-practices#global... [2] https://google.github.io/styleguide/go/best-practices#provid...

The example they've chosen is somewhat contrived in that the dependency is maintaining a collection of plugins. In such a case it makes sense to go with their approach. I'm not actually arguing against having multiple instances of some package's functionality; indeed such instances may be created and stored per controller. I'm more so arguing against IoC style Dependency Injection, i.e. central instantiation of the controller and its dependencies. My point is just, as far as possible, it's better to rely on init() and package import relationships. You can simply call dep.New() in any number of inits.
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?

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).
So global mutable state? How wonderfully modern!
> 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?

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.
JVMTI allows method patching: https://docs.oracle.com/javase/8/docs/platform/jvmti/jvmti.h... The capabilities are somewhat limited, and an enhancement JEP was withdrawn: https://openjdk.org/jeps/159

On the other other, there is a movement to remove such late binding/rebinding features from the Java platform. However, I think JVMTI is expected to remain supported if the agents are loaded ahead of time. Only on-demand loading of agents will be removed from OpenJDK.

I wouldn't say that there is a movement to remove such late binding features -- the "movement's" primary goal is simply to properly encapsulate and mark any such part of the code (e.g. make it explicit in the module that that should be supported).

That way AOT compilation becomes possible/efficient and dependencies can't hack into other packages in an unmaintainable way without explicit permission from the user, so that updating the JVM and dependencies will be even more streamlined.

As far as I know, there is no tooling support to propagate these marks to the launcher program even in cases where they can be statically discovered. (This could be similar how dynamic linker agents (audit modules) are handled by the Solaris linker, via DT_AUDIT and DT_DEPAUDIT markers.) Mainly because OpenJDK does not provide a tool (beyond jar) that creates launchers.

This doesn't concern features that have been traditionally abused (such as reflection on core OpenJDK classes), but also harmless (from an encapsulation perspective) uses of JNI, and access to future features such as FFI/Panama. Justification for restricting those as well is not so much OpenJDK updates, I think, but that it could cause crashes that might be blamed on OpenJDK.

I agree -- I was just trying to guess what OP might have meant by "monkey-patching" when talking about Go (which is TTBOMK statically typed, which precludes what I'd personally call "monkey-patching" at the language level).

Java does have "agents", which enable arbitrary control of bytecode at class loading time -- bytecode in existing .class files can be transformed, or completely new bytecode generated on-the-fly. But generating bytecode just to replace a class with a test double would be insane.

From another comment I learned there's also a separate "Tool Interface" that can do this bytecode-replacement job, but again, it's a huge amount of tricksiness and bother for something more easily and clearly accomplished in source code with a boring old anonymous subclass. (Though it won't work if the class is sealed, or final, or your code uses reflection to check the class name, or...)

> Test using monkey-patching.

Can you elaborate? What library do you use?

Hard disagree to literally every thing that you have said in your reply above.
seems to be mostly criticism of spring rather than java

the company behind spring should ruin go by porting their crappy library to it

(oh look, it's broadcom....)

Have you seen Fx (uber dep injection for go)? If you want to have the imprint of your keyboard on your forehead, try to get a complex Fx app working after you’ve refactored. Pure, absolute misery. I yearn for dagger every time I touch the system that has it, but black magic that my IDE understands is a close second dagger compared to a dep graph that you can only figure out if it’s correct by running it. Real fun on a go app that can’t be run locally.
Agreed, the spring framework is completely against the spirit of Java. Yeah, auto-wiring was a terrible idea - why do I have to guess what components are going to be pulled in? And why do I have to figure this out a runtime?

The features of Spring Boot are nice, but Spring itself should probably be put out to pasture. Someone needs to write a good alternative to Spring and start promoting it like crazy (probably something exists).

went to springone conference in vegas in 2016. eight years later, there are still no good alternatives :)
Sad, spring is killing java
if it is, it is doing a very, very, very bad job of it
Okay, maybe I spoke to strongly, it is very widely used and does have some nice ideas (Spring Boot is awesome). But it's just my opinion that Spring does give a lot of new devs the idea that Java is overly complex. And for me at least, I've only needed to use dependency injection once throughout my years as a dev (worked in many companies, large to small). Had to rename a class in a large microservice and didn't have to do a search and replace to rename all the places it was used.

But, in my opinion, the cons outweigh the benefits. It adds a lot of complexity for a feature that is rarely used. Am sure if used in a major refactor it'd be nice, but some search and replace is much better than adding an entire layer to your system that in many ways is not strongly typed (auto-wiring), and also adds an entire set of steps to the debugging process. Am actually pretty good at debugging Spring wiring problems, but still don't think you should have to have learned the art of Spring wiring just to debug something relatively simple - how your components are linked together - and all done at runtime.

To me, if tool builders want to support dependency injection, it should be done at the compiler level, along with the rest of the linking of objects. Yeah, don't think dynamically wiring objects is all that useful for a very large majority of builds... ... thought it over, I think I'd like this actually. A new, dedicated component type where the Java compiler checks your DI. That might be very nice. Yeah, also because having a component type will support using DI where it should be, at the component level, not on every class like it tends to be used.

> eight years later, there are still no good alternatives

Micronaut? Quarkus? Depends on what you need.

Quarkus seems pretty good overall. No real issues with it.

Still more config than I'd like but that comes down to the fact it is more for services than MPA webapps.

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.

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
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.

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.
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.

I worked on Wire right at the beginning, happy to answer any questions about it.
No questions, but a heartfelt thank you. I've used wire for years and plan to continue doing so. :)
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?
Can’t comment on the slowdown, I left the team a long time ago.

The goal at the time I thought was very sound: it was a massive PITA that Go programs that ran on cloud servers (of which I would hazard most of them do) were not Write Once Run Anywhere. It was all the same stuff, right? A blob store. A SQL backend. Etc etc. What we wanted was to say “you write a Go program, here run it on GCP. Then Azure. Why not AliBaba if you’re in China”.

What I am no longer convinced of today is that anyone is really looking for those greased rails because it’s _so much more complicated_ to run on cloud servers than I envisioned. Surely it was going to get more simple? But it didn’t. Networking, authorization, scaling, Kubernetes. The list goes on and on.

A fully uneducated guess is that Wire does what people who come to it want it to do, but it’s not attracting new users because the use cases are more constrained than envisaged.

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.

.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);
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 }
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.

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.

> 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.

I thought we had moved on from flame bait articles as a species
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.

Disasters can be created in any language, right? Has little to do with Go.
True, but most of the arguments against java in this thread are similarly about bad dev practices and not the language itself.
it's really weird to see a "Dependency Injection & Context" section as if Golang's context has anything at all to do with DI.

in particular, reading between the lines here:

> 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.

suggests to me that they've implemented one of the Golang anti-patterns that I find most annoying - overloading the context and using it as "grab bag of pseudo-global variables"

in this anti-pattern, you have an HTTP request handler, and want to make a database query, so you need access to the database connection pool. having a global variable for the connection pool feels wrong...so what many people do instead is call context.WithValue in their server startup code to put the connection pool into the grab bag, and then in the request handler call ctx.Value to pull the connection pool out of the grab bag.

the Golang docs [0] explicitly say not to do this:

> Use context Values only for request-scoped data

the much better way, in my experience, is to make the request handler a method on a struct, and then the struct holds references to things like the connection pool. this can also be done with closures and captured variables, of course, but that tends to get unwieldy for non-trivial usage.

if you do this, then your "dependency injection" in Golang tends to look pretty much identical to how it would look in Java, if you wired everything up by hand rather than using a framework/library. and then if you want, you can use a library such as Fx [1] for automatic Dependency Injection along the lines of Spring Boot.

0: https://pkg.go.dev/context#WithValue

1: https://github.com/uber-go/fx

> as if Golang's context has anything at all to do with DI

Yes, Go's context can be used as a DI container.

I thought it was mostly used to terminate an asynchronous event?
> Go's context can be used as a DI container.

the WithValue/Value API allows you to treat the context as essentially a map[string]any. [0]

so if you want to make the definition so expansive as to be meaningless, then yes, map[string]any can be used as a "DI container". and so can a context object, because it exposes a map[string]any-ish API.

0: though if you read the implementation [1] looking up a value in a context is essentially a linked-list traversal and not a hashtable lookup

1: https://cs.opensource.google/go/go/+/refs/tags/go1.24.0:src/...

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.
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?”

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.

99% of people who never written go will know what the go version does
Then... look it up?

If someone saw "go funcName()" for the first time, would they know how it worked without looking it up?

Prob not but I would still argue go is way easier to read than Rust for someone who does not know either.
and 99% of the people will learn the ? shortcut in fraction second. it's just like every other operator ffs. are you dumbfounded everytime you see the channel operators (->) ? noone takes a second thought to them after the first couple of seconds when they encounter them the first time.
equivalent exceptions:

    let res = bar(foo());
(I think you meant: let res = bar(foo()?)?;
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.

> 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.

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.
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.
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.

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.

It's perfectly possible, and a lot less work, to wrap exceptions on their way up the call stack. The difference is you have to remember it at EVERY SINGLE freaking call site in Go.
I don't see how this is different from exceptions though? Exceptions just make it optional if you want to handle it at that level (and you can).

    > Most of my code will annotate a lower error with the context of the operation that was happening.
This is easy to solve with chained exceptions to add context.

    > 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.
You cannot add method args to an exception message? I am confused.
It is definitely possible with exceptions, but it is not the norm (you can do it yourself, but will a library also do it?) because the norm in Java is to silently pass up exceptions as that is the most ergonomic thing.

And once you start doing it with exceptions, there’s not much difference in the code you end up writing between errors and exceptions.

In practice, I’ve found that when I write Go, I end up annotating most error returns, so the benefit of exceptions for me would be minimal.

> And once you start doing it with exceptions, there’s not much difference in the code you end up writing between errors and exceptions

The difference is where you want to catch the error, and not doing a bunch of "plumbing code" for intermediate callers that don't need to know about that error.

> because the norm in Java is to silently pass up exceptions as that is the most ergonomic thing

Adding args to an exception is completely localized. Adding additional args to an error in Go could mean changing dozens of files.

Not to mention I can actually make my own exceptions for the problem. They are like enums with data.

> difference is where you want to catch the error

Catching is pretty similar no? In Java you match by type and in Go, you match by `errors.Is`? I guess the static checking in Java js better, but in terms of code written it is no different.

> additional args to error in Go could mean changing dozens of files

Just to be clear, here we are talking about a function that already returns an exception/error and adding args to it? That is also a local change in Go as well. The call site already handle the interface for error, not sure why changing a field or modifying the error message would make a difference.

Arguably, this type of thing is harder in Java. Adding a new type of exception requires modifying all dependent callers to declare/handle the exception (unless they handle the generic Exception), whereas in Go it is a local only change (except if you need to actually handle the error).

> Catching is pretty similar no?

In Go, you have to "catch" it at every call level.

> Just to be clear, here we are talking about a function that already returns an exception/error and adding args to it

Yes, but adding an arg doesn't mean modifying the error string, it means adding another piece of data which could be a different type. That's another var, and now every call level has to update to pass along that new var. Unless you change the var from a string to a map, which is a whole different set of headaches.

> Arguably, this type of thing is harder in Java. Adding a new type of exception requires modifying all dependent callers to declare/handle the exception

Only if they need to handle it. If you just want it to bubble up, that function doesn't even need to know about that error or what args it has. That's not the case in Go. Every function has to know about every error that passes through. It's the difference between changing two files and changing 10 files.

> That’s another var

By another var, do you mean another return value? That’s not how it works in Go at all. It is possible to do it that way, but that would not be idiomatic.

You have a single error returned regardless of how many “errors” you have (> 0). If you need to return a new error and it is a custom struct that includes fields, you just implement Error interface on the struct and return it as the single error return. If you need to add new args on the struct, nothing changes other than the error implementation.

Do you want to return 2 errors from the same call site? You have to use something like multierror or a custom struct that includes 2 errors and implement the interface yourself. But the actual thing you return is still a single error.

> unless you want to change the var from string to map

Errors are not strings. It is an interface. If you want to return a string, you implement the interface (although it is much simpler to create a new error with errors.New). If you want to change it to a map later, you implement the interface on the map. It is transparent to the caller, because errors are dynamically dispatched the majority of the time.

> only if they need to handle it

Well, every function needs to declare which exceptions it throws, so you will have to modify every function in the call stack if you don’t want to handle it and it is a new type of Exception.

> That’s not the case in Go

That IS the case in Go. The most common pattern is to return an implementation of the error interface. Nothing changes if the underlying type of error changes except (potentially) the sites that want to handle a specific type of error.

If it's easy then why does nobody do it? ;)

In all exception-based languages I know of, catching an exception is so syntactically heavy that annotating intermediate exceptions is never done:

    try {
        Foo()
    } catch (err) {
        throw new Exception("message", err)
    }
One line just turned into four and the call to Foo() is in a nested scope now, ew. At that point even Go is more ergonomic and less verbose:

    err := Foo()
    if err != nil {
        return fmt.Errorf("dfjsdlfkd %w", err)
    }
> If it's easy then why does nobody do it? ;)

People do this all the time with exceptions.

> One line just turned into four

The Go version has one line of difference?

> At that point even Go is more ergonomic and less verbose

You can't compare it to your Go version because you have to write the error check at every single level, whereas once I throw that exception I can catch it wherever I want. Obviously the Go version will have much more code just around one error.

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.
Can you give the equivalent in Go?
[flagged]
This is satire, right?
No.

It is just an explicit rendition of a complex topic.

Do you have a more economical example that handles all of the corner cases of cleanup routines throwing errors?

I recommend ignoring the other reply you just got. They are clearly building a bad faith argument to try to make Go look terrible while claiming to sing its praises. That is not at all how that would look in Go. The point being made was that the exception-based code has lots of hidden gotchas, and being more explicit makes the control flow more obvious.

Something like this:

    a, err := f()
    if err != nil {
        c, err := h()
        if err != nil {
            return fmt.Errorf("h failed: %w", err)
        }
        cleanupC(c)
        return fmt.Errorf("f failed: %w", err)
    }
    defer cleanupA(a)

    b, err := a.g()
    if err != nil {
        c, err := h()
        if err != nil {
            return fmt.Errorf("h failed: %w", err)
        }
        cleanupC(c)
        return fmt.Errorf("a.g failed: %w", err)
    }
    defer cleanupB(b)

    // the rest of the function continues after here
It’s not crazy.

With Java’s checked exceptions, you at least have the compiler helping you to know (most of) what needs to be handled, compared to languages that just expect you to find out what exceptions explode through guess and check… but some would argue that you should only handle the happy path and let the entire handler die when something goes wrong.

I generally prefer the control flow that languages like Rust, Go, and Swift use.

Errors are rarely exceptional. Why should we use exceptions to handle all errors? Most errors are extremely expected, the same as any other value.

I’m somewhat sympathetic to the Erlang/Elixir philosophy of “let it crash”, where you have virtually no error handling in most of your code (from what I understand), but it is a very different set of trade offs.

Or, if you really hate duplication, you could optionally do something like this, where you extract the common error handling into a closure:

    handleError := func(origErr error, context string) error {
        c, err := h()
        if err != nil {
            return fmt.Errorf("%s: h failed: %w", context, err)
        }
        cleanupC(c)
        return fmt.Errorf("%s: %w", context, origErr)
    }

    a, err := f()
    if err != nil {
        return handleError(err, "f failed")
    }
    defer cleanupA(a)

    b, err := a.g()
    if err != nil {
        return handleError(err, "a.g failed")
    }
    defer cleanupB(b)

    // the rest of the function continues after here
bracket pattern

    def bracket[A, T](ctor: () -> A, next: (a: A) -> T): T =
        val a = ctor();
        try { return next(a) } finally { a.dispose() }
> So is calling abort(). (Which is the Unix way to do what you described.

But in gui applications and servers, you will need to catch and report the error in some intermediate boundary, not exit the application. That's where go falls short.

Well, my favorite nightmare with exceptions was code that was littered with

throw NoopException()

You tell me what that did.

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.

Go can abort at any point as well.

Also, ignoring an error condition (by either forgetting about it, or simply doing the nice and tidy if err dance with no real error handling in place, just a log or whatever) is much worse and can lead to silent data corruption.

You can abort via panic(), but that is expected to crash the application, which is perfectly fine. It's the act of attempting to catch the abort that is fraught with problems. While in Go that is rare, in languages with exceptions it's normal and expected.

Ignoring an error condition is possible in Go, but so unlikely that it's not practical to worry about it.

As an aside, not that it matters, but logging an error is one of the valid ways of handling it, depending on context.

Logging can be a valid handling of error.

I just don't believe that most errors can be handled locally so besides returning an error (bubbling up), not much can be done. Go makes this part of the happy path, so neither can be easily seen/reasoned about anymore.

Exceptions do auto bubbling up, while languages with ADTs have more strictness than go, and often have some syntactic sugar/macro to help with the common case of returning the error (rust's ?).

>I just don't believe that most errors can be handled locally so besides returning an error

Sure, but explicit error handling reminds you that the call may fail, and you may want to handle it in some way (or not - then you bubble it up). With exceptions, it creates the illusion of a simple linear flow.

>ignoring an error condition (by either forgetting about it

We rely on linters to catch that, which is pretty easy to implement (no expensive intra-procedural analysis needed, which is the case with exceptions).

>Go can abort at any point as well.

Panics, unlike errors, are exceptional situations which generally should not be caught (a programming error, like index out of range). They're usually much rarer than errors.

Linters can't know if a case has been properly handled or not. Just because it logs something it may or may not be the proper semantic way to handle that error.
It's rather the opposite, it helps avoid bugs when something is terminated, e.g. by ensuring that if an exception is thrown it rolls back a database transaction.

Generally you want to do several classes of things with exceptions in web apps:

1. Return a 500, possibly rendering a nice error page.

2. Log why it occurred.

3. Roll back changes you were making at the time in the database.

And you might also want to do other things, like propagate the error via tracing, increment monitored metrics and many other things. All these are easily done with exceptions, and will often be done by frameworks for you. In desktop apps you may also want to trigger a crash reporter, display a sorry message to the user, and if the exception was inside something like a button click handler you may even be able to proceed safely too.

Given the level of bugginess in untested error handling codepaths that crop up in languages without them, exceptions are definitely the way to go.

Not everything can be wrapped in a single transaction, though. The problem in question involved talking to other services. Frameworks wouldn't help out of the box.
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.

Nothing in languages that use exceptions prevent you for catching the generic exception type and handling it if you can recover/clean up. As I stated, my own rule of thumb (point (d) above) with respect to handling exceptions is "when I can/need to do something". If I can't do anything, then just bubble it up and log.

Case in point is accessing a remote REST endpoint where I might be throttled or the service might be down. I can do something (retry) so I'll write code that probably looks similar to Go with Err:

    var retries = 0;

    do {
      try {
        result = await makeServiceCall();
      } catch {
        if (retries == 3) throw;
        retries++;
        await Task.Delay(retries * 1000);
      }
    } while (retries < 3)
The exception bubbling mechanism just makes it optional so you determine when you want to stop the bubbling.
No, I disagree with you.

If you are using Go, you have no idea what the error you're going to receive is, because the code you call could be calling other code that you have no idea about. You might use existing code that gets modified to call into another module and then you're going to get a whole set of errors that you aren't expecting and won't be able to react to.

What this means is that you have NO way to handle errors except to just error out and bubble up. Because all you can do is look at the error, the best you can do is throw your hands up and say "okay just returning this error." How is this any better than an exception?

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).

    > 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?
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.

    > anything beyond trivial using Go generics
This is the first complaint that I have heard about Go generics on this board. I believe you. Can you share a specific example? It might spur some interesting discussion.
Haven't written much Go the last year and a half...

Mostly novel limitations that I'm not used to from C++.

Instantiating generic parameter types is one thing I couldn't figure out at some point, but that's pretty much useless in C# and impossible in Java if I remember correctly.

I recognize the frustration from following implementations in both Java and C# though, it takes a while for generics to settle, each implementation has its own quirks.

It seems like the std library has slowly been adding generic iterators methods. Maybe one day?
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.
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...

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.

>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.

    > 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"?
> 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.

(comment deleted)
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.
>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.

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.
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
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.
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.

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?

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.

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.

Is it really an actual issue? How often do you restart instances or scale up horizontally?

A cheap 10 years old desktop PC can easily handle all the traffic a medium-sized website generates at all times.

What do you define as medium size?
Stackoverflow famously ran on a single, although quite beefy server PC for a long time (not any longer, but not for performance reasons AFAIK).

I think it's a good data point to have to scale your workload to stackoverflow's, and reconsider the hardware costs.

(Obviously horizontal scaling has its place, but if it's that variably scalable, maybe there are better solutions, e.g. a single bigger instance -- often times even for cheaper)

why the downvote on this? is it wrong?

    > 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.
well if the argument is cheaper, I can argue that JS/python dev would be more cheaper since there are more pool of talent
Double agree. If Java was the right choice 10-15 years ago for cheap enterprise apps, then certainly NodeJS is the way to go today. There are heaps of cheap developers and the ecosystem is ginormous.
Is it really to the point of challenging java ? Seems like Java is still the way to go here.
Have you tried AoT compilation with graalvm?
No, the product has a lot of aop and I figure it would be difficult to make that work.
> 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.

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

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).

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.

Sure, Optional is not the most optimal thing to use/do, though depending on how many entities you were operating with, that itself may still be negligible.
That’s the pain point - well written java code may not be the most performant depending on the situation.

It wasn’t just optional but other areas as well.

That's not my experience, though game development is certainly a niche and Java may not be the top choice for that.

You might sometimes have to reach for SoA-like structures and reference them via indices, at least for the core ECS, but for the rest you can easily use bog-standard Java -- not everything has to be "ultra-fast, specially written java", just certain hot loops.

Right. I didn’t started digging into it until I realized the application was clearly sluggish in certain scenarios. As I am doing this for fun I am going to see what kind of speed up I get in rust for this.
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.

I’ll have to look into burst cpu usage. Thanks
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.
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.

Yeah, language ecosystems get no love here. Part of me dreads Java, but the developer experience is mostly worse elsewhere.
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.

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.

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.

I'm pretty fond of Java; it's definitely a superior language to Go if you ask me, which I've also written a ton of code in.

But I stay away from Spring Boot, end the entire EE stack of crap that came before it, if at all possible.

I've had more success adding whatever I need on top of embedded Jetty.

It's mostly a cultural problem, no one is forcing you to go the AdapterFacadeInjectionBuilderWhatever way.

I've been working on a library to simplify interfacing with relational databases for a while now. With several implementations in Go and other languages. And the java version looks at least as nice as the rest to my eyes:

https://github.com/codr7/tyred-java

[flagged]
I could tell you why, based on writing a ton of code in both, but I doubt that would lead anywhere.
https://github.com/codr7/tyred-java/tree/main/src/codr7/tyre...

24 of those files are under 100 lines - some of them are as small as three lines of code. and that's not a personal preference - that's mandated by Java that each type needs to be in its own file, ridiculous.

I don't see that as a problem at all, just like I don't see header files in C++ as a major problem, there are benefits as well and Java has the best IDEs of any language I've worked in except maybe SmallTalk.
Java has the best IDEs, because as a language development is essentially impossible without one. contrast with Go where I can put as many types into one file as I like, and I can (and do) use Go without an IDE, both personally and professionally
What part of Java development is impossible without an IDE?

It's a stupidly simple language.

Just because people actually use it and thus IDEs were developed to aid the development process doesn't make it a necessity at all. I have programmed Java countless times from vim without any plugins. The only pain point is imports, which would be similarly painful in any other language ever created.

Let me know when you can work on a medium-sized Java codebase in Emacs or Vim smoothly.
I more or less live in Emacs, but I wouldn't use it for drawing pictures or writing SmallTalk either. The right tool for the job.
I can use Emacs and Vim for pretty much everything, definitely not Java or JVM-like languages.

    > that's mandated by Java that each type needs to be in its own file, ridiculous.
Nested classes?
> mandated by Java that each type needs to be in its own file, ridiculous.

Each public type, but in general I agree, this is ridiculous decision.

I've written ORMs for both Java and Go and agree that Java is a wonderful language with a much more complete type system than Go. However, it feels like Java web development teams typically suffer from not having their own Wagtail/Payload/etc and build way too much from scratch. Maybe there is some great open source framework+CMS out there making waves in Java land that I'm unaware of?
But Javas has so many of these web frameworks?!

* Spring (https://spring.io/)

* Spring Boot (https://spring.io/projects/spring-boot)

* Helidon (https://helidon.io/)

* Micronaut (https://micronaut.io/)

* Quarkus (https://quarkus.io/)

* JHipster (https://www.jhipster.tech/)

* Vaadin (https://vaadin.com/)

That's just to mention the bigger ones, there's lots of mini frameworks like Javalin (https://javalin.io/) and Dropwizard (https://www.dropwizard.io/en/stable/)...

There's even the previous generation of big app servers like Weblogic and JBoss.

It's just incredibly weird for me to see someone say Java is missing some sort of framework, it just has EVERY kind you can probably imagine.

Framework _and_ CMS so most of these don't seem to apply. I haven't seen Vaadin before so that's interesting. JHipster seems like the closest to the ones I mentioned but the UI looks unpolished in the overview video.
The best thing about Go is no Spring framework. I like Java but finding Java projects without Spring is difficult.
What is forcing the use of Spring?

I've been a frequent Java programmer since its earliest days (started around 1996) and I've yet to ever use Spring.

None of the bad parts of Java are actually a part of Java!

But you see nobody really wants to be “different” if there are 100 people and the first 90 went right the probability of the remaining 10 going right will be higher. Java was designed to be business oriented and I think this is why Go and Java is very different not in terms of language but the way the community makes decisions
All k8s operators are written in Go. It's really unsurprising that Java doesn't fit there. Java has huge advantages for typical web applications (observability, deployment flexibility, deep toolchain beyond IDE etc.). I've seen companies trying to use Go in the environment where the JVM excels, then breaking down the problem to thousands of small microservices which end up making something simple into shards of complexity.

Java is a general purpose application development language. Go is a system language that isn't as deep as Rust. They are very different things and comparing them doesn't make any sense. Like the people comparing Rust to JavaScript, these are not interchangeable.

There are operators written in Java and Rust.
There are even operator written with Ansible
>> Of course, Java still has its strengths, and for certain projects, it remains a solid choice. But for cloud-native applications, Kubernetes tooling, and our self-hostable software distribution platform, Go just feels like the right tool for the job.

Yeah. I see Android app development is still mostly dominated by Java/Kotlin. Of course you can do it with Go, e.g: https://fyne.io. Never try to write something serious with it, just messing around with the examples.

> In the course of a developer's years, if I only restart the server two times an hour, this saves me an additional day (!) of development time per year.

You are working too much.

Any suggestions for how to store request scoped data without context? Specifically when using middlewares, like in the example of an auth middleware, needs store isAdmin or isLoggedIn or UserId
Presumably you control the entire handler stack in Go. So you can extend the HandlerFunc signature with parameters customizations to carry explicit structs, or return “error”, for example.