When working with client backends, sometimes you end up using a language that you have no prior experience with. I worked on a backend written in Go and had to get up to speed quickly. Although the Go programming language is a C-like language, it has a handful of design decisions that can snag you and trip you up. I hope this Go Quickstart article will help you avoid some of these pitfalls and make you more productive!

Go Conventions

Go is an opinionated language and has several strong conventions for how the code should be written. The first thing you should learn is how to run gofmt on your code. It will reformat your code to match Go guidelines. This includes 8-space tabs for indentation! It is the official formatter of the language and ensures everyone is comfortable reading Go the one true way.

Another Go convention is using 1-2 letter variable names for shortly scoped variables because variable names should only grow in length as the distance between definition and use grows. This rule tends to apply to the type variable of methods as well.

Scoping

A peculiarity of Go is scoping. Scoping in Go is only done on the package level and by the name of the type, variable or function. If it starts with a capital letter, it will be exported. For example, the type Animal would be available outside of its package, but the variable legCount would not be. Additionally, packages can be somewhat limited by being under an internal folder. This allows you to create subpackages that will not be exported with the rest of the package.

Zero Values

One of the first quirks I noticed when switching to Go was the concept of zero values. The authors of Go wanted types to have values as soon as they are declared i.e. not have nil values. This means that most built-in types have a meaningful value when they are declared; numbers are 0, strings are the empty string "", and booleans are false. Arrays are filled with the zero value of their type. Unfortunately, slices (growable arrays) and maps do not have zero values and are instead nil; therefore, they have to be initialized to some value before they can be used. Some example variable declarations illustrate their zero values:

var a int // 0
var b string // ""
var c bool // false
var d [5]int // {0, 0, 0, 0, 0}
var e []int // nil
var f map[string]int // nil

Arrays, Slices and Maps

Go comes with three built-in data structures: arrays, slices and maps. Arrays are exactly what you’d expect in a programming language, and maps are the common key-value dictionary that you see in many languages. Slices are built on top of arrays and offer a flexible length and ordered data structure. You may have noticed that there is no set data structure in Go; instead, a set can be mimicked with a map with a boolean as the value type (and is the recommended pattern). For example, for a set of strings, you would use a map[string]bool.

In Go, slices are almost exclusively used in lieu of arrays. They are backed by an array and will grow the backing array as necessary to accommodate more data (but never shrink it). They start out as nil and can be initialized or grown with the built-in functions make and append. They also have two internal counters: a length and a capacity. Length is the number of elements in the slice and capacity is the length of the backing array.

The three base data types are used in a confusing combination of operators, keywords and functions. Defining the type of an array will give you a zero-valued instance, but slices and maps need to be instantiated with either make or a literal instantiation:

var a [5]int // ? An int array of length 5

var b []int // nil
b = make([]int, 5) // ? An int slice with capacity and length of 5
// OR
b = make([]int, 3, 5) // ? An int slice with capacity of 5 and length of 3 
// OR
// The := operator combines a variable declaration with an initialization
c := []int{} // ? An int slice with capacity and length of 0

var d map[string]int // nil
d = make(map[string]int) // ? A map of string keys and int values
// OR
e := map[string]int{} // ? A map of string keys and int values

Adding an item to a map is done with subscripts, but slices use append since it may need the underlying array to be resized. Changing an existing value for all three can be done with subscripts as well. You can’t remove an element from an array, only set the value at an index to its zero value. For maps, you use the delete function and for slices and reslice around the index, you want to remove. You can read from all three with a specific index value or by iterating through them all. Accessing a specific index is through the usual subscript operator. Iterating through all values is done with the keyword range.

// Add item to a map
a := map[string]int{}
a["car"] = 3
a["truck"] = 1

// Remove item from a map
delete(a, "car")

// Access items with a subscript
b := a["truck"] // 1

// Add item to a slice
c := []int{3, 4, 2, 1, 8}
c = append(c, 2)

// Remove item at index 1 from the slice
c = append(c[:1], c[2:]...)

// Access items with a subscript
d := c[2] // 1

Optional Second Values

Several of Go’s built-in keywords and operators have optional values. These are often neither obvious nor intuitive. Probably the most common keyword that returns a second, optional value is range. Range is used to iterate over Go’s collections, and the first value is usually the index and the second is a copy of the value:

for i, v := range []int{5, 6, 7, 8, 9} {
    // i is the indices from 0 to 4
    // v is the values from 5 to 9
}

A nonintuitive optional second value is the map accessor. If you supply a second variable on the left side, it will be assigned a boolean value based on whether the first value is contained in the map. This is most useful when the zero value of the value type is a valid value:

a := map[string]int
a["dogs"] = 3
a["cats"] = 0

cats, contains := a["cats"] // cats will be 0, contains will be true
lizards, contains := a["lizards"] // lizards will be 0, contains will be false

The third use case of the optional second return value you’ll most likely see is the type assertion operator. In Go, there may be times when you want to cast an interface to a concrete type. The type assertion will allow you to check and cast an instance to a concrete type. The one return value operator assumes that the assertion will pass and if it doesn’t, it will runtime panic. The two return value operator will return a bool, informing you if it was able to successfully cast the value:

var a interface{}
a = 1
aInt := a.(int) // ?

a = "Atlanta"
aInt = a.(int) // Runtime panic!
aInt, ok := a.(int) // ok will be false and no panic


Related: More from Sean on Creating a Custom UICollectionViewLayout in Swift

Interfaces

Go allows interfaces for defining contracts for objects, much like many programming languages. All types in Go implicitly conform to interface{} which is the base type for the entire language. You can define your own interfaces with required functions as well. A type conforms to an interface if it implements all the functions of an interface – there is no explicit annotation on the type!

type Barker interface {
    Bark()
}

type Dog struct {}
func (d Dog) Bark() {
    fmt.Println("Woof!")
}

var b Barker
b = Dog{}
b.Bark()

Struct Embedding

Go does not have the typical object-oriented programming inheritance. There are no classes, only structs and interfaces. However, it is possible to extend existing structs with struct embedding. You can embed a struct within another struct by adding an instance variable without a variable name. The parent struct can call any of its instance variable struct’s methods without prefixing a name, and all instances of the parent struct can call the embedded methods. This allows for extension through composition while also allowing behavior reuse between structs:

type Animal struct {}
func (a Animal) Eat() {
    fmt.Println("Eating")
}

type FamilyMember struct {}
func (fm FamilyMember) LastName() {
    fmt.Println("My last name is Smith")
}

type Dog struct {
    Animal // This is the embedded struct
    FamilyMember // Likewise
}

d := Dog{}
d.Eat() // Prints "Eating"
d.LastName() // Prints "My last name is Smith"

Error chaining

In Go, errors are usually handled by returning multiple values from a function. The last value returned is traditionally the error with type error. The error interface only requires that the implementor returns a reason for the error, so errors are really just a string describing the error. The Go pattern for creating error strings is a short lowercase string describing the error. Each function calls down the stack then adds its own error message like an onion, with the end error message describing from top down how the error occurred:

"starting app: querying database: loading migrations: run migration #12: pq: invalid syntax near 'JOIN'"

The recommendation for wrapping the errors is with fmt.Sprintf and embedding the existing error message at the end of the format string. However, the pkg/errors package greatly simplifies the wrapping of lower error messages!

_, err := ioutil.ReadAll(r)
if err != nil {
        return errors.Wrap(err, "read failed") // "read failed: improperly formatted data"
}

I hope that with these Go Quickstart tips and notes you can clear any hurdles and pitfalls you may encounter when first starting off with Go! It is a fun and interesting language, but quite different in some regards to other mainstream programming languages.

Leave a Reply to Darren Cancel reply

Your email address will not be published. Required fields are marked *