Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Idiomatic way to make a request-response communication using channels

Tags:

go

channel

Maybe I'm just not reading the spec right or my mindset is still stuck with older synchronization methods, but what is the right way in Go to send one type as receive something else as a response?

One way I had come up with was

package main
import "fmt"

type request struct {
    out chan string
    argument int
}
var input = make(chan *request)
var cache = map[int]string{}
func processor() {
    for {
        select {
            case in := <- input:
                if result, exists := cache[in.argument]; exists {
                    in.out <- result
                }
                result := fmt.Sprintf("%d", in.argument)
                cache[in.argument] = result
                in.out <- result
        }
    }
}

func main() {
    go processor()
    responseCh := make(chan string)
    input <- &request{
        responseCh,
        1,
    }
    result := <- responseCh
    fmt.Println(result)
}

That cache is not really necessary for this example but otherwise it would cause a datarace.

Is this what I'm supposed to do?

like image 725
transistor09 Avatar asked Dec 01 '14 20:12

transistor09


1 Answers

There're plenty of possibilities, depends what is best approach for your problem. When you receive something from a channel, there is nothing like a default way for responding – you need to build the flow by yourself (and you definitely did in the example in your question). Sending a response channel with every request gives you a great flexibility as with every request you can choose where to route the response, but quite often is not necessary.

Here are some other examples:

1. Sending and receiving from the same channel

You can use unbuffered channel for both sending and receiving the responses. This nicely illustrates that unbuffered channels are in fact a synchronisation points in your program. The limitation is of course that we need to send exactly the same type as request and response:

package main

import (
    "fmt"
)

func pow2() (c chan int) {
    c = make(chan int)
    go func() {
        for x := range c {
            c <- x*x
        }
    }()
    return c
}

func main() {
    c := pow2()
    c <- 2
    fmt.Println(<-c) // = 4
    c <- 4
    fmt.Println(<-c) // = 8
}

2. Sending to one channel, receiving from another

You can separate input and output channels. You would be able to use buffered version if you wish. This can be used as request/response scenario and would allow you to have a route responsible for sending the requests, another one for processing them and yet another for receiving responses. Example:

package main

import (
    "fmt"
)

func pow2() (in chan int, out chan int) {
    in = make(chan int)
    out = make(chan int)
    go func() {
        for x := range in {
            out <- x*x
        }       
    }()
    return
}

func main() {
    in, out := pow2()
    go func() {
        in <- 2
        in <- 4
    }()
    fmt.Println(<-out) // = 4
    fmt.Println(<-out) // = 8
}

3. Sending response channel with every request

This is what you've presented in the question. Gives you a flexibility of specifying the response route. This is useful if you want the response to hit the specific processing routine, for example you have many clients with some tasks to do and you want the response to be received by the same client.

package main

import (
    "fmt"
    "sync"
)

type Task struct {
    x int
    c chan int
}

func pow2(in chan Task) {
    for t := range in {
        t.c <- t.x*t.x
    }       
}

func main() {
    var wg sync.WaitGroup   
    in := make(chan Task)

    // Two processors
    go pow2(in)
    go pow2(in)

    // Five clients with some tasks
    for n := 1; n < 5; n++ {
        wg.Add(1)
        go func(x int) {
            defer wg.Done()
            c := make(chan int)
            in <- Task{x, c}
            fmt.Printf("%d**2 = %d\n", x, <-c)
        }(n)
    }

    wg.Wait()
}

Worth saying this scenario doesn't necessary need to be implemented with per-task return channel. If the result has some sort of the client context (for example client id), a single multiplexer could be receiving all the responses and then processing them according to the context.

Sometimes it doesn't make sense to involve channels to achieve simple request-response pattern. When designing go programs, I caught myself trying to inject too many channels into the system (just because I think they're really great). Old good function calls is sometimes all we need:

package main

import (
    "fmt"
)

func pow2(x int) int {
    return x*x
}

func main() {
    fmt.Println(pow2(2))
    fmt.Println(pow2(4))
}

(And this might be a good solution if anyone encounters similar problem as in your example. Echoing the comments you've received under your question, having to protect a single structure, like cache, it might be better to create a structure and expose some methods, which would protect concurrent use with mutex.)

like image 102
tomasz Avatar answered Feb 07 '23 05:02

tomasz