Tutorial

General Principles

Running & Compiling Programs

Packages

Functions

Formatting & Best Practices

Variables & Assignment

Loops & Control Flow

Maps

Input & Output

Concurrency

Error Handling

Functions & Types

Pointers

Standard Library & Documentation


Program Structure

Reserved Keywords and Naming Conventions

Function Execution

Variable Declaration

Each variable declaration follows the syntax:

var name type = expression

Initialization Rules

Alternative Initialization Example

var f, err = os.Open(name) // os.Open returns a file and an error

Short Variable Declarations

i := 100                  // an int
var boiling float64 = 100 // a float64
i, j := 0, 1

Behavior of Short Variable Declarations

Pointers in Go

Flag Parsing

Variable Lifetime and Scope

Type Declarations

type name underlying-type

Package Initialization

Scope vs. Lifetime

Multiple Declarations


Basic Data Types

Types in Go

Go’s types fall into four categories:

  1. Basic Types
  2. Aggregate Types
  3. Reference Types
  4. Interface Types

Basic Types

Basic types include:

Aggregate Types

Aggregate types are:

Reference Types

Reference types are a diverse group, which includes:

What they have in common is that they all refer to program variables or state indirectly. This means that an operation applied to one reference affects all copies of that reference.

Special Type Aliases

Data Types in Go

Complex Numbers

Two complex numbers are equal if their real parts and imaginary parts are both equal.


Data Types in Go

Arrays

q := [...]int{1, 2, 3}
fmt.Printf("%T\n", q) // "[3]int"

Slices

make([]T, len)
make([]T, len, cap) // same as make([]T, cap)[:len]

Maps

Structs

type Employee struct {
    ID            int
    Name, Address string
    DoB           time.Time
    Position      string
    Salary        int
    ManagerID     int
}

JSON Handling in Go

Example:

Year  int  `json:"released"`
Color bool `json:"color,omitempty"`

Unmarshaling JSON

By understanding these core concepts, you can effectively work with Go’s data structures and JSON handling.


Functions

A function lets us wrap up a sequence of statements as a unit that can be called from elsewhere in a program, perhaps multiple times.

Function Declaration

func name(parameter-list) (result-list) {
    body
}

Error Handling

An error may be nil or not nil.

When a function call returns an error, it’s the caller’s responsibility to check it and take appropriate action.

Because error messages are frequently chained together, message strings should not be capitalized and newlines should be avoided. When designing error messages:

Approaches for Error Handling

  1. Check for err ≠ nil and log it.
  2. Retry (e.g., for HTTP requests).
  3. Exit the program.

Error handling in Go follows a particular rhythm. After checking an error, failure is usually dealt with before success. If failure causes the function to return, the logic for success follows at the outer level instead of being indented within an else block. Functions tend to have a common structure, with:

Function Literals and Anonymous Functions

Named functions can be declared only at the package level, but we can use a function literal to denote a function value within any expression. A function literal is written like a function declaration but without a name following the func keyword. It is an expression, and its value is called an anonymous function.

Variadic Functions

A variadic function is one that can be called with varying numbers of arguments. The most familiar examples are fmt.Printf and its variants. Printf requires one fixed argument at the beginning, then accepts any number of subsequent arguments.

Example:

func sum(vals ...int) int {
    total := 0
    for _, val := range vals {
        total += val
    }
    return total
}

Defer

The defer function and argument expressions are evaluated when the statement is executed, but the actual call is deferred until the function that contains the defer statement has finished—whether normally (by executing a return statement or falling off the end) or abnormally (by panicking). Any number of calls may be deferred; they are executed in the reverse order in which they were deferred.

A defer statement is often used with paired operations like:

This ensures resources are released in all cases, no matter how complex the control flow. The right place for a defer statement that releases a resource is immediately after the resource has been successfully acquired.

Panic and Recovery

Go’s type system catches many mistakes at compile time, but others, like an out-of-bounds array access or nil pointer dereference, require runtime checks. When the Go runtime detects these mistakes, it panics.

Behavior of a Panic

This log message includes:

This log often has enough information to diagnose the root cause of the problem without running the program again, so it should always be included in a bug report about a panicking program.

Not all panics come from the runtime. The built-in panic function may be called directly, accepting any value as an argument. A panic is often the best thing to do when some “impossible” situation occurs, such as execution reaching a case that logically can’t happen.

Avoiding Panics

Recovery Considerations

Recovering indiscriminately from panics is a dubious practice because the state of a package’s variables after a panic is rarely well defined or documented. Possible issues include:

Furthermore, replacing a crash with a log entry may cause bugs to go unnoticed.


Methods

Objects and Methods

An object is simply a value or a variable that has methods, and a method is a function associated with a particular type. An object-oriented program is one that uses methods to express the properties and operations of each data structure so that clients need not access the object’s representation directly.

Calling a method is akin to sending a message to an object. The extra parameter p is called the method’s receiver. Since the receiver name will be frequently used, it’s a good idea to choose something short and to be consistent across methods. A common choice is the first letter of the type name, like p for Point.

// Same thing, but as a method of the Point type
func (p Point) Distance(q Point) float64 {
    return math.Hypot(q.X-p.X, q.Y-p.Y)
}

Pointer Receivers

Because calling a function makes a copy of each argument value, if a function needs to update a variable, or if an argument is so large that we wish to avoid copying it, we must pass the address of the variable using a pointer. The same goes for methods that need to update the receiver variable: we attach them to the pointer type, such as *Point.

func (p *Point) ScaleBy(factor float64) {
    p.X *= factor
    p.Y *= factor
}

In a realistic program, convention dictates that if any method of Point has a pointer receiver, then all methods of Point should have a pointer receiver, even ones that don’t strictly need it.

Furthermore, to avoid ambiguities, method declarations are not permitted on named types that are themselves pointer types:

type P *int
func (P) f() { /* ... */ } // compile error: invalid receiver type

If the receiver p is a variable of type Point but the method requires a *Point receiver, we can use this shorthand:

p.ScaleBy(2)

Struct Embedding

import "image/color"

type Point struct{ X, Y float64 }

type ColoredPoint struct {
    Point
    Color color.RGBA
}

Encapsulation

A variable or method of an object is said to be encapsulated if it is inaccessible to clients of the object. Encapsulation, sometimes called information hiding, is a key aspect of object-oriented programming.

Go has only one mechanism to control the visibility of names: capitalized identifiers are exported from the package in which they are defined, and uncapitalized names are not. The same mechanism that limits access to members of a package also limits access to the fields of a struct or the methods of a type. As a consequence, to encapsulate an object, we must make it a struct.

Benefits of Encapsulation

  1. Because clients cannot directly modify the object’s variables, one needs to inspect fewer statements to understand the possible values of those variables.
  2. Hiding implementation details prevents clients from depending on things that might change, which gives the designer greater freedom to evolve the implementation without breaking API compatibility.
  3. The third benefit of encapsulation, and in many cases the most important, is that it prevents clients from setting an object’s variables arbitrarily.

Getters and Setters

Functions that merely access or modify internal values of a type, such as the methods of the Logger type from the log package, are called getters and setters. However, when naming a getter method, we usually omit the Get prefix.


Understanding Interface Types

Interface types express generalizations or abstractions about the behaviors of other types. By generalizing, interfaces allow us to write functions that are more flexible and adaptable because they are not tied to the details of a single implementation.

Many object-oriented languages have some notion of interfaces, but what makes Go’s interfaces distinctive is that they are satisfied implicitly.

Nature of Interfaces

An interface is an abstract type. It does not expose the representation or internal structure of its values, nor does it specify the set of basic operations they support. Instead, it only reveals some of their methods. When you have a value of an interface type, you know nothing about what it is; you only know what it can do, or more precisely, what behaviors are provided by its methods.

This freedom to substitute one type for another that satisfies the same interface is called substitutability, a hallmark of object-oriented programming.

Defining Interface Types

An interface type specifies a set of methods that a concrete type must possess to be considered an instance of that interface. The order in which the methods appear in an interface is immaterial; all that matters is the set of methods.

Type Assertions

A type assertion is an operation applied to an interface value. Syntactically, it looks like x.(T), where x is an expression of an interface type and T is a type, called the “asserted” type. A type assertion checks whether the dynamic type of its operand matches the asserted type.

When to Use Interfaces

Interfaces are only needed when there are two or more concrete types that must be dealt with in a uniform way.


Goroutines and Channels

Go enables two styles of concurrent programming. This chapter presents goroutines and channels, which support Communicating Sequential Processes (CSP), a model of concurrency where values are passed between independent activities (goroutines), while variables are largely confined to a single activity.

Goroutines

In Go, each concurrently executing activity is called a goroutine.

When a program starts, its only goroutine is the one that calls the main function, referred to as the main goroutine. New goroutines are created using the go statement, which precedes an ordinary function or method call. A go statement causes the function to execute in a newly created goroutine, and the statement itself completes immediately.

f()    // call f(); wait for it to return
go f() // create a new goroutine that calls f(); don't wait

Channels

If goroutines are the activities of a concurrent Go program, channels are the connections between them. A channel is a communication mechanism that allows one goroutine to send values to another. Each channel is associated with a specific element type.

ch := make(chan int) // ch has type 'chan int'

Channel Operations

A channel has two primary operations: send and receive, collectively known as communications.

ch <- x  // send statement
x = <-ch // receive expression in an assignment
<-ch     // receive statement; result is discarded

Closing a Channel

Channels support a third operation, close, which indicates that no more values will be sent. Any further send operations on a closed channel will panic. Receive operations on a closed channel return the remaining sent values; once exhausted, they return the zero value of the channel’s element type.

close(ch)

Buffered vs Unbuffered Channels

A channel created with make is an unbuffered channel unless a second argument (capacity) is provided, in which case it becomes a buffered channel.

ch = make(chan int)    // unbuffered channel
ch = make(chan int, 0) // unbuffered channel
ch = make(chan int, 3) // buffered channel with capacity 3

Behavior Differences

Synchronization

Unbuffered channels synchronize the sending and receiving goroutines, so they are sometimes called synchronous channels.

Example: Closing a Channel in a Pipeline

func main() {
    naturals := make(chan int)
    squares := make(chan int)

    // Counter
    go func() {
        for x := 0; x < 100; x++ {
            naturals <- x
        }
        close(naturals)
    }()

    // Squarer
    go func() {
        for x := range naturals {
            squares <- x * x
        }
        close(squares)
    }()

    // Printer (in main goroutine)
    for x := range squares {
        fmt.Println(x)
    }
}

Send-Only and Receive-Only Channels

Since close asserts that no more sends will occur, only the sending goroutine can call it. Attempting to close a receive-only channel results in a compile-time error.

Buffered Channel Queue

A buffered channel maintains a queue of elements, with its maximum size defined at creation.

ch = make(chan string, 3) // Buffered channel of capacity 3

Checking Channel Capacity and Length

fmt.Println(cap(ch)) // Output: 3 (buffer capacity)
fmt.Println(len(ch)) // Output: 2 (number of buffered elements)

Avoiding Leaked Goroutines

Leaked goroutines are not automatically collected, so it’s crucial to ensure that goroutines terminate when no longer needed.

Choosing Between Unbuffered and Buffered Channels


Definition of Concurrency Safety

A function that works correctly in a sequential program is considered concurrency-safe if it continues to function correctly even when called concurrently—meaning from two or more goroutines—without requiring additional synchronization.

This notion can be extended to a set of collaborating functions, such as the methods and operations of a particular type. A type is concurrency-safe if all its accessible methods and operations are concurrency-safe.

Concurrency Safety in Programs

A program can be made concurrency-safe without requiring every concrete type within it to be concurrency-safe. In fact, concurrency-safe types are the exception rather than the rule. A variable should be accessed concurrently only if the documentation for its type explicitly states that it is safe to do so.

To avoid concurrent access to most variables, we use one of two approaches:

  1. Confinement: Restricting a variable’s use to a single goroutine.
  2. Mutual Exclusion: Maintaining a higher-level invariant that prevents simultaneous access by multiple goroutines.

Concurrency Safety in Exported Package-Level Functions

Exported package-level functions are generally expected to be concurrency-safe. Since package-level variables cannot be confined to a single goroutine, functions that modify them must enforce mutual exclusion to ensure safe concurrent access.


Compilation in Go

When we change a file, we must recompile the file’s package and potentially all the packages that depend on it. Go compilation is notably faster than most other compiled languages, even when building from scratch. There are three main reasons for the compiler’s speed:

  1. Explicit Imports: All imports must be explicitly listed at the beginning of each source file, so the compiler does not have to read and process an entire file to determine its dependencies.
  2. Dependency Graph: The dependencies of a package form a directed acyclic graph. Because there are no cycles, packages can be compiled separately and potentially in parallel.
  3. Efficient Object Files: The object file for a compiled Go package records export information not just for the package itself, but for its dependencies too. When compiling a package, the compiler must read one object file for each import but does not need to look beyond these files.

Import Paths

For packages you intend to share or publish, import paths should be globally unique. To avoid conflicts, the import paths of all packages, other than those from the standard library, should start with the Internet domain name of the organization that owns or hosts the package. This also makes it easier to locate packages.

Package Declaration

A package declaration is required at the start of every Go source file. Its main purpose is to determine the default identifier for that package (called the package name) when it is imported by another package.

Package Naming Conventions

  1. A package defining a command (an executable Go program) must always have the name main, regardless of the package’s import path. This signals to go build that it must invoke the linker to create an executable file.
  2. Some files in the directory may have the suffix _test on their package name if the file name ends with _test.go. Such a directory may define two packages: the usual one and another one called an external test package.

Suppressing Unused Import Errors

To suppress the “unused import” error, we must use a renaming import where the alternative name is _, the blank identifier. As usual, the blank identifier can never be referenced.

GOPATH Structure

GOPATH has three subdirectories:

  1. src Directory: Holds source code. Each package resides in a directory whose name relative to $GOPATH/src is the package’s import path, such as gopl.io/ch1/helloworld. A single GOPATH workspace can contain multiple version-control repositories beneath src, such as gopl.io or golang.org.
  2. pkg Directory: Stores compiled packages.
  3. bin Directory: Holds executable programs like helloworld.

Testing

Special Functions in _test.go Files

Within *_test.go files, three kinds of functions are treated specially: tests, benchmarks, and examples.

Testing Philosophy

Black-box testing > White-box testing

Go’s approach to testing is unique. It expects test authors to handle most of the work themselves, defining functions to avoid repetition, just as they would for ordinary programs. Testing is not a mere form-filling exercise; it has a user interface too, where the users are its maintainers.

A good test:

Buggy vs. Brittle Tests

Just as a buggy program frustrates its users, brittle tests exasperate maintainers. The most brittle tests, which fail for almost any change (good or bad), are called change detector or status quo tests. These consume more time than they save.

How to avoid brittle tests:

“Testing shows the presence, not the absence, of bugs.”

Benchmark Functions

Example Benchmark Function

import "testing"

func BenchmarkIsPalindrome(b *testing.B) {
    for i := 0; i < b.N; i++ {
        IsPalindrome("A man, a plan, a canal: Panama")
    }
}

Running Benchmarks

go test -bench=.

Premature optimization is the root of all evil.

Programmers often waste time optimizing noncritical parts of their programs. This not only reduces maintainability but also increases debugging time. We should ignore minor efficiencies 97% of the time and focus on writing clear, maintainable code instead.

Special Functions in go test

  1. Benchmark
  2. Coverage
  3. Example

Example Function

func ExampleIsPalindrome() {
    fmt.Println(IsPalindrome("A man, a plan, a canal: Panama"))
    fmt.Println(IsPalindrome("palindrome"))
    // Output:
    // true
    // false
}

Benefits of Example Functions

  1. Documentation - Examples serve as self-explanatory documentation.
  2. Executable Tests - Examples are automatically run by go test.
  3. Hands-on Experimentation - Examples can be edited and tested interactively in the Go Playground.