Contents

GO Programming Language

Contents

What’s Go?

Note: This notes is taken from GO as a part of my preparation.

Go (or Golang) is one of the youngest programming languages, making waves in the tech world—especially in cloud engineering. Introduced by Google in 2007 and open-sourced in 2009, Go has quickly climbed the ranks as the language of choice for many modern applications.

But why do we need Go when we already have so many programming languages? Let’s dive in.

Why Go?

The evolution of infrastructure, particularly with scalable multi-core processors and cloud-based environments, demanded a language that could harness these advancements efficiently. Traditional languages struggled to write applications that fully utilized these resources. Go was created to address these challenges.

Concurrency and Multi-threading

Go shines in scenarios requiring concurrent operations—multiple tasks running in parallel without stepping on each other’s toes. Think:

  • Google Drive: Simultaneous file uploads and UI navigation.
  • YouTube: Watching videos, scrolling comments, and interacting with the interface—all at once.

However, concurrency comes with challenges, like avoiding data conflicts (e.g., simultaneous updates on shared resources). Go simplifies this complexity, making it a go-to choice for such scenarios.

Key Features of Go

  • Simplicity: Intuitive syntax akin to Python.
  • Speed: Performance comparable to C++.
  • Resource Efficiency: Minimal CPU and RAM requirements.
  • Portability: Easily deployable across multiple platforms using a single compiled binary.

Where Is Go Used?

  • Server-side and backend development.
  • DevOps automation tools.
  • Technologies like Kubernetes, Docker, and CockroachDB.

Setting Up Go for Development

Choosing an IDE: GoLand

GoLand, JetBrains’ IDE, is perfect for Go. It’s beginner-friendly yet powerful for experienced developers. Features include:

  • Code completion and refactoring.
  • Built-in tools for debugging and package management.

Installing Go

GoLand simplifies setup by allowing you to download the Go compiler directly within the IDE:

  1. Install GoLand from JetBrains.
  2. Configure the Software Development Kit (SDK):
    • GoLand will prompt you to download the latest Go version during project setup.

Setting Up Go: Linux Edition

If you’re on Linux like me (shoutout to Arch users 🖤), here’s how to get Go up and running without relying on an IDE.

Step 1: Download Go

First, grab the latest Go release from the official site:

wget https://go.dev/dl/go1.21.0.linux-amd64.tar.gz

Step 2: Extract the Files

Extract the tarball to /usr/local:

sudo tar -C /usr/local -xzf go1.21.0.linux-amd64.tar.gz

Step 3: Update Your PATH

You need to let your system know where to find Go. Add this to your .bashrc or .zshrc:

export PATH=$PATH:/usr/local/go/bin
source ~/.bashrc  # or source ~/.zshrc

Step 4: Verify Installation

Check if Go is ready to roll:

go version

If you see something like go version go1.21.0 linux/amd64, you’re all set!


Writing Your First Go Program

The journey begins with a simple “Hello, Go World” program.

Understanding the Go Program Structure

A basic Go program includes:

  1. Package Declaration:
    • Every Go file belongs to a package.
    • For executables, the package name must be main.
  2. Imports:
    • Go organizes reusable functionalities into packages. For example, fmt is the standard package for formatted I/O.
  3. Main Function:
    • The entry point of the program.

The Code

Go is not considered an "indentation error language"== like Python, meaning that while indentation is important for readability, incorrect indentation in Go will not cause a syntax error; the language relies primarily on curly braces to define code blocks, not indentation alone.

Go developers primarily use the gofmt tool to automatically format their code, including indentation, ensuring consistency.

package main // Main package --> Mandatory

import "fmt" // Format Package

func main() { // Main Function --> Starting Point for execution
    fmt.Println("Hello, Go World!")
}

Execution

  1. Save your program in a file, e.g., hello.go.
  2. Open a terminal and navigate to the directory containing hello.go.
  3. Run the program:
go run hello.go

Basic Variables

Go’s Variable Types

  • bool: a boolean value, either true or false.
  • string: a sequence of characters.
  • int: a signed integer.
  • float64: a floating-point number.
  • byte: exactly what it sounds like—8 bits of data.

Declaring a Variable the Sad Way

var mySkillIssues int
mySkillIssues = 42
  • The first line, var mySkillIssues int, defaults the mySkillIssues variable to its zero value, 0.
  • On the next line, 42 overwrites the zero value.

The GOATed Variable Declaration

mySkillIssues := 42
  • The walrus operator, :=, declares a new variable and assigns a value to it in one line.
  • Go infers that mySkillIssues is an int because of the 42 value.

When to Use the Walrus Operator

Use := (walrus operator) instead of var declarations wherever possible. The exception is in the global/package scope where := is not allowed.

Examples

mySkillIssues := 42             // An Integer
pi := 3.14159                   // A float64 (Floating Point Number)
message := "Hello, world!"      // A String
isGoat := true                  // A Boolean Value

Same Line Declarations

You can declare multiple variables on the same line:

mileage, company := 80276, "Tesla"

Comments

Go has two styles of comments:

// This is a single-line comment

/*
   This is a multi-line comment
   neither of these comments will execute
   as code
*/

Constants

Constants are declared with the const keyword. They can’t use the := short declaration syntax.

const pi = 3.14159

Constants can be primitive types like strings, integers, booleans and floats. They can not be more complex types like slices, maps and structs, which are types we will explain later.

As the name implies, the value of a constant can’t be changed after it has been declared.

Computed Constants

Constants must be known at compile time. They are usually declared with a static value. However, constants can be computed as long as the computation can happen at compile time.

For example, this is valid:

const firstName = "Lane"
const lastName = "Wagner"
const fullName = firstName + " " + lastName

That said, you cannot declare a constant that can only be computed at run-time like you can in JavaScript. This breaks:

// the current time can only be known when the program is running
const currentTime = time.Now()

The Compilation Process

Computers need machine code—they don’t understand English or even Go. We need to convert our high-level (Go) code into machine language, which is really just a set of instructions that some specific hardware can understand, in your case, your CPU.

The Go compiler’s job is to take Go code and produce machine code, which results in an .exe file on Windows or a standard executable on Mac/Linux.

./compile.png

Go Program Structure

We’ll go over this in more detail later, but to satisfy your curiosity:

  • package main: Lets the Go compiler know that we want this code to compile and run as a standalone program, as opposed to being a library that’s imported by other programs.
  • import "fmt": Imports the fmt (formatting) package from the standard library. It allows us to use fmt.Println to print to the console.
  • func main(): Defines the main function, the entry point for a Go program.

Two Kinds of Bugs

Generally speaking, there are two kinds of errors in programming:

  1. Compilation errors: Occur when code is compiled. It’s generally better to have compilation errors because they’ll never accidentally make it into production. You can’t ship a program with a compiler error because the resulting executable won’t even be created.

  2. Runtime errors: Occur when a program is running. These are generally worse because they can cause your program to crash or behave unexpectedly.

Compilation vs Runtime in Browser

While we’re in the browser, it can be a bit hard to tell the difference because we run and compile the code in the same step.

Generally speaking, languages that compile directly to machine code produce programs that are faster than interpreted programs.

Go is one of the fastest programming languages, beating JavaScript, Python, and Ruby handily in most benchmarks.

However, Go programs don’t run quite as fast as its compiled Rust, Zig, and C counterparts. That said, it compiles much faster than they do, which makes the developer experience super productive. Unfortunately, there are no sword fights on Go teams…


Type Sizes

In Go, integers, unsigned integers, floats, and complex numbers all have defined type sizes. Here’s a breakdown of these types:

Whole Numbers (No Decimal)

  • int
  • int8
  • int16
  • int32
  • int64

Positive Whole Numbers (No Decimal)

“uint” stands for “unsigned integer”, which represents positive whole numbers.

  • uint
  • uint8
  • uint16
  • uint32
  • uint64
  • uintptr

Signed Decimal Numbers

  • float32
  • float64

Imaginary Numbers (Rarely Used)

  • complex64
  • complex128

What’s the Deal With the Sizes?

The size (8, 16, 32, 64, 128, etc) represents how many bits in memory will be used to store the variable. The “default” int and uint types refer to their respective 32 or 64-bit sizes depending on the environment of the user.

Standard Sizes

The “standard” sizes that should be used unless you have a specific performance need (e.g., using less memory) are:

  • int
  • uint
  • float64
  • complex128

Converting Between Types

Some types can be easily converted like this:

temperatureFloat := 88.26
temperatureInt := int64(temperatureFloat)

Casting a float to an integer in this way truncates the floating point portion.

Which Type Should I Use?

With so many types for what is essentially just a number, developers coming from languages that only have one kind of Number type (like JavaScript) may find the choices daunting.

Prefer “Default” Types

A problem arises when we have a uint16, and the function we are trying to pass it into takes an int. We’re forced to write code riddled with type conversions like:

var myAge uint16 = 25
myAgeInt := int(myAge)

This style of code can be slow and annoying to read. When Go developers stray from the “default” type for any given type family, the code can get messy quickly. Unless you have a good performance-related reason, you’ll typically just want to use the “default” types:

  • bool
  • string
  • int
  • uint
  • byte
  • rune
  • float64
  • complex128

When Should I Use a More Specific Type?

When you’re super concerned about performance and memory usage.

That’s about it. The only reason to deviate from the defaults is to squeeze out every last bit of performance when you are writing an application that is resource-constrained. (Or, in the special case of uint64, you need an absurd range of unsigned integers).

You can read more on this subject here if you’d like, but it’s not required.


Go Is Statically Typed

One of the key features of Go is its static typing system. In simple terms, static typing means that the type of a variable is known at compile-time, not runtime. This helps prevent a variety of issues and bugs that could arise if types were determined dynamically (like in languages such as JavaScript or Python).

Benefits of Static Typing

  1. Early Error Detection: Since Go knows the types of all variables before the program runs, it can catch type errors during the compilation stage, even before the code is executed. This results in safer, more reliable code. For example, you can’t accidentally try to add a string to an integer without explicitly converting one of them.

  2. Improved Performance: Static typing can lead to better performance because the compiler knows exactly what types it’s working with. This means the generated machine code is more optimized. The Go compiler doesn’t need to perform type checking during runtime, leading to faster execution.

  3. Better Tooling Support: Since types are explicitly declared and known, developers get the benefit of better auto-completion, refactoring support, and error highlighting in IDEs. This makes development smoother and more productive.

Contrast with Dynamic Typing

In dynamically typed languages like JavaScript, the type of a variable is determined at runtime. This can lead to hard-to-find bugs because the types aren’t checked until the program is executed. This is often a source of frustration for developers, especially when the program behaves unexpectedly in production after passing local tests. Dynamic languages may offer more flexibility, but this flexibility can come at the cost of safety and reliability.

Go, with its static typing, avoids these pitfalls by enforcing a stricter and more predictable coding environment.

Concatenating Strings

In Go, string concatenation is a common operation that you’ll use frequently. You can concatenate two strings using the + operator. However, Go does not allow implicit type conversion between strings and other types like integers or floats, unlike some other languages.

Why is Type Safety Important Here?

This is where Go’s static typing comes into play. If you try to concatenate a string with an integer, Go will throw a compilation error. This prevents accidental mistakes where you might unknowingly combine incompatible types, leading to bugs.

For example, if you tried to do this:

name := "Alice"
age := 25
greeting := name + " is " + age + " years old." // Error: cannot use age (type int) as type string

Go will stop you right there, telling you that age is an integer and cannot be directly concatenated with a string. While this might seem like an inconvenience, it’s actually a feature that promotes better coding practices and minimizes bugs.

How to Concatenate Strings with Non-String Types

To properly concatenate non-string types (like integers or floats) with strings, you’ll need to convert them explicitly to strings. Go provides several built-in functions in the strconv package for this:

import "strconv"

name := "Alice"
age := 25

greeting := name + " is " + strconv.Itoa(age) + " years old."
fmt.Println(greeting)  // Output: Alice is 25 years old.

Here, we used strconv.Itoa(age) to convert the integer age to a string. The Itoa function stands for “integer to ASCII” and is the proper way to handle integer-to-string conversion in Go.

String Interpolation: A Better Way?

You might be thinking, “This feels a bit cumbersome. Isn’t there an easier way to format strings?” Fortunately, Go also offers another great way to concatenate strings with variables using the fmt.Sprintf function. This function gives you more control over the output format, much like string interpolation in other languages.

import "fmt"

name := "Alice"
age := 25

greeting := fmt.Sprintf("%s is %d years old.", name, age)
fmt.Println(greeting)  // Output: Alice is 25 years old.

Here, %s and %d are format specifiers that represent a string and an integer, respectively. This method is more flexible and often used for complex string formatting scenarios.


Compiled vs. Interpreted: What’s the Difference?

Compiled Languages: Fast and Independent

In compiled languages like Go, C, or Rust, your code is translated into machine code ahead of time. This means you get an executable file that can run independently—no need for the original source code or an interpreter.

Pros:

  • No dependencies: Just run the final file.
  • Faster performance: Pre-compiled machine code runs quickly.
  • Easy distribution: Give someone the file, and they’re good to go.

Interpreted Languages: Run On-the-Fly

In interpreted languages like Python or Ruby, the code is read and executed line-by-line by an interpreter while the program runs. The source code needs to be present, and the interpreter must be installed on the user’s machine.

Pros:

  • Easier to debug: Changes are instantly reflected.
  • Flexible development: No need for a compilation step.

Cons:

  • Slower: The interpreter translates code as it runs.
  • Dependency: Requires an interpreter on the user’s machine.

./compiler.png

JIT Compilation: The Best of Both Worlds?

Languages like JavaScript use Just-in-Time (JIT) compilation, where parts of the code are compiled as it runs, offering speed while still using an interpreter.

So, Which Should You Use?

  • Compiled languages (Go, C++) are best for performance and when you want to distribute a final product.
  • Interpreted languages (Python, Ruby) are great for rapid development, but require an interpreter and might be slower.

It’s all about the trade-offs!


Small Memory Footprint

Go programs are fairly lightweight. Each program includes a small amount of extra code that’s included in the executable binary called the Go Runtime. One of the purposes of the Go runtime is to clean up unused memory at runtime. It includes a garbage collector that automatically frees up memory that’s no longer in use.

Comparison

As a general rule, Java programs use more memory than comparable Go programs. There are several reasons for this, but one of them is that Java uses a virtual machine to interpret bytecode at runtime and typically allocates more on the heap.

On the other hand, Rust and C programs use slightly less memory than Go programs because more control is given to the developer to optimize the memory usage of the program. The Go runtime just handles it for us automatically.

./memory.png

In the chart above, Dexter Darwich compares the memory usage of three very simple programs written in Java, Go, and Rust. As you can see, Go and Rust use very little memory when compared to Java.

Comparing Go’s Speed

Go is generally faster and more lightweight than interpreted or VM-powered languages like:

  • Python
  • JavaScript
  • PHP
  • Ruby
  • Java

However, in terms of execution speed, Go does lag behind some other compiled languages like:

  • C
  • C++
  • Rust

Go is a bit slower mostly due to its automated memory management, also known as the “Go runtime”. Slightly slower speed is the price we pay for memory safety and simple syntax!

./faster.png


Formatting Strings in Go

Go follows the printf tradition from the C language. In my opinion, string formatting/interpolation in Go is less elegant than Python’s f-strings, unfortunately.

These following “formatting verbs” work with the formatting functions above:

Default Representation

The %v variant prints the Go syntax representation of a value, it’s a nice default.

s := fmt.Sprintf("I am %v years old", 10)
// I am 10 years old

s := fmt.Sprintf("I am %v years old", "way too many")
// I am way too many years old

If you want to print in a more specific way, you can use the following formatting verbs:

s := fmt.Sprintf("I am %s years old", "way too many") // String
// I am way too many years old

s := fmt.Sprintf("I am %d years old", 10)  // Integer
// I am 10 years old

s := fmt.Sprintf("I am %f years old", 10.523) // Float
// I am 10.523000 years old

// The ".2" rounds the number to 2 decimal places
s := fmt.Sprintf("I am %.2f years old", 10.523)
// I am 10.52 years old

s := fmt.Sprintf("I am %t years old", true)  // Boolean

Runes and String Encoding

In many programming languages (cough, C, cough), a “character” is a single byte. Using ASCII encoding, the standard for the C programming language, we can represent 128 characters with 7 bits. This is enough for the English alphabet, numbers, and some special characters.

In Go, strings are just sequences of bytes: they can hold arbitrary data. However, Go also has a special type, rune, which is an alias for int32. This means that a rune is a 32-bit integer, which is large enough to hold any Unicode code point.

When you’re working with strings, you need to be aware of the encoding (bytes -> representation). Go uses UTF-8 encoding, which is a variable-length encoding.

What Does This Mean?

There are 2 main takeaways:

  1. When you need to work with individual characters in a string, you should use the rune type. It breaks strings up into their individual characters, which can be more than one byte long.
  2. We can use zany characters like emojis and Chinese characters in our strings, and Go will handle them just fine.

See these examples and its outputs

package main

import (
	"fmt"
	"unicode/utf8"
)

func main() {
	const name = "boots"
	fmt.Printf("constant 'name' byte length: %d\n", len(name))
	fmt.Printf("constant 'name' rune length: %d\n", utf8.RuneCountInString(name))
	fmt.Println("=====================================")
	fmt.Printf("Hi %s, so good to have you back in the arcanum\n", name)
}

Output

constant 'name' byte length: 5
constant 'name' rune length: 5
=====================================
Hi boots, so good to have you back in the arcanum

If I replace boots with 🐻 then Output is:

constant 'name' byte length: 4
constant 'name' rune length: 1  // Length is 1 but using 4 bytes 🤔
=====================================
Hi 🐻, so good to have you back in the arcanum

Conditionals

1. IF condition

if statements in Go do not use parentheses around the condition and else if and else are supported as you might expect:

if height > 6 {
    fmt.Println("You are super tall!")
} else if height > 4 {
    fmt.Println("You are tall enough!")
} else {
    fmt.Println("You are not tall enough!")
}

Note: The else or else-if should be next to the if closed bracket( } ), else that else block will be unexpected and unnoticed as that if's else block

if height > 6 {
    fmt.Println("You are super tall!")
}
else {
    fmt.Println("You are not tall enough!")
}

Output is an error like: err: exit status 1, # ./main.go:24:2: syntax error: unexpected else, expected }

Some of the comparison operators in Go:

- `==` equal to
- `!=` not equal to
- `<` less than
- `>` greater than
- `<=` less than or equal to
- `>=` greater than or equal to

The Initial Statement of an If Block

An if conditional can have an “initial” statement. The variable(s) created in the initial statement are only defined within the scope of the if body.

if INITIAL_STATEMENT; CONDITION {
}
Why Would I Use This?

It has two valuable purposes:

  1. It’s a bit shorter
  2. It limits the scope of the initialized variable(s) to the if block

For example, instead of writing:

length := getLength(email)
if length < 1 {
    fmt.Println("Email is invalid")
}

we can do

if length := getLength(email); length < 1 {
    fmt.Println("Email is invalid")
}

In the example above, length isn’t available in the parent scope, which is nice because we don’t need it there - we won’t accidentally use it elsewhere in the function.

2. Switch Condition

Switch statements are a way to compare a value against multiple options. They are similar to if-else statements but are more concise and readable when the number of options is more than 2.

func getCreator(os string) string {
    var creator string
    switch os {
    case "linux":
        creator = "Linus Torvalds"
    case "windows":
        creator = "Bill Gates"
    case "mac":
        creator = "A Steve"
    default:
        creator = "Unknown"
    }
    return creator
}

Notice that in Go, the break statement is not required at the end of a case to stop it from falling through to the next case. The break statement is implicit in Go.

If you do want a case to fall through to the next case, you can use the fallthrough keyword.

func getCreator(os string) string {
    var creator string
    switch os {
    case "linux":
        creator = "Linus Torvalds"
    case "windows":
        creator = "Bill Gates"

    // all three of these cases will set creator to "A Steve"
    case "macOS":
        fallthrough
    case "Mac OS X":
        fallthrough
    case "mac":
        creator = "A Steve"

    default:
        creator = "Unknown"
    }
    return creator
}

The default case does what you’d expect: it’s the case that runs if none of the other cases match.


Loops in Go

The basic loop in Go is written in standard C-like syntax:

for INITIAL; CONDITION; AFTER{
  // do something
}

INITIAL is run once at the beginning of the loop and can create
variables within the scope of the loop.

CONDITION is checked before each iteration. If the condition doesn’t pass
then the loop breaks.

AFTER is run after each iteration.

For example:

for i := 0; i < 10; i++ {
  fmt.Println(i)
}
// Prints 0 through 9

Omitting Conditions from a for Loop in Go

Loops in Go can omit sections of a for loop. For example, the CONDITION (middle part) can be omitted which causes the loop to run forever.

for INITIAL; ; AFTER {
  // do something forever
}

There Is No While Loop in Go

Most programming languages have a concept of a while loop. Because Go allows for the omission of sections of a for loop, a while loop is just a for loop that only has a CONDITION.

for CONDITION {
  // do some stuff while CONDITION is true
}

For example:

plantHeight := 1
for plantHeight < 5 {
  fmt.Println("still growing! current height:", plantHeight)
  plantHeight++
}
fmt.Println("plant has grown to ", plantHeight, "inches")

Which prints:

still growing! current height: 1
still growing! current height: 2
still growing! current height: 3
still growing! current height: 4
plant has grown to 5 inches

Go supports the standard modulo operator:

7 % 3 // 1

The AND logical operator:

true && false // false
true && true // true

As well as the OR operator:

true || false // true
false || false // false

Continue & Break

Whenever we want to change the control flow of a loop we can use the continue and break keywords.

continue

The continue keyword stops the current iteration of a loop and continues to the next iteration. continue is a powerful way to use the guard clause pattern within loops.

for i := 0; i < 10; i++ {
  if i % 2 == 0 {
    continue
  }
  fmt.Println(i)
}
// 1
// 3
// 5
// 7
// 9

break

The break keyword stops the current iteration of a loop and exits the loop.

for i := 0; i < 10; i++ {
  if i == 5 {
    break
  }
  fmt.Println(i)
}
// 0
// 1
// 2
// 3
// 4

Functions

Functions in Go can take zero or more arguments.

To make code easier to read, the variable type comes after the variable name.

For example, the following function:

func sub(x int, y int) int {
  return x-y
}

Accepts two integer parameters and returns another integer.

Here, func sub(x int, y int) int is known as the “function signature”.

Multiple Parenthesis

When multiple arguments are of the same type, and are next to each other in the function signature, the type only needs to be declared after the last argument.

Valid & Best ways of parameter declarations

func addToDatabase(hp, damage int) {
  // here both the variable types are int so it can be written after both params as a single datatype
}

func addToDatabase(hp, damage int, name string) {
  // ?
}

func addToDatabase(hp, damage int, name string, level int) {
  // ?
}

Declaration Syntax

Developers often wonder why the declaration syntax in Go is different from the tradition established in the C family of languages.

C-Style Syntax

The C language describes types with an expression including the name to be declared, and states what type that expression will have.

int y;

The code above declares y as an int. In general, the type goes on the left and the expression on the right.

Interestingly, the creators of the Go language agreed that the C-style of declaring types in signatures gets confusing really fast - take a look at this nightmare 😂.

int (*fp)(int (*ff)(int x, int y), int b)

Go-Style Syntax

Go’s declarations are clear, you just read them left to right, just like you would in English.

x int
p *int
a [3]int

It’s nice for more complex signatures, it makes them easier to read.

f func(func(int,int) int, int) int

Go’s language declaration syntax reads like English from Left to Right.

Explanation:

The key distinction here is about readability and order. Let me ask you: when you read a book in English, which direction do you read? Left to right, correct?

Now, let’s compare two declarations:

  1. C-style: int x
  2. Go-style: x int

Which of these reads more naturally in English? If you were to say it out loud, would you say:

  • “integer x” (C-style)
  • “x is an integer” (Go-style)
var f func(func(int,int) int, int) int

Readability → A function named f that takes a function and an int as arguments and returns int.

Reference

The following post on the Go blog is a great resource for further reading on declaration syntax.

Passing Variables by Value

Variables in Go are passed by value (except for a few data types). “Pass by value” means that when a variable is passed into a function, that function receives a copy of the variable. The function is unable to mutate the caller’s original data.

func main() {
    x := 5
    increment(x)

    fmt.Println(x)
    // still prints 5,
    // because the increment function
    // received a copy of x
}

func increment(x int) {
    x++
}

Ignoring Return Values

A function can return a value that the caller doesn’t care about. We can explicitly ignore variables by using an underscore, or more precisely, the blank identifier _.

For example:

func getPoint() (x int, y int) {
  return 3, 4
}

// ignore y value
x, _ := getPoint() // _ should be as many as you want to ignore -> a single _ is not defined for all.

Even though getPoint() returns two values, we can capture the first one and ignore the second. In Go, the blank identifier isn’t just a convention; it’s a real language feature that completely discards the value.

Why Might You Ignore a Return Value?

Maybe a function called getCircle returns the center point and the radius, but you only need the radius for your calculation. In that case, you can ignore the center point variable.

The Go compiler will throw an error if you have any unused variable declarations in your code, so you need to ignore anything you don’t intend to use.

Named Return Values

Return values may be given names, and if they are, then they are treated the same as if they were new variables defined at the top of the function.

Named return values are best thought of as a way to document the purpose of the returned values.

According to the tour of go:

A return statement without arguments returns the named return values. This is known as a "naked" return. Naked return statements should be used only in short functions. They can harm readability in longer functions.

Use naked returns if it’s obvious what the purpose of the returned values is. Otherwise, use named returns for clarity.

func getCoords() (x, y int){
  // x and y are initialized with zero values (declared by default & if we declare here then it's a redeclaration)

  return // automatically returns x and y
}

Is same as:

func getCoords() (int, int){
  var x int
  var y int
  return x, y
}

In the first example, x and y are the return values. At the end of the function, we could simply write return to return the values of those two variables, rather than writing return x,y.

The Benefits of Named Returns

Good for Documentation (Understanding)

Named return parameters are great for documenting a function. We know what the function is returning directly from its signature, no need for a comment.

Named return parameters are particularly important in longer functions with many return values.

func calculator(a, b int) (mul, div int, err error) {
    if b == 0 {
      return 0, 0, errors.New("Can't divide by zero")
    }
    mul = a * b
    div = a / b
    return mul, div, nil
}

Which is easier to understand than:

func calculator(a, b int) (int, int, error) {
    if b == 0 {
      return 0, 0, errors.New("Can't divide by zero")
    }
    mul := a * b
    div := a / b
    return mul, div, nil
}

We know the meaning of each return value just by looking at the function signature: func calculator(a, b int) (mul, div int, err error)

Note: nil is the zero value of an error.

Less Code (Sometimes)

If there are multiple return statements in a function, you don’t need to write all the return values each time, though you probably should.

When you choose to omit return values, it’s called a naked return. Naked returns should only be used in short and simple functions.

Explicit Returns

Even though a function has named return values, we can still explicitly return values if we want to.

func getCoords() (x, y int){
  return x, y // this is explicit
}

Using this explicit pattern we can even overwrite the return values:

func getCoords() (x, y int){
  return 5, 6 // this is explicit, x and y are NOT returned
}

Otherwise, if we want to return the values defined in the function signature we can just use a naked return (blank return):

func getCoords() (x, y int){
  return // implicitly returns x and y
}

Early Returns

Go supports the ability to return early from a function. This is a powerful feature that can clean up code, especially when used as guard clauses.

Guard Clauses leverage the ability to return early from a function (or continue through a loop) to make nested conditionals one-dimensional. Instead of using if/else chains, we just return early from the function at the end of each conditional block.

func divide(dividend, divisor int) (int, error) {
	if divisor == 0 {
		return 0, errors.New("Can't divide by zero")
	}
	return dividend/divisor, nil
}

Error handling in Go naturally encourages developers to make use of guard clauses.

Let’s take a look at an exaggerated example of nested conditional logic:

func getInsuranceAmount(status insuranceStatus) int {
  amount := 0
  if !status.hasInsurance(){
    amount = 1
  } else {
    if status.isTotaled(){
      amount = 10000
    } else {
      if status.isDented(){
        amount = 160
        if status.isBigDent(){
          amount = 270
        }
      } else {
        amount = 0
      }
    }
  }
  return amount
}

This could be written with guard clauses instead:

func getInsuranceAmount(status insuranceStatus) int {
  if !status.hasInsurance(){
    return 1
  }
  if status.isTotaled(){
    return 10000
  }
  if !status.isDented(){
    return 0
  }
  if status.isBigDent(){
    return 270
  }
  return 160
}

The example above is much easier to read and understand. When writing code, it’s important to try to reduce the cognitive load on the reader by reducing the number of entities they need to think about at any given time.

In the first example, if the developer is trying to figure out when 270 is returned, they need to think about each branch in the logic tree and try to remember which cases matter and which cases don’t. With the one-dimensional structure offered by guard clauses, it’s as simple as stepping through each case in order.

Functions As Values

Go supports first-class and higher-order functions, which are just fancy ways of saying “functions as values”. Functions are just another type – like ints and strings and bools.

Let’s assume we have two simple functions:

func add(x, y int) int {
	return x + y
}

func mul(x, y int) int {
	return x * y
}

We can create a new aggregate function that accepts a function as its 4th argument:

func aggregate(a, b, c int, arithmetic func(int, int) int) int {
  firstResult := arithmetic(a, b)
  secondResult := arithmetic(firstResult, c)
  return secondResult
}

It calls the given arithmetic function (which could be add or mul, or any other function that accepts two ints and returns an int) and applies it to three inputs instead of two. It can be used like this:

func main() {
	sum := aggregate(2, 3, 4, add)
	// sum is 9
	product := aggregate(2, 3, 4, mul)
	// product is 24
}

Anonymous Functions

Anonymous functions are true to form in that they have no name. They’re useful when defining a function that will only be used once or to create a quick closure.

Let’s say we have a function conversions that accepts another function, converter as input:

func conversions(converter func(int) int, x, y, z int) (int, int, int) {
	convertedX := converter(x)
	convertedY := converter(y)
	convertedZ := converter(z)
	return convertedX, convertedY, convertedZ
}

We could define a function normally and then pass it in by name… but it’s usually easier to just define it anonymously:

func double(a int) int {
    return a + a
}

func main() {
    // using a named function
	newX, newY, newZ := conversions(double, 1, 2, 3)
	// newX is 2, newY is 4, newZ is 6

    // using an anonymous function
	newX, newY, newZ = conversions(func(a int) int {
	    return a * a
	}, 1, 2, 3)
	// newX is 1, newY is 4, newZ is 9
}

Defer

The defer keyword is a fairly unique feature of Go. It allows a function to be executed automatically just before its enclosing function returns. The deferred call’s arguments are evaluated immediately, but the function call is not executed until the surrounding function returns.

Deferred functions are typically used to clean up resources that are no longer being used. Often to close database connections, file handlers and the like.

Which means in the below function wherever function exits, before that automatically runs the command defer.

For example:

func GetUsername(dstName, srcName string) (username string, err error) {
	// Open a connection to a database
	conn, _ := db.Open(srcName)

	// Close the connection *anywhere* the GetUsername function returns
	defer conn.Close()

	username, err = db.FetchUser()
	if err != nil {
		// The defer statement is auto-executed if we return here
		return "", err
	}

	// The defer statement is auto-executed if we return here
	return username, nil
}

In the above example, the conn.Close() function is not called here:

defer conn.Close()

It’s called:

// here
return "", err
// or here
return username, nil

Depending on whether the FetchUser function errored.

Defer is a great way to make sure that something happens before a function exits, even if there are multiple return statements, a common occurrence in Go.

Let’s take an assignment as example:

Complete the bootup function. Notice that it potentially returns in three places. No matter where it returns, it should print the following message just before it returns:

TEXTIO BOOTUP DONE

Use defer so that you only have to write this message once.

// Pre commands

func bootup() {
	defer fmt.Println("TEXTIO BOOTUP DONE")
	ok := connectToDB()
	if !ok {
		return
	}
	ok = connectToPaymentProvider()
	if !ok {
		return
	}
	fmt.Println("All systems ready!")
}

// don't touch below this line

// Post functions

Output:

Connecting to database...
Connected!
Connecting to payment provider...
Connected!
All systems ready!
TEXTIO BOOTUP DONE
====================================
Connecting to database...
Connection failed
TEXTIO BOOTUP DONE  // Just printing the text before exiting (No matter what's going in the function it just executes the defer command before exiting the function...)
====================================

Block Scope

Unlike Python, Go is not function-scoped, it’s block-scoped. Variables declared inside a block are only accessible within that block (and its nested blocks). There’s also the package scope. We’ll talk about packages later, but for now, you can think of it as the outermost, nearly global scope.

package main

// scoped to the entire "main" package (basically global)
var age = 19

func sendEmail() {
    // scoped to the "sendEmail" function
    name := "Jon Snow"

    for i := 0; i < 5; i++ {
        // scoped to the "for" body
        email := "snow@winterfell.net"
    }
}

Blocks are defined by curly braces {}. New blocks are created for:

  • Functions
  • Loops
  • If statements
  • Switch statements
  • Select statements
  • Explicit blocks

It’s a bit unusual, but occasionally you’ll see a plain old explicit block. It exists for no other reason than to create a new scope.

package main

func main() {
    {
        age := 19
        // this is okay
        fmt.Println(age)
    }

    // this is not okay
    // the age variable is out of scope
    fmt.Println(age) // ----> Gives age undefined error
}

Closures

A closure is a function that references variables from outside its own function body. The function may access and assign to the referenced variables.

In this example, the concatter() function returns a function that has reference to an enclosed doc value. Each successive call to harryPotterAggregator mutates that same doc variable.

func concatter() func(string) string {
	doc := ""
	return func(word string) string {
		doc += word + " "
		return doc
	}
}

func main() {
	harryPotterAggregator := concatter()
	harryPotterAggregator("Mr.")
	harryPotterAggregator("and")
	harryPotterAggregator("Mrs.")
	harryPotterAggregator("Dursley")
	harryPotterAggregator("of")
	harryPotterAggregator("number")
	harryPotterAggregator("four,")
	harryPotterAggregator("Privet")

	fmt.Println(harryPotterAggregator("Drive"))
	// Mr. and Mrs. Dursley of number four, Privet Drive
}

Structs in Go

We use structs in Go to represent structured data. It’s often convenient to group different types of variables together. For example, if we want to represent a car we could do the following:

type car struct {
	brand      string
	model      string
	doors      int
	mileage    int
}

This creates a new struct type called car. All cars have a brand, model, doors and mileage.

Structs in Go are often used to represent data that you might use a dictionary or object for in other languages.

Nested Structs in Go

Structs can be nested to represent more complex entities:

type car struct {
  brand string
  model string
  doors int
  mileage int
  frontWheel wheel
  backWheel wheel
}

type wheel struct {
  radius int
  material string
}

The fields of a struct can be accessed using the dot . operator.

myCar := car{}
myCar.frontWheel.radius = 5

Anonymous Structs in Go

An anonymous struct is just like a normal struct, but it is defined without a name and therefore cannot be referenced elsewhere in the code.

To create an anonymous struct, just instantiate the instance immediately using a second pair of brackets after declaring the type:

myCar := struct {
  brand string
  model string
} {
  brand: "tesla",
  model: "model 3",
}

You can even nest anonymous structs as fields within other structs:

type car struct {
  brand string
  model string
  doors int
  mileage int
  // wheel is a field containing an anonymous struct
  wheel struct {
    radius int
    material string
  }
}

When Should You Use an Anonymous Struct?

In general, prefer named structs. Named structs make it easier to read and understand your code, and they have the nice side-effect of being reusable. Sometimes we can use anonymous structs when we know that we won’t ever need to use a struct again. For example, sometimes we can use one to create the shape of some JSON data in HTTP handlers.

If a struct is only meant to be used once, then it makes sense to declare it in such a way that developers down the road won’t be tempted to accidentally use it again.

You can read more about anonymous structs here if you’re curious.

Embedded Structs

Go is not an object-oriented language. However, embedded structs provide a kind of data-only inheritance that can be useful at times. Keep in mind, Go doesn’t support classes or inheritance in the complete sense, but embedded structs are a way to elevate and share fields between struct definitions.

type car struct {
  brand string
  model string
}

type truck struct {
  // "car" is embedded, so the definition of a
  // "truck" now also additionally contains all
  // of the fields of the car struct
  car
  bedSize int
}

Embedded vs. Nested

  • Unlike nested structs, an embedded struct’s fields are accessed at the top level like normal fields.
  • Like nested structs, you assign the promoted fields with the embedded struct in a composite literal.
lanesTruck := truck{
  bedSize: 10,
  car: car{
    brand: "toyota",
    model: "camry",
  },
}

fmt.Println(lanesTruck.bedSize)

// embedded fields are promoted to the top-level
// instead the nested equivalent of lanesTruck.car.brand
fmt.Println(lanesTruck.brand)
fmt.Println(lanesTruck.model)

Struct Methods in Go

While Go is not object-oriented, it does support methods that can be defined on structs. Methods are just functions that have a receiver. A receiver is a special parameter that syntactically goes before the name of the function.

type rect struct {
  width int
  height int
}

// area has a receiver of (r rect)
// rect is the struct
// r is the placeholder
func (r rect) area() int {
  return r.width * r.height
}

var r = rect{
  width: 5,
  height: 10,
}

fmt.Println(r.area())
// prints 50

A receiver is just a special kind of function parameter. In the example above, the r in (r rect) could just as easily have been rec or even x, y or z. By convention, Go code will often use the first letter of the struct’s name.

Memory Layout

In Go, structs sit in memory in a contiguous block, with fields placed one after another as defined in the struct. For example this struct:

type stats struct {
	Reach    uint16
	NumPosts uint8
	NumLikes uint8
}

Looks like this in memory:

./struct.png

Field ordering… Matters?

the order of fields in a struct can have a big impact on memory usage. This is the same struct as above, but poorly designed:

type stats struct {
	NumPosts uint8
	Reach    uint16
	NumLikes uint8
}

It looks like this in memory:

./mem_struct.png

Notice that Go has “aligned” the fields, meaning that it has added some padding (wasted space) to make up for the size difference between the uint16 and uint8 types. It’s done for execution speed, but it can lead to increased memory usage.

Should I Panic?

To be honest, you should not stress about memory layout. However, if you have a specific reason to be concerned about memory usage, aligning the fields by size (largest to smallest) can help. You can also use the reflect package to debug the memory layout of a struct:

typ := reflect.TypeOf(stats{})
fmt.Printf("Struct is %d bytes\n", typ.Size())

Empty Struct

Empty structs are used in Go as a unary value.


// anonymous empty struct type
empty := struct{}{}

// named empty struct type
type emptyStruct struct{}
empty := emptyStruct{}

The cool thing about empty structs is that they’re the smallest possible type in Go: they take up zero bytes of memory.

./usage.png


Interfaces in Go

Interfaces are just collections of method signatures. A type “implements” an interface if it has methods that match the interface’s method signatures.

In the following example, a “shape” must be able to return its area and perimeter. Both rect and circle fulfill the interface.

type shape interface {
  area() float64
  perimeter() float64
}

type rect struct {
    width, height float64
}
func (r rect) area() float64 {
    return r.width * r.height
}
func (r rect) perimeter() float64 {
    return 2*r.width + 2*r.height
}

type circle struct {
    radius float64
}
func (c circle) area() float64 {
    return math.Pi * c.radius * c.radius
}
func (c circle) perimeter() float64 {
    return 2 * math.Pi * c.radius
}

When a type implements an interface, it can then be used as that interface type.

func printShapeData(s shape) {
	fmt.Printf("Area: %v - Perimeter: %v\n", s.area(), s.perimeter())
}

Because we say the input is of type shape, we know that any argument must implement the .area() and .perimeter() methods.

As an example, because the empty interface doesn’t require a type to have any methods at all, every type automatically implements the empty interface, written as:

interface{}

Interfaces allow you to focus on what a type does rather than how it’s built. They can help you write more flexible and reusable code by defining behaviors (like methods) that different types can share. This makes it easy to swap out or update parts of your code without changing everything else.

Tip: The length of a string can be obtained using the len function, which returns the number of bytes.

s := "Hello, World!"
fmt.Println(len(s))
// 13

Here’s why interfaces are useful in Go:

  1. Decoupling: Interfaces decouple your code from specific implementations, making it more flexible and maintainable. You can write code that interacts with an interface, and it will work with any type that implements that interface, even if the type is defined in a different package.

  2. Polymorphism: Interfaces enable polymorphism, allowing you to treat different types in a uniform way. You can write functions that accept an interface as a parameter, and they can work with any type that implements that interface.

  3. Mocking: Interfaces make it easier to write unit tests by enabling the use of mocks. You can define an interface for a dependency, and then create a mock implementation that returns specific values for testing purposes.

  4. Code Organization: Interfaces help organize code by defining contracts between different parts of your program. They provide a clear specification of what a type should do, making your code more readable and understandable.

Interface Implementation

Interfaces are implemented implicitly.

A type never declares that it implements a given interface. If an interface exists and a type has the proper methods defined, then the type automatically fulfills that interface.

A quick way of checking whether a struct implements an interface is to declare that a function takes an interface as an argument. If the function can take the struct as an argument, then the struct implements the interface.

Interfaces Are Implemented Implicitly

A type implements an interface by implementing its methods. Unlike in many other languages, there is no explicit declaration of intent, there is no “implements” keyword.

Implicit interfaces decouple the definition of an interface from its implementation. You may add methods to a type and in the process be unknowingly implementing various interfaces, and that’s okay.

Remember, interfaces are collections of method signatures. A type “implements” an interface if it has all of the methods of the given interface defined on it.

type shape interface {
  area() float64
}

If a type in your code implements an area method, with the same signature (e.g. accepts nothing and returns a float64), then that object is said to implement the shape interface.

type circle struct{
	radius int
}

func (c circle) area() float64 {
  return 3.14 * c.radius * c.radius
}

This is different from most other languages, where you have to explicitly assign an interface type to an object, like with Java:

class Circle implements Shape

Name Your Interface Parameters

Consider the following interface:

type Copier interface {
  Copy(string, string) int
}

Based on the code alone, can you deduce what kinds of strings you should pass into the Copy function?

We know the function signature expects 2 string types, but what are they? Filenames? URLs? Raw string data? For that matter, what the heck is that int that’s being returned?

Let’s add some named parameters and return data to make it more clear.

type Copier interface {
  Copy(sourceFile string, destinationFile string) (bytesCopied int)
}

Much better. We can see what the expectations are now. The first parameter is the sourceFile, the second parameter is the destinationFile, and bytesCopied, an integer, is returned.

Type Assertions in Go

When working with interfaces in Go, every once-in-awhile you’ll need access to the underlying type of an interface value. You can cast an interface to its underlying type using a type assertion.

The example below shows how to safely access the radius field of s when s is an unknown type:

  • we want to check if s is a circle in order to cast it into its underlying concrete type
  • we know s is an instance of the shape interface, but we do not know if it’s also a circle
  • c is a new circle struct cast from s
  • ok is true if s is indeed a circle, or false if s is NOT a circle
type shape interface {
	area() float64
}

type circle struct {
	radius float64
}

c, ok := s.(circle)
if !ok {
	// log an error if s isn't a circle
	log.Fatal("s is not a circle")
}

radius := c.radius

Type Switches

A type switch makes it easy to do several type assertions in a series.

A type switch is similar to a regular switch statement, but the cases specify types instead of values.

func printNumericValue(num interface{}) {
	switch v := num.(type) {
	case int:
		fmt.Printf("%T\n", v)
	case string:
		fmt.Printf("%T\n", v)
	default:
		fmt.Printf("%T\n", v)
	}
}

func main() {
	printNumericValue(1)
	// prints "int"

	printNumericValue("1")
	// prints "string"

	printNumericValue(struct{}{})
	// prints "struct {}"
}

fmt.Printf("%T\n", v) prints the type of a variable.

Clean Interfaces

Writing clean interfaces is hard. Frankly, any time you’re dealing with abstractions in code, the simple can become complex very quickly if you’re not careful. Let’s go over some rules of thumb for keeping interfaces clean.

1. Keep Interfaces Small

If there is only one piece of advice that you take away from this lesson, make it this: keep interfaces small! Interfaces are meant to define the minimal behavior necessary to accurately represent an idea or concept.

Here is an example from the standard HTTP package of a larger interface that’s a good example of defining minimal behavior:

type File interface {
    io.Closer
    io.Reader
    io.Seeker
    Readdir(count int) ([]os.FileInfo, error)
    Stat() (os.FileInfo, error)
}

Any type that satisfies the interface’s behaviors can be considered by the HTTP package as a File. This is convenient because the HTTP package doesn’t need to know if it’s dealing with a file on disk, a network buffer, or a simple []byte.

2. Interfaces Should Have No Knowledge of Satisfying Types

An interface should define what is necessary for other types to classify as a member of that interface. They shouldn’t be aware of any types that happen to satisfy the interface at design time.

For example, let’s assume we are building an interface to describe the components necessary to define a car.

type car interface {
	Color() string
	Speed() int
	IsFiretruck() bool
}

Color() and Speed() make perfect sense, they are methods confined to the scope of a car. IsFiretruck() is an anti-pattern. We are forcing all cars to declare whether or not they are firetrucks. In order for this pattern to make any amount of sense, we would need a whole list of possible subtypes. IsPickup(), IsSedan(), IsTank()… where does it end??

Instead, the developer should have relied on the native functionality of type assertion to derive the underlying type when given an instance of the car interface. Or, if a sub-interface is needed, it can be defined as:

type firetruck interface {
	car
	HoseLength() int
}

Which inherits the required methods from car as an embedded interface and adds one additional required method to make the car a firetruck.

3. Interfaces Are Not Classes

  • Interfaces are not classes, they are slimmer.
  • Interfaces don’t have constructors or deconstructors that require that data is created or destroyed.
  • Interfaces aren’t hierarchical by nature, though there is syntactic sugar to create interfaces that happen to be supersets of other interfaces.
  • Interfaces define function signatures, but not underlying behavior. Making an interface often won’t DRY up your code in regards to struct methods. For example, if five types satisfy the fmt.Stringer interface, they all need their own version of the String() function.

Optional: Further Reading

Best Practices for Interfaces in Go


The Error Interface

Go programs express errors with error values. An Error is any type that implements the simple built-in error interface:

type error interface {
    Error() string
}

When something can go wrong in a function, that function should return an error as its last return value. Any code that calls a function that can return an error should handle errors by testing whether the error is nil.

Atoi

Let’s look at how the strconv.Atoi function uses this pattern. The function signature of Atoi is:

func Atoi(s string) (int, error)

This means Atoi takes a string argument and returns two values: an integer and an error. If the string can be successfully converted to an integer, Atoi returns the integer and a nil error. If the conversion fails, it returns zero and a non-nil error.

Here’s how you would safely use Atoi:

// Atoi converts a stringified number to an integer
i, err := strconv.Atoi("42b")
if err != nil {
    fmt.Println("couldn't convert:", err)
    // because "42b" isn't a valid integer, we print:
    // couldn't convert: strconv.Atoi: parsing "42b": invalid syntax
    // Note:
    // 'parsing "42b": invalid syntax' is returned by the .Error() method
    return
}
// if we get here, then the
// variable i was converted successfully

A nil error denotes success; a non-nil error denotes failure.

Because errors are just interfaces, you can build your own custom types that implement the error interface. Here’s an example of a userError struct that implements the error interface:

type userError struct {
    name string
}

func (e userError) Error() string {
    return fmt.Sprintf("%v has a problem with their account", e.name)
}

It can then be used as an error:

func sendSMS(msg, userName string) error {
    if !canSendToUser(userName) {
        return userError{name: userName}
    }
    ...
}

Formatting Strings Review

A convenient way to format strings in Go is by using the standard library’s fmt.Sprintf() function. It’s a string interpolation function, similar to Python’s f-strings. The %v substring uses the type’s default formatting, which is often what you want.

Default Values

const name = "Kim"
const age = 22
s := fmt.Sprintf("%v is %v years old.", name, age)
// s = "Kim is 22 years old."

The equivalent Python code:

name = "Kim"
age = 22
s = f"{name} is {age} years old."
# s = "Kim is 22 years old."

Rounding Floats

fmt.Printf("I am %f years old", 10.523)
// I am 10.523000 years old

// The ".2" rounds the number to 2 decimal places
fmt.Printf("I am %.2f years old", 10.523)
// I am 10.52 years old

Go programs express errors with error values. Error-values are any type that implements the simple built-in error interface.

Keep in mind that the way Go handles errors is fairly unique. Most languages treat errors as something special and different. For example, Python raises exception types and JavaScript throws and catches errors. In Go, an error is just another value that we handle like any other value - however we want! There aren’t any special keywords for dealing with them.

The Errors Package

The Go standard library provides an “errors” package that makes it easy to deal with errors.

Read the godoc for the errors.New() function, but here’s a simple example:

var err error = errors.New("something went wrong")

Remember that it's conventional to return the "zero" values of all other return values when you return a non-nil error in Go.

Panic

As we’ve seen, the proper way to handle errors in Go is to make use of the error interface. Pass errors up the call stack, treating them as normal values:

func enrichUser(userID string) (User, error) {
    user, err := getUser(userID)
    if err != nil {
        // fmt.Errorf is GOATed: it wraps an error with additional context
        return User{}, fmt.Errorf("failed to get user: %w", err)
    }
    return user, nil
}

However, there is another way to deal with errors in Go: the panic function. When a function calls panic, the program crashes and prints a stack trace.

As a general rule, do not use panic!

The panic function yeets control out of the current function and up the call stack until it reaches a function that defers a recover. If no function calls recover, the goroutine (often the entire program) crashes.

func enrichUser(userID string) User {
    user, err := getUser(userID)
    if err != nil {
        panic(err)
    }
    return user
}

func main() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("recovered from panic:", r)
        }
    }()

    // this panics, but the defer/recover block catches it
    // a truly astonishingly bad way to handle errors
    enrichUser("123")
}

Sometimes new Go developers look at panic/recover, and think, “This is like try/catch! I like this!” Don’t be like them.

I use error values for all “normal” error handling, and if I have a truly unrecoverable error, I use log.Fatal to print a message and exit the program.


Arrays in Go

Arrays are fixed-size groups of variables of the same type. For example, [4]string is an array of 4 values of type string.

To declare an array of 10 integers:

var myInts [10]int

or to declare an initialized literal:

primes := [6]int{2, 3, 5, 7, 11, 13}

Slices in Go

99 times out of 100 you will use a slice instead of an array when working with ordered lists.

Arrays are fixed in size. Once you make an array like [10]int you can’t add an 11th element.

A slice is a dynamically-sized, flexible view of the elements of an array.

The zero value of slice is nil.

Non-nil slices always have an underlying array, though it isn’t always specified explicitly. To explicitly create a slice on top of an array we can do:

primes := [6]int{2, 3, 5, 7, 11, 13}
mySlice := primes[1:4]
// mySlice = {3, 5, 7}

The syntax is:

arrayname[lowIndex:highIndex]
arrayname[lowIndex:]
arrayname[:highIndex]
arrayname[:]

Where lowIndex is inclusive and highIndex is exclusive.

lowIndex, highIndex, or both can be omitted to use the entire array on that side of the colon.

Slices wrap arrays to give a more general, powerful, and convenient interface to sequences of data. Except for items with explicit dimensions such as transformation matrices, most array programming in Go is done with slices rather than simple arrays.

Slices hold references to an underlying array, and if you assign one slice to another, both refer to the same array. If a function takes a slice argument, any changes it makes to the elements of the slice will be visible to the caller, analogous to passing a pointer (we’ll cover pointers later) to the underlying array. A Read function can therefore accept a slice argument rather than a pointer and a count; the length within the slice sets an upper limit of how much data to read. Here is the signature of the Read() method of the File type in package os:

Referenced from Effective Go

func (f *File) Read(buf []byte) (n int, err error)

Make

Most of the time we don’t need to think about the underlying array of a slice. We can create a new slice using the make function:

// func make([]T, len, cap) []T
mySlice := make([]int, 5, 10)

// the capacity argument is usually omitted and defaults to the length
mySlice := make([]int, 5)

Slices created with make will be filled with the zero value of the type.

If we want to create a slice with a specific set of values, we can use a slice literal:

mySlice := []string{"I", "love", "go"}

Notice the square brackets do not have a 3 in them. If they did, you’d have an array instead of a slice.

Length

The length of a slice is simply the number of elements it contains. It is accessed using the built-in len() function:

mySlice := []string{"I", "love", "go"}
fmt.Println(len(mySlice)) // 3

Capacity

The capacity of a slice is the number of elements in the underlying array, counting from the first element in the slice. It is accessed using the built-in cap() function:

mySlice := []string{"I", "love", "go"}
fmt.Println(cap(mySlice)) // 3

Generally speaking, unless you’re hyper-optimizing the memory usage of your program, you don’t need to worry about the capacity of a slice because it will automatically grow as needed.

Len and Cap Review

The length of a slice may be changed as long as it still fits within the limits of the underlying array; just assign it to a slice of itself. The capacity of a slice, accessible by the built-in function cap, reports the maximum length the slice may assume. Here is a function to append data to a slice. If the data exceeds the capacity, the slice is reallocated. The resulting slice is returned. The function uses the fact that len and cap are legal when applied to the nil slice, and return 0.

Referenced from Effective Go

func Append(slice, data []byte) []byte {
    l := len(slice)
    if l + len(data) > cap(slice) {  // reallocate
        // Allocate double what's needed, for future growth.
        newSlice := make([]byte, (l+len(data))*2)
        // The copy function is predeclared and works for any slice type.
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[0:l+len(data)]
    copy(slice[l:], data)
    return slice
}

Variadic

Many functions, especially those in the standard library, can take an arbitrary number of final arguments. This is accomplished by using the “…” syntax in the function signature.

A variadic function receives the variadic arguments as a slice.

func concat(strs ...string) string {
    final := ""
    // strs is just a slice of strings
    for i := 0; i < len(strs); i++ {
        final += strs[i]
    }
    return final
}

func main() {
    final := concat("Hello ", "there ", "friend!")
    fmt.Println(final)
    // Output: Hello there friend!
}

The familiar fmt.Println() and fmt.Sprintf() are variadic! fmt.Println() prints each element with space delimiters and a newline at the end.

func Println(a ...interface{}) (n int, err error)

Spread Operator

The spread operator allows us to pass a slice into a variadic function. The spread operator consists of three dots following the slice in the function call.

func printStrings(strings ...string) {
	for i := 0; i < len(strings); i++ {
		fmt.Println(strings[i])
	}
}

func main() {
    names := []string{"bob", "sue", "alice"}
    printStrings(names...)
}

Append

The built-in append function is used to dynamically add elements to a slice:

func append(slice []Type, elems ...Type) []Type

If the underlying array is not large enough, append() will create a new underlying array and point the returned slice to it.

Notice that append() is variadic, the following are all valid:

slice = append(slice, oneThing)
slice = append(slice, firstThing, secondThing)
slice = append(slice, anotherSlice...)

Slice of Slices

Slices can hold other slices, effectively creating a matrix, or a 2D slice.

rows := [][]int{}

Tricky Slices

The append() function changes the underlying array of its parameter AND returns a new slice. This means that using append() on anything other than itself is usually a BAD idea.

// dont do this!
someSlice = append(otherSlice, element)

Take a look at these head-scratchers:

Example 1: Works As Expected

a := make([]int, 3)
fmt.Println("len of a:", len(a))
fmt.Println("cap of a:", cap(a))
// len of a: 3
// cap of a: 3

b := append(a, 4)
fmt.Println("appending 4 to b from a")
fmt.Println("b:", b)
fmt.Println("addr of b:", &b[0])
// appending 4 to b from a
// b: [0 0 0 4]
// addr of b: 0x44a0c0

c := append(a, 5)
fmt.Println("appending 5 to c from a")
fmt.Println("addr of c:", &c[0])
fmt.Println("a:", a)
fmt.Println("b:", b)
fmt.Println("c:", c)
// appending 5 to c from a
// addr of c: 0x44a180
// a: [0 0 0]
// b: [0 0 0 4]
// c: [0 0 0 5]

With slices a, b, and c, 4 and 5 seem to be appended as we would expect. We can even check the memory addresses and confirm that b and c point to different underlying arrays.

Example 2: Something Fishy

i := make([]int, 3, 8)
fmt.Println("len of i:", len(i))
fmt.Println("cap of i:", cap(i))
// len of i: 3
// cap of i: 8

j := append(i, 4)
fmt.Println("appending 4 to j from i")
fmt.Println("j:", j)
fmt.Println("addr of j:", &j[0])
// appending 4 to j from i
// j: [0 0 0 4]
// addr of j: 0x454000

g := append(i, 5)
fmt.Println("appending 5 to g from i")
fmt.Println("addr of g:", &g[0])
fmt.Println("i:", i)
fmt.Println("j:", j)
fmt.Println("g:", g)
// appending 5 to g from i
// addr of g: 0x454000
// i: [0 0 0]
// j: [0 0 0 5]
// g: [0 0 0 5]

In this example, however, when 5 is appended to g it overwrites j’s fourth index because j and g point to the same underlying array. The append() function only creates a new array when there isn’t any capacity left. We created i with a length of 3 and a capacity of 8, which means we can append 5 items before a new array is automatically allocated.

Again, to avoid bugs like this, you should always use the append function on the same slice the result is assigned to:

mySlice := []int{1, 2, 3}
mySlice = append(mySlice, 4)

Range

Go provides syntactic sugar to iterate easily over elements of a slice:

for INDEX, ELEMENT := range SLICE {
}

Note: the element is a copy of the value at that index.

For example:

fruits := []string{"apple", "banana", "grape"}
for i, fruit := range fruits {
    fmt.Println(i, fruit)
}
// 0 apple
// 1 banana
// 2 grape