Day 4 — Functions, returns & defer
Day 4 — Functions, returns & defer
Stage I · ~3h (theory-heavy)
Goal: Make functions the unit of design—multiple returns, named results, defer timing, and first-class function values—then refactor a small pipeline so main only wires and exits.
Go has no exceptions for ordinary failure. The idiomatic channel is (T, error). Panic is for truly exceptional situations (Day 14). Today builds the habits that make panic rare.
Why this day exists
Most Go APIs are shaped by function conventions:
- Multiple return values instead of out-params or exceptions
erroras the last return value
deferfor cleanup that must run on every path
- Small functions composed into pipelines
If you treat functions like C (single return, manual cleanup everywhere) or like Java (exceptions), you will fight the language. Day 4 aligns muscle memory with the stdlib.
Theory 1 — Declaration, parameters, and results
Signature anatomy
func Name(param1 Type1, param2 Type2) (Result1, Result2) {
return value1, value2
}Same-type parameters can share a type:
func add(a, b int) int { return a + b }Multiple returns
func div(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero")
}
return a / b, nil
}Call site:
q, err := div(10, 2)
if err != nil {
return err
}Blank identifier
q, _ := div(10, 2) // discard error — almost always wrong in real code
_, err := os.Stat(path)Use _ deliberately (e.g., you truly need only one result), never to silence errors you should handle.
Named result parameters
func lookup(id string) (name string, err error) {
name, ok := db[id]
if !ok {
err = fmt.Errorf("id %q not found", id)
return // naked return
}
return name, nil
}Named results:
- Document intent at the signature
- Are zero-initialized
- Enable naked returns (
returnalone)
Judgment: naked returns are fine in short functions; in long ones they obscure what is returned. Prefer explicit return name, err for clarity when the body grows.
Theory 2 — Errors as values (preview that you use today)
func readConfig(path string) ([]byte, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read config %s: %w", path, err)
}
return data, nil
}Conventions:
| Rule | Why |
|---|---|
error is last |
Matches entire stdlib |
nil error = success |
Check err != nil first |
Wrap with %w |
Enables errors.Is / As later (Day 13) |
| Do not panic for expected failure | Missing file, bad input, network blip |
Today: return and check. Full wrapping toolkit on Day 13.
Theory 3 — defer: schedule, LIFO, and arguments
What defer does
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()defer registers a call to run when the surrounding function returns—normally or via panic (before the panic continues unwinding).
LIFO order
func order() {
defer fmt.Println("first deferred") // runs second
defer fmt.Println("second deferred") // runs first
fmt.Println("body")
}
// prints: body, second deferred, first deferredOpen A, defer close A; open B, defer close B → B closes before A. Nested resource lifetimes stay correct.
Arguments are evaluated at defer time
func trace() {
x := 1
defer fmt.Println("x=", x) // prints x= 1
x = 2
}The call’s arguments are evaluated when defer executes, not when the deferred function runs. Closures can observe later mutations:
func trace2() {
x := 1
defer func() { fmt.Println("x=", x) }() // prints x= 2
x = 2
}Common cleanup patterns
mu.Lock()
defer mu.Unlock()
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
rows, err := db.Query(q)
if err != nil {
return err
}
defer rows.Close()defer and named returns
func readAll(path string) (data []byte, err error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer func() {
cerr := f.Close()
if err == nil {
err = cerr // surface close error if read succeeded
}
}()
return io.ReadAll(f)
}This pattern matters for durable correctness. You will revisit it when writing real I/O packages.
What not to use defer for
- Replacing ordinary control flow in the middle of a hot loop (cost is small but not free; more importantly, defer runs at function end, not loop end)
- “Maybe cleanup” without clear ownership
- Business logic that callers need to observe as primary control flow
Theory 4 — Function values and closures
Functions are first-class:
func apply(nums []int, fn func(int) int) []int {
out := make([]int, len(nums))
for i, n := range nums {
out[i] = fn(n)
}
return out
}
doubled := apply([]int{1, 2, 3}, func(n int) int { return n * 2 })Closures capture variables
func counter() func() int {
n := 0
return func() int {
n++
return n
}
}
c := counter()
fmt.Println(c()) // 1
fmt.Println(c()) // 2Each call to counter() gets its own n.
Methods as values (preview)
var w io.Writer = os.Stdout
write := w.Write // method value; receiver bound
write([]byte("hi\n"))Day 7–11 deepen methods and interfaces. Today: know functions and methods can be passed around.
Theory 5 — Variadic parameters
func sum(vals ...int) int {
total := 0
for _, v := range vals {
total += v
}
return total
}
sum()
sum(1, 2, 3)
nums := []int{4, 5}
sum(nums...) // expand sliceOnly the last parameter may be variadic. Inside the function, vals has type []int.
fmt.Println and fmt.Sprintf are the classic stdlib examples.
Theory 6 — Structuring main
Idiomatic shape for CLIs:
func main() {
if err := run(os.Args[1:]); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
}
func run(args []string) error {
// all real work; return errors
return nil
}Benefits:
deferinrunworks;os.Exitinmainskips defers inmainif you exit there without returning—keeping work inrunpreserves defer semantics for cleanup registered inrun
- Testable: call
runfrom tests later without starting a process
Worked examples bank
Example A — Multiple returns + validation
package main
import (
"fmt"
"strconv"
"strings"
)
func parsePair(s string) (int, int, error) {
parts := strings.Split(s, ",")
if len(parts) != 2 {
return 0, 0, fmt.Errorf("want a,b got %q", s)
}
a, err := strconv.Atoi(strings.TrimSpace(parts[0]))
if err != nil {
return 0, 0, fmt.Errorf("left: %w", err)
}
b, err := strconv.Atoi(strings.TrimSpace(parts[1]))
if err != nil {
return 0, 0, fmt.Errorf("right: %w", err)
}
return a, b, nil
}Example B — Defer LIFO with files
func copyFile(dst, src string) (err error) {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return err
}
defer func() {
cerr := out.Close()
if err == nil {
err = cerr
}
}()
_, err = io.Copy(out, in)
return err
}Example C — Pipeline of helpers
func run(args []string) error {
if len(args) != 1 {
return fmt.Errorf("usage: tool <path>")
}
data, err := os.ReadFile(args[0])
if err != nil {
return err
}
lines := strings.Split(string(data), "\n")
summary, err := summarize(lines)
if err != nil {
return err
}
fmt.Println(summary)
return nil
}Example D — Function value table
var ops = map[string]func(int, int) int{
"+": func(a, b int) int { return a + b },
"-": func(a, b int) int { return a - b },
"*": func(a, b int) int { return a * b },
}
func eval(op string, a, b int) (int, error) {
fn, ok := ops[op]
if !ok {
return 0, fmt.Errorf("unknown op %q", op)
}
return fn(a, b), nil
}Example E — Defer argument evaluation trap
func demo() {
for i := 0; i < 3; i++ {
defer fmt.Println("arg", i) // args fixed per defer: 0,1,2 then LIFO print 2,1,0
}
}
func demoClosureWrongOldMentalModel() {
for i := 0; i < 3; i++ {
defer func() { fmt.Println("clos", i) }() // Go 1.22+: 2,1,0 still LIFO but each i per iteration
}
}Run both; write down what you observe on Go 1.26.
Labs
Suggested workspace: ~/lab/90daysofx/go/day04
Lab 1 — Refactor classifier (or units) behind run
mkdir -p ~/lab/90daysofx/go/day04
cd ~/lab/90daysofx/go/day04
go mod init example.com/day04Requirements:
mainonly callsrunand mapserror→ stderr + exit code.
- At least three helpers with
(T, error)or(…, error)signatures.
- At least one
deferthat closes a resource (os.Open+defer f.Close(), orbufioover a file).
- Usage errors return a distinct sentinel or message so
maincan exit2vs1.
Example exit policy:
func main() {
err := run(os.Args[1:])
if err == nil {
return
}
fmt.Fprintf(os.Stderr, "%v\n", err)
if errors.Is(err, errUsage) {
os.Exit(2)
}
os.Exit(1)
}Define var errUsage = errors.New("usage") or wrap with a clear prefix—keep it simple.
Lab 2 — Defer timing journal
Write a tiny program that prints:
- Body vs deferred order (LIFO)
- Argument evaluation vs closure capture
- That
os.Exit(1)frommainskips deferred functions inmain
Document findings in NOTES.md or comments.
Lab 3 — Variadic + function value
Implement func join(sep string, parts ...string) string without calling strings.Join first—then compare to strings.Join. Pass a func(string) string mapper over a slice of names (trim + lower).
Common gotchas
| Gotcha | Fix |
|---|---|
Ignoring error with _ |
Always handle or return |
defer f.Close() before checking err from Open |
Only defer after successful open |
Expecting defer at end of loop iteration |
Deferred until function returns |
os.Exit inside helper |
Return error; exit in main |
| Naked return in long function | Prefer explicit returns |
Shadowing err in nested scopes |
Reuse carefully; name inner errors if needed |
| Thinking deferred arg sees final variable value | Args evaluated at defer line; use closure if needed |
Checkpoint
- Multiple returns used idiomatically with
errlast
- Named results explained (even if you rarely naked-return)
deferLIFO demonstrated
- Argument evaluation vs closure capture understood
main/runsplit in place
- At least one real resource cleaned with
defer
- Exit codes distinguish usage vs runtime failure
Commit
git add .
git commit -m "day04: functions, defer, run() pattern"Tomorrow
Day 5 — Arrays, slices & append: the slice header, capacity, aliasing bugs, and why almost all “list” code in Go is really about slices—not arrays.