Saltar a contenido

04 - Functions

What this session is

About an hour. You'll learn how to define your own functions, and why doing so changes everything about how you write programs.

The problem functions solve

So far every program you've written has been one block of code inside main(). That works for 5-line programs. It stops working around 30 lines, for three reasons:

  1. You can't see what the program does by looking at it. The structure is gone.
  2. You can't reuse anything. If you compute "the square of a number" in three places, you've typed x * x three times.
  3. You can't test pieces. The whole program runs or doesn't run; you can't easily check one calculation in isolation.

A function solves all three. It's a named block of code that takes some input and (usually) gives you some output back.

The shape of a function

func name(parameters) returnType {
    // body
    return something
}

Concrete example:

package main

import "fmt"

func double(x int) int {
    return x * 2
}

func main() {
    fmt.Println(double(5))   // 10
    fmt.Println(double(7))   // 14
}

Type it. Run it. You should see 10 and 14.

Walk through it slowly:

  • func double(x int) int { - defines a function called double. It takes one input (called a parameter), named x, of type int. It returns an int. The body is between the curly braces.
  • return x * 2 - computes x * 2 and sends that value back to whoever called the function.
  • double(5) - calls the function with 5 as the value of x. The function runs, returns 10, and Println prints it.

Notice the function double exists outside main. Functions go at the top level of the file, not nested inside main. (Other languages let you nest them; Go does not, in the basic form.)

Multiple parameters

A function can take more than one input:

func add(a int, b int) int {
    return a + b
}

Or, when several parameters share a type, you can collapse the declaration:

func add(a, b int) int {
    return a + b
}

Both forms mean the same thing. The second is more idiomatic in Go.

Functions that don't return anything

Sometimes a function just does something - it doesn't produce a value to give back. In that case, leave out the return type:

func sayHi(name string) {
    fmt.Println("Hi,", name)
}

func main() {
    sayHi("Alice")
    sayHi("Bob")
}

sayHi doesn't return anything. There's no return line in its body. Its job is just to print.

The unusual thing: returning two values at once

In most languages, a function returns one thing. Go lets you return two or more things at once. This is uncommon enough to be worth its own section.

func divide(a, b int) (int, int) {
    quotient := a / b
    remainder := a % b
    return quotient, remainder
}

func main() {
    q, r := divide(17, 5)
    fmt.Println("quotient:", q, "remainder:", r)
    // quotient: 3 remainder: 2
}

Type and run.

The new things:

  • func divide(a, b int) (int, int) { - the return type is (int, int). The parentheses mean "two things, both ints."
  • return quotient, remainder - return both, separated by a comma.
  • q, r := divide(17, 5) - receive both, by writing two names separated by a comma on the left of :=.

This pattern is everywhere in Go. The reason: Go's whole story for handling errors (which you'll learn in a later page) uses it. Every function that can fail returns two things: the result it computed, and an error value telling you whether something went wrong. We'll get there.

For now, just internalize: in Go, "two return values" is normal.

Why functions are worth the trouble

Here's a small program that uses two functions:

package main

import "fmt"

func square(x int) int {
    return x * x
}

func sumOfSquares(a, b int) int {
    return square(a) + square(b)
}

func main() {
    fmt.Println(sumOfSquares(3, 4))   // 9 + 16 = 25
}

Look at sumOfSquares. It calls square twice. Functions can call other functions. This is how programs get built up - small named pieces, composed.

Now imagine you write the same logic without functions:

func main() {
    a := 3
    b := 4
    result := (a * a) + (b * b)
    fmt.Println(result)
}

Shorter, sure. But: - The reader has to figure out what (a * a) + (b * b) means. sumOfSquares(3, 4) says it. - If you need the squares in three different places, you re-type x * x three times. - If you later decide squaring should clamp to a maximum (say, return 1000 if the result is bigger), you change one function. In the long version, you change every place.

These benefits compound. They're invisible at 30 lines and decisive at 300.

Variables inside vs outside

A variable declared inside a function exists only inside that function. The technical word is scope.

func double(x int) int {
    result := x * 2
    return result
}

func main() {
    fmt.Println(result)   // ERROR - `result` doesn't exist out here
}

Each function has its own world. Variables don't leak between them. The way to get information into a function is parameters; the way to get information out is the return value(s).

Exercise

In a new file iseven.go:

  1. Write a function isEven(n int) bool that returns true if n is even and false otherwise.

Hint: use the % operator from Page 02. n % 2 == 0 is a bool.

  1. From main, print isEven(4) and isEven(7). You should see true and false.

  2. Write a second function countEvens(max int) int that counts how many even numbers are in 1, 2, 3, ..., max. It should use a for loop and call your isEven function for each.

  3. From main, print countEvens(10). You should see 5 (the evens 2, 4, 6, 8, 10).

  4. Now print countEvens(0). What happens? Is that what you expected?

Don't move on until your code works for both countEvens(10) and countEvens(100) (expected: 50).

What you might wonder

"Why do I have to declare the return type?" So Go can catch mistakes for you at compile time. If you say double returns an int and you accidentally write return "hello", Go refuses to build the program and tells you exactly what's wrong. The alternative - types figured out at runtime, like in Python - means some bugs only show up when a real user hits them. Go trades a small amount of typing for a large reduction in runtime surprises.

"What if my function has nothing to return but I forgot to handle a case?" If you declare a return type but a path through your function doesn't return anything, Go refuses to compile. You'll see "missing return at end of function." This is on purpose - it catches the "I forgot the else branch" bug.

"Can a function call itself?" Yes. That's called recursion and it's a useful tool for certain problems. We'll meet it later when we have something worth recursing through.

"Can I have two functions with the same name?" No, not in the same package. Pick distinct names.

Done

You can now: - Define your own functions with parameters and a return type. - Return zero, one, or two values from a function. - Call functions from other functions. - Use functions to give names to operations and compose programs out of small pieces.

You've now learned the core of imperative programming in Go: variables, types, expressions, decisions, loops, and functions. Every Go program - including the ones in real OSS projects you'll eventually contribute to - is built from these primitives. The remaining pages introduce things that make those primitives more powerful: ways to group data (structs), ways to handle many things (slices and maps), ways to handle failure (errors), and so on.

The next page introduces structs - how Go lets you make your own types that group related data together. This is where your programs start to model real-world things.

Next: Making your own types05-structs-and-methods.md

Comments