12. Declaring functions

📖 Reading · 8 min
💡 Every code box below is live — edit it and hit Run.

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 namename 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 a greet(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.