Language awards: war of codes

amit bezalel
ITNEXT
Published in
15 min readOct 2, 2018

--

Which language is best for you? TS, Javascript, Go, Java, Py, C# Let’s go deep and find out!

Some time after the initial TS vs Go comparison I thought I should revisit the language comparison to do something bigger, I wanted to go deeper and broader, picking at some pain points, comparing syntax and functionality and ecosystems, as well as throwing more languages into the mix, to see how the newcomers stack against the older languages. Why not compare everything in a big ol’ table, and see who gains the most points? Ok. It will be messy, and you might not like my grading, but I promise I’ll try to be fair ;)

TL;DR: Consider the following table, feel free to fight it out in the comments:

My grades for the language comparison

Now that you’ve got your spoiler, let’s get on with the comparison. I won’t go into Java / C# / Py parallel code samples as I want to focus on TS & Go in this area, but such samples are not hard to find.

Resource consumption

Memory footprint

Memory utilization varies widely when using different languages, this might sound surprising at first, since you may think that the code should use up approximately what the algorithm requires, but since the data can be presented in many different ways in memory language and base library implementations make a huge difference in memory usage. For example: objects can be represented as pointer networks (C#/Java), sequential structs (Go) or hash maps (JS).

The chart below shows Memory consumption during different code tasks, a cutoff of 850MB was introduced to retain graph detail against over scaling, C# code benchmarks are of .Net core performance.

Memory benchmarks (data: the benchmark game)

When speaking about Garbage collected languages, Golang is your best bet for top memory utilization, and this can also be seen in benchmarks:

  • Memory sequential structs: in contrast with most languages (including JS) Go has no classes, it only has structs. This means that Golang objects are encouraged to be more like blocks of binary data than a tree of pointers, I say “encouraged” since you could use pointers for everything in Go if you wish to do so.
  • Pure code compilation vs linking (dynamic & static) means that you don’t need to load the whole *.dll/*.sa/*.js file into memory for a few function calls, think of using lodash, how many code lines are loaded vs used.
  • Strong types: precise control over memory usage e.g. int16 only uses 16 bits.
  • No Inheritance: Golang has no need for virtual tables, which means you reduce the overhead for almost every object created, since in Java/C# inheritance is the norm.
  • Multi-Threading: With JS you will need to create new processes, to utilize additional CPU cores, which means that you load duplicate copies of base classes and library code.

CPU Utilization

We programmers have a hobby of asking which language is better at squeezing out the juice, and I am no exception. With each new language I have learned, I always think: “Is this one much faster than the previous few?”, picking a clear winner in this category will always be a scam, but I can offer some thoughts on the matter.

We can turn to benchmarks, which show us that Golang will most likely outperform JS, which is not surprising since more cores can be used, but it is a different story when comparing it to Java & C#.

The chart below shows the time to complete different benchmarks, I had to introduce a cutoff of 90 secs, to avoid python times washing out the more important results.

CPU benchmarks (data: the benchmark game)

I know that benchmarks are “artificial”, statistics lie, X didn’t check gcc-go, and so on. My thoughts on the matter is that like most things in life, this is a trade off. Golang is geared towards servers, it has a fast, single gen GC which sacrifices object creation/removal speed for sub-millisecond “stop the world” pauses, this means that serving will be quick and without strange GC hickups, as shown in server benchmarks, where go-fasthttp comes in at #4 out of 307 while nodeJS is #77 and while Java appears on #3, vert.x is not yet a popular way to write java servers, undertow is #19 and to find jetty you would need to go down to #78.

Source: techempower.com/benchmarks

This GC strategy in Golang also means that when creating a huge amount of short lived instances, Golang will be slower, as shown in the bin-tree benchmark where a huge amount of nodes are created to construct large tree structures: Golang is 11 times slower than C, while C#.netCore (3.1 times), Java (3.4 times slower), NodeJS (9.2 times). Proof of this issue can be seen here, by using custom allocation which avoids GC thus fixing the performance problem, I do not believe most normal usecases create similar situations, so Golang should be more than ok with handling your server workflows.

In most cases, code implementation specifics count much more towards real world performance than anything language specific, but if you want to get picky (and we do), Golang is a solid bet for performant code seeing that it won 1st place in 5 out of 10 benchmarks, 4 going to C# and 1 to Java.

Async & parallel programming

Java: old threadpooling paradigm, uses OS threads, allows self management and some pooling abilities, sync primitives like mutex and semaphore are provided as well as synchronized access to functions. Async IO was introduced in recent versions, but Java lacks some modern approaches which should make this messy field easy to implement and stir you away from thread leaks, dead locks and a slew of other parallel coding horrors.

C#: Some good approaches for sync and parallel, a bit confusing at times
C# has all the parallel methods they could give it, OS threads, threadPools, sync primitives and advanced sync objects as well as Tasks scheduling (TPL), but it isn’t very straight forward: tasks can be concatenated by using continueWith, which is allows for a more convenient approach to parallel execution of code but task preemption is not always handled by the framework so an http request can still occupy an OS thread until timed out or response is received, in order to ensure asyncIO, the newer http client’s async/await methods should be used which means the rest your code should also be async… the correct usage is not as clear or as simple as should be, as a result of the many different async & parallel options within the framework. For Async programming we have the Async/Await functionality, which works well while you don’t mix it with fat clients which require synchronous UI threads. As you can understand C# tries very hard to give us everything… but it is lacks the smoothness of something that was built from scratch with the benefit of hindsight.

TS: Simplicity, no parallel, always async code
NodeJS takes the JS approach in which there is a single thread for processing JS commands, and offloading all IO work to async IO threads managed by the OS, to carry out any transactions with the Disk/Network. the main thread is managed by a task queue which means that at the end there is only one JS command being handled at any time in the NodeJS process. This means that your code is always async, removing the problem of settling sync code and sticky async code which tends to infect all adjacent code with it’s async way of life.

This “one thread” mode of operation was very surprising to me when I first heard of it, since servers are all about efficiently using all cores to maximize calls per second handling. But if you think of it a bit longer, some prominent DBs used multi-process architectures for a long time, so if the overhead per process is not large, it can make sense to have a good “single thread per process” management scheme, and NodeJS succeeds in providing just that.

The downside is that your code is completely written in an async way, both callbacks and promises have a learning curve, using async/await is much better but you still need to remember that your code is being paused, and plan accordingly.

GO: Parallel Power, Sync code until requested
Golang gives you much more control over parallel and async flows, and you may argue that this was already the case with previous languages like java/C#, but when you see that a million go-routines can be run simultaneously without impacting system performance, you realize that there is something new here (that auto-preemption I was talking about)…
Goroutines are not threads, they take up only 2k stacks and are held within GO’s managed memory pool, so both creating them and disposing of them are incredibly fast actions. go also provides you with Channels which are producer-consumer queues that are used to tie go-routines together and offer a simplified solution to most synchronization problems, steering you away from those pesky sync catastrophes.

After talking about power we should still think of simplicity; In Golang writing sync is simple, but when going async there is a learning curve, you need to think of “the Go way”: how to correctly create go-routines, pass channels, execute select statements and check that routines close properly when done. In short,despite Golang’s superior parallel facilities, You would still not want to leave async/parallel code design up to your weakest programmers.

Check out this cool example of Golang’s power:

100K-long goroutine chain: (~600 Milisecs on my machine)package main
import "fmt"
func f(left, right chan int) { left <- 1 + <-right }func main() {
leftmost := make(chan int);
var left, right chan int = nil, leftmost;
for i:= 0; i< 100000; i++ {
left, right = right, make(chan int);
go f(left, right);
}
right <- 0; // bang!
x := <-leftmost; // wait for completion
fmt.Println(x); // 1000000
}

Code writing experience

Serialization

serialization support in TS just uses JS, with the additional ability to have interfaces defined which outline the serialized objects, allowing for easy code completion support. It is by far the easiest serialization experience I have had in any language. GO serialization support is nice too, but it requires either defining and filling structs along with some serialization notations to indicate the json field name, or using Goalng’s unknown type “interface{}”:

  • map[string]interface{} //A map of unknown type
  • []interface{} //An Array of unknown type
Using Object Definitions:TS:
class ChildStruct {
Text: string;
SomeField: string;
}
class SomeStruct {
isAFoo: boolean;
someField: string;
children Array<ChildStruct>;
}
var p0 = new SomeStruct(true, "blah", [{"aa","bb"},{"cc","dd"}]);
var s = JSON.stringify(p0);
GO:
type ChildStruct struct {
Text string `json:"text,omitempty"`
SomeField string `json:"someField,omitempty"`
}
type SomeStruct struct {
IsAFoo bool `json:"isAFoo"`
Text string `json:"text,omitempty"`
Children []*ChildStruct `json:"children,omitempty"`
}
jstr, _ := json.Marshal(SomeStruct{
IsAFoo: true,
Text: "blah",
Children: []*ChildStruct{
&ChildStruct{
Text: "aa",
SomeField: "bb",
},
&ChildStruct{
Text: "cc",
SomeField: "dd",
},
},
})
Inline Serialization:TS:
let obj= {
"isAFoo": true,
"text": "blah",
"children": [
{
"text": "aa",
"someField": "bb",
},
{
"text": "cc",
"someField": "dd",
},
],
}
let str = JSON.serialize(ob);
GO:
obj := map[string]interface{}{
"isAFoo": true,
"text": "blah",
"children": []interface{}{
map[string]interface{}{
"text": "aa",
"someField": "bb",
},
map[string]interface{}{
"text": "cc",
"someField": "dd",
},
},
}
jstr, _ := json.Marshal(obj)

But this is the price of real strong typing.

Network protocol manipulation:

JS has all the standard mechanisms for creating servers & proxies so in most cases like REST services, Websockets & your run of the mill proxies this will suffice, but when required to alter low level http server calls, or create some special protocol adaptations, the base lib doesn’t allow for enough control, and you eventually resort to hacks & workarounds.

Go is a natural choice for deep protocol programming, since it allows for extreme flexibility. In http handling it provides a way to hijack the underlying TCP channel, it’s simple io.reader / writer interfaces allow for full control, without the abstractions of streams. My only gripe with the mechanism is that for some reason the http base library uses buffered io directly, instead of just implementing io.reader.

Native integrations:

JavaScript/TS won’t get you far in this category, usually just invoke some binary executable that you have packaged along with the module in several OS versions, there is always Gyp, but i would never recommend it.

Go has a several mechanisms, including:

  • Unsafe Go code
  • CGo: writing c inside comments which is then compiled (can also include cpp or ObjectiveC syntax & files)
  • The “syscall” module is built into the language and is handy when loading and invoking windows dlls.

for writing cross-platforming edge cases with specifically built native code per OS I would prefer Golang, but if you ask me, most of the time I would opt to have cmdline integrations (a small exe to do the job) and stick with 100% Cross-OS code for most of my solution and 100% native code for some small satellite binary for some few native operations (not talking about mobile here which is a different matter completely).

Error handling:

Java/C#/Python: Error handling by Exceptions, in these languages most errors, down to the base libraries are treated as exceptions including trivial stuff like “file not found”, Java is the worst by far, forcing the programmer to declare which exceptions can be thrown, which begs the question: “if you expect them, why are they called exceptions?”…

Error handling in JS/TS depends on whether you are using promises, if not then the error is usually returned as a callback parameter, for promise errors they are similar enough to exception handling, that we can say that errors are usually thrown trough the application unless explicitly addressed. Also, like most previous languages, null or undefined values are prone to create the notorious null pointer errors which usually account for 90% of code errors.

In Golang errors are a bit different, although the exception mechanism exists (called panic), it is not encouraged. Framework libraries will seldomly panic, and never on routine stuff, instead there is an err return value which requires your attention, even if only to ignore it by using the underscore “_”.

Null pointer errors are reduced by passing the receiver object to the function (receiver = the “this” object in most languages, and sometimes in JS too), so null checks can be done on the receiver before running the function, this means that foo.RunMe() doesn’t have to throw a “foo is undefined” error automatically.

Code cleanliness:

Most of the languages we are discussing are C syntax, and being so are somewhat similar, JS, TS, Java, C# syntax feels the same when coding.
Python and Go both break from the pack, with python striving for better readability, and Go for a simplified language and better performance.

Go is a very thought out language, but it sometimes enforces longer code in basic cases, getting in the way of fluent code writing:

  • Bitty operations (no triary operator in Go)
JS/TS (+Java/C#): 
res = (aa%2 == 0)?"even":"odd";
Py3:
res = "even" if (aa%2 == 0) else "odd"
Go:
var res string
if aa%2 == 0 {
res = "even"
}
res = "odd"
  • Basic String manipulation
JS/TS (similarly Java/C#):
("stUff"+1).toLowerCase().startsWith("stu");
Py3:
("stUff"+str(1)).lower().startswith("stu")
Go:
strings.HasPrefix(strings.ToLower("stUff")+strconv.Itoa(1),"stu")
  • Append to array
JS/TS:
arr.push("Hola");
Py3:
arr.append("Hola");
Go:
arr=append(arr,"Hola")

Ecosystem (Tooling, DevOps & Community):

Modules & Dep management

Java: Maven/Gradel: good dep management, but takes very long to download and really hard to understand when it fails.

C#: Nuget is ok, but it is definitely not as rich as other community component ecosystems.

Python: Pip is much better than most, but the confusion between Py 2.7 and 3, and the fact that most people use a virtual-env to avoid collisions, shows it is less then perfect.

TS/JS: In my opinion NPM is currently the best package manager & module repository in the market, it handles deep dependency trees, multiple dependency versions, semantic versioning and shrink-wrapping for deep dependency version stability, as well as having solutions for local dep-repositories and global dep proxying. This is both a blessing and a curse since it created an ecosystem where importing a single dependency can drag 50 or more sub dependencies onto your project.

Golang has a very fresh approach to dependencies, it uses git repos to get all dependencies as code, and compiles everything at the end, this approach solves several typical issues for compiled languages: dependency linking and OS Specific binary formats. But when going on to building enterprise grade projects it also creates problems with module versioning, dependency repetition with different versions, etc. There have been a ton of different community projects trying to define the right way to handle this and several official ones. Currently there are 2 official tools plus one in the making:

  • go get: the built in tool to get dependencies, has no solution to versioning doesn’t really work with tfs/stash/other non-github git servers, (but if you really want to make it work there is a hack)
  • dep: self defined as an “official experiment” for go 1.9+, and offers some solutions to the problem by keeping a file that maps the versions your code imports and locks them to specific commits in the respective git repos.
  • VGO / go modules: an experimental dependency management, different in approach from dep, which is now tentatively adopted for go 1.11, I took it for a spin, and it seems much better than prev methods, allowing for projects to be outside of the gopath, and working trough “go get” I hope it is finalized in Go 1.12, so we will finally have a good dep management built in.

In short, Golang’s CI/CD story is still ramping up, and any toolchain you adopt will likely be modified in the near future, but we should keep in mind that this was also the case for many typescript facilities (e.g. typings) just a couple of years ago.

Deployment, Binary Size, Signing, Obfuscation

NodeJS binaries are just a seaming heap of JS code, including all the dependencies you have recursively imported. it usually gets to ~300Mb and more without any trouble for a medium sized project.

This is where Go really shines:

  • Binaries are self contained and OS Specific everything is portable, just download and run.
  • Native binaries can be signed and the signature would be verified by the OS when running them.
  • Binaries are hard to understand, even when compared to cpp binaries, since they have the addition of the garbage collection mechanism to confuse IDA debugging / cracking.

Debugging / IDE Environments

We are all used to very advanced debugger environments carefully tuned by professionals, this goes for JS / TS, as well as C#, Java and most other languages I wrote, C# having the best of microsoft support in VsCode & VStudio with shiny features like Edit and continue and support for moving instruction pointer while debugging, as well as advanced refactoring support with jetbrains resharper. Java obviously having the most IDEs around, and JavaScript having some of the best support as well with both Jetbrains support and VsCode, as well as various editors and in-browser debugging. Typescript debugging is supported where JS is supported with map files, and it also has good VsCode support and IntelliJ/WebStorm support.

Python doesn’t have many IDE choices, and many use advanced text editors, it does have Good VsCode support and PyCharm/IntelliJ, but it doesn’t have as much options or as deep an integration as some of the other languages here.

Golang debugging is not yet perfect. The delve debugger has only reached 1.0 status around 6+ months ago, and it is not yet as fully featured and tuned. The IDE environments are also catching up: VSCode has basic Go support, but it runs into trouble doing basic refactoring like renaming variables, has no function call support in console or watch, no new breakpoints set while debugging, and a limited visibility into variable reference trees and large arrays. Goland / IntelliJ plugin is better and is said to have google’s support, fast refactoring, quicker response times and better variable visibility, but it is a payed option.

The bottom line:

While C# and Java are obviously fueled by legacy-code driven inertia, TS, Py and Go are here for a reason, NodeJS/TS wins in categories like: “Fastest time to market” and “Best tool-chain”, Python wins “Easiest collection manipulation and number crunching”, which explains its popularity in DataSciense, and Golang would take: “Best memory consumption”, “Stable under pressure” and “Easiest to deploy on premise”. Golang is a more powerful platform than NodeJS, allowing more abilities to tamper with protocol channels, and to wield the might of parallel computing in a comparably streamlined way, most advanced programmers will enjoy working with it, although an average Dev might prefer JS.

As for me, Golang takes me back to the time when “one exe” was a worthy goal and the most professional software was extremely portable, tightly packaged and tamper proofed. I love it for that… somehow I still think there are types of software that should come out of the factory with a shiny hard shell, and not as a pile of a thousand loosely connected parts that function well when assembled correctly.

--

--

After 17 years developing software, Coding is still a passion of mine, a hands-on SaaS cloud architect working in TS and GO & learning something new everyday.