Starting Goroutines

A goroutine is a function running alongside the rest of your program; you start one by writing go before a call.

Syntaxgo functionCall(args)

A goroutine is a function that runs on its own, at the same time as the rest of your program. You start one by writing go in front of a function call. The call returns straight away, and the function carries on running behind you.

Every Go program already has one goroutine: the one running main. Every other goroutine comes from a go statement.

Why they are cheap

A goroutine is not an operating-system thread. A thread is created by the operating system and costs a large, fixed amount of memory. A goroutine is managed by the runtime, the Go code linked into every binary, and starts with a tiny stack that grows only when the function needs more room. Tens of thousands of goroutines in one program is ordinary.

Two forms of the same thing

You can name a function, or write the body inline as a function literal. These are the same thing:

go greet("ada")

go func() {
	greet("ada")
}()

The second form is useful when the goroutine needs more than one statement. The trailing () is the call; without it you have only a function value.

What trips people up

When main returns, the whole program exits and unfinished goroutines are killed mid-work. go f() promises that f starts, not that it finishes, and its return value is thrown away. You need something that waits, which is the next lesson.

Example

Example · go
package main

import (
	"fmt"
	"sync"
)

// greet writes one line into results at position i.
// Each goroutine owns a different position, so no two of them
// ever write to the same memory.
func greet(name string, i int, results []string, wg *sync.WaitGroup) {
	// defer runs this when greet returns, marking the goroutine done.
	defer wg.Done()
	results[i] = "hello, " + name
}

func main() {
	names := []string{"ada", "grace", "alan"}
	results := make([]string, len(names))

	// A WaitGroup counts unfinished goroutines; Wait blocks until the
	// count is zero. The next lesson covers it properly.
	var wg sync.WaitGroup
	for i, name := range names {
		wg.Add(1)
		go greet(name, i, results, &wg)
	}
	wg.Wait()

	// Printing happens here, in slice order, so the output never varies.
	for _, line := range results {
		fmt.Println(line)
	}
}

// Output:
// hello, ada
// hello, grace
// hello, alan

When to use it

  • Send three independent HTTP requests at once instead of waiting for each in turn.
  • Run a background cleanup task while the main program keeps serving requests.
  • Process every file in a directory in parallel rather than one at a time.

More examples

Arguments are evaluated before the goroutine starts

The language specification says the function value and its arguments are evaluated in the calling goroutine, before the new one starts. A function literal captures the variable instead of a copy, so it sees later changes. Reach for the argument form when you want the value frozen at launch time.

Example · go
package main

import "fmt"

func show(n int) {
	fmt.Println("got", n)
}

func main() {
	count := 1

	// The value 1 is read now, in this goroutine, and passed in.
	go show(count)

	// Changing count afterwards cannot affect that goroutine.
	count = 99

	// A function literal is different: it reads count when it runs.
	go func() {
		fmt.Println("literal sees", count)
	}()
}

One goroutine per loop iteration

Starting one goroutine per item is the most common shape in real code. Before Go 1.22 the loop variable was shared by every iteration and all three goroutines could see "notify"; the usual fix was to pass it as an argument. From Go 1.22 each iteration gets its own variable, so capturing it is safe.

Example · go
package main

import (
	"fmt"
	"sync"
)

func main() {
	jobs := []string{"resize", "upload", "notify"}

	var wg sync.WaitGroup
	for _, job := range jobs {
		wg.Add(1)
		go func() {
			defer wg.Done()
			// job is a fresh variable each iteration (Go 1.22 and later),
			// so each goroutine sees its own job name.
			_ = job
		}()
	}
	wg.Wait()
	fmt.Println("all jobs started and finished")
}

A goroutine must recover its own panic

recover only sees panics from the goroutine it runs in. Any goroutine that handles untrusted work, such as a background job or a connection handler you wrote yourself, needs its own recover at the top of its body.

Example · go
package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		defer wg.Done()
		// Without this defer, the panic below kills the whole program,
		// even though main is nowhere near it.
		defer func() {
			if r := recover(); r != nil {
				fmt.Println("worker recovered:", r)
			}
		}()
		panic("bad input")
	}()
	wg.Wait()
	fmt.Println("main still running")
}

Discussion

  • Be the first to comment on this lesson.