concurrencia-goroutines-diferencia-hilos

Goroutines in Go: lightweight, simple and efficient concurrency

  • 4 min

A goroutine is a function that executes concurrently, managed by the Go runtime. It is lightweight, cheap to create, and one of the reasons Go fits so well in servers.

We have reached one of the most well-known parts of Go: concurrency.

Go makes it simple to start a concurrent task, although coordinating it correctly still requires care.

Let’s learn about goroutines and the role of the runtime in distributing them across operating system threads.

What is a goroutine

A Goroutine is a function that runs concurrently with other functions.

It is not the same as an operating system thread. It is a concurrent execution unit that the Go runtime schedules across one or more threads.

The program starts the task, and the scheduler decides when and on which thread it can run.

Syntax with the go keyword

We only need to add the go keyword before a function call.

package main

import (
    "fmt"
    "time"
)

func sayHello() {
    fmt.Println("Hello from the goroutine!")
}

func main() {
    // Synchronous (normal) execution
    sayHello()

    // Concurrent execution
    go sayHello()

    fmt.Println("End of main")
}
Copied!

The main trap

If you run the code above as-is, you will most likely not see the goroutine’s message. Or you might see it sometimes and not others.

Why? Because the main function is itself a goroutine (the main one). When main finishes, the program dies immediately, killing all goroutines that were running mid-execution.

To “see” the result (in a hacky way, just for now), we can pause main:

func main() {
    go sayHello()

    fmt.Println("Waiting a bit...")
    time.Sleep(1 * time.Second) // Temporary hack
    fmt.Println("End")
}
Copied!

Using time.Sleep to wait for a goroutine is a bad practice. In production we will use WaitGroups or Channels, which we will cover in the following articles.

Goroutines vs system threads

The main difference lies in the dynamic stack and the runtime scheduler. This allows creating many more goroutines than system threads, although each one still consumes memory and other resources.

Stack size

  • System thread: its initial stack and costs depend on the operating system and the library used, but they are usually larger.
  • Goroutine: starts with a small stack that grows and shrinks as needed. The exact size is an implementation detail and may change between versions.

Creation and destruction cost

  • System thread: creating it involves the operating system and usually has a noticeable cost.
  • Goroutine: the runtime creates it with less overhead, making it much cheaper, although not free.

Context switches

The processor can only execute one thing at a time per core. To give the illusion of multitasking, it switches rapidly between processes.

  • System thread: the operating system kernel intervenes in its scheduling.
  • Goroutine: the runtime can suspend it and run another without turning each task into an independent thread.

The scheduler

Go uses its own scheduler based on three pieces: G (goroutines), M (operating system threads), and P (logical processors available for executing Go).

The runtime distributes many goroutines across a set of operating system threads. For Go code that executes CPU work, GOMAXPROCS limits how many logical processors P can execute work simultaneously.

  • If a goroutine waits for an operation, the runtime can let other goroutines progress. For networking it usually relies on its poller; a blocking system call may require another thread.
  • The operation remains blocking for that goroutine, although it does not necessarily have to stop the rest of the program.

This model fits well with servers handling many concurrent input and output operations. One goroutine per request does not imply one thread per request.

Concurrency vs parallelism

It is useful to distinguish both concepts: concurrency organizes tasks that can progress independently; parallelism executes work simultaneously.

  • Concurrency: One person making coffee and toast. They interleave tasks (start coffee, start toast, finish coffee, finish toast). Progress is made on both, but only one thing is done at a time.
  • Parallelism: Two people; one makes coffee and the other makes toast at the same time.

Goroutines give us concurrency. If multiple logical processors are available and the tasks can progress simultaneously, the runtime can also execute them in parallel.