Go – goroutines and channels

Go – goroutines and channels

When you have to deal with concurrency problems, Go has a strong advantage to deal with it.

goroutine it is the way that Go supports concurrency, running simultaneous function in a differently Thread from the main, cleverly managed by the runtime. From the golang documentation, this is how they explain about it.

goroutine is a lightweight thread managed by the Go runtime.

In this blog post, i will try to show this, exploring why they are a game-changer for concurrent programming and how they contribute to the efficiency and elegance of Go’s design.

Goroutine

Let start with a simple example using the goroutine, we have a function that scrape websites concurrently and return the amount of words from it

func Fetcher() {
	urls := []string{
		"https://pkg.go.dev/sync/atomic",
		"https://devwizard.me",
	}
	for _, url := range urls {
		go fetch(url)
	}
}

In this code we are passing the website URLs to the fetch function and this function will scrape the HTML data and pass to another function to count the words.

func fetch(url string) string {
	count, err := wordCount(url)
	return fmt.Sprintf("Words counted %d - Error: %v", count, err)
}
func wordCount(url string) (int, error) {
	// Make the HTTP request
	resp, err := http.Get(url)
	if err != nil {
		return 0, err
	}
	// Close the body do re use the http connection
	defer resp.Body.Close()
	doc, err := html.Parse(resp.Body)
	if err != nil {
		return 0, err
	}
	var count int
	var visit func(n *html.Node)
	visit = func(n *html.Node) {
		if n.Type == html.TextNode {
			text := strings.TrimSpace(n.Data)
			words := strings.Fields(text)
			count += len(words)
		}
		for c := n.FirstChild; c != nil; c = c.NextSibling {
			visit(c)
		}
	}
	visit(doc)
	return count, nil
}

Now we want to receive this value from the goroutines, how can we achieve this?

Channels

it is the way you can send and receive data thought a pipe in golang, using the ← operator

channel <- data // Send value
data <- channel // Receive value
// data goes in the <- direction

so if we want to receive the data from the fetch function we have to use channels, so lets make some changes

func Fetcher() {
	pipe := make(chan string)
	urls := []string{
		"https://pkg.go.dev/sync/atomic",
		"https://devwizard.me",
	}
	for _, url := range urls {
		go fetch(url, pipe)
		defer close(pipe)
	}
	// Receive and print messages from the channel
	for msg := range pipe {
		fmt.Println(msg)
	}
	// Close the channel after all goroutines are done
	close(pipe)
}

Now we are defining and initializing a channel typeof string and sending to the fetch function, after that we are iterating the channel waiting for messages that arrives and printing in the console.

func fetch(url string, ch chan<- string) {
	count, err := wordCount(url)
	ch <- fmt.Sprintf("Words counted %d - Error: %v", count, err)
}

the changes in the fetch function was just to pass the text to the channel.

now the main thread is waiting for the other goroutines to finish, this is happing because the channel is a blocker for the main thread, until all messages are processed or receive a close signal, the main thread will keep waiting.

Close Signal

This close signal we used is important to informe the receiver that no more data will be sent from the producer.

In our example, the main function (Fetcher) launches several goroutines to fetch and process data from different websites concurrently. The channel (pipe) facilitates communication between these goroutines and the main function. Without the proper closure of the channel, the main function might prematurely exit, assuming that all tasks are complete, while goroutines are still executing.

Comments are closed.