Go Introduction
Go, often called Golang, is an open-source, compiled, statically typed programming language created at Google to make building reliable, fast software simple at scale. It combines the quick compile times and readability of a scripting language with the performance and safety of a compiled one, and it treats concurrency as a first-class feature through goroutines and channels. Tools you may already know — Docker, Kubernetes, Terraform — are written in Go. This lesson explains what Go is, how its compiler and toolchain actually work, and gets you writing real, running Go code.
Overview: How Go Works
Go was designed starting in 2007 by Robert Griesemer, Rob Pike, and Ken Thompson at Google, open-sourced in 2009, and reached a stable 1.0 release in 2012 with a strong backward-compatibility promise that still holds today. The team was frustrated with slow C++ build times, tangled dependency graphs, and languages that made safe concurrent programming hard. The result is a deliberately small language — around 25 keywords — that compiles fast, produces a single self-contained binary, and bakes concurrency into the language itself.
Go is a compiled language, not interpreted or bytecode-based like Python or Java. Running go build invokes the Go toolchain’s compiler, which translates your source directly into native machine code for your target operating system and CPU, links in everything the program needs — including the Go runtime that manages memory allocation, garbage collection, and goroutine scheduling — and produces one standalone executable. There is no separate virtual machine, and no runtime needs to be installed on the machine that eventually runs the program; you just copy the binary over. That is a major reason Go is popular for command-line tools and backend services.
Go is also statically typed: every variable’s type is checked at compile time, catching a whole class of bugs before the program ever runs. Type inference via the := operator (covered below) means you rarely write types out explicitly, so it feels almost dynamically typed day to day. Memory is managed automatically by a concurrent garbage collector, so you never call free() like in C, but you also skip the overhead of a slow interpreter loop.
Modern Go code is organized into modules. A module is described by a go.mod file at your project root, created with go mod init <module-path>, which records the module’s name and the versions of any external packages it depends on. This replaced the older GOPATH workflow, where every project had to live inside one global workspace; modules let a project live anywhere on disk. Every program is built from packages: a package is a directory of .go files that all start with the same package declaration. The special package main marks an executable, and it must contain a function named main with no parameters and no return value — that is where execution begins.
You will use a handful of command-line tools constantly:
| Command | What it does |
|---|---|
go run file.go |
Compiles and immediately runs a program, without leaving a binary behind |
go build |
Compiles the package into a standalone executable |
go fmt |
Rewrites source files into Go’s one canonical formatting style |
go mod init |
Creates a go.mod file to start a new module |
go vet |
Statically analyzes code for likely mistakes beyond what the compiler checks |
go test |
Runs tests in files named *_test.go |
Formatting is not a matter of taste in Go: gofmt rewrites every file into one shape — tabs for indentation, opening braces on the same line as the statement that introduces them. That brace rule is not just style: Go’s compiler automatically inserts semicolons at the end of certain lines based on the final token, so writing an opening brace on its own line actually breaks compilation, because a semicolon gets inserted right before it. Because of this rule, virtually every Go codebase you will ever read has the same shape.
Syntax
Every Go source file follows the same basic pattern:
package main
import "fmt"
func main() {
// statements go here
fmt.Println("Hello, Go!")
}
package main— declares which package this file belongs to;mainis the special name that marks a runnable program.import "fmt"— pulls in the standard library’s formatting package so you can callfmt.Println. Every import must be used, or the compiler rejects the file.func main()— the entry point. When you run the compiled binary, execution starts here.- No semicolons — Go statements do not end with
;in source; the compiler inserts them automatically at line breaks.
Examples
Example 1: Hello, Go!
package main
import "fmt"
func main() {
fmt.Println("Hello, Go!")
}
Hello, Go!
Save this as hello.go and run go run hello.go. The compiler builds a temporary binary, runs it, and fmt.Println writes the string followed by a newline to standard output.
Example 2: Variables and Types
package main
import "fmt"
func main() {
var name string = "Gopher"
age := 15
height := 3.5
fmt.Println("Name:", name)
fmt.Println("Age:", age)
fmt.Println("Height:", height)
fmt.Println("Age next year:", age+1)
}
Name: Gopher
Age: 15
Height: 3.5
Age next year: 16
This shows Go’s two ways to declare a variable. var name string = "Gopher" spells out the type explicitly. age := 15 uses the short declaration operator, which is only legal inside a function body; the compiler infers the type (int) from the value on the right. Both forms produce statically typed variables — age cannot later be assigned a string.
Example 3: Multiple Return Values and Errors
package main
import (
"errors"
"fmt"
)
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
func main() {
result, err := divide(10, 2)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("10 / 2 =", result)
}
result, err = divide(5, 0)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("5 / 0 =", result)
}
}
10 / 2 = 5
Error: division by zero
This is the idiomatic Go error pattern: a function that can fail returns its normal result plus a value of the built-in error interface, which is nil when nothing went wrong. There are no exceptions to throw or catch — the caller is required to look at err and decide what to do. The second call reuses the existing result and err variables with = instead of :=, since they were already declared above.
How Go Programs Start and Run
Understanding the path from source file to running program clarifies a lot of Go’s behavior:
- Compile:
go build(orgo run, which does this and then executes the result) parses every.gofile in the package, type-checks it, and translates it straight into native machine code — there is no intermediate bytecode. - Link: the compiler links in the Go runtime (the garbage collector and goroutine scheduler) and any imported packages, producing one self-contained binary with no external dependencies to install.
- Package-level initialization: when the binary runs, package-level variables are initialized first, in dependency order.
initfunctions: if a package defines one or morefunc init()functions, they run next, in the order they appear in the source.main.main: only after all of that does the runtime call yourfunc main(). In Example 3, this is the point wheredivide(10, 2)is actually called.- Exit: when
mainreturns, the program exits immediately — Go does not wait for any goroutines you may have started elsewhere to finish, which is why real programs coordinate goroutine shutdown explicitly (covered in the concurrency lessons).
Common Mistakes
Mistake 1: Discarding an error with _
Beginners often throw away the error return because the extra if feels like boilerplate:
value, _ := strconv.Atoi("abc")
fmt.Println(value * 2)
Here strconv.Atoi fails because "abc" is not a number, so value silently becomes 0 and the program prints 0 as if nothing went wrong — the real problem is swallowed. Always check the error:
package main
import (
"fmt"
"strconv"
)
func main() {
value, err := strconv.Atoi("abc")
if err != nil {
fmt.Println("Conversion failed:", err)
return
}
fmt.Println(value * 2)
}
Conversion failed: strconv.Atoi: parsing "abc": invalid syntax
Mistake 2: Accidental shadowing with :=
Using := inside a nested block creates a brand-new variable that only exists in that block, even if an outer variable has the same name:
package main
import "fmt"
func main() {
x := 10
if true {
x := 20
fmt.Println("inner x:", x)
}
fmt.Println("outer x:", x)
}
inner x: 20
outer x: 10
A programmer expecting x to become 20 everywhere is surprised: the inner x := 20 shadowed the outer variable instead of updating it. To modify the existing variable, use plain assignment (=) instead of a new short declaration:
package main
import "fmt"
func main() {
x := 10
if true {
x = 20
fmt.Println("inner x:", x)
}
fmt.Println("outer x:", x)
}
inner x: 20
outer x: 20
Best Practices
- Run
go fmt(or let your editor run it on save) so every file matches the language’s one canonical style. - Check every error where it occurs; don’t defer error handling to “later” or discard it with
_outside of a deliberate, documented reason. - Keep package names short, lowercase, and free of underscores — the package name plus its exported identifiers should read naturally, like
strings.Split. - Run
go vetalongside the compiler; it catches likely mistakes the type checker doesn’t, like malformedPrintfformat verbs. - Prefer small, focused functions and packages over large ones — Go’s fast compiler rewards splitting code into well-scoped units.
- Use
go mod initat the start of every new project instead of the legacyGOPATHlayout. - Write tests in
*_test.gofiles and run them withgo testas you go, rather than only at the end.
Practice Exercises
- Write a complete program that declares a
string, anint, and aboolvariable (using whichever declaration form you like), then prints all three on one line withfmt.Println. - Write a function
square(n int) intthat returnsn * n, then call it frommaininside aforloop for the numbers 1 through 5, printing each result. Expected output is five lines:1,4,9,16,25. - Write a function
safeDivide(a, b int) (int, error)that returns an error instead of panicking whenbis 0, and a normal quotient otherwise. Call it twice frommain— once with a valid divisor and once with 0 — and print either the result or the error message.
Summary
- Go is a compiled, statically typed language built at Google for fast builds, simple deployment, and safe concurrency.
go build/go runcompile straight to native machine code and link in the Go runtime — no virtual machine, no separate runtime install.- Every program needs a
package mainand a parameterless, return-lessfunc main()as its entry point. - Modules (
go.mod, created withgo mod init) are the modern way to organize dependencies, replacing the oldGOPATHworkflow. gofmtenforces one formatting style for the whole language, and Go’s automatic semicolon insertion is why opening braces must stay on the same line.- Go has no exceptions: functions that can fail return an
errorvalue that callers are expected to check withif err != nil. :=inside a nested block declares a new, shadowed variable rather than reusing an outer one — use=when you mean to modify the existing variable.
