Why Go

Go was created at Google in 2007 by Robert Griesemer, Rob Pike, and Ken Thompson. Thompson co-created Unix and C. Pike co-created UTF-8 and Plan 9. They designed Go because they were frustrated. C++ compilation at Google took 45 minutes. The language had become so complex that even experts disagreed about what code meant. The build system was a separate problem from the language. Concurrency was an afterthought bolted onto a sequential model.

Go is their answer: a language that compiles in seconds, has a tiny specification, handles concurrency as a first-class feature, and deliberately omits most of what other languages consider essential. No inheritance. No exceptions. No operator overloading. No implicit conversions. No macros. No header files. One way to write a loop.

The result feels like C with garbage collection, goroutines, and a proper string type. If you know C, you can read Go immediately. If you know any language at all, you can learn Go in a weekend. That is not an accident. It is the entire design philosophy.

“Simplicity is complicated.”

— Rob Pike

I. Hello, World

package main import "fmt" func main() { fmt.Println("Hello, World") }

Every Go program starts with a package declaration. The main package is the entry point. import pulls in packages. fmt is the formatted I/O package (like C’s stdio). func main() is where execution begins. No semicolons needed (the lexer inserts them). Braces are required, but the opening brace must be on the same line as the statement.

Run it:

$ go run main.go Hello, World

Build a binary:

$ go build -o hello main.go $ ./hello Hello, World

The binary is statically linked. It has no dependencies. You can copy it to any machine with the same OS and architecture and it will run. No runtime to install, no shared libraries to manage. This is one of Go’s most underappreciated features for deployment.

II. Variables and Types

Go is statically typed. Types go after names (the opposite of C), which reads more naturally in complex declarations.

var x int = 42 // explicit type var y = 42 // type inferred (int) z := 42 // short declaration (inside functions only) var name string = "Go" var pi float64 = 3.14159 var active bool = true

The := shorthand is idiomatic for local variables. Outside functions, you must use var.

Zero Values

Every type has a zero value. Uninitialized variables are not undefined—they are zero-valued. This eliminates an entire class of bugs.

TypeZero Value
int, float640
string"" (empty string)
boolfalse
pointer, slice, map, channel, function, interfacenil

Basic Types

// Integers: sized and unsized var a int // platform-dependent (64-bit on 64-bit systems) var b int8 // -128 to 127 var c int16 var d int32 var e int64 var f uint8 // unsigned: 0 to 255 (alias: byte) // Floats var g float32 var h float64 // default for float literals // Other var i byte // alias for uint8 var j rune // alias for int32 (a Unicode code point)

There are no implicit numeric conversions. You must explicitly cast: float64(x), int(y). This catches type errors at compile time that C would silently allow.

Constants

const Pi = 3.14159265358979 const MaxRetries = 3 // iota: auto-incrementing constant generator const ( Sunday = iota // 0 Monday // 1 Tuesday // 2 Wednesday // 3 Thursday // 4 Friday // 5 Saturday // 6 ) // iota with expressions (bit flags) const ( Read = 1 << iota // 1 Write // 2 Execute // 4 )

iota is Go’s replacement for C enums. It starts at 0 and increments within a const block. You can apply expressions to it, and the expression is repeated for each subsequent constant.

III. Control Flow

Go has one loop construct: for. No while, no do-while. The for keyword does everything.

// Classic C-style for loop for i := 0; i < 10; i++ { fmt.Println(i) } // While loop (omit init and post) for x > 0 { x-- } // Infinite loop (omit everything) for { // runs forever until break or return } // Range over a slice nums := []int{10, 20, 30} for i, v := range nums { fmt.Printf("index %d, value %d\n", i, v) } // Range over a string (iterates runes, not bytes) for i, ch := range "Hello, 世界" { fmt.Printf("%d: %c\n", i, ch) }

If statements can include a short statement before the condition:

if err := doSomething(); err != nil { // handle error // err is scoped to this if/else block } // Switch (no fall-through by default, no break needed) switch day { case "Monday", "Tuesday", "Wednesday", "Thursday", "Friday": fmt.Println("Weekday") case "Saturday", "Sunday": fmt.Println("Weekend") default: fmt.Println("Unknown") } // Type switch switch v := x.(type) { case int: fmt.Println("int:", v) case string: fmt.Println("string:", v) }

Go’s switch does not fall through by default (the opposite of C). If you want fall-through, use the fallthrough keyword explicitly. This eliminates one of the most common C bugs.

IV. Functions

// Basic function func add(a int, b int) int { return a + b } // Shorthand: consecutive params of the same type func add(a, b int) int { return a + b }

Multiple Return Values

This is one of Go’s most distinctive features. Functions can return multiple values, and this is the idiomatic way to handle errors.

func divide(a, b float64) (float64, error) { if b == 0 { return 0, fmt.Errorf("division by zero") } return a / b, nil } // Calling it: result, err := divide(10, 3) if err != nil { fmt.Println("Error:", err) return } fmt.Println(result) // 3.3333...

The pattern result, err := f() followed by if err != nil is the most common pattern in Go. You will write it hundreds of times. Some people find this verbose. The Go team considers it a virtue: error handling is explicit, visible, and impossible to accidentally ignore (the compiler rejects unused variables).

Named Return Values

func split(sum int) (x, y int) { x = sum * 4 / 9 y = sum - x return // "naked" return: returns x and y }

Functions as Values

// Functions are first-class values func apply(f func(int) int, x int) int { return f(x) } double := func(n int) int { return n * 2 } fmt.Println(apply(double, 5)) // 10 // Closures func counter() func() int { n := 0 return func() int { n++ return n } } c := counter() fmt.Println(c()) // 1 fmt.Println(c()) // 2 fmt.Println(c()) // 3

Defer

func readFile(path string) (string, error) { f, err := os.Open(path) if err != nil { return "", err } defer f.Close() // runs when the function returns, no matter what data, err := io.ReadAll(f) if err != nil { return "", err } return string(data), nil }

defer schedules a function call to run when the surrounding function returns. It is Go’s replacement for RAII, try/finally, and destructors. You open a resource, immediately defer its cleanup, and never worry about forgetting to close it. Deferred calls execute in LIFO (last-in, first-out) order.

V. Structs and Methods

Go has no classes. It has structs (data) and methods (functions attached to types). This is closer to C than to Java or Python.

type Point struct { X, Y float64 } // Method with a value receiver func (p Point) Distance() float64 { return math.Sqrt(p.X*p.X + p.Y*p.Y) } // Method with a pointer receiver (can modify the struct) func (p *Point) Scale(factor float64) { p.X *= factor p.Y *= factor } // Usage p := Point{3, 4} fmt.Println(p.Distance()) // 5 p.Scale(2) fmt.Println(p) // {6 8}

The (p Point) before the function name is the receiver. It is syntactic sugar: p.Distance() is equivalent to Distance(p). Methods are just functions. There is no this or self; the receiver is an explicit, named parameter.

Use a pointer receiver (*Point) when the method needs to modify the struct or when the struct is large (to avoid copying). Use a value receiver (Point) when the method only reads.

Struct Embedding (Composition, Not Inheritance)

type Animal struct { Name string } func (a Animal) Speak() string { return a.Name + " makes a sound" } type Dog struct { Animal // embedded struct (no field name) Breed string } d := Dog{ Animal: Animal{Name: "Rex"}, Breed: "German Shepherd", } fmt.Println(d.Name) // "Rex" — promoted from Animal fmt.Println(d.Speak()) // "Rex makes a sound" — promoted method

Embedding promotes the inner struct’s fields and methods to the outer struct. It looks like inheritance but is composition. Dog is not a subtype of Animal. It contains an Animal. You can access promoted fields directly (d.Name) or through the embedded field (d.Animal.Name). There is no method resolution order, no virtual dispatch, no diamond problem.

VI. Interfaces

Interfaces in Go are satisfied implicitly. There is no implements keyword. If a type has the methods an interface requires, it satisfies the interface. This is structural typing, not nominal typing, and it is one of Go’s best design decisions.

type Shape interface { Area() float64 Perimeter() float64 } 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 } type Rectangle struct { Width, Height float64 } func (r Rectangle) Area() float64 { return r.Width * r.Height } func (r Rectangle) Perimeter() float64 { return 2*(r.Width + r.Height) } // Both Circle and Rectangle satisfy Shape — no declaration needed func printShape(s Shape) { fmt.Printf("Area: %.2f, Perimeter: %.2f\n", s.Area(), s.Perimeter()) } printShape(Circle{Radius: 5}) printShape(Rectangle{Width: 3, Height: 4})

The key insight: the Circle and Rectangle types know nothing about the Shape interface. They do not import it. They do not declare that they implement it. They just happen to have the right methods. The interface is satisfied automatically. This decouples the interface definition from the implementation and enables powerful patterns where interfaces are defined by consumers, not providers.

The Key Standard Interfaces

// The empty interface — satisfied by every type interface{} // or, since Go 1.18: any // The most important interfaces in the standard library: type Reader interface { Read(p []byte) (n int, err error) } type Writer interface { Write(p []byte) (n int, err error) } type Stringer interface { String() string } type error interface { Error() string }

io.Reader and io.Writer are the backbone of Go’s I/O system. Files, network connections, HTTP bodies, buffers, compressors, and encryptors all implement these interfaces. You can compose them freely: wrap a file in a buffered reader, wrap that in a gzip reader, wrap that in a JSON decoder. Each layer knows nothing about the others.

Go proverb: “The bigger the interface, the weaker the abstraction.”

Go interfaces are typically small: one or two methods. io.Reader has one method. error has one method. fmt.Stringer has one method. Small interfaces are easy to implement, easy to compose, and hard to get wrong. This is the opposite of Java, where interfaces often have a dozen methods and an AbstractFactoryBeanManager implementing them.

VII. Slices and Arrays

Arrays in Go have a fixed size and are values (they copy on assignment). You almost never use them directly. Instead, you use slices: dynamically-sized views into arrays.

// Array (fixed size, rarely used directly) var a [5]int // [0 0 0 0 0] // Slice (dynamic, this is what you use) s := []int{1, 2, 3, 4, 5} // Make a slice with length 5, capacity 10 s2 := make([]int, 5, 10) // Append (grows the slice if needed) s = append(s, 6, 7, 8) // Slicing (like Python, but no negative indices) first3 := s[:3] // [1 2 3] last3 := s[5:] // [6 7 8] middle := s[2:5] // [3 4 5] // Length and capacity fmt.Println(len(s), cap(s)) // Iterate for i, v := range s { fmt.Printf("s[%d] = %d\n", i, v) }

A slice is a three-word struct under the hood: a pointer to the underlying array, a length, and a capacity. Slicing does not copy data; it creates a new header pointing into the same array. This is efficient but means that modifying a slice element affects all slices sharing that array. If you need an independent copy, use copy() or the slices.Clone() function.

VIII. Maps

Maps are Go’s built-in hash table. They are the equivalent of Python’s dict, Java’s HashMap, or C++’s unordered_map.

// Create a map m := make(map[string]int) // Or with a literal ages := map[string]int{ "Alice": 30, "Bob": 25, "Carol": 35, } // Insert or update ages["Dave"] = 28 // Lookup age := ages["Alice"] // 30 // Lookup with existence check (the "comma ok" idiom) age, ok := ages["Eve"] if !ok { fmt.Println("Eve not found") } // Delete delete(ages, "Bob") // Iterate (order is randomized by design) for name, age := range ages { fmt.Printf("%s is %d\n", name, age) } // Length fmt.Println(len(ages))

Key points about maps:

  • Maps are reference types. Passing a map to a function does not copy it.
  • The zero value of a map is nil. Reading from a nil map returns the zero value. Writing to a nil map panics. Always initialize with make or a literal.
  • Looking up a missing key returns the zero value for the value type (0, "", false, nil). Use the two-value form (v, ok := m[k]) to distinguish “key exists with zero value” from “key missing.”
  • Map iteration order is deliberately randomized. Do not rely on it. If you need sorted output, extract the keys into a slice, sort it, and iterate the slice.
  • Maps are not safe for concurrent use. If multiple goroutines read and write the same map, you need a mutex or sync.Map.

Maps as Sets

Go has no built-in set type. The idiomatic replacement is a map[T]bool or map[T]struct{}:

// Set of strings seen := make(map[string]bool) seen["apple"] = true seen["banana"] = true if seen["apple"] { fmt.Println("already seen apple") } // Memory-efficient version: struct{} takes zero bytes type void = struct{} seen2 := make(map[string]void) seen2["apple"] = void{} _, exists := seen2["apple"] // exists == true

Maps for Counting and Grouping

// Word frequency counter func wordCount(text string) map[string]int { counts := make(map[string]int) for _, word := range strings.Fields(text) { counts[word]++ // zero value of int is 0, so this just works } return counts } // Group by first letter func groupByFirstLetter(words []string) map[byte][]string { groups := make(map[byte][]string) for _, w := range words { key := w[0] groups[key] = append(groups[key], w) } return groups } // Adjacency list for a graph graph := map[string][]string{ "A": {"B", "C"}, "B": {"A", "D"}, "C": {"A"}, "D": {"B"}, }

Maps are the workhorse data structure in Go. Because the zero value of a slice is nil and append handles nil slices gracefully, you can build up map[K][]V structures (group-by, adjacency lists, inverted indices) without any initialization boilerplate.

IX. Error Handling

Go has no exceptions. Functions that can fail return an error as the last return value. The caller checks it explicitly.

// The error interface is built-in: // type error interface { Error() string } // Creating errors err1 := errors.New("something went wrong") err2 := fmt.Errorf("failed to open %s: %w", filename, err) // wrapping // Custom error types type NotFoundError struct { Name string } func (e *NotFoundError) Error() string { return e.Name + " not found" } // Checking error types var nfe *NotFoundError if errors.As(err, &nfe) { fmt.Println("Missing:", nfe.Name) } // Checking sentinel errors if errors.Is(err, os.ErrNotExist) { fmt.Println("File does not exist") }

The %w verb in fmt.Errorf wraps the original error, creating a chain. errors.Is walks the chain to check for a specific sentinel error. errors.As walks the chain to find an error of a specific type. This is Go’s approach to structured error handling: simple, explicit, composable.

For truly unrecoverable errors (out of memory, index out of bounds, nil pointer dereference), Go has panic and recover. These are not exceptions. They are for bugs, not expected errors. You should almost never use panic in library code.

X. Goroutines and Channels

This is Go’s signature feature. Goroutines are lightweight concurrent functions. Channels are typed conduits for communication between goroutines. Together, they implement Tony Hoare’s CSP (Communicating Sequential Processes) model of concurrency.

Goroutines

// Launch a goroutine: just put "go" before a function call go doSomething() // With an anonymous function go func() { fmt.Println("running in a goroutine") }() // A goroutine is NOT a thread. // It starts with ~8 KB of stack (grows as needed). // The Go runtime multiplexes thousands of goroutines // onto a small number of OS threads (M:N scheduling). // You can easily run 100,000 goroutines on a laptop.

A goroutine costs about 8 KB of memory. A thread costs about 1–8 MB. This means you can have tens of thousands of goroutines where you could have maybe a hundred threads. The Go runtime scheduler handles the mapping to OS threads, including preemption (since Go 1.14), work stealing, and blocking I/O integration.

Channels

// Create a channel ch := make(chan int) // unbuffered bch := make(chan int, 10) // buffered (capacity 10) // Send a value ch <- 42 // Receive a value v := <-ch // Close a channel (signals "no more values") close(ch) // Range over a channel (loops until closed) for v := range ch { fmt.Println(v) }

An unbuffered channel blocks the sender until a receiver is ready, and vice versa. This provides synchronization: the two goroutines rendezvous at the channel operation. A buffered channel blocks only when the buffer is full (send) or empty (receive). Choose unbuffered when you want synchronization, buffered when you want to decouple producer and consumer speeds.

Example: Concurrent Web Fetcher

func fetchAll(urls []string) []string { results := make(chan string, len(urls)) for _, url := range urls { go func(u string) { resp, err := http.Get(u) if err != nil { results <- fmt.Sprintf("%s: error: %v", u, err) return } defer resp.Body.Close() results <- fmt.Sprintf("%s: %s", u, resp.Status) }(url) } var out []string for range urls { out = append(out, <-results) } return out }

This fetches all URLs concurrently. Each goroutine sends its result on the channel. The main function collects all results. The URLs are fetched in parallel, but the results are gathered sequentially. No threads, no thread pools, no callbacks, no async/await. Just go and channels.

Example: Pipeline Pattern

// Generate numbers func generate(nums ...int) <-chan int { out := make(chan int) go func() { for _, n := range nums { out <- n } close(out) }() return out } // Square each number func square(in <-chan int) <-chan int { out := make(chan int) go func() { for n := range in { out <- n * n } close(out) }() return out } // Use: generate → square → print for v := range square(generate(2, 3, 4)) { fmt.Println(v) // 4, 9, 16 }

Each stage runs in its own goroutine. Data flows through channels. Stages are composable. You can add filtering, fan-out, fan-in, and rate limiting by inserting new stages into the pipeline. This is the CSP model in practice.

Select

// select is like switch, but for channel operations select { case msg := <-ch1: fmt.Println("received from ch1:", msg) case msg := <-ch2: fmt.Println("received from ch2:", msg) case ch3 <- 42: fmt.Println("sent 42 to ch3") case <-time.After(5 * time.Second): fmt.Println("timed out") default: fmt.Println("no channel ready") }

select blocks until one of its cases can proceed, then executes that case. If multiple are ready, it picks one at random. With a default case, it never blocks (non-blocking I/O). select is how you multiplex channels, implement timeouts, and build cancellation into concurrent code.

Synchronization: sync.WaitGroup and sync.Mutex

// WaitGroup: wait for a collection of goroutines to finish var wg sync.WaitGroup for i := 0; i < 5; i++ { wg.Add(1) go func(id int) { defer wg.Done() fmt.Printf("Worker %d done\n", id) }(i) } wg.Wait() // blocks until all 5 goroutines call Done() // Mutex: protect shared state var ( mu sync.Mutex balance int ) func deposit(amount int) { mu.Lock() balance += amount mu.Unlock() } // Or more idiomatically with defer: func deposit(amount int) { mu.Lock() defer mu.Unlock() balance += amount }

“Don’t communicate by sharing memory; share memory by communicating.”

— Go Proverb

This is Go’s concurrency philosophy. Instead of protecting shared data with mutexes (which is error-prone), pass data between goroutines through channels. The channel send/receive inherently synchronizes the goroutines. Use mutexes when channel communication is genuinely awkward (counters, caches, connection pools), but prefer channels for coordination and data flow.

XI. Packages and Modules

// Initialize a module $ go mod init github.com/user/myproject // Add a dependency $ go get github.com/some/library@latest // This creates go.mod: module github.com/user/myproject go 1.22 require ( github.com/some/library v1.2.3 )

Go’s module system uses semantic versioning. Dependencies are specified by their Git URL. go.sum pins exact hashes for reproducibility. There is no central registry (unlike npm or PyPI); packages are fetched directly from their source repository. The Go module proxy (proxy.golang.org) caches and serves modules for reliability and speed.

Package Naming

// File: math/vector.go package math // Exported (capitalized): visible outside the package func Add(a, b float64) float64 { return a + b } // Unexported (lowercase): visible only within the package func clamp(x, lo, hi float64) float64 { if x < lo { return lo } if x > hi { return hi } return x }

Visibility is determined by capitalization. Add is exported (public). clamp is unexported (private to the package). No public, private, or protected keywords. This is one of Go’s most distinctive design choices: the casing of the first letter is the entire access control system.

XII. Generics

Go added generics in version 1.18 (2022). They are deliberately minimal compared to C++ templates or Haskell type classes.

// A generic function func Min[T cmp.Ordered](a, b T) T { if a < b { return a } return b } fmt.Println(Min(3, 7)) // 3 (int) fmt.Println(Min("a", "z")) // "a" (string) // A generic data structure type Stack[T any] struct { items []T } func (s *Stack[T]) Push(item T) { s.items = append(s.items, item) } func (s *Stack[T]) Pop() (T, bool) { if len(s.items) == 0 { var zero T return zero, false } item := s.items[len(s.items)-1] s.items = s.items[:len(s.items)-1] return item, true } // Usage s := &Stack[int]{} s.Push(1) s.Push(2) v, _ := s.Pop() // v == 2

Type constraints (cmp.Ordered, any) specify what operations are available on the type parameter. any is an alias for interface{}—no constraints. cmp.Ordered allows <, >, etc. You can define custom constraints as interface types.

XIII. Testing

Testing is built into the language and the toolchain. No framework needed.

// File: math_test.go (must end in _test.go) package math import "testing" func TestAdd(t *testing.T) { got := Add(2, 3) want := 5.0 if got != want { t.Errorf("Add(2, 3) = %f, want %f", got, want) } } // Table-driven tests (idiomatic Go) func TestAddTable(t *testing.T) { tests := []struct { a, b, want float64 }{ {1, 2, 3}, {0, 0, 0}, {-1, 1, 0}, {1e18, 1e18, 2e18}, } for _, tt := range tests { got := Add(tt.a, tt.b) if got != tt.want { t.Errorf("Add(%f, %f) = %f, want %f", tt.a, tt.b, got, tt.want) } } } // Benchmarks func BenchmarkAdd(b *testing.B) { for i := 0; i < b.N; i++ { Add(2, 3) } }
$ go test ./... # run all tests $ go test -v ./... # verbose output $ go test -run TestAdd ./... # run specific test $ go test -bench=. ./... # run benchmarks $ go test -race ./... # enable the race detector $ go test -cover ./... # show coverage percentage

The -race flag deserves special attention. It enables Go’s built-in race detector, which instruments all memory accesses and reports data races at runtime. It catches real concurrency bugs with minimal false positives. Run it in CI. It has saved millions of developer hours.

XIV. The Standard Library

Go’s standard library is comprehensive enough that many projects need no external dependencies at all. The most important packages:

PackageWhat It Does
fmtFormatted I/O (Printf, Sprintf, Scanf)
net/httpHTTP client and server. Production-ready out of the box.
encoding/jsonJSON marshal/unmarshal via struct tags
ioReader/Writer interfaces, composition utilities
osOS interaction: files, env vars, processes
strings, strconvString manipulation and conversion
syncMutex, WaitGroup, Once, Pool
contextCancellation, deadlines, and request-scoped values
testingTesting and benchmarking framework
database/sqlGeneric SQL database interface
crypto/*TLS, AES, SHA, RSA, ECDSA, etc.
log/slogStructured logging (Go 1.21+)
slices, mapsGeneric slice/map utilities (Go 1.21+)

A Complete HTTP Server in 15 Lines

package main import ( "fmt" "log" "net/http" ) func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello, %s", r.URL.Path[1:]) }) log.Fatal(http.ListenAndServe(":8080", nil)) }

This is a production-quality HTTP server. net/http handles TLS, HTTP/2, keep-alive, timeouts, and graceful shutdown. It is what Docker, Kubernetes, and most Go microservices are built on. No framework needed.

JSON

type User struct { Name string `json:"name"` Email string `json:"email"` Age int `json:"age,omitempty"` } // Marshal (struct → JSON) u := User{Name: "Alice", Email: "alice@example.com", Age: 30} data, err := json.Marshal(u) // data: {"name":"Alice","email":"alice@example.com","age":30} // Unmarshal (JSON → struct) var u2 User err = json.Unmarshal(data, &u2)

Struct tags (the backtick strings after field types) tell the JSON encoder/decoder how to map fields. omitempty omits zero-valued fields. The same tag system is used by database drivers, YAML parsers, and validation libraries.

XV. What Go Leaves Out

The omissions are as important as the inclusions. Each was deliberate.

FeatureGo’s AlternativeWhy
Classes and inheritanceStructs, methods, embedding, interfacesComposition over inheritance. No fragile base class problem. No diamond inheritance.
Exceptions (try/catch)Multiple return values, error interfaceErrors are values. Control flow is always visible. No hidden exception paths.
Operator overloadingNamed methodsCode does what it looks like it does. + always means addition.
Implicit conversionsExplicit castsNo silent data loss or type confusion.
Macros / preprocessorgo generate, code generation toolsOne language, not two. The code you read is the code that runs.
Header filesPackages import directlyNo duplicate declarations. No circular include hell.
Ternary operator (? :)if/else statementOne way to write conditionals.
Enumsiota constantsSimpler. Less machinery.

The philosophy is: every feature you add to a language makes every other feature harder to use, because the feature space is combinatorial. Go optimizes for the total cost of a feature over the lifetime of a large codebase maintained by a large team. Features that save a line of code but add cognitive load were cut.

“A little copying is better than a little dependency.”

— Go Proverb

XVI. Putting It Together

Here is a complete, working program that demonstrates most of what we have covered: structs, interfaces, maps, goroutines, channels, error handling, and the standard library.

package main import ( "fmt" "math" "sort" "sync" ) // --- Types --- type Point struct{ X, Y float64 } func (p Point) DistanceTo(other Point) float64 { dx, dy := p.X-other.X, p.Y-other.Y return math.Sqrt(dx*dx + dy*dy) } func (p Point) String() string { return fmt.Sprintf("(%.1f, %.1f)", p.X, p.Y) } // --- Concurrent nearest-neighbor search --- type Result struct { Point Point Distance float64 } func findNearest(query Point, points []Point, workers int) Result { ch := make(chan Result, workers) chunkSize := (len(points) + workers - 1) / workers var wg sync.WaitGroup for i := 0; i < workers; i++ { start := i * chunkSize end := start + chunkSize if end > len(points) { end = len(points) } if start >= end { continue } wg.Add(1) go func(chunk []Point) { defer wg.Done() best := Result{chunk[0], query.DistanceTo(chunk[0])} for _, p := range chunk[1:] { d := query.DistanceTo(p) if d < best.Distance { best = Result{p, d} } } ch <- best }(points[start:end]) } go func() { wg.Wait(); close(ch) }() best := Result{Distance: math.Inf(1)} for r := range ch { if r.Distance < best.Distance { best = r } } return best } func main() { // Build a dataset points := []Point{ {1, 2}, {3, 4}, {5, 1}, {7, 8}, {2, 9}, {6, 3}, {8, 5}, {4, 7}, {9, 2}, {0, 6}, } query := Point{4.5, 4.5} result := findNearest(query, points, 3) fmt.Printf("Nearest to %s: %s (distance %.2f)\n", query, result.Point, result.Distance) // Group points by quadrant using a map quadrants := make(map[string][]Point) for _, p := range points { var q string switch { case p.X >= 5 && p.Y >= 5: q = "NE" case p.X < 5 && p.Y >= 5: q = "NW" case p.X < 5 && p.Y < 5: q = "SW" default: q = "SE" } quadrants[q] = append(quadrants[q], p) } // Print sorted by quadrant name keys := make([]string, 0, len(quadrants)) for k := range quadrants { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { fmt.Printf("%s: %v\n", k, quadrants[k]) } }
Nearest to (4.5, 4.5): (3.0, 4.0) (distance 1.58) NE: [(7.0, 8.0) (8.0, 5.0)] NW: [(2.0, 9.0) (4.0, 7.0) (0.0, 6.0)] SE: [(5.0, 1.0) (6.0, 3.0) (9.0, 2.0)] SW: [(1.0, 2.0) (3.0, 4.0)]

Summary

Go is not trying to be the most expressive language, or the most powerful, or the most academically interesting. It is trying to be the most practical language for building software that is correct, concurrent, readable, and deployable. Its entire design can be summarized in a few principles:

  • Composition over inheritance. Structs embed other structs. Interfaces are small and implicitly satisfied.
  • Errors are values, not exceptions. Control flow is always visible. You always know where errors are handled.
  • Concurrency is a first-class feature. Goroutines are cheap. Channels are typed. select multiplexes. The race detector catches bugs.
  • One obvious way to do things. One loop construct. One string type. One way to handle errors. gofmt formats all code identically.
  • Fast compilation. The compiler is fast enough that the edit-compile-run cycle feels like an interpreted language. Large codebases compile in seconds.
  • Batteries included. HTTP server, JSON, crypto, testing, benchmarking, profiling, race detection—all in the standard library.

The language you learn in a weekend is the same language that runs Docker, Kubernetes, Terraform, Prometheus, CockroachDB, and a significant fraction of the infrastructure of the modern internet. That is the argument for simplicity.