Pro Tips: Functions
Functional Programming Patterns — map, filter, reduce
Go 1.18+ generics let you write truly reusable Map, Filter, and Reduce without code generation or empty-interface casts:
package main
import (
"fmt"
"strings"
)
// Map transforms each element of a slice using fn.
func Map[T, U any](s []T, fn func(T) U) []U {
result := make([]U, len(s))
for i, v := range s {
result[i] = fn(v)
}
return result
}
// Filter returns elements for which keep returns true.
func Filter[T any](s []T, keep func(T) bool) []T {
var result []T
for _, v := range s {
if keep(v) {
result = append(result, v)
}
}
return result
}
// Reduce folds s into a single value, starting from initial.
func Reduce[T, U any](s []T, initial U, fn func(U, T) U) U {
acc := initial
for _, v := range s {
acc = fn(acc, v)
}
return acc
}
func main() {
words := []string{" Go ", " is ", " great "}
trimmed := Map(words, strings.TrimSpace)
fmt.Println("trimmed:", trimmed)
upper := Map(trimmed, strings.ToUpper)
fmt.Println("upper:", upper)
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
evens := Filter(nums, func(n int) bool { return n%2 == 0 })
fmt.Println("evens:", evens)
sum := Reduce(nums, 0, func(acc, n int) int { return acc + n })
fmt.Println("sum:", sum)
// Chain: sum of squares of odd numbers
result := Reduce(
Map(
Filter(nums, func(n int) bool { return n%2 != 0 }),
func(n int) int { return n * n },
),
0,
func(acc, n int) int { return acc + n },
)
fmt.Println("sum of squares of odds:", result) // 1+9+25+49+81 = 165
}
Output:
trimmed: [Go is great]
upper: [GO IS GREAT]
evens: [2 4 6 8 10]
sum: 55
sum of squares of odds: 165
Middleware Chain Implementation
The idiomatic Go middleware pattern wraps handlers without frameworks. The key is that Middleware is just func(Handler) Handler — plain function composition.
package main
import (
"fmt"
"log"
"net/http"
"strings"
"time"
)
type Middleware func(http.Handler) http.Handler
// Chain composes middlewares left-to-right: first listed = outermost.
func Chain(h http.Handler, ms ...Middleware) http.Handler {
for i := len(ms) - 1; i >= 0; i-- {
h = ms[i](h)
}
return h
}
func Logging(logger *log.Logger) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
logger.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
})
}
}
func RequireHeader(header, value string) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.EqualFold(r.Header.Get(header), value) {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
}
func Recovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
http.Error(w, "Internal Server Error", 500)
}
}()
next.ServeHTTP(w, r)
})
}
func main() {
logger := log.New(log.Writer(), "[http] ", 0)
core := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "OK")
})
handler := Chain(core,
Recovery,
Logging(logger),
RequireHeader("X-API-Key", "secret"),
)
http.Handle("/api", handler)
fmt.Println("Middleware chain assembled (server not started in this demo)")
_ = handler
}
Output:
Middleware chain assembled (server not started in this demo)
Functional Options Pattern
The Functional Options Pattern(coined by Dave Cheney) solves the configuration problem: how to provide optional parameters with sensible defaults without combinatorial constructor overloads.
package main
import (
"fmt"
"time"
)
// ServerConfig holds all server configuration.
type ServerConfig struct {
host string
port int
timeout time.Duration
maxConns int
tlsEnabled bool
readBuffer int
}
// Option is a function that modifies a ServerConfig.
type Option func(*ServerConfig)
// WithHost sets the listening host.
func WithHost(host string) Option {
return func(c *ServerConfig) { c.host = host }
}
// WithPort sets the listening port.
func WithPort(port int) Option {
return func(c *ServerConfig) { c.port = port }
}
// WithTimeout sets the request timeout.
func WithTimeout(d time.Duration) Option {
return func(c *ServerConfig) { c.timeout = d }
}
// WithMaxConns limits concurrent connections.
func WithMaxConns(n int) Option {
return func(c *ServerConfig) { c.maxConns = n }
}
// WithTLS enables TLS.
func WithTLS() Option {
return func(c *ServerConfig) { c.tlsEnabled = true }
}
// NewServer constructs a ServerConfig, applying defaults then options.
func NewServer(opts ...Option) *ServerConfig {
cfg := &ServerConfig{
host: "0.0.0.0",
port: 8080,
timeout: 30 * time.Second,
maxConns: 1000,
readBuffer: 4096,
}
for _, opt := range opts {
opt(cfg)
}
return cfg
}
func main() {
// Default server
s1 := NewServer()
fmt.Printf("default: %s:%d timeout=%v tls=%v\n",
s1.host, s1.port, s1.timeout, s1.tlsEnabled)
// Custom server
s2 := NewServer(
WithHost("127.0.0.1"),
WithPort(443),
WithTimeout(10*time.Second),
WithMaxConns(500),
WithTLS(),
)
fmt.Printf("custom: %s:%d timeout=%v tls=%v maxConns=%d\n",
s2.host, s2.port, s2.timeout, s2.tlsEnabled, s2.maxConns)
}
Output:
default: 0.0.0.0:8080 timeout=30s tls=false
custom: 127.0.0.1:443 timeout=10s tls=true maxConns=500
This pattern scales perfectly: adding a new option never changes existing call sites.
Function Inlining and Performance
The Go compiler (gc) inlines small functions automatically — it replaces the call with the function body, eliminating call overhead. You can inspect inlining decisions with:
go build -gcflags="-m" ./...
Key inlining rules in Go 1.24:
- Functions with a complexity budget below a threshold are inlined.
- Functions containing
defer,recover, closures over stack variables, or complex control flow are not inlined. - The
//go:noinlinedirective forces the compiler to skip inlining a specific function.
package main
import "fmt"
// Small functions are inlined by the compiler — zero call overhead.
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
// max is simple enough to be inlined.
func max(a, b int) int {
if a > b {
return a
}
return b
}
//go:noinline
// noinlineExample forces the compiler NOT to inline this function,
// useful when profiling to keep the function visible in stack traces.
func noinlineExample(n int) int {
return n * n
}
func main() {
// These calls may be inlined — abs and max body inserted at call site.
fmt.Println(abs(-42))
fmt.Println(max(3, 7))
fmt.Println(noinlineExample(6))
}
Output:
42
7
36
Practical advice: write small, focused functions for correctness and readability. The compiler handles inlining. Do not sacrifice code clarity to "help" the compiler — measure first with go test -bench and pprof.
When to Use Recursion vs Iteration
| Situation | Prefer |
|---|---|
| Naturally recursive structure (tree, graph, grammar) | Recursion |
| Linear data, simple counting, arithmetic | Iteration |
| Input depth is bounded and small | Recursion |
| Unbounded user input / deep nesting | Iteration (or explicit stack) |
| Tail-recursive logic in Go | Convert to iteration |
| Divide-and-conquer (merge sort, quicksort) | Recursion |
| Performance-critical tight loop | Iteration |
When recursion is the right approach but the depth could be large, use an explicit stack(a []Frame slice) to simulate recursion iteratively:
package main
import "fmt"
// TreeNode for demonstration.
type TreeNode struct {
Val int
Left, Right *TreeNode
}
// inOrderIterative performs in-order traversal without recursion.
func inOrderIterative(root *TreeNode) []int {
var result []int
var stack []*TreeNode
current := root
for current != nil || len(stack) > 0 {
// Reach the leftmost node
for current != nil {
stack = append(stack, current)
current = current.Left
}
// Process node
current = stack[len(stack)-1]
stack = stack[:len(stack)-1]
result = append(result, current.Val)
current = current.Right
}
return result
}
func main() {
// 4
// / \
// 2 6
// / \ / \
// 1 3 5 7
root := &TreeNode{4,
&TreeNode{2, &TreeNode{1, nil, nil}, &TreeNode{3, nil, nil}},
&TreeNode{6, &TreeNode{5, nil, nil}, &TreeNode{7, nil, nil}},
}
fmt.Println("In-order (iterative):", inOrderIterative(root))
// [1 2 3 4 5 6 7]
}
Output:
In-order (iterative): [1 2 3 4 5 6 7]
defer + Function Combination Patterns
defer executes a function call at the moment the surrounding function returns, regardless of which return path is taken. Combined with function values, it enables powerful cleanup and instrumentation patterns.
package main
import (
"fmt"
"os"
"time"
)
// trace logs entry and exit of a function, returning the elapsed time.
// Usage: defer trace("functionName")()
func trace(name string) func() {
start := time.Now()
fmt.Printf("→ entering %s\n", name)
return func() {
fmt.Printf("← leaving %s (%v)\n", name, time.Since(start))
}
}
// withTempFile creates a temp file, passes it to fn, and cleans up.
func withTempFile(pattern string, fn func(*os.File) error) error {
f, err := os.CreateTemp("", pattern)
if err != nil {
return fmt.Errorf("create temp: %w", err)
}
defer func() {
f.Close()
os.Remove(f.Name())
}()
return fn(f)
}
// mustClose wraps a closer in a deferred error-logging call.
// Named return 'err' allows the deferred function to modify the return value.
func processFile(path string) (err error) {
f, err := os.Open(path)
if err != nil {
return
}
defer func() {
if cerr := f.Close(); cerr != nil && err == nil {
err = fmt.Errorf("close %s: %w", path, cerr)
}
}()
// Process f here...
fmt.Printf("Processing %s\n", f.Name())
return
}
func expensiveWork() {
defer trace("expensiveWork")()
// Simulate work
time.Sleep(5 * time.Millisecond)
fmt.Println(" doing work...")
}
func main() {
expensiveWork()
fmt.Println()
err := withTempFile("demo*.txt", func(f *os.File) error {
fmt.Fprintf(f, "Hello from temp file: %s\n", f.Name())
fmt.Printf("Wrote to temp file: %s\n", f.Name())
return nil
})
if err != nil {
fmt.Fprintln(os.Stderr, "Error:", err)
}
fmt.Println("Temp file cleaned up automatically")
}
Output:
→ entering expensiveWork
doing work...
← leaving expensiveWork (5ms)
Wrote to temp file: /tmp/demo123456.txt
Temp file cleaned up automatically
Key patterns:
defer trace("name")()— the outer call runs immediately (recording start time and printing entry), the returned closure is deferred (printing exit and elapsed time).- Deferred closures over named return values let you intercept and wrap errors from the cleanup path — a technique used throughout the standard library.
withTempFile/withDB/withTransactionwrappers ensure resources are always released, even whenfnpanics.