Do you often use a channel? Have you ever considered the channel direction? To use a channel, someone sends data to the channel. Then, a counterpart can receive the data. This is the direction. The user wants to receive the data but doesn’t send data to the channel. It’s a common case where the data sender is only in one place.
The following post is helpful if you are not familiar with how to use a channel.
Let’s try to set the direction when using a channel in this post.
Bidirectional channel could cause a wrong usage
Let’s start without defining a direction. Create a struct with a channel and send the data in the internal function.
type channelBidirection struct {
Bidirectional chan int
}
func newChannelBidirection() *channelBidirection {
return &channelBidirection{
Bidirectional: make(chan int, 3),
}
}
func (c *channelBidirection) run(baseValue int) {
limit := baseValue + 3
for i := baseValue; i < limit; i++ {
c.Bidirectional <- i
}
}
If we know how to use the struct, we use it in the following way for example.
func runChannelBidirection() {
middleMan := newChannelBidirection()
fmt.Println("---TryToSend/receive---")
go middleMan.run(9)
fmt.Println(<-middleMan.Bidirectional) // 9
fmt.Println(<-middleMan.Bidirectional) // 10
fmt.Println(<-middleMan.Bidirectional) // 11
}
The data is added on another goroutine and the main thread receives it. This is the expected usage but we can use it in the following way too.
func runChannelBidirection() {
middleMan := newChannelBidirection()
fmt.Println("--- self send/receive---")
middleMan.Bidirectional <- 11
middleMan.Bidirectional <- 12
middleMan.Bidirectional <- 13
fmt.Println(<-middleMan.Bidirectional) // 11
fmt.Println(<-middleMan.Bidirectional) // 12
fmt.Println(<-middleMan.Bidirectional) // 13
}
Because Bidirectional
is a normal channel without direction. Data can be added everywhere. This is the wrong usage. If a new developer joins our project, he/she might use it in this way because of a lack of knowledge of the business logic.
We should avoid such a case by defining a direction.
Learn how to define a channel from Timer implementation
Let’s check the implementation of time.Timer
first. This is a good example.
type Timer struct {
C <-chan Time
r runtimeTimer
}
func NewTimer(d Duration) *Timer {
c := make(chan Time, 1)
t := &Timer{
C: c,
r: runtimeTimer{
when: when(d),
f: sendTime,
arg: c,
},
}
startTimer(&t.r)
return t
}
As you might know, data is set to C
when the specified time elapsed. And of course, it can’t be set in our code. If we can set a value there, the Timer doesn’t work as expected.
To avoid the case, it creates a channel without direction followed by setting it to the exposed variable that has a direction. Let’s check the syntax here.
C <-chan Time // --> Receive only
C chan<- Time // --> Send only
So, a Timer user can only receive the data.
Check the following post too if you are interested in the usage of timer.
Define two directional variables that has the same channel
Let’s define a struct that has an exposed channel with direction.
type channelWithDirection struct {
Receiver <-chan int
sender chan<- int
}
func newChanelwithDirection() *channelWithDirection {
c := make(chan int, 3)
return &channelWithDirection{
Receiver: c,
sender: c,
}
}
There are two ways to expose the channel that allows a user only to receive data. It’s either to define a variable or a function that returns a directional channel. If we use a getter function, the implementation will look like this.
type channelWithDirection struct {
internalChannel chan int
}
func newChanelwithDirection() *channelWithDirection {
return &channelWithDirection{
internalChannel: make(chan int, 3),
}
}
// Use "<-chan int" instead of "chan int"
func (c *channelWithDirection) getReceive() <-chan int {
return c.internalChannel
}
The point here is to define a channel with direction to the return data type.
It’s up to you which one to apply but I think it’s better to apply the former one because it’s obvious that the internal function can only send data via sender
. The data can’t be consumed in the struct itself. It’s more robust than the latter one.
If a user tries to send data via Receiver channel, the IntelliSense shows an error because it’s a wrong usage.
middleMan := newChanelwithDirection()
// invalid operation: cannot send to receive-only channel middleMan.Receiver (variable of type <-chan int)
middleMan.Receiver <- 1
This is an example code to use the struct.
func runChannelWithDirection() {
sleepTime := 10 * time.Millisecond
middleMan := newChanelwithDirection()
go func() {
loopCount := 5
for i := 0; i < loopCount; i++ {
middleMan.tryToSend(i)
}
for i := loopCount; i < 2*loopCount; i++ {
middleMan.sendWithBlock(i)
}
}()
for i := 0; i < 5; i++ {
data := <-middleMan.Receiver
fmt.Printf("Receiver(%d): %d\n", i, data)
time.Sleep(sleepTime)
}
for i := 0; i < 5; i++ {
select {
case data := <-middleMan.getReceive():
fmt.Printf("GetReceiver(%d): %d\n", i, data)
default:
fmt.Println("no more value")
}
time.Sleep(sleepTime)
}
}
// failed to send a data because buffer size was full. Data: [4]
// Receiver(0): 0
// Receiver(1): 1
// Receiver(2): 2
// Receiver(3): 3
// Receiver(4): 5
// GetReceiver(0): 6
// GetReceiver(1): 7
// GetReceiver(2): 8
// GetReceiver(3): 9
// no more value
It makes our code safer than using a bidirectional channel.
Comments