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 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 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.