Function Declarations

How to define functions in Go using the func keyword with parameters return types multiple return values named results and variadic arguments

Functions are the primary way you organize behavior in Go. A function declaration names a reusable block of code, specifies what it needs to operate (parameters), and what it produces (return values). Go’s function syntax is small but carefully designed to avoid ambiguity — once you know the handful of forms described here, you can read and write almost any function signature in the language.

Basic Function Syntax

A function declaration in Go has four parts: the func keyword, a name, a parameter list, and an optional result list followed by the body.

func add(a int, b int) int {
    return a + b
}

func signals that a function is being declared. add is the name. The parentheses hold the parameters — a int, b int — each with a name and a type. The int after the closing parenthesis is the return type. The body is the block between braces; it must contain a return statement if a return type is present.

When consecutive parameters share the same type, you can group them:

func multiply(a, b float64) float64 {
    return a * b
}

Here both a and b are float64. The grouping only works for parameters of the same type appearing one after another. The same shortcut applies to result types when a function returns multiple values of the same type.

A function with no parameters and no return values omits both lists entirely:

func sayHello() {
    fmt.Println("hello")
}

Calling a function uses its name followed by arguments in parentheses. Arguments are passed by value — the function receives a copy of each argument, never the original variable.

package main
import "fmt"
func increment(n int) int {
    n = n + 1
    return n
}
func main() {
    x := 5
    y := increment(x)
    fmt.Println(x) // prints 5
    fmt.Println(y) // prints 6
}

The increment function cannot affect x in main because it receives a copy. This pass-by-value behavior applies to all types, though some types (slices, maps, channels) carry a reference to underlying data so modifications through them are visible to the caller — but the copy itself is still a copy of the header, not the data.

The Function Signature:

The combination of a function’s name, parameter types, and return types is called its signature. Go uses signatures to distinguish functions and to check type compatibility when passing functions as values — a topic covered later in this chapter.

Multiple Return Values

Go functions can return more than one value. This is not a special wrapper type; the function literally produces two (or more) independent values on the call stack. The return types are listed in parentheses.

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

The caller receives both values and assigns them with a multi-variable assignment:

result, err := divide(10, 2)
if err != nil {
    log.Fatal(err)
}
fmt.Println(result) // prints 5

A return list with a single type does not need parentheses, but when there are two or more, the parentheses are mandatory.

Multiple return values are especially common in Go for returning a result alongside an error. This is the idiomatic way to handle failure — no exceptions, no special error channels. The caller decides whether to check the error or ignore it.

Discarding Return Values with the Blank Identifier

If you do not need one of the returned values, assign it to the blank identifier _ instead of a variable. The blank identifier tells the compiler you are intentionally ignoring it.

area, _ := rectProps(10.8, 5.6)

Using _ avoids an unused-variable compilation error and signals to readers that the value is being consciously discarded. Do not discard errors without a very good reason — ignoring an error is a fast path to silent failures.

Do Not Discard Errors Lightly:

Ignoring an error return with _ hides failures that may cascade later. A function that writes to a file but ignores a write error, for example, will appear to succeed while the file is corrupted. Always handle or explicitly log errors you choose to discard.

Named Result Parameters

Go allows you to give names to the return values in the function signature. These named result parameters act like local variables that are initialized to their zero values at the start of the function.

func rectProps(length, width float64) (area, perimeter float64) {
    area = length * width
    perimeter = 2 * (length + width)
    return
}

Because area and perimeter are declared as named results, you can assign to them in the body and then use a bare return statement — no values listed after return. The current values of the named results are returned automatically.

A bare return works only when the function has named result parameters. If you mix named results with explicit return values, Go uses the explicit values, not the named ones.

Bare Returns Can Obscure Logic:

Bare return is convenient in short functions where the meaning of each named result is obvious from the context. In longer functions, a reader scanning for what is returned may need to trace assignments across many lines. Explicit returns make the flow clearer at a glance. Use bare returns sparingly and only when the function is small enough to hold entirely on one screen.

Named results have another subtle property: they can be shadowed. If you declare a new variable with the same name inside the body using a short variable declaration (:=), that new variable lives only inside its enclosing block and does not affect the return value. The bare return still uses the original named result.

func example() (value int) {
    value = 10
    if true {
        value := 5 // shadows the named result
        fmt.Println(value) // prints 5
    }
    return // returns 10, not 5
}

This behavior surprises many newcomers. The inner value is a completely separate variable scoped to the if block. The outer value — the named result — remains untouched.

Named Results Are Clear When Used Correctly:

Named result parameters serve as documentation. A signature like func parse(input string) (token Token, pos int, err error) tells the caller exactly what each returned value represents without needing a comment. When the function is short and the assignments are obvious, this style is both clean and self-documenting.

Variadic Functions

A variadic function accepts a variable number of arguments of the same type. The last parameter of the function is declared with an ellipsis ... before its type, making it a variadic parameter.

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

Inside the function, numbers is a slice of int — specifically []int. You can iterate over it with range, check its length with len, or pass it to other functions that accept slices. The caller can supply zero or more arguments:

fmt.Println(sum())        // 0
fmt.Println(sum(1, 2))   // 3
fmt.Println(sum(3, 5, 7)) // 15

Passing a Slice to a Variadic Parameter

If you already have a slice and want to pass its elements as individual arguments, suffix the slice with ... at the call site:

values := []int{2, 4, 6}
fmt.Println(sum(values...)) // 12

The values... syntax unpacks the slice into a list of arguments. Without the ellipsis, the compiler would reject the call because sum expects individual int values, not a []int slice.

Mixing Call Styles Is a Compile Error:

You cannot pass some individual arguments and then a slice unpacked with ... in the same call. sum(1, values...) will not compile. The variadic parameter must be filled entirely from one style — either a sequence of arguments or a single unpacked slice — never both.

Only the Last Parameter Can Be Variadic

A function can have at most one variadic parameter, and it must be the final parameter in the list. This makes parsing unambiguous: every argument after the fixed parameters is collected into the variadic slice.

func logMessage(level string, messages ...string) {
    for _, msg := range messages {
        fmt.Printf("[%s] %s\n", level, msg)
    }
}

Here level receives the first argument; everything else goes into messages. If the variadic parameter were allowed in an earlier position, the compiler could not reliably decide where the fixed parameters end and the variadic slice begins.

Common Variadic Function Pitfalls

Beginners sometimes forget that a variadic parameter is a slice inside the function. Calling append on it, for example, works because append accepts a slice. But if you need to pass the variadic parameter as a slice to another function, you already have a slice — no further ... is needed unless that other function is itself variadic.

Another subtle mistake involves modifying the slice. Since the slice is a copy of the slice header (pointer, length, capacity), changes to individual elements are visible to the caller if the underlying array is shared, but appending to the slice inside the function may or may not affect the caller’s data depending on capacity. In practice, treat the slice as read-only unless you deliberately design otherwise.

Variadic Parameters Are Always Slices:

Even though you declare ...int, the parameter is of type []int. This means nil is a valid value — calling the function with no arguments passes nil to the parameter. If your function does not handle a nil slice, it may panic on operations like indexing. Always write variadic functions to work correctly with zero arguments.


Summary

Function declarations in Go are built from a small set of orthogonal features: basic signatures, multiple return values, named results, and variadic parameters. Each feature solves a specific real-world problem — multiple returns eliminate the need for out-parameters in error handling, named results serve as both documentation and zero-value initialization, and variadic parameters remove the friction of manually wrapping arguments into a slice.

What ties these features together is a principle of explicitness. Every value that comes out of a function is declared in its signature; there are no implicit output channels. When you design a function, choose the form that makes the intent clearest at the call site. If a function can fail, return an error as the last value. If the number of inputs is arbitrary, use a variadic parameter at the end. If the return values benefit from being named, add names — but keep the function short enough that bare returns do not surprise a reader.