A little about Goroutines in Go!

A little about Goroutines in Go!

Goroutines instead of threads

One of the main goals of the?Go programming language?is to make concurrency simpler, faster, and more efficient.

Goroutines are certainly one of the main features of Go and one of the main reasons why Go is so efficient at performing tasks in parallel.

When we create APIs in Go using net/http and receive a large number of requests, what does the pkg net/http use? If your answer is Goroutines, you are correct ??.

I avoid the term "lightweight threads" as it creates confusion for those who are starting or trying to understand what Goroutines are. Although it is true that Goroutines are similar to "lightweight threads" in other systems, Go uses a different implementation that is optimized for the context of Go. Therefore, it may be better to explain Goroutines in terms of how they work in Go, which I believe will make it even easier for you to understand.

Instead of saying that Goroutines are "lightweight threads", we can say that Goroutines are lightweight units of concurrency that are executed in a single execution thread or in several depending on how the Go runtime decides to manage them. This means that Go can execute many Goroutines simultaneously in a single thread or multiple threads.

Goroutines are managed by the Go runtime and not by the underlying operating system. This means that Goroutines are much lighter and more efficient than traditional operating system threads, and that Go can manage them more effectively to achieve maximum performance and scalability.

One of the goals of Goroutines in Go is to allow you to write concurrent programs easily and efficiently. This means that you can have multiple Goroutines running simultaneously, each executing a different task, without worrying about resource locks, active waits, or other common problems in concurrent programming.

To create a Goroutine in Go, you can simply add the "go" keyword before a function call. For example, consider the following code:


package main
func myFuncJeff() {
    // your code here.
}
func main() {
    // start the Goroutine
    go myFuncJeff()
    // you can put something here
}        

Goroutines?are lightweight and take advantage of all available processing power. Goroutines only exist in the virtual space of the Go runtime and not in the operating system.

Goroutine?is a method/function that can be executed independently along with other goroutines. Each concurrent activity in the Go language is usually called a?Goroutine.

Go?is a?multiparadigm?language and one of the most relevant paradigms is?concurrency. One of the most relevant and important points in the?Go?language is working with?concurrency.?Go?innovated?by breaking the?traditional?model of?threads ?and their usage by creating a new model,?goroutines .?Goroutines?are responsible for executing tasks in?Go?asynchronously. They are very powerful and a simple machine with?1GB?of?RAM and 1 CPU?can run thousands of?goroutines.

In Go,?Goroutines?make?concurrency?easy to use.?Goroutines?can be very cheap: they have little overhead besides the memory for the stack, which is only a few kilobytes.

To make stacks small, the Go runtime uses resizable, bounded stacks. A newly launched?goroutine?gets a few kilobytes, which is almost always enough. When it's not, the runtime grows (and shrinks) the memory to store the stack automatically, allowing many?goroutines?to live in a modest amount of memory.

The CPU overhead has, on average, three cheap instructions per function call. It's practical to create?hundreds of thousands of goroutines?in the same address space. If?goroutines?were just?threads, the system resources would run out at a much lower number.

package main
func enviarEmail(...string) {
    // Code to send email
}
func enviarParaFila(mensagem string) {
    // Code sending to queue
}
func main() {
    // goroutine sends email
    go enviarEmail("[email protected]", "my email")
    // goroutine sends to queue
    go enviarParaFila("Message to queue")
    // Do something else here
}        

In this example, we created two functions, enviarEmail and enviarParaFila, which simulate sending an email and sending a message to a queue, respectively. Then, in the main() function, we start two different Goroutines, one to send an email and another to send a message to the queue.

Note that we are using the go keyword before each function call to start a separate Goroutine for each task. This means that the program will continue to run normally, without waiting for the email or queue message to be completed.

The design of Go was heavily influenced by C.A.R. Hoare's 1978 article "Communicating Sequential Processes" (CSP).

Simultaneity in CSP ideas

One of the most successful models for providing high-level language support for concurrency is Hoare's Communicating Sequential Processes (CSP), Occam and Erlang are two well-known languages that derive from CSP. The concurrency primitives of Go derive from a different part of the genealogy whose main contribution is the powerful notion of channels as first-class objects. Experience with several previous languages showed that the CSP model fits well within a procedural language framework.

The biggest difference between Go and the CSP model, aside from syntax, is that Go models communication channels explicitly as channels, while Hoare's language processes send messages directly to each other, similar to Erlang.

Well, to get to this result, Go again received criticism of the adopted model. Go incorporates a variant of CSP (Communicating Sequential Processes), a formal language for describing interaction patterns in concurrent systems with first-class channels. A single-write approach was not adopted to value semantics in the context of concurrent computing as is done in Erlang, instead they adopted something practical and which resulted in something powerful, allowing for simple and safe concurrent programming, but does not prohibit incorrect programming. And the created motto was: "Don't communicate by sharing memory, share memory by communicating".

The simplicity and concurrency support offered by Go generates robustness.

package main
import (
    "fmt"
    "net/http"
    "time"
)
func getSites(url string) {
    response, err := http.Get(url)
    if err != nil {
        fmt.Printf("Error reading %s: %s\n", url, err)
        return
    }
    defer response.Body.Close()
    fmt.Printf("%s read successfully\n", url)
}
func main() {
    // URLs we will read
    urls := []string{
        "https://www.google.com",
        "https://www.facebook.com",
        "https://www.github.com",
        "https://www.dhirubhai.net",
    }
    // Separate goroutine to read each URL
    for _, url := range urls {
        go getSites(url)
    }
    // Do something else here
        // ...
    time.Sleep(5 * time.Second)
}        


In this example, we defined a?getSite?function that uses the?net/http?package to make an HTTP request to a specific website and read the response. Then, in the?main()?function, we defined a list of URLs that we want to read and started a separate Goroutine to read each URL.

Note that we're using a?for?loop to iterate through the list of URLs and start a separate Goroutine for each URL using the?go?keyword. This means that the program will continue to run normally, without waiting for each HTTP request to complete.

To give the Goroutines time to do their job, we use the?time.Sleep()?function to suspend the program's execution for a short period of time. In this example, we use a delay of 5 seconds to allow the Goroutines to finish executing.

Now, let's write the same code using?sync.WaitGroup?to synchronize our Goroutines and gain even better control over them.

package main
import (
    "fmt"
    "net/http"
    "sync"
)
func getSite(url string, wg *sync.WaitGroup) {
    // Signal the WaitGroup when the Goroutine finishes
    defer wg.Done()

    resp, err := http.Get(url)
    if err != nil {
        fmt.Printf("Error reading %s: %s\n", url, err)
        return
    }
    defer resp.Body.Close()

    fmt.Printf("%s read successfully\n", url)
}
func main() {
    // URLs we will read
    urls := []string{
        "https://www.google.com",
        "https://www.facebook.com",
        "https://www.github.com",
        "https://www.dhirubhai.net",
    }
    // Create a WaitGroup to synchronize the Goroutines
    var wg sync.WaitGroup
    for _, url := range urls {
        // Add WaitGroup for each Goroutine
        wg.Add(1)
        // Pass the WaitGroup as a pointer
        go getSite(url, &wg)
    }
    // Wait until all Goroutines finish
    wg.Wait()
    fmt.Println("done")
}        


Using WaitGroup, we can ensure that all Goroutines finish executing before the program continues. This is especially useful when we want the program to only terminate when all concurrent tasks are completed.

Concurrency is different from Parallelism

One of the famous quotes is:?"Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once."?Concurrency?is the?composition?of independently executing computations.?Concurrency?is a way of structuring software and definitely not parallelism, although it enables parallelism.

If you have only one physical core in your processor, your program can still be concurrent but cannot be parallel. On the other hand, a well-written concurrent program can efficiently run in parallel on a processor that has more than one physical core. I suggest checking out this video of a talk by?Rob Pike ?"Concurrency is not Parallelism ."

Concurrency?in?Go?is very powerful and also easy to use, this was the intention of the engineers who developed?Go. Solving many problems is much more efficient using?concurrency?and this is the?power?of?Go, which has become a?god?when it comes to?concurrency. Due to this, problems encompassed in this universe will be solved with much?efficiency?and most importantly, with very?little computational resources.

Want to further deepen your understanding of?concurrency? Just follow this?link . Want to test and see some examples??Click here .

Goroutine?is a very extensive topic, full of details and many interesting situations. So, it requires us to make a post only about it in the future so that it can be treated with due care and attention.

Check out a simple example.

package main
import (
    "fmt"
    "time"
)
func main() {
    go func(){fmt.Println("hi I'm a goroutine1!")}()
    go func(){fmt.Println("hi I'm a goroutine2!")}()
    go func(){fmt.Println("hi I'm a goroutine3!")}()
    fmt.Println("Hello, we are testing goroutines in Go!")  
    time.Sleep(time.Second)
}        


Channels

Channels in Go are a way to synchronize communication between Goroutines and share data. They allow one Goroutine to safely and efficiently send data to another Goroutine without the need for locks or semaphores.

Channels are created using the?make()?function and can be used to send or receive values of a specific type. Channels can be defined with an optional capacity, which specifies how many values can be stored in the channel before it starts to block. If the capacity is not specified, the channel will have a capacity of zero, meaning it can only store one value at a time.

To send a value to a channel, we use the syntax channel <- value. To receive a value from a channel, we use the same syntax but reversed, like value <- channel. When we use the <- syntax without a variable on the left or right side, it is used to block the execution of the Goroutine until a value is sent or received on the channel.

package main

import "fmt"

func main() {
    // Criando um canal sem buffer
    canalSemBuffer := make(chan int)
    // Criando um canal com buffer de tamanho 3
    canalComBuffer := make(chan int, 3)

    // Enviando valores para o canal sem buffer
    go func() {
        canalSemBuffer <- 1
        canalSemBuffer <- 2
        canalSemBuffer <- 3
        close(canalSemBuffer)
    }()

    // Enviando valores para o canal com buffer
    canalComBuffer <- 1
    canalComBuffer <- 2
    canalComBuffer <- 3
    close(canalComBuffer)

    // Recebendo valores do canal sem buffer
    for valor := range canalSemBuffer {
        fmt.Println("Valor recebido do canal sem buffer:", valor)
    }

    // Recebendo valores do canal com buffer
    for valor := range canalComBuffer {
        fmt.Println("Valor recebido do canal com buffer:", valor)
    }
}        


In this example, we are creating two channels, one with a buffer size of 0 and another with a buffer size of 3. When a channel has a buffer size greater than 0, it becomes a buffered channel. Buffered channels can hold a limited number of values until they are read. If the buffer is full and more values are sent to the channel, the sending goroutine will block until a value is read from the channel.

In the code above, we are sending three values to the buffered channel without blocking, and then closing the channel. After that, we are receiving these values from the channel using a for range loop.

We are also sending three values to the unbuffered channel using a goroutine, and then closing the channel. After that, we are receiving these values from the channel using a for range loop as well. Since the unbuffered channel has no capacity, the goroutine blocks until a receiver is ready to receive a value from the channel.

package main
import "fmt"
func produtor(canal chan<- int) {
    for i := 0; i < 10; i++ {
        canal <- i // Envia um valor para o canal
    }
    close(canal) // Fecha o canal
}
func consumidor(canal <-chan int) {
    for valor := range canal { // Itera sobre os valores recebidos do canal
        fmt.Println(valor) // Exibe o valor recebido
    }
}
func main() {
    // Cria um canal sem capacidade definida
    canal := make(chan int)
    // Goroutine do produtor para enviar valores para o canal
    go produtor(canal)
    // Goroutine do consumidor para receber os valores do canal
    consumidor(canal)
}        


In this example, we have two different Goroutines: a producer and a consumer. The producer Goroutine sends values to the channel, while the consumer Goroutine receives them and displays them on the screen.

Note that we use the chan keyword to define the type of the channel. In this example, we are using chan int, which means that we are creating a channel that can be used to send or receive integer values.

We also use the <-chan and chan<- keywords to specify whether the channel should be used to send or receive values. In the case of the producer Goroutine, we are using canal chan<- int, which means that we are creating a channel that can only be used to send integer values. In the consumer Goroutine, we are using canal <-chan int, which means that we are creating a channel that can only be used to receive integer values.

By calling the close() function on the channel in the producer Goroutine, we are signaling that there are no more values to be sent through the channel. This causes the range function in the consumer Goroutine to exit the loop when all values have been received.

Let's reinforce the concept: A?channel?is a communication object used by?Goroutines?and plays a fundamental role in communication?between?Goroutines. Technically, a channel is the transfer of data in which the data can be?passed?or?read. Thus, a Goroutine can send data to a channel, while other?Goroutines?can read the data from the same channel, like a queue. Channels are the safest way to communicate in the?Go?language. There are other ways to share data in?Go, not as efficient as?channels. The team that developed?Go?decided not to close the possibilities, and it is possible to share data?without using channels.

Declaring a Channel without and with buffer


type Promise struct {
  Result chan string
  Error  chan error
}
var (
  ch1  = make(chan *Promise)  // received a pointer from the structure
  ch2  = make(chan string, 1) // allows only 1 channels
  ch3  = make(chan int, 2)    // allows only 2 channels
  ch4  = make(chan float64)   // has not been set can freely receive
  ch5  = make(chan []byte)    // by default the capacity is 0
  ch6  = make(chan bool, 1)   // non-zero capacity
  ch7  = make(chan time.Time, 2)
  ch8  = make(chan struct{}, 2)
  ch9  = make(chan struct{})
  ch10 = make(map[string](chan int)) // map channel
  ch11 = make(chan error)
  ch12 = make(chan error, 2)
  // receives a zero struct
  ch14 <-chan struct{}
  ch15 = make(<-chan bool)          // can only read from
  ch16 = make(chan<- []os.FileInfo) // can only write to// holds another channel as its value
  ch17 = make(chan<- chan bool) // can read and write to
)        


Receiving values on the channel

func main() {
  ch2 <- "okay"
  defer close(ch2)
  fmt.Println(ch2, &ch2, <-ch2)
  ch7 <- time.Now()
  ch7 <- time.Now()
  fmt.Println(ch7, &ch7, <-ch7)
  fmt.Println(ch7, &ch7, <-ch7)
  defer close(ch7)
  ch3 <- 1 // okay
  ch3 <- 2 // okay
  // deadlock // ch3 <- 3 // does not accept any more 
  // values, if you do it will error : deadlockdefer close(ch3)
  fmt.Println(ch3, &ch3, <-ch3)
  fmt.Println(ch3, &ch3, <-ch3)
  ch10["lambda"] = make(chan int, 2)
  ch10["lambda"] <- 100defer close(ch10["lambda"])
  fmt.Println(<-ch10["lambda"])
}        


A small example of a goroutine using channels.

Example 1:

package main
import "fmt"
func goroutine(c chan string) {
    fmt.Println("Eu sou um canal: " + <-c + "!")
}
func main() {
  fmt.Println("start goroutine!")
  c := make(chan string)
  go goroutine(c)
  c <- "jeffotoni"
}        


Example 2:

    package main
    import (
        "fmt"
        "time"
    )
    // escrevendo no canal
    func write(ch chan int) {
        for i := 0; i < 5; i++ {
            ch <- i
            fmt.Println("escrever:", i, "to ch")
        }
        close(ch)
    }
    func main() {
        // channel com buffer
        ch := make(chan int, 2)
        // goroutine
        go write(ch)
        //aguarde um pouco
        time.Sleep(1 * time.Second)
        // listando o canal
        for v := range ch {
            fmt.Println("ler", v, "from ch")
            time.Sleep(2 * time.Second)
        }
    }        


In this example, we are defining a variable called ch that is a channel that can only be used to receive values of type struct{}. We use the <-chan keyword to specify that this channel can only be used to receive values.

Check out the code below:

package main
import "fmt"
func main() {
    // Creates an unbuffered channel
    ch := make(chan struct{})
    // Starts a Goroutine to send a value to the channel
    go func() {
        ch <- struct{}{} // Sends a value to the channel
    }()
    // Receives a value from the channel and prints a message
    <-ch
    fmt.Println("Value received from channel")
}        


Select

The select is a control structure in Go that allows you to monitor multiple channels simultaneously and execute the first operation that is ready. This allows you to write concise and efficient code that responds to events from multiple channels at the same time.

package main
import (
    "fmt"
    "time"
)
func sendMessage(channel chan<- string, message string, delay time.Duration) {
    // Wait for a random period of time
    time.Sleep(delay)
    // Send the message to the channel
    channel <- message
}
func main() {
    // Create two unbuffered channels
    channel1 := make(chan string)
    channel2 := make(chan string)
    // Start two separate goroutines to send messages to the channels
    go sendMessage(channel1, "Message for channel 1", time.Second*2)
    go sendMessage(channel2, "Message for channel 2", time.Second*1)
    // Use select to receive the first message that is ready
    select {
    case msg1 := <-channel1:
        fmt.Println("Message received from channel 1:", msg1)
    case msg2 := <-channel2:
        fmt.Println("Message received from channel 2:", msg2)
    }
}        


In this example, we have two different Goroutines that send messages to two different channels. Using select, we are monitoring both channels and displaying the first message that arrives in one of them.

Note that we are using the <- syntax to receive values from the channels inside the select cases. The select will wait until one of the cases is ready, meaning a message has been received from one of the channels.

Additionally, we are using time.Sleep() in the sendMessage() function to simulate a random delay before sending the message to the channel. This is done to demonstrate that the message sent to the channel faster may not be the first one to be received due to the delays.

Select is a powerful tool in Go that can be used to write efficient code that monitors multiple channels at the same time. It is particularly useful for handling asynchronous events and allowing the program to perform multiple tasks simultaneously.

I hope I have helped with a better understanding of Goroutines.

Another complementary and important subject is?"Workloads: CPU-Bound and IO-Bound" ?which I recommend reading to understand even more.

Here you will find more examples:

Go manual at: gobootcamp.jeffotoni

Various examples of Goroutines at: goexample

In?Go, we have an arsenal to work with Goroutines and manage concurrency without losing simplicity, readability, and productivity.

Go?is?love??

要查看或添加评论,请登录

社区洞察

其他会员也浏览了