Part 1 , Part 2 , Part 3 , Part 4 , Part 5

Return values from go routines cannot be captured in the usual way with the = operator because starting a go routine is non blocking. A channel can be used to collect the results from many concurrent go routines. Write a value to a channel inside the go routine that is doing the work, then in main() read from the channel until it is closed to collect all the results from the go routines.

Playground: https://go.dev/play/p/_7WZdef4q4W

package main

import (
	"fmt"
	"sync"
)

func main() {
	productChan := make(chan int)
	wg := sync.WaitGroup{}
	// use many goroutines to compute the product of two numbers
	for i := 0; i < 10; i++ {
		for j := 0; j < 10; j++ {
			copyI, copyJ := i, j
			wg.Add(1)
			go func() {
				defer wg.Done()
				// send the product to an unbuffered channel
				// this is a common pattern to fan in results from many goroutines
				productChan <- copyI * copyJ
			}()
		}
	}

	// start a goroutine to wait for all the goroutines
	// that are doing work to finish
	// then close the channel to signal to main() that
	// all the work is done
	go func() {
		wg.Wait()
		// closing the channel will cause the range loop to exit
		// this is a common pattern to signal the end of a channel
		close(productChan)
	}()

	// read the products from the channel
	// until the channel is closed
	for product := range productChan {
		fmt.Print(product, ",")
	}
	fmt.Println("Done")
}