Skip to main content

Go Functions: Definition, Parameters, and Return Values

Published: December 17, 2025 Updated: August 29, 2026 Larry Qu 6 min read

Functions in Go are straightforward in syntax but rich in expressiveness. Multiple return values — used pervasively for error handling — are the first thing that distinguishes Go from most languages. Variadic parameters, first-class function values, and closures round out the capability. Together they enable clean, composable code without the complexity of inheritance or method overloading.

For closures and anonymous functions in depth see Go anonymous functions and closures. For methods on types see Go function receivers and methods.

Basic Declaration

// func name(param type, ...) returnType
func greet(name string) string {
    return fmt.Sprintf("Hello, %s!", name)
}

// When multiple consecutive parameters share a type, list them together
func add(a, b, c int) int {
    return a + b + c
}

// No return value
func logEvent(msg string) {
    slog.Info(msg)
}

Functions are first-class values in Go — they can be assigned to variables, passed as arguments, and returned from other functions. This happens without any special syntax.

Multiple Return Values

Multiple return values are Go’s primary mechanism for returning both a result and a possible error. The calling code checks the error before using the result:

func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("division by zero")
    }
    return a / b, nil
}

result, err := divide(10, 3)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("%.4f\n", result)  // 3.3333

The convention: the error is always the last return value. When there’s no error, return nil. The caller checks if err != nil immediately after the call — this is the fundamental Go error handling pattern.

Multiple non-error return values are also common for functions that compute two related things:

func minMax(nums []int) (min, max int) {
    if len(nums) == 0 {
        return 0, 0
    }
    min, max = nums[0], nums[0]
    for _, n := range nums[1:] {
        if n < min { min = n }
        if n > max { max = n }
    }
    return  // naked return uses named values
}

lo, hi := minMax([]int{3, 1, 4, 1, 5, 9})

Named Return Values

Named return values declare the return variables at the function signature. A bare return statement returns their current values. This is most useful for adding context to errors via defer:

func openAndRead(path string) (data []byte, err error) {
    defer func() {
        if err != nil {
            err = fmt.Errorf("openAndRead(%s): %w", path, err)
        }
    }()

    f, err := os.Open(path)
    if err != nil {
        return  // deferred func wraps err
    }
    defer f.Close()

    data, err = io.ReadAll(f)
    return  // deferred func wraps err if non-nil
}

Without named returns, you’d repeat fmt.Errorf("openAndRead(%s): %w", path, err) at every return site. Named returns let a deferred function add context once, at the function boundary.

Use named returns only when they add clarity — for simple functions, explicit return value, err is cleaner.

Variadic Parameters

A variadic parameter (the last parameter with ...) accepts zero or more values of that type. Inside the function, it’s a slice:

func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

fmt.Println(sum(1, 2, 3))     // 6
fmt.Println(sum())             // 0
fmt.Println(sum(10, 20, 30))  // 60

To pass an existing slice to a variadic function, use ... to unpack it:

nums := []int{1, 2, 3, 4, 5}
fmt.Println(sum(nums...))  // 15

Variadic parameters are most useful for optional arguments, accumulator functions, and wrapping functions like fmt.Println and log.Printf. fmt.Sprintf(format, args...) is variadic — args is []any inside the function.

Functions as Values

Functions are values — store them in variables, pass them as arguments, return them from other functions:

// Assign a function to a variable
var op func(int, int) int
op = func(a, b int) int { return a + b }
fmt.Println(op(3, 4))  // 7

// Pass a function as an argument
func apply(nums []int, f func(int) int) []int {
    result := make([]int, len(nums))
    for i, n := range nums {
        result[i] = f(n)
    }
    return result
}

doubled := apply([]int{1, 2, 3}, func(n int) int { return n * 2 })
// [2 4 6]

// Return a function from a function
func multiplier(factor int) func(int) int {
    return func(n int) int {
        return n * factor
    }
}

triple := multiplier(3)
fmt.Println(triple(5))  // 15
fmt.Println(triple(7))  // 21

The returned function “closes over” factor — it remembers the value of factor even after multiplier has returned. This is what makes closures powerful: each call to multiplier produces an independent function with its own factor.

Function Types

Defining a named function type makes code more readable when functions are used as parameters or fields:

type Predicate func(string) bool
type Transform func(string) string
type Handler func(http.ResponseWriter, *http.Request)

func filter(items []string, keep Predicate) []string {
    var result []string
    for _, s := range items {
        if keep(s) {
            result = append(result, s)
        }
    }
    return result
}

isLong := func(s string) bool { return len(s) > 5 }
words := filter([]string{"hello", "hi", "world", "Go", "programming"}, isLong)
// ["programming"] — only "hello"(5), "world"(5) don't qualify; "programming"(11) does

Named function types can also have methods, which is how http.HandlerFunc works — it’s a named function type that implements http.Handler:

// From the standard library
type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

This lets you convert any compatible function to a http.Handler by wrapping it: http.Handle("/path", http.HandlerFunc(myFunc)).

Defer in Functions

defer executes a function call when the surrounding function returns — useful for cleanup:

func processFile(path string) error {
    f, err := os.Open(path)
    if err != nil {
        return err
    }
    defer f.Close()  // runs when processFile returns, regardless of how

    // process f...
    return nil
}

Multiple defers stack LIFO. Defer is covered in depth in Go defer panic and recover.

Conventions

Exported vs unexported: functions starting with uppercase are exported (public); lowercase are unexported (package-private). This is the entire Go access control mechanism for functions.

Error last: when a function returns both a result and an error, error is always last. func getUser(id string) (*User, error), never func getUser(id string) (error, *User).

Bool second: functions that return a value and whether it exists use the value, ok pattern — map["key"], interface assertion, channel receive.

Small, focused functions: Go style favors many small, clearly-named functions over large functions with complex control flow. If you need a comment to explain what a block of code does, it might be better as a named function.

Summary

  • Multiple return values with error last is the idiomatic Go error handling pattern — always check if err != nil immediately
  • Named return values are useful for deferred error wrapping, but don’t use them just to enable bare return
  • Variadic ...T parameters accept zero or more values; unpack slices with slice... when calling
  • Functions are first-class values — assign, pass, and return them freely
  • Define named function types (type Handler func(...)) when functions appear as parameters or struct fields for readability

Resources

Comments

👍 Was this article helpful?