Go was built at Google to solve a specific problem: large teams writing software that needs to compile fast, run fast, and stay readable years later. To do that, it made some strong choices — no classes, no exceptions, no implicit anything. This session covers the foundation: how Go organizes code, and the data structures you'll use constantly.
Packages — How Go Organizes Code
Every Go file starts with a package declaration. No exceptions.
package main
import "fmt"
func main() {
fmt.Println("Hello, Go")
}
Run it: go run main.go
A packagePackageA named collection of Go files that share the same namespace. is a named group of .go files in the same directory. Two rules you'll feel immediately:
package main is the entry point. Every executable must have exactly one package main with exactly one func main(). Library packages use any other name.
Unused imports are a compile error. Go refuses to build if you import something you don't use. This keeps codebases clean by force, not convention.
import (
"fmt"
"math"
)
// Both must be used — or it won't compile
Grouped imports use parentheses. gofmt (Go's formatter) enforces import order: stdlib first, third-party next, your own packages last, separated by blank lines. You don't think about this — you just run gofmt.
Exported Names — Go's Visibility Rule
In other languages, you write keywords to control visibility. Go does it differently: a name is exported (public) if it starts with a capital letter.
The compiler enforces this — accessing an unexportedExported NameAny identifier starting with a capital letter is public — accessible from other packages. name from outside its package is a compile error, not a runtime warning. Capital letter = public. Lowercase = private. That's the entire system.
Variables & Types
Go is statically typedStatically TypedVariable types are checked at compile time, not while the program runs.. Every variable has a type the compiler knows at compile timeCompile TimeWhen your code is being translated into a runnable program — before it runs.. No runtime type surprises.
Two things that stand out: := is the idiomaticIdiomaticThe 'natural' or conventional way of writing code in a given language. way inside functions — Go infers the type, so you rarely write explicit types. And unlike most languages, Go has zero valuesZero ValuesGo's default values for variables — 0, "", false — instead of null. — every declared variable is automatically initialized: int → 0, string → "", bool → false. No null, no undefined.
Basic types:
var i int = 42
var f float64 = 3.14
var s string = "hello"
var b bool = true
// Type conversion is always explicit
var x int = 10
var y float64 = float64(x) // no implicit casting
Control Flow
Go's control flow looks familiar, with a few deliberate differences.
for is the only loop — there's no while or do-while. for covers all three forms:
// Classic
for i := 0; i < 5; i++ { fmt.Println(i) }
// While-style
n := 1
for n < 100 { n *= 2 }
// Infinite
for { break }
if has no parentheses, and can have a short init statement:
if score >= 90 {
fmt.Println("A")
} else if score >= 80 {
fmt.Println("B")
}
// Init statement — x is scoped to the if block
if x := compute(); x > 0 {
fmt.Println("positive:", x)
}
switch doesn't fall through by default — no break needed between cases:
switch day {
case "Saturday", "Sunday":
fmt.Println("Weekend")
default:
fmt.Println("Weekday")
}
Slices — Go's Everyday List
A sliceSliceA dynamic, resizable view into an array — Go's everyday list type. is Go's dynamic list. If you use Python lists, JS arrays, or Java's ArrayList, this is your equivalent — and the most-used data structure in Go.
langs := []string{"Python", "TypeScript", "Go"}
langs = append(langs, "Rust") // always assign append back
fmt.Println(langs) // [Python TypeScript Go Rust]
fmt.Println(len(langs)) // 4
fmt.Println(langs[1:3]) // [TypeScript Go]
Under the hood, a slice is a view into an array with a length and a capacity. append grows the capacity automatically when needed. Use make when you know the expected size upfront:
scores := make([]int, 0, 10) // length 0, capacity 10 — avoids reallocation
for i := 1; i <= 5; i++ {
scores = append(scores, i*10)
}
range is the idiomatic way to iterate — it works on slices, maps, strings, and channels:
for i, lang := range langs {
fmt.Printf("%d: %s\n", i, lang)
}
for _, lang := range langs { // _ discards the index
fmt.Println(lang)
}
Maps — Key-Value Data
A mapMapA built-in key-value store — Go's equivalent of a dictionary or object. is Go's built-in key-value type. No imports needed.
roles := map[string]string{
"nanda": "engineer",
"alice": "designer",
}
roles["bob"] = "manager" // add / update
delete(roles, "alice") // remove
// Safe key check — always use the two-value form
role, ok := roles["unknown"]
if !ok {
fmt.Println("not found")
}
The two-value value, ok := m[key] is important: accessing a missing key with just value := m[key] silently returns the zero value ("" for strings) — which can hide bugs. Always use ok when the key might not exist.
Maps must be initialized before writing:
var counts map[string]int // nil — reads ok, writes panic
counts = make(map[string]int)
counts["visits"]++
Functions & Error Handling
This is where Go looks most different from languages you've used before.
Go has no exceptions. Errors are plain return values — the convention is (result, error). The caller must handle the error explicitly; you can't silently ignore it (assigning to _ is the only way to discard it, and it's obvious when you do).
In real code, wrap errors with context using fmt.Errorf:
func getUser(id int) (*User, error) {
user, err := db.Find(id)
if err != nil {
return nil, fmt.Errorf("getUser %d: %w", id, err)
}
return user, nil
}
%w wraps the original error so callers can inspect the chain with errors.Is() and errors.As(). At first this feels verbose. After a while, you realize you always know exactly where an error came from.
Go Ships With
Before any third-party library, check the standard library — it covers a lot:
| Package | What it does |
|---|---|
| fmt | Formatting and printing |
| os | File system, env variables, exit |
| net/http | HTTP server and client |
| encoding/json | JSON encode/decode |
| strings | String manipulation |
| strconv | String ↔ number conversion |
| sync | Mutex, WaitGroup, Once |
| time | Time, duration, formatting |
| testing | Unit test runner (built-in) |
| errors | Error creation and wrapping |
No npm install, no pip install. These are available in every Go project by default.
Practice
Build this before Session 2:
// Role manager:
// 1. map[string]string for username → role
// 2. assign(m, user, role string) error — error if user already has a role
// 3. promote(m, user, newRole string) error — error if user doesn't exist
// 4. list(m) — print all users and their roles
//
// Stretch: rewrite using []struct{ Name, Role string } instead of a map
Key Takeaways
- Every file belongs to a packagePackageA named collection of Go files that share the same namespace..
package main= executable entry point - Unused imports → compile error (not a warning)
- Capital letter = exportedExported NameAny identifier starting with a capital letter is public — accessible from other packages. (public). Lowercase = unexported. No keywords
:=is idiomatic — Go infers the type. Zero valuesZero ValuesGo's default values for variables — 0, "", false — instead of null. replace nullforis the only loop.switchdoesn't fall through- SlicesSliceA dynamic, resizable view into an array — Go's everyday list type.:
append(),range,make()for pre-allocation - MapsMapA built-in key-value store — Go's equivalent of a dictionary or object.:
make()before writing,value, okfor safe reads - Errors are return values — no exceptions, no hidden control flow
- The standard library covers HTTP, JSON, testing, and more out of the box