7.1 Defining Functions: Syntax and Naming Conventions
Right, let’s talk about functions. You already know the basics: you feed them some data, they do some work, they spit an answer back out. But Go, in its charmingly opinionated way, has a few twists on this old formula that range from “oh, that’s clever” to “wait, why would you do it like that?” Let’s get into the weeds.
First, the absolute bedrock. A function is declared with the func keyword. Groundbreaking stuff, I know. The basic syntax is so straightforward it barely needs explanation, but we’re being thorough, so here it is.
func add(a int, b int) int {
return a + b
}
You declare the parameters, their types, the return type, and then you write the body. If you have multiple parameters of the same type, you can use the shorthand. This isn’t a trick; it’s just a bit of syntactic sugar to save your fingers.
func addThree(a, b, c int) int {
return a + b + c
}
Naming: Be a Grown-Up
This isn’t JavaScript. We don’t name things doTheThing here. Function names should be clear, concise, and use camelCase. The name should describe the action it performs. CalculateInterest is good. calcInt is lazy. doTheMath will get you side-eyed in code reviews.
More importantly, if your function is exported (capitalized so other packages can use it), its name is part of your package’s API. Make it count. json.Marshal is a perfect name—it’s a verb that tells you exactly what it does. Also, note the convention: since Marshal is a thing you do, it’s a verb. A struct is a thing you have, so it’s a noun (json.Encoder). This isn’t a hard rule, but it’s a fantastic guideline that makes your code instantly more readable.
Multiple Return Values: The Real Game-Changer
This is where Go starts to show its pragmatic side. In most languages, returning multiple values is a hassle involving structs or arrays. In Go, it’s a first-class citizen. You just declare the return types in parentheses.
func divide(a, b float64) (float64, error) {
if b == 0.0 {
return 0.0, fmt.Errorf("cannot divide by zero")
}
return a / b, nil
}
This is the canonical Go pattern for handling errors. Instead of the exception-driven chaos of other languages or the tedious error-code checking of C, you just return the error as a second value. It’s simple, explicit, and forces you to deal with it right there on the spot. You must handle both return values.
result, err := divide(10, 0)
if err != nil {
// Handle the error. You can't ignore it. It's right there, staring at you.
log.Fatal(err)
}
fmt.Println("Result:", result)
You can return more than two values, of course. Just don’t get carried away. If you’re returning more than three or four, it’s probably a sign that those values are related and should be bundled into a struct.
Named Return Parameters: Clever, But Dangerous
Here’s a feature that feels a bit like Go is cheating on its own philosophy of simplicity. You can pre-declare names for your return values right in the signature. These are initialized to their zero values at the start of the function.
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return // This is a "naked" return—it returns x and y automatically.
}
The upside? It can make documentation clearer. The function signature (x, y int) is more descriptive than (int, int) if x and y have specific meanings.
The massive, glaring downside? That naked return statement. It’s a readability nightmare in anything but the tiniest functions. As soon as your function is more than five lines, it becomes impossible to track what’s actually being returned. You change a variable name halfway through and suddenly your function is returning nonsense. I use these sparingly, almost exclusively for pre-declaring an err value in a function with multiple complex steps, and I never use a naked return.
func processFile(filename string) (result string, err error) {
file, err := os.Open(filename)
if err != nil {
return "", err // Still explicit, which is good.
}
defer file.Close()
// ... more steps that might set err ...
return result, err // I prefer to stay explicit. It's safer.
}
Variadic Functions: Your Slice’s Best Friend
A variadic function is one that can accept a variable number of arguments. You declare it by using an ellipsis ... before the type of the final parameter. Inside the function, that parameter becomes a slice of that type.
func sumNumbers(numbers ...int) int {
total := 0
for _, num := range numbers {
total += num
}
return total
}
The beauty here is you can call it with any number of arguments: sumNumbers(1), sumNumbers(1, 2, 3, 4), or even pass in an existing slice by using the ... operator again.
nums := []int{1, 2, 3, 4, 5}
total := sumNumbers(nums...)
This is incredibly useful for functions like fmt.Println (which is, itself, variadic). Just remember: the variadic parameter must be the last one in the list. The compiler will throw a fit otherwise, and rightly so.
Defer: Your Cleanup Buddy
defer is one of those things you don’t think you need until you use it, and then you wonder how you ever lived without it. A defer statement pushes a function call onto a stack. The deferred calls are executed in last-in-first-out order after the surrounding function returns, regardless of how it returns—whether it reached the end, or a return statement, or, crucially, panicked.
This is your one-stop shop for cleanup operations.
func contents(filename string) (string, error) {
file, err := os.Open(filename)
if err != nil {
return "", err
}
defer file.Close() // This will run when contents() returns. Always.
contents, err := io.ReadAll(file)
if err != nil {
return "", err
}
return string(contents), nil
}
Without defer, you’d have to remember to call file.Close() before every single place you return from the function. It’s error-prone and ugly. With defer, you declare your intent right next to the operation that requires cleanup. It’s genius.
Just remember the LIFO order. If you defer multiple things, they run in reverse order.
func main() {
defer fmt.Println("This runs first upon return!") // Deferred 3rd (LIFO)
defer fmt.Println("This runs second upon return!") // Deferred 2nd
fmt.Println("This runs now!") // Runs first
defer fmt.Println("This runs last upon return!") // Deferred 1st (LIFO)
}
// Output:
// This runs now!
// This runs last upon return!
// This runs second upon return!
// This runs first upon return!
It feels backwards until you think about it like a stack: the last one pushed is the first one popped. Use it for closing files, unlocking mutexes, and any other resource cleanup. It’s not just convenient; it’s essential for writing robust, correct code.