Go or Not to Go (from a JavaScript Perspective)?

Having been developing in JavaScript in both the front-end and back-end for years, I thought to explore using Go, aka. GoLang, to replace my back-end Node.js development.
TL;DR: Wrote this article with an open mind. In the end, I concluded my preference is to continue to use JavaScript on the back-end and using things like Flow and Clusters to solve some of the problems that I was looking to Go to solve. You may come to a different conclusion.
My main drivers were:
- Static Typing: With the recent push towards the safety of static typing in JavaScript (think Flow or TypeScript) thought it might be sensible to use a language that supports it out of the box.
- Multiple-Threads: Your JavaScript code runs in a single-thread and Go, with Goroutines, is known for its multi-threaded performance (particularly good for compute-heavy operations). Node.js, with Cluster, offers a solution; but it is not baked into the language itself.
- It is trendy to be learning Go.
One of my fears, however, is that I will find it difficult to learn Go and more importantly context switch between Go (back-end) and JavaScript (front-end). Previously, I found it difficult to context switch between Python and JavaScript; drove me into using Node.js for the back-end in the first place.
I was encouraged by reading comments like the following:
Go is expressive, concise, clean, and efficient. Its concurrency mechanisms make it easy to write programs that get the most out of multicore and networked machines, while its novel type system enables flexible and modular program construction. Go compiles quickly to machine code yet has the convenience of garbage collection and the power of run-time reflection. It’s a fast, statically typed, compiled language that feels like a dynamically typed, interpreted language.
— Go Team
I blazed through the the first part of the documentation, A Tour of Go; feeling pretty good about the language. Then things went off the rails when I hit Pointers and Structs. The last time I thought about pointers was way back when I was writing C and I was having a hard time understanding why we needed them again.
I finally had to write comparable examples in both JavaScript and Go to illuminate why I was having this problem.
Variable Assignment
First, we observe a fundamental difference in how variable assignment works between JavaScript and Go.
JavaScript Src
const v1 = {X: 1, Y: 2};
const v2 = v1;
v2.X = 3;
v2.Y = 4;
console.log(v1);
console.log(v2);
JavaScript Output
{ X: 3, Y: 4 }
{ X: 3, Y: 4 }
Line 1 defines a variable, v1, that references a new object.
Line 2 defines a variable, v2, that references the same object that v1 references.
Lines 3 and 4 mutate the object that v2 references.
Lines 5 and 6 output the object that both v1 and v2 reference; thus the duplicated output.
Go Src
package mainimport "fmt"type vertex struct {
X, Y int
}func main() {
v1 := vertex{1, 2}
v2 := v1
v2.X = 3
v2.Y = 4
fmt.Println(v1)
fmt.Println(v2)
}
Go Output
{1 2}
{3 4}
Line 1 defines a variable, v1, that references a new object.
Line 2 defines a variable, v2, that references a new cloned object (mind blown at this point).
note: It is not the short variable declaration (the colon) that is doing this.
Lines 3 and 4 mutate the object that v2 references.
Lines 5 and 6 output the respective objects that v1 and v2 reference; thus the different output.
Function Parameters
While both JavaScript and Go pass parameters to functions by value, there is a significant difference in the implementation; as these examples illustrate.
JavaScript Src
const v1 = {X: 1, Y: 2};
function myFunction(v) {
v.X = 3;
v.Y = 4;
console.log('INSIDE');
console.log(v);
}
console.log('BEFORE');
console.log(v1);
myFunction(v1);
console.log('AFTER');
console.log(v1);
JavaScript Output
BEFORE
{ X: 1, Y: 2 }
INSIDE
{ X: 3, Y: 4 }
AFTER
{ X: 3, Y: 4 }
In the same vein as JavaScript variable assignment, the parameter v is passed the value of v1; however, the value is a reference to the object that is created in line 1. This means as we mutate the object that v references we are actually changing the object itself (thus the output INSIDE and AFTER are the same).
note: Given the recent emphasis on functional programming and immutability in JavaScript, this example is somewhat contrived.
Go Src
package mainimport "fmt"type vertex struct {
X, Y int
}func myFunction(v vertex) {
v.X = 3
v.Y = 4
fmt.Println("INSIDE")
fmt.Println(v)
}func main() {
v1 := vertex{1, 2}
fmt.Println("BEFORE")
fmt.Println(v1)
myFunction(v1)
fmt.Println("AFTER")
fmt.Println(v1)}
Go Output
BEFORE
{1 2}
INSIDE
{3 4}
AFTER
{1 2}
In the same vein as Go variable assignment, the parameter v is passed the value of v1; in this case it clones the object that v1 references (weird to my eyes). This means as we mutate the object that v references we are actually changing a second object (thus the output INSIDE and AFTER are different).
Pointers
This is about when I realized the purpose of Go Pointers; i.e., you might not want to clone objects every time you pass them to functions. Using Pointers we can refactor the previous example to be more like our JavaScript example.
Go Example
package mainimport "fmt"type vertex struct {
X, Y int
}func myFunction(v *vertex) {
v.X = 3
v.Y = 4
fmt.Println("INSIDE")
fmt.Println(*v)
}func main() {
v1 := &vertex{1, 2}
fmt.Println("BEFORE")
fmt.Println(*v1)
myFunction(v1)
fmt.Println("AFTER")
fmt.Println(*v1)
}
Go Output
BEFORE
{1 2}
INSIDE
{3 4}
AFTER
{3 4}
This time, v1 is a Pointer to the vertex object and by passing it to myFunction, we are cloning the Pointer not the vertex object itself. By using Pointers, we are getting closer to what I would expect using my JavaScript sensibilities.
Off the Rails
Just when I started feeling better about Go, I found the following example particularly disturbing. This example fundamentally illustrates how easy it is to inadvertently break the rules of functional programming (specifically about immutability of function parameters) in Go.
JavaScript Src
const v1 = {X: 1, Y: 2};
console.log('BEFORE');
console.log(v1);
myFunction(v1);
console.log('AFTER');
console.log(v1);
function myFunction(v) {
v = {X: 3, Y: 4}
console.log('INSIDE');
console.log(v);
}
JavaScript Output
BEFORE
{ X: 1, Y: 2 }
INSIDE
{ X: 3, Y: 4 }
AFTER
{ X: 1, Y: 2 }
We saw in an earlier example that we can mutate the object that both v and v1 reference in myFunction. But, we cannot change the object that v1 is referencing from myFunction; e.g., assigning a new value to v has no impact on v1.
Go Src
package mainimport "fmt"type vertex struct {
X, Y int
}func myFunction(v *vertex) {
// v = &vertex{3, 4}
*v = vertex{3, 4}
fmt.Println("INSIDE")
fmt.Println(*v)
}func main() {
v1 := &vertex{1, 2}
fmt.Println("BEFORE")
fmt.Println(*v1)
myFunction(v1)
fmt.Println("AFTER")
fmt.Println(*v1)
}
Go Output
BEFORE
{1 2}
INSIDE
{3 4}
AFTER
{3 4}
But with Go, a subtle change in code (toggling between the following lines), has dramatically different behavior.
...
// v = &vertex{3, 4}
*v = vertex{3, 4}
...
If we use the first line (commented out), the Pointer v is reassigned to a new vertex object (just like our JavaScript example). But if we use the second line, we actually change out the object that both v and v1 are referencing (this pushed me off the rails).
Conclusion
It was about this point, where I realized that picking up and using Go was not going to be as straightforward as I hoped. Starting at this point, I started to skim the rest of the documentation found that, while elegant, Go departs in many ways from what I am familiar with (JavaScript, Java, Python, and PHP), e.g.,
- Handling Arrays with Slices seemed counter-intuitive to me.
- Implementing Methods using Receiver arguments on functions is novel.
- The section of the documentation entitled “Interfaces are implemented implicitly” is intimidating.
Bottom line, my current thinking is that because JavaScript is staying around the front-end, my preference is to continue to use JavaScript on the back-end and using things like Flow and Clusters to solve some of the problems that I was looking to Go to solve.