1) What is Go
Overview
- A compiled, statically typed language (by Google, 2009) designed for simplicity, fast builds, high performance, and easy concurrency (goroutines/channels).
- No manual memory management - Go uses a garbage collector.
- Standard toolchain includes:
go build
,go run
,go test
,go fmt
,go vet
,go mod
,go work
.
Where it’s used
- Web services & APIs - fast servers, cloud backends, REST/GraphQL.
- Networking & distributed systems - microservices, proxies, load balancers.
- DevOps tools - CLIs, automation, infrastructure management.
- Cloud platforms - containers, orchestration, serverless runtimes.
- Data processing - streaming pipelines, concurrent jobs.
Example apps & companies
- Docker - container platform.
- Kubernetes - container orchestration.
- Terraform - infrastructure as code.
- Prometheus - monitoring system.
- Cloudflare, Uber, Dropbox, Netflix - production systems built with Go.
2) Installation
Install & verify
Official guide: https://go.dev/doc/install
After installing, check in your terminal: go version
and go env
Important variables:
- GOROOT - This is simply the folder where Go itself is installed.
- GOPATH: This is a working folder that Go uses in the background.
You don’t put your projects here anymore, but Go still uses it for:
- bin - where Go puts ready-to-use programs when you run go install.
- pkg/mod - a storage where Go keeps downloaded dependencies (libraries).
- pkg/sumdb - a small database Go uses to verify dependencies are safe and unchanged.
3) Creating a Basic Go Project
Initialize a module:
go mod init <module_name>
- Example:
go mod init example.com
- Best practice for version control: use the repository URL as the module name like
go mod init github.com/username/projectname
. This ensures imports work correctly when others use your module:import "github.com/username/projectname/pkg/foo
Basic program (main.go)
package main
import "fmt"
func main() {
fmt.Println("Hello, Go")
}
Run and build
go run .
- compiles and runs the program.go build .
- builds an executable file that we can run later on.- Why use modules? - Since Go 1.16, module mode is the default. Enables versioning of dependencies. Guarantees reproducible builds. Without go.mod you can still run a single file (go run main.go), but for any real project you should always use modules.
4) Package and program's entrypoint
Essentials
package
: The first line in every.go
file. Usemain
for executable programs and any other name for libraries/modules.func main()
: The entry point of a Go program. An executable must have exactly one main package with exactly one func main().
Example (main.go)
package main
import "fmt"
func main() {
fmt.Println("The program starts here")
}
Naming best practices for packages
- Use short, lowercase names (strings, json, http).
- Avoid underscores, dashes, or CamelCase.
- The name should describe what the package provides, not the project (math, not mathutils).
- Keep names singular, not plural (user, not users).
If this happens, extract common code into a third package.
5. Go.mod and go.sum - what they are and why
Overview
go.mod
- The recipe for your project.
- Says: this is my module name, this is my Go version, these are the libraries (dependencies) I need.
go.sum
- The security list. Go writes down a checksum (fingerprint) for every library version you use.
- It is valuable because it guarantees integrity and reproducibility of dependencies, making supply-chain attacks much harder to succeed. Always commit
go.sum
to Git together withgo.mod
. - https://socradar.io/npm-supply-chain-attack-crypto-stealing-malware
replace (inside go.mod)
A quick way to tell Go: don’t use this version from the internet, use this other version instead.
- Local development: replace github.com/username/lib => ../lib (use the local folder instead of downloading it)
- Force a version: replace bad/library v1.2.3 => good/library v1.2.4 (redirects dependency to another version or fork)
6) Workspaces with go work - how, where, and why
What it is?
go work
lets you develop several Go modules together so your app can use your local libraries while you code.
What is a Go module?
- A module is a folder with its own go.mod (a versioned bundle of packages). The module line in go.mod sets the import path prefix (e.g., module example.com/mod1 => you import packages as example.com/mod1/hello).
- You always import by the module path declared in the library’s go.mod. With a workspace, Go resolves that import to your local folder listed in go.work (not from the internet). If paths don’t match or the workspace isn’t active, Go will try to fetch online and you may see a 404.
- Without a workspace (or a replace), Go treats example.com/mod1 as external and tries to fetch it via the module proxy/VCS (e.g., GitHub via proxy.golang.org, or a vanity path via https://example.com/mod1?go-get=1). If nothing is there, you get a 404.
When to use it?
- Use go work only when you have several Go projects that should work together (for example, an app and your own library).
- It lets your projects see each other’s code while you develop, without publishing anything online.
When you don’t need it
One project, one go.mod, multiple binaries. Start simple. Use workspaces only if you later split code into separate modules.
Where to put it
Create one go.work file in the repo root (a folder that contains your modules).
7) Polymorphism in Go
Polymorphism means one function, multiple different behaviours. In Go, there are two main forms:
Compile-time polymorphism (Generics)
- Introduced in Go 1.18.
- Achieved with type parameters and constraints.
- The compiler generates specialized code for each type, so there’s no runtime overhead.
- Similar to C++ templates.
- Example:
package main
import (
"fmt"
"golang.org/x/exp/constraints"
)
// Max works for any type that supports ordering (<, >, ==, …)
func Max[T constraints.Ordered](a, b T) T {
if a > b {
return a
}
return b
}
func main() {
fmt.Println(Max(2, 5)) // ints
fmt.Println(Max(3.14, 2.71)) // floats
fmt.Println(Max("a", "b")) // strings
}
Runtime polymorphism (Interfaces)
- Achieved with interfaces.
- The concrete type is only known at runtime.
- Method calls go through an interface table lookup (like virtual calls in C++).
package main
import "fmt"
type Greeter interface {
Greet() string
}
type Person struct{}
func (Person) Greet() string { return "Hello" }
type Robot struct{}
func (Robot) Greet() string { return "Beep Boop" }
func SayHi(g Greeter) {
fmt.Println(g.Greet())
}
func main() {
SayHi(Person{}) // "Hello"
SayHi(Robot{}) // "Beep Boop"
}
Summary
- Use generics when you want zero-cost abstraction and compile-time safety.
- Use interfaces when you need flexibility and don’t know the concrete type until runtime.
8) Go Compiler
When you run go build, the Go toolchain orchestrates the entire compilation pipeline. Under the hood, the **Go compiler (`cmd/compile`)** translates your `.go` source files into optimized machine code, and the **linker (`cmd/link`)** produces the final binary.
Module & Dependency Resolution
- Reads
go.mod
/go.sum
. - Downloads any missing dependencies into the module cache.
- If
vendor/
exists and you build with-mod=vendor
, vendored code is used instead. - Builds happen in dependency order, using cached results if available.
Lexical Analysis (Scanning)
- Source code is read as a stream of characters.
- Whitespace and comments are discarded.
- Tokens are recognized (identifiers, keywords, literals, operators, punctuation).
- Output: token stream.
Parsing
- Tokens are organized into an Abstract Syntax Tree (AST).
- AST = a tree-shaped representation of your code where each node is a construct (function, block, statement, expression).
- The AST encodes the program’s structure and hierarchy in a form the compiler can analyze.
- Syntax errors are reported at this stage.
Type Checking & Semantic Analysis
- Uses
cmd/compile/internal/types2
(derived fromgo/types
). - Verifies type correctness, function signatures, method receivers, interfaces.
- Resolves identifiers to declarations, evaluates constants.
- Detects errors like invalid conversions, undeclared names, wrong argument counts.
- Output: typed AST.
Intermediate Representation Construction
- Intermediate Representation (IR) is the compiler’s private form of your program - a simplified tree that’s easier to analyze and transform than raw Go code.
- The typed AST is converted into Go’s IR (
cmd/compile/internal/ir
). - Go uses a Unified IR, which supports generics, cross-package inlining, and import/export of inlineable code.
- This IR is the main input for optimizations and later passes like Walk and SSA.
Optimizations on IR
- Escape analysis: The compiler checks if a variable can safely live on the stack (cheap, fast) instead of the heap (slower, garbage-collected). If a value’s lifetime doesn’t “escape” the function, it stays on the stack. Otherwise, it’s allocated on the heap.
- Inlining: Small function calls may be replaced with the function’s body directly at the call site. This removes the overhead of calling a function and can unlock further optimizations.
- Devirtualization: In Go, methods are really just functions with the struct (or pointer) passed as the first argument - similar to how C++ methods become functions with a hidden this pointer.
type Person struct{}
func (p Person) Greet() string { // really: func Greet(p Person) string
return "Hello"
}
Normally, when you call a method through an interface, Go must look up the method in an internal table (like a C++ vtable).
If the compiler can prove the exact concrete type, it can skip the lookup and call the function directly.
This optimization is called devirtualization, and it effectively turns runtime polymorphism into a direct compile-time call (faster).
Example where devirtualization can happen:
package main
import "fmt"
type Greeter interface {
Greet() string
}
type Person struct{}
func (Person) Greet() string {
return "Hello from Person"
}
func sayHi(g Greeter) string {
return g.Greet()
}
func main() {
// The compiler sees g is always a Person
// Devirtualizes to a direct call Person.Greet()
fmt.Println(sayHi(Person{}))
}
Example where devirtualization cannot happen:
package main
import (
"fmt"
"math/rand"
"time"
)
type Greeter interface {
Greet() string
}
type Person struct{}
func (Person) Greet() string {
return "Hello from Person"
}
type Robot struct{}
func (Robot) Greet() string {
return "Beep Boop"
}
func sayHiDynamic(g Greeter) string {
// Here g could be Person, Robot, or any Greeter
// Compiler cannot know the type at compile time
// Must do interface method lookup (runtime polymorphism)
return g.Greet()
}
func main() {
rand.Seed(time.Now().UnixNano())
var g Greeter
if rand.Intn(2) == 0 {
g = Person{}
} else {
g = Robot{}
}
fmt.Println(sayHiDynamic(g))
}
Early dead code elimination: Any code that can never run (like if false { ... }
) or whose results are never used gets removed at this stage, keeping the program smaller and faster.
Walk (Lowering Go Constructs)
- Converts high-level Go features into simpler steps the compiler can handle.
switch
=> turns into jump tables or a chain ofif
statements.range
=> becomes a normalfor
loop with indexes.- Map/channel operations => replaced with calls into the Go runtime.
defer
/recover
=> expanded into helper code from the runtime.- Adds temporary variables to guarantee Go’s left-to-right order of evaluation.
- Produces IR made only of primitive operations, ready for SSA.
SSA Form (Static Single Assignment)
- IR is translated into SSA (
cmd/compile/internal/ssa
), where each variable gets only one assignment. - This makes dataflow easy to track and optimize.
- Key optimizations at this stage:
- Constant folding: replace
2+3
with5
. - Nil-check removal: skip repeated checks like
if x==nil
. - Bounds-check removal: skip slice/array checks proven safe.
- Branch pruning: cut out
if true { ... }
or unreachable code. - CSE (common subexpr elimination): reuse the result of repeated expressions.
- Intrinsics: swap Go functions (e.g.
math.Sqrt
) with fast CPU instructions.
Machine-Dependent Lowering
- SSA is converted into code tailored for your CPU (amd64, arm64, etc.).
- The compiler applies small CPU-specific tweaks (rewrite rules, peephole optimizations).
- Instruction selection: choose the actual machine instructions to run.
- Instruction scheduling: order them for best CPU performance.
- Register allocation: put values into CPU registers; if not enough, spill extras to memory.
- Stack frame layout: decide where local variables go on the stack and mark safe points for the garbage collector.
Code Generation
- SSA is turned into
obj.Prog
instructions (Go’s own assembly format). - The assembler (
cmd/internal/obj
) translates these into real machine code. - The result is an object file containing:
- The compiled instructions.
- Export data so other packages can use this code.
- Type info needed for reflection.
- Debug info (symbols, line numbers) for stack traces and tooling.
Linking
- The Go linker (
cmd/link
) merges all object files and the runtime. - Resolves symbols across packages.
- Produces a final self-contained binary executable for the target OS/arch.
Execution
- The binary can be run directly.
- The Go runtime (scheduler, GC, goroutines, channels) is statically linked in, so no external dependencies are needed.
9) Build Options & Best Practices
When to use plain go build vs custom targets
Use go build
when you build for your computer.
go build ./cmd/app
Use custom targets when you need a binary for a different system (release builds, CI, servers).
# Linux/amd64 (from any OS)
GOOS=linux GOARCH=amd64 go build ./cmd/app
# Windows
GOOS=windows GOARCH=amd64 go build -o app.exe ./cmd/app
# macOS Apple Silicon
GOOS=darwin GOARCH=arm64 go build ./cmd/app
Should I disable cgo?
CGO_ENABLED=0
= pure Go. Smaller, simpler, cross-compiles easily. Use this unless you really need C code.CGO_ENABLED=1
only when you import"C"
or depend on C libraries.
# Pure Go build
CGO_ENABLED=0 go build ./cmd/app
Production build
Goal: small, reproducible, fast startup, easy to ship.
# pick your target
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -trimpath -buildvcs=false -ldflags="-s -w" -o app .
What this does:
CGO_ENABLED=0
=> pure Go binary (no dependency on system C libraries)GOOS=linux GOARCH=amd64
=> GOOS=linux GOARCH=amd64-trimpath
=> strips local file system paths from the binary-buildvcs=false
=> disables embedding Git metadata (commit hash, dirty state)-ldflags="-s -w"
- -
-s
: strip symbol table. - -
-w
: strip DWARF debug info -o app
=> names the output binary app instead of the default..
=> tells go build to build the package in the current directory.