a) Go was really sold as a "systems programming language" early on. Maybe some adopters never really questioned what that means or if that's true?
b) Go produces actual executables, with what in Java would be the JIT executed ahead of time and any runtime bits statically linked in. That is completely orthogonal to abstraction level, of course. But maybe the fact the there's no externally visible runtime makes people group the language into a bucket with C?
The other thing is that by not having a VM, a lot of usual Go performance optimizations end up being "closer to the metal." E.g. optimizing struct alignment is a fairly pedestrian Go performance optimization. On the other hand, if you rely on guarantees of how the Hotspot JVM (as opposed to other JVMs) lays things out in memory, that's considered pretty deep hacking in Java.
Having access to pointers is a superficiality. Java is getting value types, and C# already has them.
Valhalla's value types aren't the same thing as pointer access (or lack thererof). The first thing is that this is baked into the definition site rather than being something that can be decided at a call site. The second thing is that value types entail way more than just pointer access (as befits a higher-level approach). In particular it gets rid of the built-in ability of Java objects to act as locks, gets rid of mutation, etc. It's a much larger change than just referencing and dereferencing.
More to the point, the notion of pointers referencing and dereferencing is a lower level concept than reference vs value types. The former is usually used to implement the latter.
If the end result is the same, then the claim is really meaningless.
I'm just pointing out the parroting that goes on in the golang community.
One being used to implement the other doesn't mean the end result is the same (you cannot use value types and reference types to implement pointers because of the call-site vs definition-site distinction). As I mentioned, value types are far more than pointer dereferencing.
You absolutely _can_ do pointer arithmetic—it just requires using the "unsafe" package for obvious reasons (https://go.dev/play/p/Batmgb1KJcg).
Hell, there's even a helper function (https://pkg.go.dev/unsafe#Add) for addition!
This is not true even for C.
Secondly, it seems that its common in the golang community to parrot things that not only are incorrect, but provably wrong.
> Secondly, it seems that its common in the golang community to parrot things that not only are incorrect, but provably wrong.
Whatever your intentions may have been, this is just language war flame bait, and it's against the rules.
And it is good thing to have. But if one desperately comment same thing multiple times, I feel reasonable to ask why the hell they weren't added in all these years?
Value types have proven their worth, especially with the gap between CPUs and memory bandwidth, so they're being added.
However, given that Go also brings its green threading into the picture, I'd argue that it's actually higher level than either Java or .NET in that one respect.
RE threading, green threads though are coming (back!) to Java as well with Loom, but I consider that more of a library/runtime thing than a language thing (as is the case with Loom). A hypothetical Go library could expose system threads and have an API for manipulating them, it just wouldn't be first-class in the same way goroutines are. But this is similar to how having async-await in C# doesn't negate the fact that you can use direct system threads from the standard library.
And then there's stuff like Span, which these days lets you do something similar to Go slices:
Span<byte> p = stackalloc byte[n];
(note that this is also safe code!)As for green threads, I think in case of Go they can't really be considered a library thing due to language integration of goroutines and channels. It's also not quite the same as C# async/await, because the latter isn't really a threading system so much so as syntactic sugar for CPS; it's entirely possible to have a single thread running the event loop.
> As for green threads, I think in case of Go they can't really be considered a library thing due to language integration of goroutines and channels. It's also not quite the same as C# async/await, because the latter isn't really a threading system so much so as syntactic sugar for CPS; it's entirely possible to have a single thread running the event loop.
Again this isn't really a language thing anymore though, it's more of a runtime/operational detail. You could implement goroutines with CPS if you wanted and async-await could be implemented by green threads. Off the top of my head I can't think of anything in the API of goroutines or async-await that would allow you to distinguish whether they're being implemented by CPS or green threads in the background.
This is not entirely true. Refs on lower level are pointers - when it compiles down to CIL, the resulting types are rendered as T&, and referred to as managed pointer, as opposed to T* the unmanaged pointer.
And you can use T& as an independent type - it's not a high-level annotation on a pointer, but a fundamental CIL concept. The limitations that you describe are all about not allowing a T& that points to a local on the stack escape the function where said local is defined (whereas Go silently lifts the local to heap in this case). Hence why you can't have T&&, and why a struct that has a T& field must itself be declared as a "ref struct" (and have the same restrictions applied to it).
On C# level, refs were originally a function argument modifier, as you say, solely about pass-by-value vs pass-by-reference, implemented under the hood as managed pointers. But since then, we've got the ability to return refs, declare local ref variables and fields (in ref structs), and reassign them. So at this point they're effectively types, with some restrictions on use similar to Rust borrowing.
> You could implement goroutines with CPS if you wanted and async-await could be implemented by green threads. Off the top of my head I can't think of anything in the API of goroutines or async-await that would allow you to distinguish whether they're being implemented by CPS or green threads in the background.
To implement goroutines with CPS, you'd need to rewrite every call into CPS-style. This is technically not observable, yes; but perf would be atrocious in practice, so it's not going to happen.
Async/await, though, is literally defined in terms of callbacks in the language spec; threads are completely orthogonal to all that. The dispatch loop can decide to run continuations however it sees fit, including threads - but at this point we're talking about callback-oriented code that is already desugared from "await". And running that on top of green threads is possible but pointless, since the semantics of await requires that async context switching happens only at awaits; but the ability to pre-empt at any arbitrary point is precisely the advantage of green threads.
> This is not entirely true. Refs on lower level are pointers - when it compiles down to CIL, the resulting types are rendered as T&, and referred to as managed pointer, as opposed to T* the unmanaged pointer.
Right and I don't think anybody would disagree that CIL is not a low-level language. But plenty of high-level language features compile down to just pointers. This doesn't mean that the language offers pointers though! In that world I could say that Python offers pointers (or indeed any high-level language that compiles to LLVM and has a feature that just corresponds to a pointer).
> But since then, we've got the ability to return refs, declare local ref variables and fields (in ref structs), and reassign them. So at this point they're effectively types, with some restrictions on use similar to Rust borrowing.
No they're not. You still have the `Ref<...>` vs `ref` distinction (which is why ref fields aren't a thing), you still can't have nested `ref`s, and you still can't switch between "ref"ing and "unref"ing something, i.e. ref-ing is infectious.
Likewise the fact that refs have some linear characteristics doesn't make them pointers (in the same way that Linear Haskell doesn't mean that Haskell now has pointers). The reason why we say that Rust has pointers (apart from the raw pointers), is that they don't have any of these issues!
> To implement goroutines with CPS, you'd need to rewrite every call into CPS-style. This is technically not observable, yes; but perf would be atrocious in practice, so it's not going to happen.
Yes there are sound reasons for why Go has chosen certain implementation decisions, but those are not part of the language semantics, which is ultimately what determines whether a language feels high-level or low-level.
> And running that on top of green threads is possible but pointless, since the semantics of await requires that async context switching happens only at awaits; but the ability to pre-empt at any arbitrary point is precisely the advantage of green threads.
No current industrial green thread implementation I know of offers pre-emption, they are AFAIK all cooperative. IIRC Go's implementation yields on channel sends and GHC's implentation yields on memory allocation. I think Loom is heading for pre-emption, but it hasn't been finalized yet. But that's not really the point. My point is special syntax for a concurrency feature that is not backed by system threads does not preclude a language from using system threads, which is the fundamental relationship between Go's goroutines and any FFI that would expose system threads. Or put another way, being unable to use system threads are not a fundamental part of the semantics of Go, while being unable to use pointers are a fundamental part of the semantics of safe C#.
Our experience is that typical Java programs will consume a lot more memory, and spend a lot more time doing GC and other unproductive work than typical Go programs.
Go provides a lot more tools that give more bang for the buck when optimizing. For example, everything in Java is a reference to some other distant blob of memory. This does not work well on todays cpus compared to being able to colocate things.
We run more stuff on the same machines than we could with Java.
Team wise, Go is easier to learn, and scales better with teams. We don't have trouble with team members struggling with certain team member's code / abstractions like we did with Java or C++. Switching between the two, there is a lot more cognitive overhead in Java land. It's understandable that this may be boring to some, but it is a great feature of the language.
That’s definitely way in the other direction - Java’s GCs run circles around Go’s. (Part of) the reason for Java’s bigger memory consumption is that it is very lazy when it comes to GC. Go runs it all the time for lower latency (at the price of much lower throughput) while Java allows for control it.
By default Go will be faster because of the difference of value types between the two languages, Go is also easier to optimize since the language is simpler and has less abstraction.
Compared to Java?
> it feels more like a JITed language than compiled