12. Declaring functions
You have already been writing one function in every lesson: main. Now you
get to write your own. A function is a named piece of work — you describe
what goes in, what comes out, and what happens in between.
The shape of a function
package main
import "fmt"
func greet(name string) {
fmt.Println("Hello,", name)
}
func main() {
greet("Ada")
greet("Grace")
}
Read the declaration left to right: the keyword func, the name greet,
the parameter list (name string), and then the body. Notice that the
type comes after the name — name string, not string name. Go is
consistent about this: variables, parameters, struct fields, everything
reads "the thing, then its type".
Functions can be declared in any order. greet is defined above main
here, but below would work just as well — Go doesn't need forward
declarations.
Returning a value
Add a type after the parameter list and the function hands something back:
package main
import "fmt"
func double(n int) int {
return n * 2
}
func main() {
fmt.Println(double(21))
fmt.Println(double(double(5)))
}
The int after (n int) is the result type. A function that declares
a result type must return one on every path — leave a branch without a
return and the program won't compile.
Sharing a type between parameters
When neighbouring parameters have the same type, you can write the type once at the end of the run:
package main
import "fmt"
func add(a, b int) int {
return a + b
}
func rect(width, height float64, label string) string {
return fmt.Sprintf("%s: %.1f x %.1f", label, width, height)
}
func main() {
fmt.Println(add(3, 4))
fmt.Println(rect(2.5, 4, "floor"))
}
add(a, b int) means both are int. It's a small thing, but you'll see it
constantly in real Go code, so it's worth reading fluently.
Returning early
Go code leans hard on the guard clause: check the bad case, return immediately, and let the interesting code sit unindented at the bottom.
package main
import "fmt"
func describe(age int) string {
if age < 0 {
return "not a real age"
}
if age < 18 {
return "minor"
}
return "adult"
}
func main() {
fmt.Println(describe(-4))
fmt.Println(describe(12))
fmt.Println(describe(30))
}
Compare that to a nested if/else pyramid. Go programmers overwhelmingly
prefer the flat version — you'll see this shape again in the errors module,
where the guard is if err != nil.
Two things Go deliberately doesn't have
Coming from Python or JavaScript, two absences will surprise you:
- No default arguments.
func greet(name string, greeting string)must always be called with both. - No overloading. One name, one function. You can't have a
greet(string)and agreet(int)side by side.
That's a design choice, not an oversight: when you read a call in Go, there
is exactly one function it could be going to. If you want optional
behaviour, you pass a struct of options or write a second, differently named
function — greet and greetFormally.
Parameters are copies
Go passes arguments by value — the function gets its own copy.
package main
import "fmt"
func bump(n int) {
n = n + 100
fmt.Println("inside bump:", n)
}
func main() {
count := 1
bump(count)
fmt.Println("back in main:", count)
}
count is still 1. The function changed its own copy and threw it away.
This is one of the most useful things to internalise early: a Go function
can't quietly reach out and modify your variable unless you deliberately
hand it a pointer — which is exactly what the Pointers & Memory module
is about.
Your turn
Write a function perimeter that takes a width and a height (both int)
and returns the perimeter of a rectangle — 2 * (width + height). Print
the result for a 3×5 rectangle so the program outputs exactly:
16
package main
import "fmt"
// write perimeter here
func main() {
fmt.Println(perimeter(3, 5))
}
package main
import "fmt"
func perimeter(width, height int) int {
return 2 * (width + height)
}
func main() {
fmt.Println(perimeter(3, 5))
}
One function, one value back. Next: what makes Go functions genuinely different — they can return more than one thing.