Welcome back to our Go series! đź‘‹
In this lesson, we’re going to explore how to define behavior in Go programs using functions. It's time to go a bit deeper and honestly, there's no better place to start than functions. If you're planning to "go" anywhere in Go, you'll be writing a lot of them.
So, let's get into it. Don’t worry, we’ll keep it clean, clear, and practical.
Why Functions Matter (More Than You Think)
You’ve probably already seen a function in Go:
func main() {
fmt.Println("Hello World!")
}
That’s the entry point for every Go program — the main() function. But functions in Go aren’t just necessary boilerplate. They're powerful tools for breaking your code into manageable, reusable, testable chunks.
A function is like a named recipe. It describes what ingredients (parameters) it needs and what dish (result) it returns. The best part? You can reuse it as many times as you want.
Writing Your Own Function
Let’s write our first custom function.
func greet(name string) {
fmt.Println("Hello,", name)
}
func main() {
greet("Gopher")
}
That’s it. The function greet
takes one string parameter and prints a friendly hello. Notice the parentheses after the function name — that’s where parameters live.
Reuse: The Real Power of Functions
Imagine needing to print greetings for 100 users. Would you copy-paste the fmt.Println
line 100 times? Hopefully not. You just reuse the function:
greet("Alice")
greet("Bob")
greet("Charlie")
Functions make your code smaller, cleaner, and less error-prone.
Functions with Return Values
Need a function that does something and then returns a result? Go makes this really straightforward:
func add(x int, y int) int {
return x + y
}
func main() {
sum := add(3, 4)
fmt.Println("Sum is:", sum)
}
Multiple Return Values? Yup.
func minMax(x int) (int, int) {
return x - 1, x + 1
}
Passing Data: By Value vs. Reference
By default, Go copies arguments when calling functions. This is called call by value.
func increment(val int) {
val++
}
func main() {
x := 5
increment(x)
fmt.Println(x) // Still prints 5
}
Want the function to actually change the value? Pass a pointer:
func increment(val *int) {
*val++
}
func main() {
x := 5
increment(&x)
fmt.Println(x) // Now it prints 6
}
Pointers can be a little intimidating at first, but once you get used to them, they’re a game changer.
Arrays, Slices, and Function Arguments
Passing arrays copies the whole thing — not great for big data. Instead, Go has slices, which are lightweight and efficient.
func doubleFirstElement(nums []int) {
nums[0] *= 2
}
func main() {
numbers := []int{1, 2, 3}
doubleFirstElement(numbers)
fmt.Println(numbers) // [2, 2, 3]
}
Slices behave like references but are safe and predictable. In Go, slices > arrays, almost always.
Go-Style Functional Programming (Just a Bit)
Go isn’t a “functional” language, but you can treat functions as values.
func apply(fn func(int) int, val int) int {
return fn(val)
}
func main() {
double := func(x int) int { return x * 2 }
fmt.Println(apply(double, 5)) // 10
}
You can also return functions — hello, closures!
func makeMultiplier(factor int) func(int) int {
return func(x int) int {
return x * factor
}
}
This flexibility lets you write dynamic, clean, and composable code — when you need it.
Variadic Functions – The Flexible Argument Trick
Sometimes, you don’t know exactly how many arguments you’ll need. Maybe you want a function that can take 2, 3, or 50 integers — all at once. Go makes this easy with variadic functions.
Let’s build a simple one: getMax
, which finds the largest number from a list of integers.
func getMax(vals ...int) int {
max := vals[0]
for _, v := range vals {
if v > max {
max = v
}
}
return max
}
func main() {
fmt.Println(getMax(1, 3, 6, 4)) // Outputs 6
}
So What’s Going On Here?
- The
...int
means the function can accept any number ofint
values. - Inside the function,
vals
behaves just like a slice ([]int
).
You Can Also Pass a Slice
Already have your values in a slice? Just use the ... syntax to unpack it:
vals := []int{1, 3, 6, 4}
fmt.Println(getMax(vals...)) // Still prints 6
This makes your functions super flexible — you can accept both raw arguments and pre-built slices.
Defer – Clean Up Later, Worry Less Now
In Go, you can schedule a function to run after the current function finishes, using the defer
keyword.
Here’s a simple example:
func main() {
defer fmt.Println("Bye")
fmt.Println("Hello")
}
// output:
Hello
Bye
That’s right — defer
delays the call until main()
is about to return.
Why Use defer?
It’s perfect for cleanup tasks:
- Closing files
- Unlocking resources
- Logging exits
it's better to use it in prior article, right?
file := openFile()
defer file.Close() // Closes when function ends
Quick Gotcha: Argument Evaluation Timing
Even though the function call is deferred, its arguments are evaluated immediately.
Check this out:
func main() {
i := 1
defer fmt.Println(i + 1)
i++
fmt.Println("Hello")
}
// output:
Hello
2
Why not 3? Because i + 1
was evaluated when defer
was called, not when it ran.
This little quirk can save you some debugging time later.
--
Function Design Tips (The Human Side)
Let’s get real: code isn't just for machines. It’s for humans too — including future you who returns to this code six months from now.
Here are some friendly tips:
- Name your functions well.
computeRMS()
is better thandoMath()
. - Keep them focused. One function = one job.
- Avoid long parameter lists. If you need many values, consider a struct.
- Shorter is better. If a function's more than ~15 lines, ask yourself why.
We’ve covered a lot of ground in this article. From understanding what functions really are to writing your own, passing data in and out, and keeping things clean and understandable — you’ve now got a solid grasp on how Go handles functions. We also dipped into some more advanced ideas like using functions as values, creating flexible functions with variadic arguments, and making your code safer and cleaner with defer.
Up next, we’ll dive into how go take advantage objected oriented programming. Stay tuned, and happy coding!
Top comments (0)