Intermediate

Functions in Go

Learn how to define and use functions in Go, including multiple return values, variadic parameters, closures, recursion, and methods

Functions are the fundamental building blocks of Go programs. They package reusable logic, accept inputs through parameters, and hand results back through return values. Beyond the entry point func main() you’ve already seen, Go encourages breaking programs into small, well-named functions that each do one thing clearly.

Go’s functions have a few characteristics that set them apart from many other procedural languages. Most notably, a function can return multiple values at once — a feature Go leans on heavily for returning a result alongside an error. Functions are also first-class values: you can store them in variables, pass them as arguments, and return them from other functions, which makes closures and higher-order patterns natural to write.

In this tutorial you’ll learn how to define functions, work with parameters and multiple return values, use variadic and named returns, write recursive functions, build closures, and attach methods to your own types.

Defining Functions

A Go function is declared with the func keyword, a name, a parameter list (each with a type), and an optional return type. When consecutive parameters share a type, you can write the type just once (a, b int).

Create a file named functions.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main

import "fmt"

// greet takes a name and returns a greeting string.
func greet(name string) string {
    return "Hello, " + name + "!"
}

// add takes two ints and returns their sum.
func add(a, b int) int {
    return a + b
}

// divmod returns multiple values: the quotient and the remainder.
func divmod(a, b int) (int, int) {
    return a / b, a % b
}

// stats uses named return values, initialized to their zero values.
func stats(numbers []int) (sum int, count int) {
    for _, n := range numbers {
        sum += n
        count++
    }
    return // a "naked" return sends back the named values
}

func main() {
    fmt.Println(greet("Gopher"))
    fmt.Println("3 + 4 =", add(3, 4))

    q, r := divmod(17, 5)
    fmt.Printf("17 / 5 = %d remainder %d\n", q, r)

    total, n := stats([]int{10, 20, 30})
    fmt.Printf("sum=%d count=%d\n", total, n)
}

Here divmod returns two values that the caller receives with a single := assignment. The stats function shows named return values: sum and count are declared in the signature, used like local variables, and returned by a bare return.

Recursion and Variadic Functions

Go supports recursion just like other procedural languages — a function may call itself. Go also supports variadic functions, which accept any number of trailing arguments collected into a slice using the ... syntax.

Create a file named recursion.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import "fmt"

// factorial computes n! recursively.
func factorial(n int) int {
    if n <= 1 {
        return 1
    }
    return n * factorial(n-1)
}

// sum accepts a variable number of int arguments.
func sum(numbers ...int) int {
    total := 0
    for _, n := range numbers {
        total += n
    }
    return total
}

func main() {
    fmt.Println("5! =", factorial(5))
    fmt.Println("sum(1,2,3) =", sum(1, 2, 3))
    fmt.Println("sum(4,5,6,7) =", sum(4, 5, 6, 7))

    // A slice can be expanded into variadic arguments with ...
    nums := []int{10, 20, 30}
    fmt.Println("sum(nums...) =", sum(nums...))
}

Inside sum, the parameter numbers is an []int slice. You can call it with separate arguments or expand an existing slice into the call with nums....

Closures and Higher-Order Functions

Because functions are first-class values in Go, a function can return another function or accept one as a parameter. A function that captures and remembers variables from its surrounding scope is called a closure.

Create a file named closures.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import "fmt"

// counter returns a closure that increments its own captured count.
func counter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

// apply is a higher-order function: it takes a function as an argument
// and applies it to every element of a slice.
func apply(nums []int, fn func(int) int) []int {
    result := make([]int, len(nums))
    for i, n := range nums {
        result[i] = fn(n)
    }
    return result
}

func main() {
    next := counter()
    fmt.Println(next()) // 1
    fmt.Println(next()) // 2
    fmt.Println(next()) // 3

    // An anonymous function passed as an argument.
    double := func(x int) int { return x * 2 }
    fmt.Println(apply([]int{1, 2, 3, 4}, double))
}

Each call to counter() produces a fresh closure with its own private count variable. The apply function demonstrates the higher-order pattern: it accepts fn func(int) int and calls it for each element.

Methods on Types

A method is a function with a special receiver argument bound to a type. Methods are how Go associates behavior with your own types, such as structs. A value receiver operates on a copy, while a pointer receiver can modify the original.

Create a file named methods.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import "fmt"

// Rectangle is a simple struct with a width and a height.
type Rectangle struct {
    Width  float64
    Height float64
}

// Area is a method with a value receiver; it reads but does not modify.
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

// Scale is a method with a pointer receiver; it modifies the struct.
func (r *Rectangle) Scale(factor float64) {
    r.Width *= factor
    r.Height *= factor
}

func main() {
    rect := Rectangle{Width: 3, Height: 4}
    fmt.Printf("Area: %.1f\n", rect.Area())

    rect.Scale(2)
    fmt.Printf("After scaling: %.1f x %.1f\n", rect.Width, rect.Height)
    fmt.Printf("New area: %.1f\n", rect.Area())
}

Area uses a value receiver (r Rectangle) because it only reads fields. Scale uses a pointer receiver (r *Rectangle) so its changes persist on the original rect. Go automatically takes the address of rect when you call rect.Scale(2), so you don’t need to write (&rect).Scale(2).

Running with Docker

Run each example with the official Go image — no local Go installation required.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Pull the official Go image
docker pull golang:1.23

# Run the basic functions example
docker run --rm -v $(pwd):/app -w /app golang:1.23 go run functions.go

# Run the recursion and variadic example
docker run --rm -v $(pwd):/app -w /app golang:1.23 go run recursion.go

# Run the closures example
docker run --rm -v $(pwd):/app -w /app golang:1.23 go run closures.go

# Run the methods example
docker run --rm -v $(pwd):/app -w /app golang:1.23 go run methods.go

Expected Output

Running functions.go:

Hello, Gopher!
3 + 4 = 7
17 / 5 = 3 remainder 2
sum=60 count=3

Running recursion.go:

5! = 120
sum(1,2,3) = 6
sum(4,5,6,7) = 22
sum(nums...) = 60

Running closures.go:

1
2
3
[2 4 6 8]

Running methods.go:

Area: 12.0
After scaling: 6.0 x 8.0
New area: 48.0

Key Concepts

  • Type-after-name syntax — Parameters and return types are written as name type, and shared types can be collapsed (a, b int).
  • Multiple return values — Go functions can return several values at once, the idiomatic way to return a result alongside an error.
  • Named return values — Naming returns in the signature lets you use a bare return, which can clarify intent in short functions.
  • Variadic parameters...int collects trailing arguments into a slice; expand an existing slice into a call with slice....
  • First-class functions — Functions are values you can store in variables, pass as arguments, and return from other functions.
  • Closures — An inner function captures variables from its enclosing scope, giving each closure its own private state.
  • Methods and receivers — Methods bind behavior to a type; use a value receiver to read and a pointer receiver to modify the original.
  • Capitalization controls visibility — An exported (capitalized) name like Area is visible outside its package; a lowercase name like greet is package-private.

Running Today

All examples can be run using Docker:

docker pull golang:1.23
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining