Why Go Exists

Updated

September 13, 2026

Why Go Exists

Go exists because a small group of people at Google were tired of waiting on compiles, tired of languages that rewarded cleverness, and tired of concurrency that looked like an accident. The boring default in this book is the same default Go was built for: code a stranger can read on Monday morning.

Mental model

Go is a compiled, statically typed language with garbage collection, a tiny feature set, and a toolchain that is part of the language. You trade inheritance, exceptions, and operator overloading for:

  • fast builds
  • one obvious way to layout a package
  • goroutines that are cheap enough to use as a unit of work
  • errors you can see in the function signature

That trade is the point. If you keep reaching for magic, you will dislike Go. If you like software that stays boring as it grows, you will like it.

Worked examples

Case 1: A complete program, no ceremony

Save as hello.go. This is the smallest useful Go program: a package main, a main function, and a print.

// hello.go
package main

import "fmt"

func main() {
    fmt.Println("desk is open")
}

Run:

go run hello.go

Output:

desk is open

go run compiles a temporary binary and executes it. There is no separate “interpreter mode.” What you run is what you ship, minus the install path.

Case 2: Explicit control flow, explicit failure

Other languages hide failure in exceptions. Go puts it in the return value. The next program takes a list of table numbers and “opens” each one. If a number is nonsense, the function returns an error and the caller prints it. Nothing unwinds the stack in secret.

// open_tables.go
package main

import (
    "fmt"
    "os"
)

func openTable(n int) error {
    if n <= 0 {
        return fmt.Errorf("table %d: number must be positive", n)
    }
    fmt.Printf("opened table %d\n", n)
    return nil
}

func main() {
    tables := []int{3, 0, 11}
    for _, n := range tables {
        if err := openTable(n); err != nil {
            fmt.Fprintln(os.Stderr, err)
            os.Exit(1)
        }
    }
}

Run:

go run open_tables.go

Output (stdout then the process exits 1):

opened table 3
table 0: number must be positive

The if err != nil line is repetitive on purpose. You see every failure path when you read the function.

Case 3: Concurrency without a thread pool class

A goroutine is a function the runtime can run alongside others. Channels pass values between them. This program starts two workers that send a line each, then the main goroutine prints what it receives.

// two_shifts.go
package main

import "fmt"

func shift(name string, out chan<- string) {
    out <- name + " clocked in"
}

func main() {
    ch := make(chan string, 2)
    go shift("Amina", ch)
    go shift("Bo", ch)

    fmt.Println(<-ch)
    fmt.Println(<-ch)
}

Run:

go run two_shifts.go

Possible output (order of the two lines is not guaranteed):

Amina clocked in
Bo clocked in

or:

Bo clocked in
Amina clocked in

You did not import a thread library. go f() is the whole launch API. Later chapters will slow this down and make the order deterministic. For now, notice how little machinery there is.

Case 4: What Go refuses to do

This program is valid Go. The version that overloads + for tickets, subclasses Worker, or catches a TableException is not. The language is small so the team shares one dialect.

// no_magic.go
package main

import "fmt"

type Ticket struct {
    ID    int
    Table int
}

func (t Ticket) Label() string {
    return fmt.Sprintf("ticket %d → table %d", t.ID, t.Table)
}

func main() {
    t := Ticket{ID: 7, Table: 12}
    fmt.Println(t.Label())
}

Run:

go run no_magic.go

Output:

ticket 7 → table 12

Methods exist. Classes, inheritance, and default arguments do not. Composition (a struct with fields, functions that take those structs) is the whole design story.

The trap

Coming from a language that prizes abstraction, it is tempting to wrap every fmt.Println in a framework on day one. That is how a 40-line desk tool becomes a 12-package “platform.”

This program does too much for its size: an interface, a constructor, and a type that only exists to print a string.

// too_clever.go
package main

import "fmt"

type Printer interface {
    Print(string)
}

type stdoutPrinter struct{}

func NewPrinter() Printer { return stdoutPrinter{} }

func (stdoutPrinter) Print(s string) { fmt.Println(s) }

func main() {
    NewPrinter().Print("desk is open")
}

Run:

go run too_clever.go

Output:

desk is open

It works. It is also a waste. The boring version is Case 1. Introduce an interface when you have two real implementations or a test double you cannot avoid — not when you have a slogan about “SOLID.”

The boring rule

  • Prefer a function and a struct over a hierarchy.
  • Return error. Do not panic for the caller’s mistake (see the errors part).
  • Use a goroutine when you have concurrent work, not because it looks modern.
  • Keep packages few. Split them when names collide or when a boundary is real.
  • If a feature is not in the language, that is usually a hint, not a gap to fill with a library.

Try this

  1. Change hello.go so it prints the number of tables (an int) next to the message. Use fmt.Printf.
  2. In open_tables.go, keep going after a bad table instead of calling os.Exit. Print the error and open the rest.
  3. In two_shifts.go, start a third worker. Receive three times. Confirm the program still exits (it will hang if you forget a receive).