In the grand space of all programming languages, Go is fast. In the space of compiled programming languages, it's on the slower end. If you're in a "counting CPU ops" situation it's not a good choice.
There is an intermediate space in which one is optimizing a particular tight loop, certainly, I've been there, and this can be nice to know. But if it's beyond "nice to know", you have a problem.
I don't know what you're doing with reflection but the odds are that it's wildly slower than anything in that article though, because of how it works. Reflection is basically like a dynamically-typed programming language runtime you can use as a library in Go, and does the same thing dynamically-typed languages (modulo JIT) do on their insides, which is essentially deal with everything through an extra layer of indirection. Not just a function call here or there... everything. Reading a field. Writing a field. Calling a function, etc. Everywhere you have runtime dynamic behavior, the need to check for a lot of things to be true, and everything operating through extra layers of pointers and table structs. Where the article is complaining about an extra CPU instruction here and an extra pointer indirection there, you've signed up for extra function calls and pointer indirections by the dozens. If you can convert reflection to generics it will almost certainly be a big win.
(But if you cared about performance you were probably also better off with an interface that didn't fully express what you meant and some extra type switches.)
Go is positioned to be most useful as an alternative to Java, and to C++ where performance isn't the key factor (i.e. projects where C++ would be chosen because "Enh, it's a big desktop application and C++ is familiar to a lot of developers," not because the project actually calls for being able to break out into assembly language easily or where fine-tuning performance is more important than tool-provided platform portability).
Go is a perfectly capable replacement for Java and C#. Many huge projects that would likely never be written in Python have been written in Go when they would have otherwise been written in Java or C# in years past: Kubernetes, Prometheus, HashiCorp Vault and Terraform, etcd, CoreDNS, TiDB, Loki, InfluxDB, NATS, Docker, Caddy, Gitea, Drone CI, Faktory, etc. The list goes on and on.
What, exactly, are you saying that Go can't do that Java can?
Go is not a perfectly capable replacement for Rust, for example, because Rust offers extremely low level control over all resource usage, making it much easier to use for situations where you need every last ounce of performance, but neither C# nor Java offer the capabilities Rust offers either.
I like C# just fine (Java... not so much), but your comment makes no sense. Certainly, I would rather use Go than most scripting languages; having static types and great performance makes a lot of tasks easier. But that doesn't mean Go is somehow less capable than Java or C#... it is a great alternative to both. If someone needs more than Go can provide, they're going to rewrite in Rust, C++, or C, not Java or C#.
Runtime library addition (plugins) and dependency injection are two big ones. (We can argue the merit separately, but they're not possible in Go)
I think if Java had easily distributable static binaries k8s would have stayed Java (it started out as Java).
Linux only, but it exists and it works… I just wouldn’t recommend that particular pattern for almost anything.
Either some kind of RPC or compile-time plugins would be better for almost all cases.
- With RPC plugins (using whatever kind of RPC that you prefer), you get the benefit of process isolation in case that plugin crashes, the plugin can be written in any language, and other programs can easily reuse those plugins if they desire. The Language Server Protocol is a great example of an RPC plugin system, and it has had a huge impact throughout the developer world.
- With compile-time plugins, you get even better performance due to the ahead-of-time optimizations that are possible. Go programs compile so quickly that it’s not a big deal to compile variants with different plugins included... this is what Caddy did early on, and this plugin architecture still works out well for them last I checked.
> dependency injection
Java-style DI isn’t very idiomatic for Go, and it’s just a pattern (the absence of which would not prevent applications from being developed, the purpose of this discussion)… but there are several options for doing DI in Go, including this one from Google.
I don't see anything inherit to Go that would prevent it. gc even added rudimentary support some time ago, fostering the addition of the plugin package[1], but those doing the work ultimately determined that it wasn't useful enough to dedicate further effort towards improving it.
There was a proposal to remove it, but it turns out that some people are using runtime library addition, and so it remains.
DI is totally possible, just about every system I build is nothing but dependency injection. What confuses people in the Java world is that you don't need a framework for it, you just do it. You could say the language simply supports a simple version of it natively.
If you want something much more complicated like the Java way, there are some libraries that do it, but few people find them worthwhile. They are a lot of drama for what is in Go not that much additional functionality.
This is one of the many places the interfaces not requiring declaration of conformance fundamentally changes Go vs. Java and leaves me still preferring Go even if Java picks up every other thing from Go. You don't need a big dependency injection framework; you just declare yourself an interface that matches what you use out of some 3rd-party library, then pass the value from the 3rd-party library in to your code naturally. Dependency injected. All other things you may want to do with that dependency, like swap in a testing implementation, you just do with Go code.
(And I personally think that if Java's interfaces were satisfied like Go's, there would never have been a Go.)
All that said, what makes your list any different than a similar list comparing Java to Blub? I don't see your list as preventing one from writing a Java app in Go using Go's idioms instead. (It can't fully do what Java or C# do.)
They're inferior, and do not cover the same grounds (e.g. "enums" in golang are just integer constants, you can't code gen using golang tags, JFR is way way more comprehensive than anything that golang has etc.)
The GC selection, JIT, and hot swapping/reloading are features that do not exist in golang, and we've seen what hoops people have to jump through when they face issues that can only be resolved by them. Basically, they're features most people don't know they need them until they do, then they'er already in a big mess.
You can write a Java app in golang or assembly, but it won't be anywhere near as maintainable, clear, concise, debuggable, or correct.
It could probably use some performance optimizations, but JIT would be redundant when it's already compiled.
Nice-to-have (I've certainly enjoyed something similar writing JavaScript in a browser for years), but most developers aren't writing code under tight-enough constraints to justify a profiler running under a virtual machine.
Of course you can code gen using Go's tags. It's done all the time.
As already pointed out, Go has the JFR it needs, not what Java has.
Go doesn't need a JITter.
Go isn't perfect, but neither is Java or C#. Go code is known for being maintainable and easy for newcomers to read so your last line applies to Java more than Go. But it seems like you want to hate Go. Why?
It does, I have to think twice because I know I'm not getting the compile time features that Java offers, and may end up just using strings in an awkward manner.
> Go has the JFR it needs, not what Java has
That doesn't make sense. All programs need the JFR that Java has, otherwise you wouldn't see companies like Datadog doing well.
> But it seems like you want to hate Go. Why?
I've used it in several large projects at my employer, and in every single project, Java would have been a superior alternative. We've had issues that would not have happened in Java due to many things, such as better error handling in Java, enum support, better frameworks and libraries to interact with the DB, etc.
And no, golang is not known for being maintainable, it's just things that some people parrot without proof. It's basically marketing. There's nothing in the language that inherently makes it more maintainable than Java/C#/etc., and in fact, it has many things that make it less maintainable.
I am curious, what all stuff it cannot do?
If you're using real interfaces, you should keep using interfaces.
If you care about performance, you should not try to write Java-Streams / FP-like code in a language with no JIT and a non-generational non-compacting GC.
What this article says is that a function that is generic on an interface introduces a tiny bit of reflection (as little as is necessary to figure out if a type conforms to an interface and get an itab out of it), and that tiny bit of reflection is quite expensive. This means two things.
One, if you're not in a position where you're worried about what does or does not get devirtualized and inlined, this isn't a problem for you. If you're using reflection at all, this definitely doesn't apply to you.
Two, reflection is crazy expensive, and the whole point of the article is that the introduction of that tiny bit of reflection can make function calls literally twice as slow. If you are in a position where you care about the performance of function calls, you're never really going to improve upon the situation by piling on even more reflection.
Just implement naively, then if you have performance issues identify the bottleneck.
Knowing where performance issues with certain techniques might arise is not premature optimization. Implement with an appropriate level of care, including performance concerns. Not every kind of poor performance appears as a clear spike in a call graph, and even fewer can be fixed without changing any external API.
If I'm using low torque, I don't need to know the yield strength of my wrench
And I never said anything remotely close to contradict this statement.
> Knowing where performance issues with certain techniques might arise is not premature optimization.
It is:
- Python: should I use a for loop, a list comprehension or the map function?
- C++: should I use a std::list, std::vector, ...?
- Go: should I use interface{} or generics?
The difference between those options is subtle and completely unrelated to the problem you want to solve.> Implement with an appropriate level of care, including performance concerns.
Step 1: solve your problem naively, aka: make it work
Step 2: add tests, separate business logic from implementation details, aka: make it right
Step 3: profile / benchmark to see where the chokepoints are and optimize them, aka: make it fast
Chances are that if you have deeply nested loops, generics vs interface{} will be the last of your problems.To take the C++ example again, until you have implemented your algorithm, you don't know what kind of operations (and how often) you will do with your container. So you can't know whether std::list or std::vector fits best.
In Go, until you have implemented your algorithm, you don't know how often you will have to use generics / reflection, so you can't know what will be the true impact on your code.
The "I know X is almost always faster so i'll use it instead of Y" will bite you more often than you can count.
> Not every kind of poor performance appears as a clear spike in a call graph
CPU usage, memory consumption, idling/waiting times, etc... Those are the kind of metrics you care about when benchmarking your code. No one said you only look at spike in a call graph.
But still, to look for such information, you need to have at least a first implementation of your problem's solution. Doing this before is a waste of time and energy because 80% of the time, your assumptions are wrong.
> and even fewer can be fixed without changing any external API.
This is why you "make it work" and "make it right" before you "make it fast".
This way you have a clear separation between your API and your implementation details.
> until you have implemented your algorithm, you don't know what kind of operations (and how often) you will do with your container.. until you have implemented your algorithm, you don't know how often you will have to use generics / reflection, so you can't know what will be the true impact on your code.
I don't mean to brag, but I guess I'm a lot better at planning ahead than you. I don't usually have the whole program written in my head before I start, but I also can't remember any time I had to reach for a hammer as big as reflect and didn't expect to very early on, and most of the time I know what I intend to do to my data!
> This is why you "make it work" and "make it right" before you "make it fast"... This way you have a clear separation between your API and your implementation details.
This is not possible. APIs force performance constraints. Maybe wait until your API works before micro-optimizing it, but also maybe think about how many pointers you're going to have to chase and methods your users will need to implement in the first place because you probably don't get to "optimize" those later without breaking the API. You write about "the bottleneck", but there's not always a single bottleneck distinct from "the API". Sometimes there's a program that's slow because there's a singular part that takes 10 seconds and could take 1 second. But sometimes it's slow because every different bit of it is taking 2ns where it could take 1ns.
Consider the basic read-some-bytes API in Go vs. Python (translated into Go, so the difference is obvious):
type GoReader interface { Read([]byte) (int, error) }
type PyReader interface { Read(int) ([]byte, error) }
You're never going to make an API like PyReader anywhere near as fast as GoReader, no matter how much optimization you do!https://baptiste-wicht.com/posts/2012/11/cpp-benchmark-vecto...
> some of us have been around the block a few times though, and now need to make sure those spaces are delineated for others in a way that won't force them into a performance corner.
This, just like the rest of your comment, is just patronizing and condescendant.
> I don't mean to brag, but I guess I'm a lot better at planning ahead than you
See previous point...
> I also can't remember any time I had to reach for a hammer as big as reflect and didn't expect to very early on
This is not what I said at all. Let's say you know early on, before any code is written, you will need reflection. Can you tell me how many calls to the reflection API will happen before-hand? Is it `n`? `nlog(n)`? `n²`? Will you use reflection at every corner, or just on the boundaries of your task? Once implemented, could it be refactored in a simpler way? You don't know until you wrote the code.
> most of the time I know what I intend to do to my data
"what" is the spec, "how" is the code, and there is multiple answers to the "how", until you write them and benchmark them, you can't know for sure which one is the best, you can only have assumptions/hypothesis. Unless you're doing constantly exactly the same thing.
> but also maybe think about how many pointers you're going to have to chase and methods your users will need to implement in the first place because you probably don't get to "optimize" those later without breaking the API.
Basically, "write the spec before jumping into code". Which is the basis of "make it work, make it right, make it fast" because if you don't even know what problem you're solving, there is no way you can do anything relevant.
> You write about "the bottleneck", but there's not always a single bottleneck distinct from "the API".
I never implied there is a single bottleneck. But If you separate the implementation details from the High-Level API, they sure are distinct. For example, you can solve the N+1 problem in a GraphQL API without changing its schema.
If your implementation details leaks to your API, it just means it's poorly separated.
> You're never going to make an API like PyReader anywhere near as fast as GoReader, no matter how much optimization you do!
Because Python is interpreted and Go is compiled. Under the hood, the OS uses the `int read(int fd, void dest, size_t count)`, and there is an upper limit to the `count` parameter (specific to the OS/kernel).
Python's IO API knows this and allocates a buffer only once under the hood, it would be equivalent to having a PyReader implementation using a GoReader interface + preallocated []byte slice.
I can't tell you which one is faster without a benchmark because the difference is so subtle, so I won't.
I tried to be a bit self-effacing also, but platitudes like "make it right, then make it fast" are condescending to begin with. Sometimes "make it right" requires knowing the performance implications of your architecture decisions.
> Because Python is interpreted and Go is compiled.
This is not why the Go API beats the Python API, which you can tell from knowing the basic language semantics plus method signature.