Right, so you’ve come from a language where a function can only give you one thing back. Maybe you’re used to contorting your data into some miserable little struct or passing around pointers just to get a few values out. Forget all that. In Go, we do this properly. A function can return multiple values, and this isn’t some niche feature; it’s the absolute bedrock of how we handle errors, operations that return a result and a status, and just general sanity.

The syntax is beautifully simple. You just declare the return types in parentheses. Let’s say you have a function that divides two numbers. In the real world, this can fail (thanks, math). So a robust function needs to return the result and an error.

func divide(a, b float64) (float64, error) {
    if b == 0.0 {
        return 0.0, errors.New("are you trying to create a black hole? division by zero")
    }
    return a / b, nil
}

Notice that? We’re returning two things: a float64 and an error. The nil for the error is a huge deal. It’s our signal that everything went fine. This (result, error) pattern is the most important idiom in the entire language. You will see it everywhere. To use this function, you assign both return values.

func main() {
    result, err := divide(10, 2)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println("Result:", result) // Output: Result: 5

    result, err = divide(10, 0)
    if err != nil {
        fmt.Println("Error:", err) // Output: Error: are you trying to create a black hole? division by zero
        return
    }
    fmt.Println("Result:", result) // This won't execute
}

The Obligatory Error Check

See how we immediately check if err != nil? This isn’t a suggestion; it’s the law. You must handle the error right away. Ignoring an error by assigning it to the blank identifier _ is a declaration of cowardice and will likely come back to haunt you in production at 3 AM. Don’t be that person.

// This is a terrible idea. Please don't.
result, _ := divide(10, 0)
fmt.Println(result) // Prints 0, which is completely wrong and useless.

Ignoring Specific Returns with _

But wait, what if a function returns multiple values and you only care about one? This is where the blank identifier _ earns its keep. Let’s use the strings.Cut function as an example, which returns the parts before and after a separator, and a boolean.

func main() {
    greeting := "hello, world"
    
    // If I only care if the comma exists, I can ignore the actual parts.
    _, _, found := strings.Cut(greeting, ",")
    fmt.Println("Contains a comma?", found) // Output: Contains a comma? true

    // If I only want the part after the comma, I can ignore the first return and the bool.
    _, after, _ := strings.Cut(greeting, ",")
    fmt.Println("After comma:", after) // Output: After comma: world
}

This is perfectly acceptable and idiomatic. You’re explicitly telling the compiler (and anyone reading your code) that you are consciously discarding that value. It’s the difference between ignoring your mail and having it stolen by a raccoon. One is a choice; the other is a mess.

Why This Is a Game Changer

This pattern eliminates a whole class of bugs. You can’t accidentally use a result that is in an error state because the error is right there, staring you in the face, forcing you to deal with it. It makes for incredibly clear and robust code. The function signature func() (ResultType, error) is a promise: “I will either give you what you want, or I will tell you why I couldn’t.”

Other languages use exceptions, which are like a fire alarm that can go off anywhere in your code, forcing you to run for the exits. Go’s multiple returns are more like a well-trained assistant: they hand you a report, and if there’s a problem, it’s neatly paperclipped to the front. You deal with it immediately, right there, before you even read the report. It’s less “dramatic,” but it leads to code where the flow of execution is obvious and predictable. And in software, predictable is another word for “not on fire.”