funciones-go-declaracion-retorno-multiple

Functions in Go: parameters, signatures and multiple return values

  • 5 min

A function in Go is a reusable block of code that performs a specific task. It can receive parameters, return values, or do neither.

So far we’ve written all our code inside main. But putting everything in a single block is a great recipe for getting lost. We need modularity.

In Go, functions are a basic unit for organizing and reusing code.

If you’re coming from an object-oriented language like Java, you’ll see that Go functions don’t need to live inside a class: they are declared at the package level. Additionally, they can return more than one value at a time.

Basic declaration

The syntax is very clean. We use the func keyword.

func Name(parameters) ReturnType { ... }

package main

import "fmt"

// Simple function that receives an integer and returns an integer
func Double(number int) int {
    return number * 2
}

func main() {
    result := Double(5)
    fmt.Println(result) // 10
}
Copied!

Go uses names in MixedCaps. Visibility is controlled by the initial letter: a function starting with an uppercase letter is exported; one starting with a lowercase letter is only accessible within its package.

Grouping parameters of the same type

Go has a nice little “syntactic sugar”. If you have several consecutive parameters of the same type, you don’t need to repeat the type each time. You can put it only at the end.

// Verbose form
func Sum(a int, b int, c int) int { ... }

// Idiomatic Go form (Clean)
func Sum(a, b, c int) int {
    return a + b + c
}
Copied!

Multiple return values

This feature appears frequently in Go APIs.

In C# or Java, if you want to return two things (e.g., a result and an error code), you have to:

  1. Create a wrapper class/struct (ResultObject).
  2. Or use output parameters (out in C#).
  3. Or return a tuple (which is often cumbersome to handle).

In Go, functions can return as many values as you want, separated by commas.

Example: Division with remainder

Let’s look at a function that calculates integer division and also returns the remainder.

// Returns two integers: (quotient, remainder)
func Divide(a, b int) (int, int) {
    quotient := a / b
    remainder := a % b

    return quotient, remainder // Return both at once
}

func main() {
    // Capture both values in separate variables
    q, r := Divide(10, 3)

    fmt.Printf("Quotient: %d, Remainder: %d\n", q, r)
}
Copied!

The real-world use case: result, error

90% of the time, you’ll use multiple return values to return the value you’re looking for and an error. It’s the standard Go pattern.

func OpenFile(name string) (*os.File, error) {
    file, err := os.Open(name)
    if err != nil {
        return nil, err
    }
    return file, nil
}
Copied!

If you assign the result of a function with multiple return values, you must collect all of its values. Use _ to discard a specific one. You can also execute the call as a statement and discard all its return values, although this usually doesn’t make sense if it produces no other effect.

Named return values

Go allows something curious: you can name the return variables in the function signature itself.

This does two things:

  1. It documents what each return value is.
  2. It initializes those variables automatically at the start of the function (with their Zero Value).
  3. It allows writing a naked return.
// We define that we are going to return 'area' and 'perimeter'
func Rectangle(width, height int) (area int, perimeter int) {
    // 'area' and 'perimeter' already exist here and start at 0

    area = width * height
    perimeter = 2 * (width + height)

    // Naked return: returns the current values of area and perimeter
    return
}
Copied!

Should I use it?

It’s advisable to use naked returns only in very short functions. In long functions, seeing an empty return forces the reader to scroll up to the beginning of the function to remember which variables were being returned, which harms readability.

Pass by value

It’s crucial to remember that in Go, arguments are always passed by value (copy).

If you pass a variable to a function, the function receives a photocopy. If the function draws a mustache on the photocopy, the original photo doesn’t change.

func TryToChange(x int) {
    x = 999 // Modifies the local copy
}

func main() {
    value := 10
    TryToChange(value)
    fmt.Println(value) // Still 10
}
Copied!

Apparent exception: if you pass a slice, a map, or a pointer, its value is also copied. However, that copy still references the same underlying data. Therefore, mutations on the shared content can be seen outside. Changing the slice descriptor, e.g., with append, does not change the caller’s descriptor; to do that, we must return the updated slice.