David's Blog

Concurrency in Go .

By David Li on Fri, 21 October 2024

Concurrency is a powerful feature of Go programming language. With its simple syntax and efficient design, Go provides an easy-to-use and high-performance environment for building concurrent programs. In this post, we’ll explore the basics of concurrency in Go and learn how to write concurrent programs using goroutines and channels.

Goroutines

Goroutines are the building blocks of concurrent programs in Go. A goroutine is a lightweight thread of execution that can be started with the ` keyword. Goroutines are multiplexed onto a smaller set of operating system threads by the Go runtime, which allows many goroutines to run simultaneously on a single CPU.

Here’s an example of how to start a goroutine:

func sayHello() {
 fmt.Println("Hello, world!")
}

func main() {
 go sayHello()
 // do other things
}

In this example, we define a function called sayHello that prints “Hello, world!” to the console. We then use the keyword to start a goroutine that executes this function. Note that the keyword does not block the main goroutine, which allows us to do other things while the sayHello goroutine is running.

Channels

Channels are another important feature of concurrency in Go. A channel is a typed conduit through which you can send and receive values with the channel operator, <-. Channels allow you to synchronize the execution of concurrent goroutines and communicate between them.

Here’s an example of how to create and use a channel:

func writeToChannel(ch chan<- string) {
 ch <- "Hello, world!"
}

func main() {
 ch := make(chan string)
 go writeToChannel(ch)
 msg := <-ch
 fmt.Println(msg)
}

In this example, we define a function called writeToChannel that takes a channel of strings as an argument and sends the string “Hello, world!” through the channel using the channel operator, <-. We then create a channel of strings using the make function, start a goroutine that calls the writeToChannel function with this channel, and finally receive the message sent through the channel using the channel operator, <-.

Conclusion

Concurrency is an important feature of modern programming languages, and Go provides a simple and efficient way to write concurrent programs using goroutines and channels. By using these building blocks, you can create high-performance and scalable programs that take full advantage of modern hardware.

Goroutines are the building blocks of concurrent programs in Go. They are lightweight threads of execution that can be started with the ` keyword. Goroutines are managed by the Go runtime, which multiplexes them onto a smaller set of operating system threads, allowing many goroutines to run simultaneously on a single CPU.

One of the key benefits of goroutines is that they make it easy to write concurrent programs in Go. By using goroutines, you can perform multiple tasks simultaneously without having to deal with the complexities of thread management or locking. Goroutines are also very efficient, as they have a small memory footprint and can be created and destroyed quickly.

To start a goroutine, you simply need to define a function that you want to run concurrently and prefix it with the keyword. For example, the following code starts a goroutine that runs thesayHello` function:

func sayHello() {
 fmt.Println("Hello, world!")
}

func main() {
 go sayHello()
 // do other things
}

When you run this code, the sayHello function will execute concurrently with the main function, allowing you to perform other tasks while the sayHello function is running.

Goroutines can also be used to perform more complex concurrent tasks, such as reading from and writing to channels. By combining goroutines and channels, you can create powerful concurrent programs that are both efficient and easy to reason about.

Channels are a powerful feature of Go that enable you to send and receive data between goroutines. A channel is a typed conduit that can be used to communicate between goroutines, allowing them to synchronize their execution and exchange information.

To create a channel, you use the make function and specify the type of data that the channel will carry. For example, the following code creates a channel of strings:

ch := make(chan string)

Once you have a channel, you can use the <- operator to send and receive data. The <- operator can be used in two ways: to send data through a channel or to receive data from a channel.

To send data through a channel, you use the <- operator on the channel, followed by the data that you want to send. For example:

ch <- "Hello, world!"

This code sends the string “Hello, world!” through the ch channel.

To receive data from a channel, you use the <- operator on the left side of the channel variable. For example:

msg := <-ch

This code receives a string from the ch channel and assigns it to the msg variable.

Channels can be used in many different ways, depending on your needs. For example, you can use channels to synchronize the execution of multiple goroutines, to implement a producer-consumer pattern, or to perform load balancing across multiple workers.

One of the key benefits of channels is that they provide a simple and efficient way to write concurrent programs in Go. By using channels to coordinate the execution of goroutines, you can create programs that are both easy to reason about and scalable.

The select statement is a powerful feature of Go that allows you to wait on multiple channels at once. It is often used in concurrent programs to synchronize the execution of multiple goroutines.

The select statement is similar to a switch statement, but instead of switching on values, it switches on channels. The select statement waits until one of the channels is ready to send or receive data, and then executes the corresponding case statement.

Here’s an example of how the select statement works:

ch1 := make(chan string)
ch2 := make(chan string)

go func() {
 time.Sleep(time.Second)
 ch1 <- "Hello"
}()

go func() {
 time.Sleep(time.Second)
 ch2 <- "World"
}()

select {
case msg1 := <-ch1:
 fmt.Println(msg1)
case msg2 := <-ch2:
 fmt.Println(msg2)
}

In this code, we create two channels ch1 and ch2, and then start two goroutines that send data through the channels after a one second delay. We then use the select statement to wait for data from either channel, and print the message that is received.

The select statement is powerful because it allows you to wait on multiple channels at once, and respond to the first one that is ready. This can be useful in situations where you want to wait for input from multiple sources, such as user input or network requests.

It’s worth noting that the select statement can also include a default case, which is executed if none of the channels are ready to send or receive data. This can be useful for situations where you want to perform some action if no input is received after a certain period of time, for example.

Sure! Here’s a simple CLI app that demonstrates the use of goroutines and channels in Go. This app simulates a server that receives requests and processes them concurrently using goroutines. The requests are sent to the server via a channel, and the responses are returned via another channel.

package main

import (
 "fmt"
 "time"
)

type Request struct {
 ID int
 Message string
}

type Response struct {
 ID int
 Message string
}

func server(requests <-chan \*Request, responses chan<- \*Response) {
 for {
 req := <-requests

 // Process request concurrently
 go func(req \*Request) {
 time.Sleep(time.Second)
 responses <- &Response{
 ID: req.ID,
 Message: fmt.Sprintf("Processed request %d: %s", req.ID, req.Message),
 }
 }(req)
 }
}

func main() {
 requests := make(chan *Request)
 responses := make(chan *Response)

 // Start server
 go server(requests, responses)

 // Send requests to server
 for i := 1; i <= 5; i++ {
 req := &Request{
 ID: i,
 Message: fmt.Sprintf("Hello %d", i),
 }
 requests <- req
 }

 // Receive responses from server
 for i := 1; i <= 5; i++ {
 res := <-responses
 fmt.Println(res.Message)
 }
}

In this code, we define two types, Request and Response, which represent the requests and responses that the server will process. We then define the server function, which takes two channels as arguments: requests, which is used to receive requests, and responses, which is used to send responses.

The server function runs in an infinite loop, waiting for requests to arrive on the requests channel. When a request arrives, it processes it concurrently using a goroutine, and then sends the response back through the responses channel.

In the main function, we create two channels, requests and responses, and start the server by calling the server function with these channels as arguments. We then send five requests to the server, and wait for the responses to arrive, printing them to the console when they do.

When you run this code, you should see the following output:

Processed request 1: Hello 1
Processed request 2: Hello 2
Processed request 3: Hello 3
Processed request 4: Hello 4
Processed request 5: Hello 5

This output shows that the requests were processed concurrently, as they were received and printed out in a random order. This demonstrates the power of goroutines and channels in Go, and how they can be used to write efficient and scalable concurrent programs.

© Copyright 2023 by Astro Tech Blog. Built with ♥ by FriendlyUser. Last updated on 2023-05-29.