Functions
Functions are Go’s principal abstraction. The func keyword introduces a function; parameters require explicit types; return values are listed after the parameter list; functions admit multiple return values (the conventional Go pattern is (result, error)); functions are first-class values with type func(...) .... The conventional Go function surface includes closures (anonymous functions capturing surrounding variables), variadic parameters (...T), named return values (rarely used), and the defer statement (deferred execution at function return). The combination — multiple returns, closures, variadics, defer — covers the function surface; generic functions are treated separately in Generics.
Function declarations
The principal form:
func name(param1 Type1, param2 Type2) ReturnType {
body
}
Examples:
func add(a int, b int) int {
return a + b
}
func add(a, b int) int { // shorthand for same-type params
return a + b
}
func print(s string) { // no return value
fmt.Println(s)
}
func makeMap() map[string]int {
return map[string]int{}
}
The form: func <name>(<params>) <returnType> { <body> }. The return type follows the parameter list (the C-style int func() is not admitted).
Parameters require explicit types; same-typed adjacent parameters may share the type annotation: (a, b, c int).
Multiple return values
Go admits multiple return values as a first-class feature:
func divmod(a, b int) (int, int) {
return a / b, a % b
}
q, r := divmod(17, 5)
fmt.Println(q, r) // 3 2
// Discarding values:
q, _ := divmod(17, 5) // discard remainder
The conventional Go pattern returns a result and an error:
func parse(s string) (int, error) {
n, err := strconv.Atoi(s)
if err != nil {
return 0, fmt.Errorf("parse %q: %w", s, err)
}
return n, nil
}
n, err := parse("42")
if err != nil {
return err
}
The pattern is one of Go’s most distinctive idioms; treated in Error handling.
Named return values
Returns may be named, declaring local variables of the named types and admitting naked returns:
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return // naked return; uses named values
}
q, r := split(100)
fmt.Println(q, r) // 44 56
Named returns admit:
- Documentation — the names appear in
go doc. - Default initialisation — the named values start at the zero value.
- Defer interaction —
defercan read and modify named return values.
The conventional discipline:
- Use named returns sparingly — they obscure the return value at the call site.
- Use named returns when documentation benefits or when
defermodifies the return. - Avoid naked returns in long functions — they make the data flow harder to follow.
Variadic parameters
A variadic parameter (the last parameter, written ...T) admits any number of arguments of type T:
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
fmt.Println(sum()) // 0
fmt.Println(sum(1, 2)) // 3
fmt.Println(sum(1, 2, 3, 4, 5)) // 15
// Spreading a slice:
v := []int{1, 2, 3}
fmt.Println(sum(v...)) // 6
Inside the function, nums is a slice ([]int). The ... at the call site spreads a slice into variadic arguments.
The conventional Println and Printf are variadic:
func Println(a ...interface{}) (n int, err error) { /* ... */ }
func Printf(format string, a ...interface{}) (n int, err error) { /* ... */ }
First-class functions
Functions are values; their type is func(...) ...:
var add func(int, int) int = func(a, b int) int {
return a + b
}
result := add(3, 4) // 7
// Function-typed parameters:
func apply(f func(int) int, x int) int {
return f(x)
}
double := func(x int) int { return x * 2 }
fmt.Println(apply(double, 5)) // 10
A named function type admits naming a function shape:
type IntOp func(int) int
func compose(f, g IntOp) IntOp {
return func(x int) int {
return g(f(x))
}
}
incThenDouble := compose(
func(x int) int { return x + 1 },
func(x int) int { return x * 2 },
)
fmt.Println(incThenDouble(5)) // 12
Closures
Anonymous functions admit capturing surrounding variables:
func makeCounter() func() int {
n := 0
return func() int {
n++
return n
}
}
c := makeCounter()
fmt.Println(c()) // 1
fmt.Println(c()) // 2
fmt.Println(c()) // 3
The closure captures n by reference — the inner function sees the same n as subsequent calls. Each call to makeCounter produces an independent counter.
A common pitfall: capturing a loop variable:
funcs := []func() int{}
for i := 0; i < 3; i++ {
funcs = append(funcs, func() int { return i })
}
for _, f := range funcs {
fmt.Println(f()) // 3 3 3 (pre-Go 1.22)
// 0 1 2 (Go 1.22+)
}
Pre-Go 1.22, the loop variable was shared across iterations; closures captured the same i. Since Go 1.22, each iteration has its own i. For the older form, the conventional defence is to introduce a new variable:
for i := 0; i < 3; i++ {
i := i // new variable each iteration
funcs = append(funcs, func() int { return i })
}
defer
The defer statement schedules a function call to run when the enclosing function returns:
func processFile(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // closes when processFile returns
// ... use f ...
return nil
}
The mechanism admits resource cleanup without elaborate try/finally constructs. Multiple defers run in LIFO order:
func example() {
fmt.Println("start")
defer fmt.Println("first deferred")
defer fmt.Println("second deferred")
defer fmt.Println("third deferred")
fmt.Println("end")
}
// Output:
// start
// end
// third deferred
// second deferred
// first deferred
defer arguments are evaluated immediately (when the defer statement runs), but the call itself runs on return:
func example() {
n := 0
defer fmt.Println("deferred n:", n) // captures n=0 NOW
n = 100
}
// Output: "deferred n: 0"
For deferred calls whose arguments should be evaluated at return time, use a closure:
defer func() {
fmt.Println("deferred n:", n) // reads n at return time
}()
The conventional uses of defer:
- Resource cleanup — closing files, releasing locks, ending traces.
- Panic recovery —
defer recover()for panic handling. - Timing —
defer trackTime()(time.Now())patterns.
Recursion
Functions admit calling themselves:
func fib(n int) int {
if n < 2 {
return n
}
return fib(n-1) + fib(n-2)
}
Go does not admit tail-call optimisation; deep recursion produces stack growth (treated in Memory and the runtime). For substantial recursion depth, an explicit iterative form is conventionally preferred.
Anonymous functions and IIFE
// Anonymous function as value:
f := func(x int) int { return x * 2 }
// Immediately invoked:
result := func() int {
if a > b { return a }
return b
}()
Immediately-invoked function expressions (IIFE) are rare in idiomatic Go; explicit if/else is the conventional choice for ternary-like value selection.
Function types and interface satisfaction
A function type may satisfy an interface that requires a single method:
type Handler interface {
ServeHTTP(w ResponseWriter, r *Request)
}
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r) // call the underlying function
}
// Now any function with the right signature can be a Handler:
var h Handler = HandlerFunc(func(w ResponseWriter, r *Request) {
/* ... */
})
The pattern is conventional in net/http and similar packages.
Common patterns
Result + error
func compute() (int, error) {
n, err := step1()
if err != nil {
return 0, fmt.Errorf("step1: %w", err)
}
return process(n), nil
}
Multiple result decomposition
min, max := bounds(values)
key, value := splitPair("a=1", "=")
parts, err := parse(input)
Functional combinators
// Higher-order function:
func filter(s []int, pred func(int) bool) []int {
out := s[:0] // reuse backing array
for _, x := range s {
if pred(x) {
out = append(out, x)
}
}
return out
}
evens := filter(numbers, func(n int) bool { return n%2 == 0 })
Builder pattern with closures
func WithTimeout(d time.Duration) Option {
return func(c *Config) {
c.Timeout = d
}
}
func WithRetries(n int) Option {
return func(c *Config) {
c.Retries = n
}
}
func New(opts ...Option) *Server {
c := &Config{Timeout: 30 * time.Second}
for _, opt := range opts {
opt(c)
}
return &Server{config: c}
}
s := New(WithTimeout(5*time.Second), WithRetries(3))
The functional options pattern admits configurable construction without elaborate builder objects.
Defer for cleanup
func doWork() error {
f, err := os.Open("file.txt")
if err != nil {
return err
}
defer f.Close()
mu.Lock()
defer mu.Unlock()
// ... do work ...
return nil
}
Defer with timing
func track(name string) func() {
start := time.Now()
return func() {
log.Printf("%s took %s", name, time.Since(start))
}
}
func slowWork() {
defer track("slowWork")() // note: invocation
// ...
}
The double parentheses are required: track("slowWork") returns a function; () invokes it; defer schedules the invocation for return.
Panic and recover
func safeDivide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("divide panic: %v", r)
}
}()
return a / b, nil
}
Treated in Error handling.
Variadic forwarding
func logf(level Level, format string, args ...interface{}) {
if level >= currentLevel {
log.Printf(format, args...) // forward variadic
}
}
logf(Info, "user %s logged in", username)
Function comparison
Function values can be compared to nil but not to each other:
var f func()
if f == nil {
fmt.Println("f is nil")
}
// f1 == f2 // ERROR: invalid comparison
A note on the conventional discipline
The contemporary Go function advice:
- Return errors as the last value; the conventional pattern is
(result, error). - Use
deferfor cleanup —Close,Unlock, etc. - Avoid named returns unless documentation or defer benefits.
- Use closures for callbacks and combinators.
- Use variadic parameters sparingly — for genuinely variable-length arguments.
- Use the functional options pattern for configurable construction.
- Use
HandlerFunc-style adapters to satisfy single-method interfaces.
The combination — multiple returns, closures, variadics, defer, first-class function values — is the substance of Go’s function surface. The discipline produces clear, explicit, structured code with substantial flexibility for higher-order programming.