Go
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions and Methods
-
Section 4: Concurrency
-
Section 5: Packages and Tooling
-
Section 6: More Standard Library
-
Section 7: Building Services
-
Section 8: Advanced Go
-
Section 9: Go in the Cloud-Native Ecosystem
-
Section 10: Data Structures and Algorithms in Go
-
Section 11: Testing and Deployment
-
Section 12: Practical Projects
-
Section 13: More Standard Library Practice
-
Section 14: More Practice Projects
-
Section 15: Design Patterns in Go
-
Section 16: Interview Practice
-
Section 17: Package fmt In Depth
-
Section 18: Package strings and strconv
-
Section 19: Package os and io
-
Section 20: Package time
-
Section 21: Package sort and container
-
Section 22: Package encoding
-
Section 23: Package net/http In Depth
-
Section 24: Package context
-
Section 25: Package regexp and bytes
-
Section 26: Package errors In Depth
-
Section 27: Package crypto and hash
-
Section 28: Package flag and log
-
Section 29: Package sync In Depth
-
Section 30: More Practice Exercises
-
Section 31: Go Modules and Workspaces In Depth
-
Section 32: Generics Deep Dive (Go 1.18+)
-
Section 33: Testing Package In Depth
-
Section 34: More Interview and Whiteboard Practice
-
Section 35: Package math and unicode
-
Section 36: Package path and filepath
-
Section 37: Package database/sql
-
Section 38: Package text/template and html/template
-
Section 39: Package archive and compress
-
Section 40: Lower-Level net Package
-
Section 41: Package runtime and reflect
-
Section 42: Package embed and io/fs
-
Section 43: Go Assembly and CGO Basics
-
Section 44: Building CLIs and TUIs
-
Section 45: Go Performance Tuning
-
Section 46: More Real-World Projects
-
Section 47: Go in Production
-
Section 48: Go Design Patterns
-
Section 49: Go Interfaces Deep Dive
-
Section 50: Final Practice and Review
17: Functions and Multiple Return Values
I was thinking about how we usually handle errors in other languages—throwing exceptions, try-catch blocks, the whole nine yards. In Go, the approach is much more grounded. It doesn't throw exceptions for standard errors; it just gives you the error back as a value. But for that to work, a function needs to be able to hand back more than one thing at a time.
The crash we didn't want
Let's start with a simple division function. I'll write it the way you'd write it in almost any other language: one input, one output.
func divide(a, b float64) float64 {
return a / b
}
At first glance, this looks fine. But what happens if I try to divide by zero? I'll run a quick test in main:
func main() {
result := divide(10, 0)
fmt.Println("Result:", result)
}
Running this doesn't actually crash the program (since these are floats, Go gives me +Inf), but in a real application, +Inf is usually a bug waiting to happen. I can't tell if the operation succeeded or if it failed because of bad input. I need the function to tell me, "Hey, I couldn't do this," and then give me a reason why.
Expanding the return signature
Since I can't "throw" an error, I'll try to return it. But wait—if I change the return type to error, I lose the actual result of the division. I can't return just the result OR the error; I need both. This is where Go's multiple return values come in.
I'm going to change the function signature. Instead of a single type, I'll use parentheses to list multiple types:
import (
"errors"
"fmt"
)
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("cannot divide by zero")
}
return a / b, nil
}
Notice what I did there. The signature (float64, error) tells Go that this function will always hand back two things. When the division fails, I return 0 as a placeholder for the result and the actual error. When it works, I return the result and nil, which is Go's way of saying "no error here."
Unpacking the results
Now, the way I call this function has to change. I can't just assign it to one variable anymore, or the compiler will complain that I'm trying to put two values into one box.
I'll try to capture both:
func main() {
val, err := divide(10, 0)
if err != nil {
fmt.Println("Error happened:", err)
return
}
fmt.Println("Success! Result is:", val)
}
This is the "Go pattern" you'll see everywhere. You get the result and the error, and you immediately check if the error is nil. It feels a bit repetitive at first, but it makes the failure points of your code incredibly explicit. There are no hidden exceptions jumping out from three levels deep in your call stack.
One last thing: what if I only care about the error and not the value? Or vice versa? Go doesn't let you ignore return values entirely. If you don't use a returned value, the code won't compile. To get around this, we use the blank identifier—the underscore _.
// I only care if it failed, not what the result was
_, err := divide(10, 0)
if err != nil {
fmt.Println("It failed, and that's all I needed to know.")
}
It's a simple mechanism, but it's the bedrock of how Go handles everything from file I/O to database queries.
📋 Practical Task
Build a Temperature Converter with Validation
Create a program that converts Fahrenheit to Celsius. However, you must ensure the input temperature is not below absolute zero (-459.67°F). If the temperature is below absolute zero, the function should return an error.
- Write a function
convertFtoC(f float64) (float64, error). - If
fis less than -459.67, return0and an error usingerrors.New(). - Otherwise, return the converted temperature using the formula
(f - 32) * 5/9andnilfor the error. - In your
mainfunction, test the converter with two different values: one valid temperature (e.g., 98.6) and one invalid temperature (e.g., -500). - Properly handle the error return in
mainby printing a custom message if the conversion fails.
There are no comments for now.