Go · lesson 1 of 25
Introduction to Go
What Go is and why it is used for cloud tools and CLIs, with a first program, a goroutine and a single static binary, each run with its real output.
By SkillByExample · Updated
What Is Go?
Go (also called Golang) is an open-source, statically typed, compiled language designed at Google. It was born out of frustration with the slow build times and complexity of C++ and the lack of efficient concurrency in other languages. The design goal was simple: the productivity of a dynamic language with the performance and safety of a compiled one. Unlike languages that run on a virtual machine, Go compiles directly to a native binary that runs on the operating system with no runtime to install and no interpreter to ship alongside it.
Source (.go files)
│
▼ go build
Single native binary
│
▼ runs directly on OS — no VM, no interpreter
Program executes
go build produces a self-contained binary — you’ll build one below. There is no virtual machine, no framework to install, and no dependency hell at deployment time.
A Brief History
Go was not created in a vacuum — it was a direct response to problems Google engineers faced working at massive scale. Understanding that context helps explain why the language makes the choices it does.
| Year | Milestone |
|---|---|
| 2007 | Design begins at Google (Griesemer, Pike, Thompson) |
| 2009 | Open-sourced and announced |
| 2012 | Go 1.0 released with stability guarantee |
| 2018 | Go modules introduced — replaced GOPATH-based dependency management |
| 2022 | Go 1.18 — generics added to the language |
| 2024 | Go 1.22 — a fresh loop variable per iteration, range over integers |
| 2024 | Go 1.23 — range over function iterators, the iter package |
| 2025 | Go 1.25 — GOMAXPROCS respects a container’s CPU limit on Linux |
| 2026 | Go 1.26 — the “Green Tea” garbage collector becomes the default |
| 2026 | Go 1.27 — methods can declare their own type parameters |
The Go team made an explicit promise with Go 1: any program that compiles today will still compile with future Go 1.x releases. That backward compatibility guarantee is a significant reason enterprises adopt Go — you don’t have to worry about a major version breaking your codebase.
Why Go?
Fast compilation
Go compiles a large codebase in seconds. This matters because it keeps the feedback loop tight — you make a change and see the result almost instantly, rather than waiting minutes for a build. Google’s internal monorepo builds that took minutes in C++ compile in seconds in Go.
Built-in concurrency
Most languages treat concurrency as an afterthought, relying on heavyweight threads or third-party libraries. Go bakes it in at the language level with goroutines — lightweight threads managed by the Go runtime that you can spin up by the hundreds of thousands. Channels provide a safe, structured way for goroutines to communicate.
Here two goroutines run alongside main, and a sync.WaitGroup makes main wait until both have finished:
package main
import (
"fmt"
"sync"
)
func main() {
results := make([]string, 2)
var wg sync.WaitGroup
for i, lang := range []string{"Go", "Gopher"} {
wg.Add(1)
go func() { // runs concurrently with main
defer wg.Done()
results[i] = "hello from " + lang
}()
}
wg.Wait() // block until both goroutines called Done
fmt.Println(results[0])
fmt.Println(results[1])
}
$ go run hello_goroutines.go
hello from Go
hello from Gopher
Each goroutine writes into its own slot, and main prints only after wg.Wait() returns, so the output is the same on every run even though the goroutines may finish in either order. Remove the wg.Wait() line and main would print two empty strings: a Go program exits when main returns, without waiting for other goroutines. The goroutines lesson starts from exactly that bug.
Single binary deployment
Deploying software is often complicated by dependency management — you need the right runtime version, the right libraries installed, the right environment. Go eliminates that problem entirely. go build produces one self-contained binary with everything included. Deploying a Go service means copying a single file — no Node modules, no JVM, no Python virtualenv.
Simplicity by design
Go has 25 keywords. The entire language specification fits in a single web page. There are no operator overloading, no implicit type coercions, no generics complexity (beyond what was carefully added in 1.18), and only one way to loop. This simplicity means a developer new to a Go codebase can become productive in hours, not weeks.
Strong standard library
Go’s standard library is unusually comprehensive. It covers HTTP servers, JSON encoding, cryptography, file I/O, testing, and more — often eliminating the need for third-party dependencies entirely. This reduces supply-chain risk and keeps projects lean.
Where Go Is Used
Go’s combination of performance, simplicity, and easy deployment has made it the dominant language for cloud infrastructure tools. If you’ve used any of these, you’ve already been running Go code.
| Domain | Real-world examples |
|---|---|
| Cloud infrastructure | Docker, Kubernetes, Terraform, Prometheus — all written in Go |
| Microservices | gRPC services, REST APIs with high throughput requirements |
| CLIs | GitHub CLI (gh), Cobra-based tools, Hugo static site generator |
| Networking tools | Caddy web server, Traefik reverse proxy |
| DevOps tooling | Helm, kubectl plugins, CI runners |
| Databases | CockroachDB, InfluxDB, etcd |
Your First Go Program
Every executable starts in package main at func main(); fmt.Println writes a line to standard output.
package main
import "fmt"
func main() {
fmt.Println("Hello, Go!")
}
$ go run main.go
Hello, Go!
$ go build -o hello main.go
$ ./hello
Hello, Go!
go run compiles to a temporary directory and runs the result; go build writes the executable to disk and prints nothing when it succeeds. Running ./hello needs no Go installation at all — that binary is the whole program. You can check that claim:
$ go build -o hello main.go
$ file hello
hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=7d6511effdfe68a89943f8488783c6aff9dac3d7, with debug_info, not stripped
$ ls -lh hello
-rwxr-xr-x 1 root root 2.2M Oct 6 18:17 hello
file reports a statically linked executable: no shared libraries to install alongside it. The roughly 2 MB size is mostly the Go runtime (garbage collector and goroutine scheduler) and the parts of fmt the program uses; it is the price of having nothing else to deploy. The setup lesson uses modules instead of a lone main.go, which is how real projects are built.
Go’s Design Philosophy
“Less is more” — Go deliberately omits features found in other languages. These omissions are not oversights; they are active decisions to prevent the kind of complexity that makes large codebases hard to read and maintain:
- No exceptions (errors are values returned explicitly)
- No inheritance (composition via embedding instead)
- No ternary operator (use a full
if/else) - No function overloading
- No default parameter values
These omissions keep code uniform and readable across large teams and codebases. When you read someone else’s Go code, there are far fewer language features you need to understand before the logic becomes clear.
“There should be one obvious way to do it” — Go’s formatter (gofmt) enforces a single code style across every Go project in the world. Code review debates about tabs vs spaces simply don’t happen in Go teams, and every Go file you open looks familiar.
Go Learning Roadmap
Work through these tutorials in order:
Module 1 — Foundations
- Introduction to Go ← you are here
- Setup — install Go, modules, VS Code
- Variables — var, :=, zero values
- Data Types — int, float64, string, bool, rune
- Operators — arithmetic, comparison, logical, bitwise
- Control Flow — if, switch, for, range
Module 2 — Core Language
- Functions — multiple returns, defer, variadic
- Strings — strings package, Builder, runes vs bytes
- Arrays & Slices — make, append, copy, internals
- Maps — literals, make, delete, iteration
Module 3 — Types and Interfaces
- Structs — embedding, tags, methods
- Interfaces — implicit implementation, type assertions
- Error Handling — error interface, wrapping, custom errors
Module 4 — Concurrency
- Goroutines — go keyword, WaitGroup
- Channels — buffered, select, patterns
- Concurrency Patterns — worker pools, pipelines, context
Module 5 — Practical Go
- Packages — modules, go.mod, internal packages
- File I/O — os, bufio, JSON
- Testing — table-driven tests, benchmarks
- Generics — type parameters, constraints
- Memory — GC, escape analysis, pprof
Module 6 — Production Patterns
- Design Patterns — functional options, middleware, DI
- HTTP Server — net/http, routing, JSON APIs
- CLI Tools — flag, cobra, stdin/stdout
- Interview Prep — top 35 Go interview Q&A
Practice
1. Change main.go to print two lines, Hello, Go! and Built with go run, using two Println calls.
Solution
package main
import "fmt"
func main() {
fmt.Println("Hello, Go!")
fmt.Println("Built with go run")
}
$ go run main.go
Hello, Go!
Built with go run
Each Println adds its own newline.
2. Print your Go version and platform from inside a program, using the runtime package.
Solution
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Println(runtime.Version(), runtime.GOOS, runtime.GOARCH)
}
$ go run main.go
go1.27.2 linux amd64
The values are baked in at compile time for GOOS/GOARCH, so a binary cross-compiled for Windows reports windows even though it was built on Linux.
3. Make hello_goroutines.go greet three names instead of two, still printing in a fixed order.
Solution
package main
import (
"fmt"
"sync"
)
func main() {
names := []string{"Go", "Gopher", "World"}
results := make([]string, len(names))
var wg sync.WaitGroup
for i, name := range names {
wg.Add(1)
go func() {
defer wg.Done()
results[i] = "hello from " + name
}()
}
wg.Wait()
for _, r := range results {
fmt.Println(r)
}
}
$ go run main.go
hello from Go
hello from Gopher
hello from World
Sizing results from len(names) and printing it in a loop keeps the order tied to the input, not to which goroutine finished first.
4. Delete wg.Wait() from your solution to exercise 3 and run it. What prints, and why?
Solution
package main
import (
"fmt"
"sync"
)
func main() {
names := []string{"Go", "Gopher", "World"}
results := make([]string, len(names))
var wg sync.WaitGroup
for i, name := range names {
wg.Add(1)
go func() {
defer wg.Done()
results[i] = "hello from " + name
}()
}
for _, r := range results {
fmt.Printf("%q\n", r)
}
}
$ go run main.go
""
""
""
On this run main read results before any goroutine had written to it, so all three are empty. That is not guaranteed — a goroutine could get there first — which is exactly why it is a bug: the program has a data race, and go run -race main.go reports it.