DEV Community

Cover image for Concurrency in Go-2(Go Channels)
Neeraj Kumar
Neeraj Kumar

Posted on

Concurrency in Go-2(Go Channels)

The channel acts as a pipe by which we send typed values from one Goroutine to another. It guarantees synchronization since only one Goroutine has access to a data item at any given time. The ownership of the data is passed between different Goroutine.
Why use channels in golang?

  • Go channels make asynchronous programming easy and fun to use.

  • Channels provide mechanisms for concurrent execution of functions to communicate by sending and receiving values of a specific element type.

Creating a Go channel
You only need to define a channel, channel type using the make function and assign that to a variable. Just like any other types like structs, maps and slices, you need to create a channel before you can use it.
Syntax

package main

func main() {
   myChannel := make(chan datatype)
}
Enter fullscreen mode Exit fullscreen mode

Steps to create a channel in Go
Channels may be created using the make function. Each channel can transmit values of a specific type, called its element type. Channel types are written using the chan keyword followed by their element type.

We use to make similar to slice and map to create channels. Channels are of specific data type represented by [value-type] of the value to be shared i.e sent or received a cross goroutine. e.g ch := make(chan int).

ch := make(chan [value-type])
Enter fullscreen mode Exit fullscreen mode
ch <- values    // sending on a channel
value := <- ch  // Receiving on a channel and assigning it to value
<-ch            // Receiving on a channel and discarding the result
close(ch)       // Closing a channel. It means that channel can't send any data

Enter fullscreen mode Exit fullscreen mode

Example:

package main

import (
    "fmt"
    "os"
    "strconv"
)

// calculating total of values
func TotalValue(array []int, ch chan int) {
    total := 0
    for _, val := range array {
        total += val
    }
    // this is a sending channel
    ch <- total // send total to channel ch
}

func main() {
    ch := make(chan int)
    var err error
    val := os.Args[1:]
    nums := make([]int, len(val))
    for i := 0; i < len(val); i++ {
        if nums[i], err = strconv.Atoi(val[i]); err != nil {
            fmt.Println(err)
        }
        go TotalValue(nums, ch)
        values := <-ch // receive from ch
        fmt.Println("Total: ", values)
    }
}

Enter fullscreen mode Exit fullscreen mode

Explanation:-
We have created a func TotalValue(array []int, ch chan int) {}that calculates totals of a given array of inputs. using the channel to send and receive values.

$ go run main.go 1 3 4 5 6 6 7 7 12 34 24
Total:  1
Total:  4
Total:  8
Total:  13
Total:  19
Total:  25
Total:  32
Total:  39
Total:  51
Total:  85
Total:  109
Enter fullscreen mode Exit fullscreen mode

Directional channel in Go
In Go, channels are bidirectional by default, but these channels can be directional in such a way that they can only send or receive data from goroutines. This is always denoted by the arrow <- the symbol we discussed above

For example on how to create send or receive only channel
Sending channel only

func TotalValue(array []int, ch chan <- int) {
    total := 0
    for _, val:= range array {
        total += val
    }
    // this is a sending channel
    ch <- total // send total to channel ch
}
Enter fullscreen mode Exit fullscreen mode

Explanation:-
ch chan <- int Using the arrow symbol indicates that our channel can only send data of type integer

Receiving channel only
ch <- chan int indicates that our channel can only receive data of type integer.

Unspecified channel
ch:= make(chan int) this channel is declared without defining direction i.e sending or receiving, thus it can either send or receive data but they are of specific data type. Mostly, they are defined in a function and can be used in any direction user needs.

Go channel use cases
In Go, With Go Channels we can achieve asynchronous and concurrency programming. This synchronization technique has a wider range of uses such as async/await patterns and actor models. Below are uses of the channel in Golang

Sending/scheduling Notification
In Go, Notification is considered as one of a kind request from a user or sending a response from the server that returns values that are not important. We use a blank struct type struct {} as the element type of notification channel since its size is zero and hence doesn't consume memory.

Example of schedule notification

package main

import (
    "fmt"
    "time"
)

func notification(t time.Duration) <-chan struct{} {
    ch := make(chan struct{}, 1)
    go func() {
        time.Sleep(t)
        ch <- struct{}{}
    }()
    return ch
}

func main() {
    fmt.Println("Go")
    <-notification(time.Second)
    fmt.Println("Linux")
    <-notification(time.Second)
    fmt.Println("Cloud")
}
Enter fullscreen mode Exit fullscreen mode

Explanation:
func notification(t time.Duration) <-chan struct{} {} mimics the After function in the time standard package. it accepts time... second, millisecond, minutes, hours, etc and it's combined with time.Sleep(duration) to ensure that the program is not blocked thus it executes concurrently.

Use of range in Go channels
In Go, Range is mainly used with a for loop flow control statement to iterate over a channel to retrieve/read values in it. It maintains the FIFO principle used in queues, FIRST IN, FIRST OUT. With range, we can use different types of channels, from unbuffered to buffered.

Example use of range in a channel

package main

import "fmt"

func main() {
// create buffered channel
    ch := make([]chan string,3 )

    close(ch)

    for data := range ch {
        fmt.Println(data)
    }
}
Enter fullscreen mode Exit fullscreen mode

Select statement in Go channels
In Go, the Select statement is always useful whenever numerous numbers of goroutines are sharing data. Each channel has to complete execution first before the next.
Syntax of select statement

select {
case <-ch1:                         // Discard received value
    fmt.Println("Got something")

case x := <-ch2:                    // Assign received value to x
    fmt.Println(x)

case ch3 <- y:                      // Send y to channel
    fmt.Println(y)

default:
    fmt.Println("None of the above")
}
Enter fullscreen mode Exit fullscreen mode

Explanation:
ere there are three possible cases specified with three different conditions.

  • If the channel ch1 is in ready to be read state, then its value will be read (and then discarded) and the text “Got something” will be printed using fmt.Println.

  • If ch2 is in ready to be read state, then its value will be read and assigned to the variable x before printing the value of x.

  • If ch3 is ready to be sent to, then the value y is sent to it before printing the value of y.
    Finally, if no cases are ready, the default statements will be executed. If there’s no default, then the select will block until one of its cases is ready, at which point it performs the associated communication and executes the associated statements. If multiple cases are ready, select will execute one at random.

comma ok idiom
In Golang, the comma ok idiom is mostly used as a for loop statement which iterates over a map[type]type{} list to retrieve values when key is passed.

The ok is true if there was a key on the map. So there are two forms of map access built into the language and two forms of this statement. I have provided a simpler code below demonstrating the map[string]string{} and use of comma ok idiom.

package main

import "fmt"

func main() {
    c := make(chan int)
    go func() {
        c <- 42
        close(c)
    }()

    // ok should be true as we have some content in channel
    v, ok := <-c
    fmt.Println(v, ok)

    // ok will be false as channel is empty here
    v, ok = <-c
    fmt.Println(v, ok)

}
Enter fullscreen mode Exit fullscreen mode

Fan in and fan out in golang channel
In Golang, Fan In and Fan Out, works together to process data from either a single/multiple stream or pipelines into one channel mainly converge and diverging of the task. A good scenario here could be a computer processor which contains several number of tasks to be executed at simultaneously. once job done notification is shared via bus and registries are updated. Fan-In and Fan-Out works the same way. Below is an example:-

package main

import (
    "fmt"
    "os"
    "strconv"
)

func main() {

    tasksNo, _ := strconv.Atoi(os.Args[1])
    fmt.Printf("You entered %d number of tasks to be executed : ", tasksNo)
    //int chanel
    chanInt := make(chan int)
    //complete task channel return bool
    done := make(chan bool)

    // beffered channel of fanout no blocking
    fan_Out_Chan := make([]chan map[int]int, tasksNo)

    // Unbuffered channel which holds a limited number of tasks
    fan_In_Chan := make(chan map[int]int)

    // Random 2d array value generated
    go func(num chan<- int) {
        for i := 0; i < 10; i++ {
            for j := 0; j < 5; j++ {
                num <- j
            }
        }
        close(num)
    }(chanInt)

    for s := 0; s < tasksNo; s++ {
        fan_Out_Chan[s] = fan_out(chanInt, done, s)
    }

    // fan_In combines all results
    fan_In(done, fan_In_Chan, fan_Out_Chan...)

    go func() {
        for s := 0; s < tasksNo; s++ {
            <-done
        }
        close(fan_In_Chan)
    }()

    for key := range fan_In_Chan {
        fmt.Println(key)
    }
}

func fan_out(chan_Int  1; j-- {
                out *= j
            }
            chanOut <- map[int]int{i: out}
            intervals++
        }
        fmt.Println("Goroutine Fan_Out ", task_Name, "processed", intervals, "items")
        close(chanOut)
        done <- true
        fmt.Println("Goroutine Fan_Out", task_Name, "is finished")
    }()
    return chanOut
}

func fan_In(done chan bool, fan_In_Chan chan map[int]int, result_chan ...chan map[int]int) {
    for key, value := range result_chan {
        go func(value chan map[int]int, key int) {
            intervals := 0
            for i := range value {
                fan_In_Chan <- i
                intervals++
            }
            fmt.Println("Goroutine Fan_In  ", key, "consumed", intervals, "items")
            done <- true
        }(value, key)
    }
}
Enter fullscreen mode Exit fullscreen mode
$ go run main.go 3
map[1:1]
map[3:6]
Goroutine Fan_Out  0 processed 15 items
Goroutine Fan_Out 0 is finished
Goroutine Fan_In   0 consumed 15 items
Goroutine Fan_Out  1 processed 18 items
Goroutine Fan_Out 1 is finished
map[4:24]
Goroutine Fan_Out  2 processed 17 items
Goroutine Fan_In   2 consumed 17 items
Enter fullscreen mode Exit fullscreen mode

context in golang channel
In Go, context is found everywhere in a Go program. context the package provides us with context.Context function which allows us to pass "context" across the whole program. We can pass the context provided by any function that calls it and its subroutes call that requires the context as an argument, i.efunc ReadingContext (ctx context.Context) error {}. To use context.Context effectively you need to understand how to use it and when it's needed.

Different aspects of using context in the Go channel

  • Retrieving and storing data in the database
  • Passing timeout or deadline of a certain function execution Sending cancellation signal

Retrieving and storing data in the database

package main

import (
    "context"
    "fmt"
    "github.com/gorilla/mux"
    "github.com/rs/zerolog"
    "github.com/rs/zerolog/log"
    logs "log"
    "net/http"
    "time"
)

var logKey = &struct{}{}

func requestData(w http.ResponseWriter, r *http.Request) {
    data := log.With().Str("Get_Method", r.Method).Str("original_path", r.URL.Path).Logger()
    ctx := context.WithValue(r.Context(), logKey, data)
    storeData(ctx)
}

func storeData(ctx context.Context) {
    logger := ctx.Value(logKey).(zerolog.Logger)
    logger.Debug().Msg("Data stored successfully")
}

func main() {
    fmt.Print("Server starting \n")
    router := mux.NewRouter()
    router.HandleFunc("/handle", requestData)
    srv := &http.Server{
        Handler: router,
        Addr:    "127.0.0.1:8080",
        WriteTimeout: 15 * time.Second,
        ReadTimeout:  15 * time.Second,
    }
    logs.Fatal(srv.ListenAndServe())
}
Enter fullscreen mode Exit fullscreen mode

In Go, channels are essential for communication between goroutines. Channels support bidirectional communication i.e sending and receiving values on the same channel. This mechanism enables goroutines to synchronize without experiencing any deadlock.
Have a good day.

Concurrency in Go-1
https://dev.to/neeraj1997dev/concurrency-in-go-1-2jop

Top comments (0)