funciones-variadicas-defer-go

Variadic Functions and defer in Go: Cleanup and Control

  • 4 min

A variadic function in Go is a function that accepts a variable number of arguments of the same type. defer, on the other hand, allows us to delay an action until the function finishes.

We close the block of functions with two common tools.

First, we will answer a question you may have asked yourself: How is it possible that fmt.Println sometimes accepts one argument, sometimes two, and sometimes twenty? The answer is Variadic Functions.

Second, we will address a classic programming problem: forgetting to close a file or release a database connection. For that, Go gives us a very practical keyword: defer.

Variadic Functions (...)

A variadic function can accept a variable number of arguments.

To define it, we use the three dots ... before the type of the last parameter.

func Sumar(numeros ...int) int {
    total := 0
    // Inside the function, 'numeros' behaves like a Slice []int
    for _, n := range numeros {
        total += n
    }
    return total
}

func main() {
    fmt.Println(Sumar(1))          // 1
    fmt.Println(Sumar(1, 2, 3))    // 6
    fmt.Println(Sumar(10, 20, 30, 40)) // 100
}
Copied!

The Parameter Inside the Function

Go is very pragmatic. When you use ...int, what the compiler does is pack all the arguments you pass into a Slice of type []int. That is why we can iterate over it with range.

Expanding a Slice (...)

What if we already have the numbers in a slice and we want to pass them to a variadic function?

We cannot pass the slice directly (Sumar(miSlice) will cause an error). We have to “unpack” or “explode” it using ... after the variable.

miSlice := []int{10, 20, 30}

// Sumar(miSlice) // ERROR: cannot use []int as int
Sumar(miSlice...) // CORRECT: Passes the elements one by one
Copied!

There can only be one variadic parameter and it must be the last in the argument list. func Error(a ...int, b string) ❌ This won’t compile.

defer: Scheduling a Call for the End

Imagine this classic scenario: You open a file, write to it, and… an error occurs halfway through and the function returns early. The file remains open. This is a resource leak.

In C++ we would use destructors (RAII), in Java or C# we would use try-catch-finally blocks. In Go, we use defer.

The defer statement postpones the execution of a function until the enclosing function is about to return.

The Open and Close Pattern

In Go, it’s good to get used to this: as soon as you acquire a resource, schedule its release immediately.

package main

import (
    "fmt"
    "os"
)

func LeerArchivo() {
    archivo, err := os.Open("datos.txt")
    if err != nil {
        panic(err)
    }

    // This is the important detail.
    // We schedule the closure NOW, even though the file will be used below.
    defer archivo.Close()

    // ... do things with the file ...
    // ... read data ...
    // ... process ...

    // When we reach the end of the function (or if there is a return before),
    // Go will automatically execute archivo.Close().
}
Copied!

This way, the opening and closing are visually together. The deferred call will also be executed if the function ends due to a panic, before the panic continues to propagate.

In write operations, Close can return an important error. A simple defer archivo.Close() discards it; if we need to detect it, we must check that error using a named return or close explicitly at the appropriate point.

LIFO Execution Order

If you put several defer statements in a single function, they are executed in LIFO (Last In, First Out) order, i.e., like a stack: the last one in is the first one out.

It’s like a stack of plates: the last one we place on top is the first one we remove.

func main() {
    fmt.Println("Start")

    defer fmt.Println("1. First defer")
    defer fmt.Println("2. Second defer")
    defer fmt.Println("3. Third defer")

    fmt.Println("End")
}
Copied!

Output:

Start
End
3. Third defer
2. Second defer
1. First defer
Copied!

Defer and Argument Values

Be careful with this: the arguments of the deferred function are evaluated at the moment the defer is declared, not when it is executed.

x := 10
defer fmt.Println("Deferred value:", x) // Captures x = 10 NOW

x = 20
fmt.Println("Final value:", x)
Copied!

Output:

Final value: 20
Deferred value: 10
Copied!

Combined Example: Measuring Time

We can use defer with anonymous functions to create a small tracing utility:

func RastrearTiempo(nombre string) func() {
    start := time.Now()
    fmt.Printf("Starting %s...\n", nombre)

    // Return a function that, when executed, will calculate the final time
    return func() {
        fmt.Printf("Exiting %s. Duration: %v\n", nombre, time.Since(start))
    }
}

func ProcesoLargo() {
    // RastrearTiempo returns the closure function.
    // Using defer ensures it is executed at the end.
    defer RastrearTiempo("ProcesoLargo")()

    time.Sleep(2 * time.Second)
    fmt.Println("Working...")
}
Copied!