The Hurdles of Go

John Tucker
codeburst
Published in
6 min readJun 28, 2020

--

Coming from JavaScript / TypeScript, there are a number of Go concepts that are confusing.

As a side note, the official Go documentation is fabulous with all of the documents being a must read:

Zero Values

Unlike JavaScript, Go does not have something akin to the JavaScript undefined primitive value.

Variables declared without an explicit initial value are given their zero value.

— Go — A Tour of Go

For example, in JavaScript we can declare a variable such that its value is undefined.

note: In TypeScript, this code will not compile with an error; Variable ‘message’ is used before being assigned.

The equivalent Go code with the value of message being the empty string:

Control Structure Initialization Statements

Control structure initialization statements are used in JavaScript for statements.

The advantage of this feature is that variables defined in the initialization statements are scoped to the for statement.

In Go, unlike JavaScript, this pattern extends to if and switch statements.

The control structures of Go are related to those of C but differ in important ways… if and switch accept an optional initialization statement like that of for

— Go — Effective Go

An example:

In the equivalent JavaScript example, we are required to define variables scoped outside of the if statement:

Dereferencing Pointers

In JavaScript, there is no way to dereference a variable reference and change the underlying value. In the example below, the variable o will continue to reference the same object until it is explicitly re-assigned.

note: The object itself, however, can be modified; but it is still the same object.

With Go, however, we can dereference a pointer and change the underlying value; thus indirectly impacting other variables. In this example, we change the value of variable i (and value pointed by variable o) indirectly through the variable p:

Arrays

In JavaScript, variables hold references to arrays, i.e., assigning a variable, e.g. b, to another, e.g. a, variable referencing an array refers to the same array (the reference is copied).

In Go, variables hold values of arrays, i.e., assigning a variable, e.g., b, to another, e.g., a, variable holding a value of an array creates a copy of the array.

Slices

We can make a subtle change to the code, by not supplying a length when defining a, and we get a slice that behaves somewhat like the JavaScript array example. But, here we think of a slice as a tuple of reference to an element of a backing array, a length (of the slice), and capacity (number of elements from the referenced element to the end of the backing array).

In this example, b is a slice that is backed by the same array as the array it was copied from, a.

But, working with Go slices is distinctly different than working with JavaScript array variables. For example, here we start with a backing array, myArray, create a slice consisting of the first two elements, a, and then we create a new slice, b, by appending a 9 to a.

In this example, both a and b are backed by the same array, myArray; when we appended 9 to a to create b we actually modified the backing array myArray.

The same general idea expressed in JavaScript behaves very differently.

Here myArray, a, and b, all refer to different array objects, e.g., creating b did not change myArray.

Even more tricky is when appending to a slice causes its length to exceed its capacity. Here we simply change the previous Go example by creating the a slice to be the entire backing array myArray (length and capacity are 3).

In this example, a is backed by myArray (unchanged) and when we appended 9 to a to create b, Go created a new backing array for b.

Bottom line, it is important to understand how slices behave under the hood and be careful when using multiple slices that conceptually refer to the same ordered list of items.

Make

When I first read the definition of the make function, it made no sense to me.

The built-in function make(T, args) serves a purpose different from new(T). It creates slices, maps, and channels only, and it returns an initialized (not zeroed) value of type T (not *T).

— Go — Effective Go

The confusion came as my first introduction to the make function was with slices as shown below:

My first, naive, interpretation of this code is that make does appear to be returning something, a, that looks zeroed and looks like a pointer (to a backing array). But, after understanding about the underlying structure of slices, a is actually initialized with the tuple:

  • A reference to the first element of a new backing array of 10 elements with each element being zeroed, e.g., 0 here
  • A length of 3
  • A capacity of 10

New

So what about the new function?

Go has two allocation primitives, the built-in functions new and make. They do different things and apply to different types, which can be confusing, but the rules are simple. Let's talk about new first. It's a built-in function that allocates memory, but unlike its namesakes in some other languages it does not initialize the memory, it only zeros it. That is, new(T) allocates zeroed storage for a new item of type T and returns its address, a value of type *T. In Go terminology, it returns a pointer to a newly allocated zero value of type T.

— Go — Effective Go

Here we implement a simple example where m is assigned a pointer to a new zeroed MyStruct value, i.e., both I and S are zeroed, i.e., 0 and “” respectively.

One interesting observation with Go, unlike JavaScript, we do not have constructors. At the same time, there is nothing to prevent us from creating parameterized factory functions to create things.

Maps

Thankfully, Go Maps are virtually the same as in JavaScript. The one exception is how each handles referencing a non-existent key. In JavaScript, such an operation returns an undefined value.

In Go, the similar operation returns two values; the first being a zero value (for the type) and the second being a boolean indicating if the key exists or not.

Embedding

A seemingly important topic, embedding is not mentioned in the A Tour of Go documentation but is mentioned in Effective Go.

Go does not provide the typical, type-driven notion of subclassing, but it does have the ability to “borrow” pieces of an implementation by embedding types within a struct or interface.

— Go — Effective Go

Embedding with interfaces is fairly straightforward; it is used to define an interface that is the union of other interfaces.

While embedding with structs is conceptually the same as interfaces (producing a struct that is a union of other structs), the syntax is not obvious. The following simplified example illustrates the creation of both embedded interfaces and structs.

Buffered Channels

While the basic example of unbuffered channels in the A Tour of Go is fairly straightforward, the example of buffered channels was lacking (was overly simplified). I ended up finding excellent material on the topic, however; Part 23: Buffered Channels and Worker Pools.

“Package” Management

When I first explored using Go, it did not have anything resembling package management; a real bummer. Today, it does and is fairly straight forward, but the terminology is reversed from JavaScript’s.

Go programs are organized into packages. A package is a collection of source files in the same directory that are compiled together. Functions, types, variables, and constants defined in one source file are visible to all other source files within the same package.

— Go — How to Write Go Code

In JavaScript, this is essentially a module; except with Go a package can be split across multiple files organized into a directory.

A repository contains one or more modules. A module is a collection of related Go packages that are released together. A Go repository typically contains only one module, located at the root of the repository.

In JavaScript, this is essentially a package.

Wrap Up

Hope you found this as useful as it was for me to write.

--

--