Go vs. C#: Compiler, Runtime, Type System, Modules
Since this article seems to be written from the point of view of someone who primarily knows C#, I&#x27;ll provide some counterpoints from the Go side, which I&#x27;m familiar with. I have some C# experience, but it&#x27;s a few years out of date.<p>&gt; The comparison there was done before against .NET Core 2.1, and I plan to share the update for the most current state (.NET Core 3.1 vs Go 1.13.6) next week. But preliminarily, the distance became even larger:<p>For starters, &quot;the current state&quot; is Go 1.14.4. Updating one and not the other seems unkind.<p>Go 1.14 also introduced this change:<p>&quot;The page allocator is more efficient and incurs significantly less lock contention at high values of GOMAXPROCS. This is most noticeable as lower latency and higher throughput for large allocations being done in parallel and at a high rate.&quot;<p>Whether it <i>improved</i> single threaded burst allocation performance or not, I don&#x27;t know, but it seems likely that such an invasive change would have affected it one way or another.<p>I also don&#x27;t find it very compelling to talk about single threaded burst allocation performance... aren&#x27;t most allocations usually being done across multiple threads, and not in a very bursty (not gigabytes all at once) manner?<p>I think that Go also stack allocates things more frequently than C#, which makes allocation throughput generally less interesting.<p>&gt; Isn’t this per-call “extra” (16 bytes on call stack + two comparisons) a bit too expensive?<p>No? I&#x27;m not sure why you think it is. Most local comparisons are <i>extremely</i> cheap on modern processors. Values on the call stack do not have any contention from other threads. Contention would lead to caches being evicted, and the instruction pipeline would be more likely to be thrown out and restarted because of a branch prediction failure. Local values on the call stack should be really easy for the branch predictor to guess, especially if you&#x27;re repeatedly calling a function. I wouldn&#x27;t go so far as to say prediction and pipelining make such comparisons free... but it really almost does, from everything I&#x27;ve seen over the years.<p>&gt; btw, pay attention to an unusual “prologue” of every call that checks for the potential need for stack expansion. That’s the price Go pays for goroutines — most of other static languages don’t do this extra check per every call.<p>Again, these kinds of checks are rather inexpensive. The cost of growing the stack is amortized over the lifetime of the goroutine, so unless you are constantly creating very short lived goroutines that need large stacks, there really isn&#x27;t much cost to this. Years ago, Go used to have split stacks, and <i>those</i> were expensive, especially if a hot path was straddling one of the splits between stacks.<p>&gt; There is seemingly no way to make e.g. a map to rely on your own equality comparer (sometimes you need this), and honestly, I don’t know how to implement a workaround assuming equality is always structural — e.g. even if you start substituting the keys with your own wrappers, wrappers still can’t override their own equality&#x2F;hashing, so…<p>Depending on exactly what you&#x27;re looking for, there are some easy solutions. You can have the &quot;key&quot; be embedded into the larger struct, or you can have a function that returns a key struct. This is an example of using embedding for this purpose, which I think would be a reasonable solution: <a href="https:&#x2F;&#x2F;play.golang.org&#x2F;p&#x2F;mkHyLZiAYOa" rel="nofollow">https:&#x2F;&#x2F;play.golang.org&#x2F;p&#x2F;mkHyLZiAYOa</a><p>The problem is if you&#x27;re hoping to later get the key back and have access to the other fields of the struct. In my opinion, that&#x27;s an antipattern. You should be storing those fields in the value of the map if you want the map to hold them but not use them as part of the key. I&#x27;ve never once (in any programming language) wanted to store an entire key in the key part of a map, but only use part of the key for comparison purposes.<p>There&#x27;s no great quote to pull from the article, but there&#x27;s a strong implication that Go has to compare every field in order to determine equality... but this simply isn&#x27;t true. Go only has to do this to determine inequality. If you have two pointers, Go will first check whether the pointers are equal. If they are equal, its job is done. Otherwise, it dives into the fields to see whether those are equal or not. Unless you want &quot;reference equality&quot; to mean that you can just cheaply compare the pointers without falling back to structural equality, but... why would you want to do that? You can certainly hack something together with `unsafe`, but I don&#x27;t recommend worrying about this until you have a benchmark pointing at a specific problematic comparison. (which I&#x27;ve also never seen) Reference equality in the sense that it doesn&#x27;t fall back to structural (or some other form of) equality is a gigantic footgun that I&#x27;ve seen trip people up in numerous languages. I would rather not have that as a language feature. A lot of people end up accidentally comparing two different pointers that contain the same structural value, and they can&#x27;t figure out why the two values aren&#x27;t being treated as equal.<p>There are a lot of other statements throughout this article that I find questionable, but I feel like I&#x27;ve already written too long of a comment at this point. Yes, &quot;language I know&quot; is a lot easier to like than &quot;language I don&#x27;t really know&quot;, but the conclusion was even more one-sided than I expected, given the level of effort the author seemed to be putting into the article. The absence of generics and sum types are real issues with Go... and even C# doesn&#x27;t have sum types either, so that&#x27;s a real problem there too. Almost everything else mentioned in the &quot;C# features missing in Go&quot; section is just completely unnecessary. More is not always better, especially when it comes to language features.<p>When I google &quot;c# web server&quot;, the first thing that pops up is a tutorial for how to write your own from the ground up with sockets. When I do the same thing with Go, I get a practical tutorial for how to use the standard library web server to build an interactive wiki application.<p>Go&#x27;s standard library is a huge component of the language&#x27;s success.<p>Being able to make an easy-to-deploy binary just by typing &quot;go build&quot; is huge, and the compile times are great. Sure, .NET supports some form of AOT compilation, but it&#x27;s not the default, and when I google &quot;.net core aot&quot;, the first thing that pops up is the CoreRT repo that says &quot;This is an experimental project. We have no plans to productize it in its current form&quot; which is not super encouraging.<p>There are a lot of things to like about Go, but it is <i>different</i> from many other popular languages, so it&#x27;s easy for it to make people feel uncomfortable. I&#x27;m eager for Go to have generics, because that really is my biggest complaint about it.<p>If you want a cohesive, awesome type system, Rust is built on a decade or two of research that didn&#x27;t exist when C# was designed, so C# has to live with a lot of baggage as it expands to include other, newer ideas. Go has avoided most baggage simply by avoiding most features, for better or worse.