- Each topic explains What, Why, When, Where, and How.
- Each topic has one complete, self-contained example.
- Unless a topic says otherwise, save the example as
main.goand run:
go run main.go- Examples are independent. Do not paste all examples into one Go file.
- The package and testing topics use a tiny multi-file module because those concepts naturally require it.
- Keep the examples simple first. Add complexity only after the foundation is clear.
You only need basic programming knowledge. No previous Go experience is required.
What: The Go toolchain gives you the compiler and common development commands such as run, build, test, fmt, and module management. A module is the basic project unit and is described by a go.mod file.
Why: You need a repeatable project structure and a reliable way to build and run Go code.
When: Do this when starting a new Go project.
Where: Run the commands in a terminal. You can use VS Code or any editor for the .go files.
How: Verify Go, create a directory, initialize a module, create main.go, and run it.
# Verify that Go is installed.
go version
# Create a new project directory.
mkdir hello-go
cd hello-go
# Create the module definition file: go.mod.
go mod init example.com/hello-go
# After creating main.go from the code below, run the program.
go run .
# Format all Go files in the module.
go fmt ./...
# Build a binary in the current directory.
go build .Create main.go:
package main
import "fmt"
func main() {
// package main + func main() define an executable entry point.
fmt.Println("Hello, Gophers!")
}VS Code with the Go extension is a convenient setup because it can provide formatting, completion, diagnostics, debugging, and test integration. The editor is only a development tool; your Go program still builds and runs with the Go toolchain.
Get comfortable using built-in help:
go help
go help mod
go doc fmt.PrintlnGo is strongly typed. The early foundation is built from strings, numbers, booleans, variables, conversions, operators, constants, and pointers. The error type is introduced here conceptually, but full error handling is covered later to avoid duplication.
What: A string stores UTF-8 text. Go supports interpreted strings with double quotes and raw strings with backticks.
Why: Most programs need names, messages, paths, JSON/XML fragments, user input, and other text.
When: Use normal quoted strings when escape sequences such as \n should be interpreted. Use raw strings when you want text kept almost exactly as written.
Where: Strings appear in CLI programs, web services, configuration, logging, APIs, and business data.
How: Use "..." for interpreted strings and backticks for raw strings.
package main
import "fmt"
func main() {
// \n is interpreted as a newline.
interpreted := "Hello\nGo"
// A raw string keeps the backslash and n as normal characters.
raw := `Hello\nGo`
// Raw strings can also span multiple lines.
multiLine := `line one
line two`
fmt.Println(interpreted)
fmt.Println(raw)
fmt.Println(multiLine)
}What: Go has signed integers such as int, unsigned integers such as uint, floating-point types float32 and float64, and complex types complex64 and complex128.
Why: Different numeric types model whole numbers, non-negative whole numbers, decimal values, and complex-number calculations.
When: Use int for normal whole-number work and float64 for most decimal calculations unless you have a specific reason to choose another size.
Where: Counters, IDs, quantities, measurements, prices, calculations, statistics, and scientific code.
How: Declare the type explicitly when the exact type matters; otherwise Go can infer common defaults.
package main
import "fmt"
func main() {
age := 30 // inferred as int
var count uint = 12 // unsigned integer
price := 19.95 // inferred as float64
var ratio float32 = 2.5
number := complex(2, 3) // 2 + 3i
fmt.Printf("age=%d type=%T\n", age, age)
fmt.Printf("count=%d type=%T\n", count, count)
fmt.Printf("price=%.2f type=%T\n", price, price)
fmt.Printf("ratio=%.1f type=%T\n", ratio, ratio)
fmt.Printf("complex=%v type=%T\n", number, number)
}What: A Boolean value is either true or false.
Why: Booleans let programs represent yes/no states and the results of comparisons.
When: Use them for flags, validation results, feature states, permissions, and conditions.
Where: They are especially common in if, for, and other control-flow decisions.
How: Assign true or false, or create a Boolean from a comparison.
package main
import "fmt"
func main() {
isReady := true
age := 20
// A comparison produces a Boolean result.
isAdult := age >= 18
fmt.Println("ready:", isReady)
fmt.Println("adult:", isAdult)
}What: Variables let a program remember data. Go supports explicit declarations, declarations with initialization, inferred types, and short declaration syntax.
Why: Programs need named values that can be read and changed over time.
When: Use var when you want an explicit type or the type's zero value. Use := inside functions when the initial value is already known.
Where: Variables can exist at package level or inside functions. Local variables must be used.
How: Common forms are var x int, var x = 10, and x := 10.
package main
import "fmt"
func main() {
var name string // zero value is ""
var count int // zero value is 0
var active bool // zero value is false
var city = "Tokyo" // type inferred as string
score := 95 // short declaration, inferred as int
name = "Asha"
count = 3
active = true
fmt.Println(name, count, active, city, score)
}What: A type conversion changes a value from one Go type to another compatible type.
Why: Go does not silently convert between different numeric types. Explicit conversion makes the programmer's intent clear.
When: Use a conversion when an API, calculation, or assignment requires a different type.
Where: Common places include integer/float calculations and values coming from APIs or storage formats.
How: Write the destination type like a function: float64(value).
package main
import "fmt"
func main() {
whole := 42
// Go requires an explicit conversion from int to float64.
decimal := float64(whole)
// Converting a float variable to an int drops the fractional part.
fraction := 9.8
truncated := int(fraction)
fmt.Printf("whole=%d type=%T\n", whole, whole)
fmt.Printf("decimal=%.1f type=%T\n", decimal, decimal)
fmt.Printf("truncated=%d type=%T\n", truncated, truncated)
}What: Arithmetic operators calculate values; comparison operators compare values and return Booleans.
Why: They provide the basic building blocks for calculations and decisions.
When: Use arithmetic for calculations and comparisons before branching or validating conditions.
Where: Business rules, counters, validation, loops, statistics, and algorithms.
How: Common arithmetic operators are +, -, *, /, %. Common comparisons are ==, !=, <, <=, >, >=.
package main
import "fmt"
func main() {
a, b := 10, 3
fmt.Println("add:", a+b)
fmt.Println("subtract:", a-b)
fmt.Println("multiply:", a*b)
fmt.Println("integer divide:", a/b) // 10 / 3 becomes 3
fmt.Println("remainder:", a%b) // remainder is 1
fmt.Println("equal:", a == b)
fmt.Println("not equal:", a != b)
fmt.Println("greater:", a > b)
fmt.Println("less or equal:", a <= b)
// Convert first when you want decimal division.
fmt.Println("decimal divide:", float64(a)/float64(b))
}What: A constant is a compile-time value that cannot be changed. iota creates related integer constants inside a constant block.
Why: Constants give stable names to values and reduce accidental changes. iota is useful for simple enumerated values.
When: Use constants for fixed configuration values, states, limits, and related symbolic values.
Where: Package configuration, status values, flags, sizes, and protocol constants.
How: Use const. Inside a constant block, iota starts at 0 and increments by position.
package main
import "fmt"
type Status int
const AppName = "Go Foundation"
const (
Pending Status = iota // 0
Running // 1: repeats the previous expression
Done // 2
)
const MaxRetries = 2 * 3 // constant expression evaluated at compile time
func main() {
fmt.Println(AppName)
fmt.Println("statuses:", Pending, Running, Done)
fmt.Println("max retries:", MaxRetries)
}What: A pointer stores the memory address of another value. &value gets an address and *pointer accesses the value at that address.
Why: Pointers let functions and other code share and modify the same data instead of working on a copy.
When: Use a pointer when shared mutation is intentional or an API requires one. Prefer values when copying is enough.
Where: Function parameters, mutable structs, shared state, and some standard-library APIs.
How: Take an address with & and dereference with *.
package main
import "fmt"
func main() {
message := "Hello"
// p stores the address of message.
p := &message
fmt.Println("original:", message)
fmt.Println("through pointer:", *p)
// Dereferencing lets us update the original value.
*p = "Hello, Gophers!"
fmt.Println("updated:", message)
}What: A CLI program communicates through standard input and standard output.
Why: CLIs are simple, scriptable, and very common for developer, operations, and DevOps tools.
When: Use a CLI when users or scripts should run commands from a terminal.
Where: The standard library packages os, bufio, fmt, and strings are common building blocks.
How: Print a prompt, read input, process it, and print a result.
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
func main() {
fmt.Print("What should I shout? ")
// Wrap standard input so we can read a full line easily.
reader := bufio.NewReader(os.Stdin)
text, err := reader.ReadString('\n')
if err != nil {
fmt.Println("input error:", err)
return
}
// Remove the newline and convert the text to uppercase.
text = strings.TrimSpace(text)
fmt.Println(strings.ToUpper(text) + "!")
}Run it and type a line when prompted:
go run main.goWhat: A web service receives HTTP requests and sends HTTP responses. In Go, the net/http package provides the core server tools.
Why: Go makes small network services easy to build using the standard library.
When: Use a web service when browsers, mobile apps, other services, or automation need to call your program over HTTP.
Where: API servers, internal services, microservices, health endpoints, and web backends.
How: Register a handler, then start an HTTP server.
package main
import (
"fmt"
"log"
"net/http"
)
func helloHandler(w http.ResponseWriter, r *http.Request) {
// The handler receives the request and writes the response.
fmt.Fprintln(w, "Hello from Go!")
}
func main() {
// Route /hello requests to helloHandler.
http.HandleFunc("/hello", helloHandler)
fmt.Println("Server listening on http://localhost:8080/hello")
// ListenAndServe blocks while the server is running.
log.Fatal(http.ListenAndServe(":8080", nil))
}Run with go run main.go, then request /hello from a browser or HTTP client. Stop the server with Ctrl+C.
What: Debugging means pausing a running program and inspecting what it is doing.
Why: It helps you understand unexpected behavior faster than adding many print statements.
When: Use a debugger when the program compiles but produces the wrong result or follows an unexpected path.
Where: VS Code and other Go-aware editors can show breakpoints, variables, watches, call stacks, and step controls.
How: Put a breakpoint inside a function, start the debugger, inspect variables, then step through the code.
package main
import "fmt"
func finalPrice(price, discount float64) float64 {
// Good breakpoint location: inspect price and discount here.
saved := price * discount
result := price - saved
return result
}
func main() {
price := 100.0
discount := 0.20
result := finalPrice(price, discount)
fmt.Println("final price:", result)
}Useful debugger actions:
- Set a breakpoint on
saved := price * discount. - Start debugging.
- Inspect
price,discount, andsaved. - Use Step Over to execute the next line.
- Add a watch such as
price * discount. - Inspect the call stack to see how execution reached the function.
Aggregate types hold multiple related values. Go's foundation includes arrays, slices, maps, and structs.
What: An array is a fixed-length collection whose elements all have the same type.
Why: Arrays give predictable size and value semantics.
When: Use an array when the number of elements is known and fixed. In everyday Go code, slices are usually more flexible.
Where: Fixed-size buffers, small known collections, and cases where array value-copy behavior is useful.
How: The length is part of the type: [3]int and [4]int are different types.
package main
import "fmt"
func main() {
scores := [3]int{90, 85, 88}
// Access and update by zero-based index.
scores[1] = 95
// Assigning an array makes a copy.
copied := scores
copied[0] = 50
fmt.Println("original:", scores)
fmt.Println("copy:", copied)
fmt.Println("length:", len(scores))
}What: A slice is a dynamically sized view over an underlying array.
Why: Slices provide the flexibility needed for most variable-length collections in Go.
When: Use a slice when the number of values can grow, shrink, or is not known in advance.
Where: Lists of records, parsed input, API results, batches, buffers, and most collection-style code.
How: Create a slice with a literal or make, add values with append, and inspect len and cap.
package main
import "fmt"
func main() {
// Start empty, with room for three strings before growth is needed.
names := make([]string, 0, 3)
names = append(names, "Asha")
names = append(names, "Ben", "Chen")
fmt.Println("names:", names)
fmt.Println("length:", len(names))
fmt.Println("capacity:", cap(names))
// Slice expressions can create a view over part of a slice.
firstTwo := names[:2]
fmt.Println("first two:", firstTwo)
}What: A map stores key/value pairs. You choose both the key type and the value type.
Why: Maps provide fast lookup by meaningful keys instead of only numeric indexes.
When: Use a map for dictionaries, lookup tables, counters, caches, settings, and keyed records.
Where: Anywhere you need to find a value by a key such as a username, ID, code, or label.
How: Create a map with make or a literal, assign with m[key] = value, check presence with the comma-ok form, and remove with delete.
package main
import "fmt"
func main() {
scores := map[string]int{
"Asha": 90,
"Ben": 82,
}
// Add or update a value.
scores["Chen"] = 95
scores["Ben"] = 88
// The second result tells us whether the key existed.
score, ok := scores["Asha"]
fmt.Println("Asha:", score, "found:", ok)
// Remove one key.
delete(scores, "Ben")
fmt.Println("all scores:", scores)
}What: A struct groups named fields, and those fields may have different types.
Why: Structs let you model real entities as one coherent value.
When: Use a struct when several pieces of data belong together, such as a user, order, vehicle, or configuration.
Where: Domain models, API request/response values, configuration, database records, and internal program state.
How: Define a struct type, create a value with a composite literal, and access fields with dot notation.
package main
import "fmt"
type User struct {
ID int
Username string
Active bool
}
func main() {
user := User{
ID: 101,
Username: "gopher",
Active: true,
}
// Access or update fields with dot notation.
user.Username = "go-learner"
fmt.Println("id:", user.ID)
fmt.Println("username:", user.Username)
fmt.Println("active:", user.Active)
}What: Go uses the for keyword for all normal looping forms.
Why: Loops repeat work without duplicating code.
When: Use a loop for counters, retries, repeated processing, polling, or iterating until a condition changes.
Where: Algorithms, collection processing, workers, input processing, and repeated business logic.
How: Go supports a classic three-part loop, a condition-only loop, and an infinite loop controlled with break.
package main
import "fmt"
func main() {
// Classic counted loop.
for i := 1; i <= 3; i++ {
fmt.Println("counted:", i)
}
// Condition-only loop.
n := 3
for n > 0 {
fmt.Println("countdown:", n)
n--
}
// Infinite loop with an explicit exit.
attempts := 0
for {
attempts++
if attempts == 2 {
break
}
}
fmt.Println("attempts:", attempts)
}What: range iterates over values in arrays, slices, strings, maps, and channels.
Why: It removes manual index management for common collection loops.
When: Use it when you want each element or each key/value pair in a collection.
Where: Data processing, aggregation, searching, transformation, and reporting.
How: For slices, range can return index and value. For maps, it can return key and value.
package main
import "fmt"
func main() {
names := []string{"Asha", "Ben", "Chen"}
// i is the index and name is the value.
for i, name := range names {
fmt.Println(i, name)
}
scores := map[string]int{"Asha": 90, "Ben": 82}
// key and value are returned for each map entry.
for name, score := range scores {
fmt.Println(name, score)
}
}What: An if statement conditionally runs code when a Boolean expression is true.
Why: Programs need to choose different behavior based on data and conditions.
When: Use if for validation, guard clauses, simple decisions, and a small number of branches.
Where: Input validation, permissions, error checks, calculations, and business rules.
How: Parentheses around the condition are not required. else if and else provide additional branches.
package main
import "fmt"
func main() {
score := 87
if score >= 90 {
fmt.Println("grade A")
} else if score >= 80 {
fmt.Println("grade B")
} else {
fmt.Println("grade C or below")
}
}What: A switch selects one branch from several cases. A switch without an expression acts like a clean chain of Boolean conditions.
Why: It is often easier to read than a long if / else if chain.
When: Use a value switch when comparing one value against several choices. Use a logical switch when cases are different conditions.
Where: Menus, state handling, command routing, categorization, and validation.
How: Cases do not fall through by default in Go.
package main
import "fmt"
func main() {
command := "start"
// Value switch.
switch command {
case "start":
fmt.Println("starting")
case "stop":
fmt.Println("stopping")
default:
fmt.Println("unknown command")
}
temperature := 31
// Logical switch: each case is a Boolean expression.
switch {
case temperature >= 35:
fmt.Println("very hot")
case temperature >= 25:
fmt.Println("warm")
default:
fmt.Println("cool")
}
}What: defer schedules a function call to run when the surrounding function is returning.
Why: It keeps cleanup close to the code that acquires a resource and makes cleanup harder to forget.
When: Use it for closing files, unlocking mutexes, stopping timers, and other guaranteed cleanup.
Where: Functions that acquire resources or need a final action before returning.
How: Deferred calls run in last-in, first-out order.
package main
import "fmt"
func work() {
fmt.Println("work starts")
defer fmt.Println("cleanup 1")
defer fmt.Println("cleanup 2")
fmt.Println("work ends")
}
func main() {
work()
}What: panic stops normal execution and begins unwinding the call stack. recover can intercept a panic from inside a deferred function.
Why: They provide an emergency mechanism for situations where normal execution cannot safely continue.
When: Use panic rarely, for truly exceptional or unstable states. Do not use it as normal error handling.
Where: Internal invariants, initialization failures that make the program unusable, or narrow recovery boundaries.
How: Put recover inside a deferred function.
package main
import "fmt"
func riskyOperation() {
panic("unexpected internal state")
}
func safeBoundary() {
// recover must run from a deferred function.
defer func() {
if value := recover(); value != nil {
fmt.Println("recovered:", value)
}
}()
riskyOperation()
fmt.Println("this line is skipped")
}
func main() {
safeBoundary()
fmt.Println("program continues after recovery")
}What: goto jumps execution to a label in the same function.
Why: It can express a very small number of low-level control-flow patterns directly.
When: Rarely. Prefer normal loops, functions, break, and continue for ordinary application code.
Where: Occasionally in generated code or tightly controlled low-level logic.
How: Define a label with name: and jump with goto name.
package main
import "fmt"
func main() {
attempts := 0
retry:
attempts++
fmt.Println("attempt", attempts)
if attempts < 3 {
goto retry
}
fmt.Println("done")
}What: A function groups reusable behavior behind a name and a signature. A signature describes parameters and return values.
Why: Functions split large programs into focused, testable, maintainable units.
When: Create a function when a piece of logic has one clear responsibility, is reused, or makes the caller easier to read.
Where: Functions can exist at package level. They may take values, pointers, and variadic parameters, and they may return one or multiple values.
How: Start with func, then the function name, parameters, optional return types, and a body. Go supports single returns, multiple returns, named returns, and variadic parameters.
package main
import (
"fmt"
"strings"
)
// summarize demonstrates normal, pointer, and variadic parameters,
// plus multiple return values.
func summarize(label string, callCount *int, scores ...int) (string, int) {
(*callCount)++ // update shared memory through the pointer
total := 0
for _, score := range scores {
total += score
}
return strings.ToUpper(label), total
}
func main() {
calls := 0
label, total := summarize("team score", &calls, 10, 20, 30)
fmt.Println(label)
fmt.Println("total:", total)
fmt.Println("function calls:", calls)
}Note: In Go,
*callCount++is parsed as(*callCount)++, so it increments the integer pointed to bycallCount.
What: A package groups related Go source files. A module can contain one or many packages.
Why: Packages separate responsibilities and provide a public API between parts of a program.
When: Create another package when code represents a distinct concern that should be reusable or isolated from main.
Where: A package normally maps to a directory containing one or more .go files. Package-level names beginning with an uppercase letter are exported; lowercase names are package-private.
How: This copy/paste example creates a tiny module with a greet package and a main package.
mkdir -p package-demo/greet
cd package-demo
go mod init example.com/package-demo
cat > greet/greet.go <<'GOFILE'
package greet
// Hello is exported because its name starts with H.
func Hello(name string) string {
return prefix() + ", " + name
}
// prefix is private to package greet because it starts lowercase.
func prefix() string {
return "Hello"
}
GOFILE
cat > main.go <<'GOFILE'
package main
import (
"fmt"
"example.com/package-demo/greet"
)
func main() {
// main can use the exported Hello function.
fmt.Println(greet.Hello("Gopher"))
}
GOFILE
go run .What: Go supports line comments with // and block comments with /* ... */. Documentation comments describe exported package members.
Why: Good documentation explains the public API without forcing readers to inspect implementation details.
When: Document exported types, functions, constants, variables, and important package behavior.
Where: Documentation lives next to the code it describes and can be read with Go documentation tools.
How: Start a documentation comment with the name of the item being documented.
package main
import "fmt"
// FahrenheitToCelsius converts a Fahrenheit temperature to Celsius.
func FahrenheitToCelsius(f float64) float64 {
return (f - 32) * 5 / 9
}
func main() {
fmt.Printf("%.1f C\n", FahrenheitToCelsius(86))
}Run the program normally, then inspect the function documentation from the same directory:
go run main.go
go doc FahrenheitToCelsiusWhat: A method is a function associated with a named type through a receiver.
Why: Methods express that a behavior belongs closely to a type.
When: Use a method when an operation naturally belongs to a value, such as calculating an area or updating an account.
Where: Methods are commonly attached to structs, but receivers can be other defined types too.
How: Put the receiver between func and the method name. Use a pointer receiver when the method must modify the original value.
package main
import "fmt"
type Rectangle struct {
Width float64
Height float64
}
// Area uses a value receiver because it only reads the rectangle.
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
// Scale uses a pointer receiver because it changes the rectangle.
func (r *Rectangle) Scale(factor float64) {
r.Width *= factor
r.Height *= factor
}
func main() {
box := Rectangle{Width: 4, Height: 3}
fmt.Println("area:", box.Area())
box.Scale(2)
fmt.Println("scaled area:", box.Area())
}What: An interface describes behavior as a set of method signatures. A concrete type satisfies an interface automatically by having the required methods.
Why: Interfaces let functions work with behavior instead of depending on one concrete type.
When: Use an interface when multiple types should be accepted through the same API.
Where: I/O, storage, logging, formatting, testing seams, and application boundaries.
How: Define the required method set, then accept the interface type in a function.
package main
import "fmt"
type Printer interface {
Print() string
}
type User struct {
Name string
}
func (u User) Print() string {
return "user: " + u.Name
}
type MenuItem struct {
Name string
}
func (m MenuItem) Print() string {
return "menu item: " + m.Name
}
func show(p Printer) {
// show does not care about the concrete type.
fmt.Println(p.Print())
}
func main() {
show(User{Name: "Asha"})
show(MenuItem{Name: "Tea"})
}What: A type assertion asks what concrete value is currently stored inside an interface. A type switch handles several possible concrete types cleanly.
Why: Sometimes you intentionally use an interface but later need behavior that exists only on a specific concrete type.
When: Use assertions sparingly at boundaries where concrete type information is genuinely needed.
Where: Interface-heavy APIs, decoded values, adapters, and generic boundary code.
How: Use value, ok := x.(Type) for one type or switch v := x.(type) for several.
package main
import "fmt"
func describe(value any) {
switch v := value.(type) {
case string:
fmt.Println("string:", v)
case int:
fmt.Println("int:", v)
default:
fmt.Printf("other type: %T\n", v)
}
}
func main() {
var value any = "Go"
// Safe assertion: ok is false instead of panicking on a mismatch.
text, ok := value.(string)
fmt.Println("asserted:", text, "ok:", ok)
describe(value)
describe(42)
describe(true)
}What: Generics let one function work with several concrete types while keeping compile-time type safety.
Why: They remove duplicated implementations when the algorithm is the same for a family of types.
When: Use generics when repeated strongly typed code differs mainly by type, not by behavior.
Where: Reusable collection helpers, algorithms, containers, cloning, searching, and numeric utilities.
How: Define type parameters in square brackets. any accepts any type, comparable accepts values that support == and !=, and custom constraints can define a permitted type set.
package main
import "fmt"
// Number is a custom type constraint.
type Number interface {
~int | ~float64
}
func Sum[T Number](values []T) T {
var total T
for _, value := range values {
total += value
}
return total
}
func Contains[T comparable](values []T, wanted T) bool {
for _, value := range values {
if value == wanted {
return true
}
}
return false
}
func main() {
fmt.Println("int sum:", Sum([]int{1, 2, 3}))
fmt.Println("float sum:", Sum([]float64{1.5, 2.5}))
fmt.Println("contains Go:", Contains([]string{"Go", "Rust"}, "Go"))
}What: In Go, an error is a normal value representing an unsuccessful operation. The built-in error interface is the conventional error type, and nil means no error.
Why: Treating errors as values keeps failure handling visible in function signatures and normal control flow.
When: Return an error when an operation can reasonably fail and the caller can decide what to do next.
Where: File I/O, parsing, network calls, validation, database work, APIs, and business operations.
How: By convention, return the error last. Callers normally check if err != nil before continuing on the success path.
package main
import (
"errors"
"fmt"
)
func divide(a, b float64) (float64, error) {
if b == 0 {
// Errors are values: create one and return it.
return 0, errors.New("cannot divide by zero")
}
return a / b, nil
}
func main() {
result, err := divide(10, 0)
if err != nil {
// Handle the failure first.
fmt.Println("error:", err)
return
}
fmt.Println("result:", result)
}- Error: the requested operation failed, but the program is still in a normal, manageable state.
- Panic: the program reached an exceptional state where normal execution should stop.
- Prefer errors for expected operational failures.
- Use panic rarely.
- If a boundary must recover from a panic, convert it into normal error handling as close to that boundary as practical.
Concurrency means a program can make progress on more than one task. It is not the same as parallelism, which means tasks are physically executing at the same instant. Go lets you write concurrent programs; the Go runtime and operating system decide how they are scheduled.
Go's concurrency style is strongly influenced by Communicating Sequential Processes (CSP): independent tasks can communicate by sending values through channels instead of relying only on shared memory.
The core foundation is:
- Goroutine - start concurrent work.
- WaitGroup - wait for a known set of goroutines to finish.
- Channel - communicate values between goroutines.
- Select - wait on multiple channel operations.
- Range over a channel - consume a stream until the channel closes.
What: A goroutine is a function executing concurrently with other work in the same Go program.
Why: Goroutines make it inexpensive and simple to express independent tasks.
When: Use them for independent I/O, background work, request handling, pipelines, and concurrent tasks.
Where: Servers, workers, network applications, background jobs, and pipelines.
How: Put go before a function call. The short sleep below is only to keep this first example focused on the go keyword; use proper synchronization such as a WaitGroup or channel in real code.
package main
import (
"fmt"
"time"
)
func say(message string) {
fmt.Println(message)
}
func main() {
// Start say in a new goroutine.
go say("hello from goroutine")
// Main continues immediately.
fmt.Println("hello from main")
// Demo only: give the goroutine time to finish.
time.Sleep(50 * time.Millisecond)
}What: A sync.WaitGroup waits until a known group of goroutines has completed.
Why: main must not exit before required concurrent work is finished.
When: Use a WaitGroup when you launch a known number of goroutines and need all of them to complete.
Where: Parallel-style batch work, fan-out jobs, startup/shutdown coordination, and worker tasks.
How: Call Add before starting work, Done when each goroutine finishes, and Wait where execution must pause.
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
for worker := 1; worker <= 3; worker++ {
wg.Add(1)
go func(id int) {
defer wg.Done() // always mark this worker complete
fmt.Println("worker", id, "done")
}(worker)
}
// Wait until all three goroutines call Done.
wg.Wait()
fmt.Println("all workers finished")
}What: A channel is a typed communication path between goroutines.
Why: Channels let concurrent tasks exchange values and coordinate without manually sharing every piece of memory.
When: Use a channel when one goroutine produces information another goroutine needs.
Where: Pipelines, worker results, event flow, handoffs, and producer/consumer designs.
How: Create a channel with make(chan Type), send with ch <- value, and receive with <-ch.
package main
import "fmt"
func main() {
messages := make(chan string)
go func() {
// Send one value to the channel.
messages <- "work complete"
}()
// Receive waits until a value is available.
message := <-messages
fmt.Println(message)
}What: select waits for one of several channel operations to become ready.
Why: Concurrent programs often need to react to whichever event or message arrives first.
When: Use it when a goroutine must listen to multiple channels or combine communication with a timeout/default path.
Where: Network services, cancellation, multiplexing, worker coordination, and event-driven code.
How: Write channel operations as case clauses inside select.
package main
import "fmt"
func main() {
first := make(chan string, 1)
second := make(chan string, 1)
// Buffered channels let these sends complete immediately.
first <- "message from first"
second <- "message from second"
// Both cases are ready; select chooses one ready case.
select {
case message := <-first:
fmt.Println(message)
case message := <-second:
fmt.Println(message)
}
}What: A for range loop can receive values from a channel until that channel is closed.
Why: It is a clean way to consume a stream of results without manually checking for every receive.
When: Use it when a producer sends a sequence of values and has a clear point where production ends.
Where: Pipelines, work queues, generated results, and streaming data between goroutines.
How: The sending side closes the channel. The receiver ranges over it until no more values remain.
package main
import "fmt"
func produce(numbers chan<- int) {
// Send a small stream of values.
for i := 1; i <= 3; i++ {
numbers <- i
}
// Closing tells receivers that no more values will arrive.
close(numbers)
}
func main() {
numbers := make(chan int)
go produce(numbers)
// The loop stops automatically after the channel is closed and drained.
for number := range numbers {
fmt.Println(number)
}
}What: Tests are Go functions that verify program behavior. Test files end in _test.go, and normal tests begin with Test.
Why: Tests catch mistakes, protect existing behavior from regressions, and make future changes safer.
When: Write tests for important logic, edge cases, bug fixes, and behavior that must remain stable.
Where: Keep tests close to the package they test. Unit tests cover small pieces; broader integration and end-to-end tests cover larger combinations of the system.
How: Use the testing package, receive *testing.T, arrange the input, act by calling the code, and assert the result. Run with go test.
The course introduces three broad concerns:
- Correctness: normal tests, fuzz tests, and example tests.
- Performance: benchmarks.
- Resource usage/diagnostics: profiling and tracing.
For a foundation, start with normal unit tests.
This is a complete copy/paste example:
mkdir test-demo
cd test-demo
go mod init example.com/test-demo
cat > math_test.go <<'GOFILE'
package mathdemo
import "testing"
// Add is the small unit we want to verify.
func Add(a, b int) int {
return a + b
}
func TestAdd(t *testing.T) {
// Arrange.
left := 1
right := 2
expected := 3
// Act.
got := Add(left, right)
// Assert.
if got != expected {
t.Errorf("Add(%d, %d) = %d; expected %d", left, right, got, expected)
}
}
GOFILE
# Run tests in the current package.
go test -v
# From a module root, this form runs tests in all subdirectories too.
go test ./...go version # show Go version
go mod init NAME # create a module
go run . # compile and run the current main package
go build . # build the current package
go fmt ./... # format Go code
go test ./... # run all tests in the module
go doc ITEM # read Go documentationname := "Go" // short variable declaration
var count int // explicit variable, zero value 0
const Max = 10 // constant
numbers := []int{1, 2, 3} // slice
lookup := map[string]int{"a": 1} // map
pointer := &count // address of count
*pointer = 5 // change count through pointer
if condition {
// ...
}
for i := 0; i < 3; i++ {
// ...
}
for _, value := range values {
_ = value
}
switch value {
case 1:
// ...
default:
// ...
}
func add(a, b int) int {
return a + b
}
result, err := doWork()
if err != nil {
// handle failure
return
}
_ = result
go doWork() // start a goroutine
value := <-channel // receive from channel
channel <- value // send to channel
Once these foundations are comfortable, the natural next steps are larger application structure, deeper testing, contexts and cancellation, richer HTTP services, file and JSON handling, synchronization patterns, profiling, and production-quality concurrency patterns.
The key foundation is simpler:
Write clear code. Prefer explicit behavior. Keep functions and packages focused. Treat errors as values. Share memory only when you mean to. Use goroutines and channels to express concurrency clearly.