Go Functions Basics — Tasks¶
Instructions¶
Each task includes a description, starter code, expected output, and an evaluation checklist. Implement the solution as a regular Go function unless the task specifies otherwise.
Task 1 — Greeting¶
Difficulty: Beginner Topic: Function declaration, single param, single return
Description: Write greet(name string) string that returns the string "Hello, <name>!".
Starter Code:
package main
import "fmt"
func greet(name string) string {
// TODO
return ""
}
func main() {
fmt.Println(greet("Ada"))
fmt.Println(greet("Linus"))
}
Expected Output:
Evaluation Checklist: - [ ] Function signature matches func greet(name string) string - [ ] Uses string concatenation or fmt.Sprintf - [ ] Returns the exact format "Hello, <name>!"
Task 2 — Min of Three¶
Difficulty: Beginner Topic: Multiple parameters, conditional logic, single return
Description: Write min3(a, b, c int) int that returns the smallest of three integers.
Starter Code:
package main
import "fmt"
func min3(a, b, c int) int {
// TODO
return 0
}
func main() {
fmt.Println(min3(5, 2, 8)) // 2
fmt.Println(min3(-1, -3, 0)) // -3
fmt.Println(min3(7, 7, 7)) // 7
}
Expected Output:
Evaluation Checklist: - [ ] Correct for all-positive, all-negative, and equal-value inputs - [ ] Does not import external libraries - [ ] Returns an int, not a different numeric type
Task 3 — Boolean Predicate: isPalindrome¶
Difficulty: Beginner Topic: Functions returning bool, basic loop, string handling
Description: Write isPalindrome(s string) bool that returns true if s reads the same forward and backward (treat empty string as a palindrome).
Starter Code:
package main
import "fmt"
func isPalindrome(s string) bool {
// TODO
return false
}
func main() {
fmt.Println(isPalindrome("")) // true
fmt.Println(isPalindrome("a")) // true
fmt.Println(isPalindrome("racecar")) // true
fmt.Println(isPalindrome("hello")) // false
fmt.Println(isPalindrome("madam")) // true
}
Expected Output:
Evaluation Checklist: - [ ] Handles the empty string and single-character strings - [ ] Works on ASCII (extra credit: handle Unicode by converting to []rune first) - [ ] No allocation beyond what []rune(s) requires
Task 4 — Function Used as Argument: applyTwice¶
Difficulty: Beginner Topic: Higher-order function, function value as parameter
Description: Write applyTwice(fn func(int) int, x int) int that returns fn(fn(x)).
Starter Code:
package main
import "fmt"
func applyTwice(fn func(int) int, x int) int {
// TODO
return 0
}
func main() {
inc := func(n int) int { return n + 1 }
sq := func(n int) int { return n * n }
fmt.Println(applyTwice(inc, 5)) // 7
fmt.Println(applyTwice(sq, 3)) // 81
}
Expected Output:
Evaluation Checklist: - [ ] Calls fn exactly twice - [ ] Works with any func(int) int value - [ ] Returns the second application's result
Task 5 — Build a Lookup Table of Operations¶
Difficulty: Intermediate Topic: Function values stored in a map, registry pattern
Description: Build a map ops from operator strings ("+", "-", "*", "/") to functions of type func(int, int) int. Then write calc(op string, a, b int) (int, error) that looks up op, returns the result, or an error if op is unknown or division by zero.
Starter Code:
package main
import (
"errors"
"fmt"
)
var ops = map[string]func(int, int) int{
// TODO: fill in +, -, *, /
}
func calc(op string, a, b int) (int, error) {
// TODO
return 0, errors.New("not implemented")
}
func main() {
for _, op := range []string{"+", "-", "*", "/", "%"} {
if r, err := calc(op, 12, 4); err != nil {
fmt.Printf("%s -> error: %v\n", op, err)
} else {
fmt.Printf("12 %s 4 = %d\n", op, r)
}
}
if _, err := calc("/", 10, 0); err != nil {
fmt.Println("divide by zero:", err)
}
}
Expected Output:
12 + 4 = 16
12 - 4 = 8
12 * 4 = 48
12 / 4 = 3
% -> error: unknown op "%"
divide by zero: division by zero
Evaluation Checklist: - [ ] Map has exactly four entries - [ ] calc returns a clear error for unknown operators - [ ] calc returns a separate error for division by zero, BEFORE calling the division function - [ ] No panics on any input
Task 6 — Functional Options Pattern¶
Difficulty: Intermediate Topic: Variadic function-typed parameters, configuration pattern
Description: Implement a Server configuration constructor using the functional options pattern. Provide three options: WithAddr(string), WithTimeout(time.Duration), and WithMaxConn(int). Defaults: addr=":8080", timeout=30s, maxConn=100.
Starter Code:
package main
import (
"fmt"
"time"
)
type Server struct {
Addr string
Timeout time.Duration
MaxConn int
}
type Option func(*Server)
// TODO: WithAddr, WithTimeout, WithMaxConn
func NewServer(opts ...Option) *Server {
// TODO: defaults + apply opts
return nil
}
func main() {
s1 := NewServer()
s2 := NewServer(WithAddr(":9000"))
s3 := NewServer(WithAddr(":7000"), WithTimeout(5*time.Second), WithMaxConn(500))
fmt.Printf("s1: %+v\n", s1)
fmt.Printf("s2: %+v\n", s2)
fmt.Printf("s3: %+v\n", s3)
}
Expected Output:
s1: &{Addr::8080 Timeout:30s MaxConn:100}
s2: &{Addr::9000 Timeout:30s MaxConn:100}
s3: &{Addr::7000 Timeout:5s MaxConn:500}
Evaluation Checklist: - [ ] Each WithX returns an Option value - [ ] Defaults applied before any option - [ ] Options are applied in the order they are passed - [ ] No options → all defaults
Task 7 — Method Value vs Method Expression¶
Difficulty: Intermediate Topic: Method binding, closures over receivers
Description: Given the Counter type below, demonstrate the difference between a method value and a method expression by writing two helpers: bumpN(c *Counter, n int) using a method value, and bumpExprN(n int) returning a function that bumps any *Counter n times using a method expression.
Starter Code:
package main
import "fmt"
type Counter struct{ n int }
func (c *Counter) Inc() { c.n++ }
func bumpN(c *Counter, n int) {
// TODO: use a method VALUE bound to c
}
func bumpExprN(n int) func(*Counter) {
// TODO: use a method EXPRESSION
return nil
}
func main() {
c1 := &Counter{}
bumpN(c1, 5)
fmt.Println(c1.n) // 5
bump3 := bumpExprN(3)
c2 := &Counter{}
bump3(c2)
fmt.Println(c2.n) // 3
}
Expected Output:
Evaluation Checklist: - [ ] bumpN uses c.Inc as a method value - [ ] bumpExprN uses (*Counter).Inc as a method expression - [ ] Both behave correctly for n=0 - [ ] No global state
Task 8 — Retry Helper¶
Difficulty: Intermediate Topic: Higher-order function, errors, backoff
Description: Implement retry(attempts int, sleep time.Duration, fn func() error) error that calls fn. On error, sleeps and retries up to attempts times. Returns the LAST error wrapped via fmt.Errorf("after %d attempts: %w", attempts, err). Returns nil on first success.
Starter Code:
package main
import (
"errors"
"fmt"
"time"
)
func retry(attempts int, sleep time.Duration, fn func() error) error {
// TODO
return nil
}
var calls int
func flakey() error {
calls++
if calls < 3 {
return errors.New("flake")
}
return nil
}
func main() {
err := retry(5, 5*time.Millisecond, flakey)
fmt.Println("err:", err, "calls:", calls)
}
Expected Output:
Evaluation Checklist: - [ ] Calls fn at most attempts times - [ ] Returns nil immediately on success (no extra calls) - [ ] Wraps the last error with %w so errors.Is works - [ ] Sleeps between attempts, not after the final one (extra credit)
Task 9 — Compose Functions¶
Difficulty: Intermediate Topic: Function composition, returning functions
Description: Write compose(fs ...func(int) int) func(int) int that returns a function applying each f in order: compose(f, g, h)(x) should return h(g(f(x))).
Starter Code:
package main
import "fmt"
func compose(fs ...func(int) int) func(int) int {
// TODO
return nil
}
func main() {
inc := func(n int) int { return n + 1 }
dbl := func(n int) int { return n * 2 }
sq := func(n int) int { return n * n }
pipeline := compose(inc, dbl, sq)
fmt.Println(pipeline(3)) // ((3+1)*2)^2 = 64
}
Expected Output:
Evaluation Checklist: - [ ] compose() (zero args) returns the identity function - [ ] Functions applied in left-to-right order - [ ] Returned function works for any number of integers - [ ] No allocations beyond the closure itself per compose call
Task 10 — Test-Doubles via Function Fields¶
Difficulty: Advanced Topic: Function-typed struct fields, dependency injection for tests
Description: Build a RateLimiter struct that has a Now func() time.Time field. The default New() constructor sets Now = time.Now. Implement Allow() bool that returns true only if at least 100 ms have elapsed since the previous true return. In main, simulate two scenarios: production (time.Now) and tests (Now returns advancing fixed times).
Starter Code:
package main
import (
"fmt"
"time"
)
type RateLimiter struct {
Now func() time.Time
lastPass time.Time
}
func New() *RateLimiter {
// TODO
return nil
}
func (r *RateLimiter) Allow() bool {
// TODO
return false
}
func main() {
// Production scenario
rl := New()
fmt.Println(rl.Allow()) // true (first call)
fmt.Println(rl.Allow()) // false (no sleep)
time.Sleep(120 * time.Millisecond)
fmt.Println(rl.Allow()) // true
// Test scenario with fake clock
fake := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
rl2 := &RateLimiter{Now: func() time.Time { return fake }}
fmt.Println(rl2.Allow()) // true
fake = fake.Add(50 * time.Millisecond)
fmt.Println(rl2.Allow()) // false
fake = fake.Add(60 * time.Millisecond)
fmt.Println(rl2.Allow()) // true (50+60=110ms ≥ 100ms)
}
Expected Output:
Evaluation Checklist: - [ ] First call to Allow always returns true - [ ] Subsequent calls compare Now() to lastPass - [ ] When the test overrides Now, the limiter uses the fake clock - [ ] No reliance on real time in the test scenario - [ ] Allow updates lastPass only on success
Bonus Task — Build Your Own defer (Conceptually)¶
Difficulty: Advanced Topic: Function values, slices of functions, LIFO execution
Description: Implement a Cleanup type that mimics defer semantics. It accumulates cleanup functions and runs them in LIFO order on .Run(). Useful when you want defer-like behavior but the cleanup must happen at a specific moment, not at function exit.
Starter Code:
package main
import "fmt"
type Cleanup struct {
fns []func()
}
func (c *Cleanup) Add(fn func()) {
// TODO
}
func (c *Cleanup) Run() {
// TODO: LIFO
}
func main() {
var c Cleanup
c.Add(func() { fmt.Println("first added") })
c.Add(func() { fmt.Println("second added") })
c.Add(func() { fmt.Println("third added") })
c.Run()
}
Expected Output:
Evaluation Checklist: - [ ] Add appends to the slice - [ ] Run calls in reverse order - [ ] Calling Run twice runs functions only once (clear the slice after Run) - [ ] Safe when fns is nil (no panic)