Go Concurrency Simply Explained with Examples

8/26/2025

Go Routine

Goroutine is a lightweight thread maintained by Go Runtime. To make a goroutine, just add the keyword go right before the function we're about to call.

In Golang, every program in minimum is driven by at least one goroutine called main goroutine:

  • When main() is executed, this will become the first goroutine
  • When main goroutine stops, other goroutines will also stop

Goroutines run concurrently with other goroutines

Example of a simple goroutine:

package main

func greet(name string) {
	fmt.Println("Hello,", name)
}

func main() {
	fmt.Println("Main thread started")

	go greet("Yehezkiel")
	go greet("Wiradhika")

	time.Sleep(100 * time.Millisecond) // without this, the goroutine won't be able to run
	fmt.Println("Main thread ended")
}

Note: without the time.Sleep(100 * time.Millisecond), the goroutine won't run since the main goroutine is over before the other goroutine able to run. We need a blocking condition, for example using time.Sleep(100 * time.Millisecond).

Goroutine State Transitions (Source: geeksforgeeks)

Yet another example:

package main

func apiCallA(start time.Time) {
	time.Sleep(100 * time.Millisecond)
	fmt.Println("API Call A started at:", time.Since(start))
}

func apiCallB(start time.Time) {
	time.Sleep(100 * time.Millisecond)
	fmt.Println("API Call B started at:", time.Since(start))
}

func sequential() {
	start := time.Now()

	apiCallA(start)
	apiCallB(start)

	time.Sleep(100 * time.Millisecond)
	fmt.Println("Sequential ends at", time.Since(start))
}

func concurrent() {
	start := time.Now()

	go apiCallA(start)
	go apiCallB(start)

	time.Sleep(100 * time.Millisecond)
	fmt.Println("Concurrent ends at", time.Since(start))
}

func main() {
	fmt.Println("Main thread started")

	sequential()
	concurrent()

  time.Sleep(100 * time.Millisecond)
	fmt.Println("Main thread ended")
}

Sequential Output:

Main thread started
API Call A started at: 100.2245ms
API Call B started at: 200.9909ms
Sequential ends at 302.0439ms
Main thread ended

Concurrent output:

Main thread started
API Call B started at: 100.5045ms
Concurrent ends at 100.5045ms
API Call A started at: 100.5045ms
Main thread ended

Channel

You can't just pass values between one thread to another the usual way with concurrency in Go, here's an example:

package main

import "fmt"

func addOne(n int) int {
	return n + 1
}

func main() {
	res := go addOne(1)
	fmt.Println(res)
}

You can't just run the above go script. For this, you'll need something called channel.

Channel Definition

Channel is a type of data that is used to communicate between goroutines. You could use it to send or receive data between goroutines. It's usually used to exchange data from one function to another.

Channel is developed based on CSP concept (Communicating Sequential Process). CSP is a communication model used to manage concurrency. It splits concurrency into two parts, which is proccess and channel. Process is a function that runs independently while Channel is a type of data used to send and receive data.

Channel Declaration

Channel is just a type of data, here's how you declare it:

names := make(chan string)

Here's how you exchange data using channel:

package main

import "fmt"

func main() {
	names := make(chan string)

	addName := func(name string) {
		names <- name
	}

	go addName("Yehezkiel")
	go addName("John")
	go addName("Jane")

	for i := 0; i < 3; i++ {
		fmt.Println(<-names)
	}
}

Here's the output (it's not always like this, see: goroutine concept):

Jane
Yehezkiel
John

Channel Synchronization

We could utilize channel to do Synchronization by using channel as blocking or waiting for the other processes to complete. Here's an example:

package main

import (
	"fmt"
	"runtime"
	"time"
)

func main() {
	runtime.GOMAXPROCS(1)
	var start = time.Now()

	fmt.Println("main started at time ", time.Since(start))
	c := make(chan string)
	go func() {
		// time.Sleep(10 * time.Millisecond)
		fmt.Printf("hello from goroutine, at time %v\n", time.Since(start))
		c <- "goroutine say hi"
	}()

	fmt.Printf("goroutine sent this: '%v'. At time %v\n", <-c, time.Since(start))
	fmt.Printf("main stopped at time %v\n", time.Since(start))
}

From the above code, you don't have to use time.Sleep(10 * time.Millisecond) so the goroutine could run before main thread terminates.

Output:

main started at time  0s
hello from goroutine, at time 537.1µs
goroutine sent this: 'goroutine say hi'. At time 537.1µs
main stopped at time 537.1µs

Channel as Parameter

You could use channel as parameter with the following syntax:

func createHelloMessage(msgs chan string, name string) {
	msg := fmt.Sprintf("Hello, %v", name)
	msgs <- msg
}

Here's the full example:

package main

import "fmt"

func createHelloMessage(msgs chan string, name string) {
	msg := fmt.Sprintf("Hello, %v", name)
	msgs <- msg
}

func main() {
	msgs := make(chan string)
	names := []string{"Yehezkiel", "Jane", "John"}

	for _, i := range names {
		go createHelloMessage(msgs, i)
	}

	for i := 0; i < len(names); i++ {
		fmt.Println(<-msgs)
	}
}

Output:

Hello, John
Hello, Yehezkiel
Hello, Jane