Go Channel(for IPC)

Go channels are used for synchronizing IPC between goroutines(Same as Rust Tasks = Green Threads). Go routines are managed by Go runtime.
NOTE: Channels are not needed if 2 or more goroutines are not present. Channels are designed exclusively for inter-goroutine communication and synchronization
If you are doing everything sequentially inside the single main goroutine, channel is unnecessary
we can send/recv data via channel with operator, <- between goroutines. The data flows in the direction of the arrow.
channels are datastructures allocated on heap that reference both sender and receiver goroutines
close(): Only sender can close a channel to indicate that no more values will be sent.

Write/Read on Channel


package main
import "fmt"

func main() {
  ch := make(chan int)  //Create a Channel

	go func() {   // Start goroutine
		v := 1      // Local variable int
		ch <- v  //Write v on channel
	}()

	v, ok := <-ch    // Read from channel
	if !ok {
		fmt.Println("Closed")
	} else {
		fmt.Println("Received first value:", v)
	}
}

$ go run
Received first value: 1
        

//        (channel, capacity)
ch := make(chan int, 100)
// Creates 1 channel that can hold up to 100 items
// ie ints without blocking the sender
        

Types of Channels

Unbuffered, Buffered

Unbuffered / Unbounded Buffered / Bounded
What Like a hand-to-hand pass. The sender must wait until the receiver does not read the value from channel. If no one is there to take it, the sender BLOCKS/DEADLOCK. Like a mailbox with a fixed slot capacity. The sender can drop off letters and leave immediately, as long as the mailbox isn't full. The receiver can pick them up later.
Code

package main
import "fmt"
import "time"

func main() {
    ch := make(chan int) // Unbuffered

    go func() {
        fmt.Println("Sender: Attempting to send...")
        ch *lt;- 42 // Blocks here until main goroutine reads it
        fmt.Println("Sender: Sent successfully!")
    }()

    time.Sleep(2 * time.Second)
    fmt.Println("In main before channel read")
    v := <-ch 
    time.Sleep(2 * time.Second)
    fmt.Println("Receiver: Got", v)
}

$ go run
Sender: Attempting to send...
In main before channel read
Sender: Sent successfully!
Receiver: Got 42
            

package main
import "fmt"

func main() {
    // Buffered channel that can hold up to 2 items
    ch := make(chan int, 2) 

    // We can send 2 items sequentially without any background goroutine!
    ch <- 10 
    ch <- 20 
    fmt.Println("Sent 10 and 20 to buffer without blocking.")

    // ch <- 30 // UNCOMMENTING THIS WOULD BLOCK (Buffer is full!)

    fmt.Println(<-ch) // Reads 10
    fmt.Println(<-ch) // Reads 20
}

$ go run
Sent 10 and 20 to buffer without blocking.
10
20
            
Can Deadlock Yes.
Easier in unbuffered. Conditions for deadlock:
1. Sender goroutine sent data on channel and reciever goroutine is not present to read from it

package main
func main() {
    ch := make(chan int) 

    v := 1
    // DeadLock Main goroutine blocks forever because nobody is reading.
    ch <- v
}
            

2. Receiving without a concurrent sender. reading from channel where no writer

package main
import "fmt"

func main() {
  ch := make(chan int) 

  // DeadLock Main goroutine blocks forever because nobody is writing.
	v, ok := <-ch    // Read from channel
	if !ok {
		fmt.Println("Closed")
	} else {
		fmt.Println("Received first value:", v)
	}
}
            
Yes
Conditions for deadlock:
1. Overfilling the buffer: Sending more items than the buffer's capacity without anyone reading them

func main() {
    // Capacity = 1
    ch := make(chan int, 1)
    ch <- 10 // Fine, fills the buffer.
    ch <- 20 // Deadlock Buffer is full, main goroutine blocks forever.
}
            

2. Reading from an empty buffer: Trying to read from an empty buffered channel when no other goroutines are alive to write to it.

2. Bidirectional / Directional

Directional vs. Undirectional is completely different from Buffered vs. Unbuffered.
Buffered/Unbuffered defines how the channel behaves internally (capacity/blocking rules).
Directional/Bidirectional defines who is allowed to read or write to it (type safety).
By default, all channels you create are bidirectional (you can read and write). However, you can restrict them in function signatures to prevent bugs (e.g., preventing a function that should only read from accidentally writing).

Bidirectional

The unbuffered and buffered channels above are bidirectional ie allows flow in both directions

Directional Channels

1. Write-Only Channel: chan<- int. Look at where the arrow is pointing. It points into the chan. You are taking an integer (int) and pushing it into the channel (chan). You are writing(send) data to chan. ch <- 10.
2. Read-Only Channel: <-chan int. Look at where the arrow is pointing now. It points away from the chan. Data is flowing out of the channel (chan). You can only read (receive) data from it. val := <-ch

Write Only (chan <- int) Read Only (<-chan int)
What Reads not allowed on channel Writes not allowed on channel
Code

package main
func main() {
    // Write only Channel
    ch := make(chan<-int) 

    // reading from WO Channel
    // invalid operation: cannot receive from send-only channel
    v := <-ch
}
          

package main
func main() {
    // Read only Channel
    ch := make(<-chan int) 

    // Writing from RO Channel
    // invalid operation: cannot send to receive-only channel
    ch <- 1
}
          

wait on multiple channels. select


package main

import "fmt"

func main() {
	ch1 := make(chan int)
	ch2 := make(chan int)

	go func() {
		ch1 <- 1
		ch2 <- 2
		// 1. Must close channels when finished sending
		close(ch1)
	    close(ch2)
	}()

	for {
		select {
		case v1, ok := <-ch1:
			if ok {
				fmt.Println("ch1:", v1)
			} else {
				// 2. Setting a channel to nil disables this case in a select block
				ch1 = nil 
			}
		case v2, ok := <-ch2:
			if ok {
				fmt.Println("ch2:", v2)
			} else {
				ch2 = nil
			}
		}

		// 3. If both channels are nil, break out of the infinite loop safely
		if ch1 == nil && ch2 == nil {
			break
		}
	}
	
	fmt.Println("Done!")
}
$ go run
ch1: 1
ch2: 2
Done!
      

Internal Implementation of Channels

When we call make(chan int, 10), the runtime allocates a struct hchan on heap

func main() {
	ch1 := make(chan int)
}

// Channel struct 
// (heap, hchan with buffer + sendq/recvq queues)
type hchan struct {
    qcount   uint           // bytes queued
    dataqsiz uint           // buffer size
    buf      unsafe.Pointer // ring buffer (heap)
    elemsize uint16  // elem size
    closed   uint32
    elemtype *_type
    sendx    uint          // send index
    recvx    uint          // recv index
    recvq    waitq         // blocked RECEIVERS  
    sendq    waitq         // blocked SENDERS ← your goroutine lives here
}
      

Why Channels are Fast

1. Direct Copying: If a sender is waiting and a receiver arrives, the Go runtime copies the data directly from the sender's stack to the receiver's stack, skipping the channel buffer entirely
2. No busy-waiting: Instead of "busy-waiting" (spinning), a blocked goroutine is "parked" by the Go scheduler.