Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Channels in Go #33

Open
japananh opened this issue Feb 19, 2024 · 0 comments
Open

Channels in Go #33

japananh opened this issue Feb 19, 2024 · 0 comments
Assignees
Labels

Comments

@japananh
Copy link
Owner

japananh commented Feb 19, 2024

Channels in Go

1. How channel was invented?

The communication in the Go channel is inspired by CSP and guarded command.

CSP stands for “Communicating Sequential Processes,” a technique and the paper's name that introduced it. In this paper, Hoare suggests that input and output are two overlooked programming primitives—particularly in concurrent code. CSP was only a simple programming language constructed solely to demonstrate the power of communicating sequential processes.

A guarded command, which Edgar Dijkstra had introduced in a previous paper written in 1974, “Guarded commands, nondeterminacy and formal derivation of programs”, is simply a statement with a left and righthand side, split by a . The lefthand side served as a conditional, or guard for the righthand side in that if the lefthand side was false or, in the case of a command, returned false or had exited, the righthand side would never be executed.

writeCh := make(chan<- interface{})
readCh := make(<-chan interface{})

<-writeCh
readCh <- struct{}{}

This will cause error.

invalid operation: <-writeCh (receive from send-only type
    chan<- interface {})
invalid operation: readCh <- struct {} literal (send to receive-only
    type <-chan interface {})

2. How channels are created in Go?

When the Go compiler encounters the statement ch := make(chan int), it creates a channel capable of transmitting integers. The process involves several steps under the hood, both at compile time and runtime, to set up and initialize this channel for use in your Go program. Here's a simplified view of what happens:

2.1. Compile Time

  1. Type Checking: The compiler verifies that the make function is called with a valid channel type, in this case, chan int. This ensures type safety, meaning the channel will only accept integers.
  2. Code Generation: The compiler generates the necessary instructions to allocate and initialize a channel at runtime, including setting up any internal data structures required for the channel's operation.

2.2. Runtime

When the compiled code reaches the make(chan int) statement during execution, the Go runtime performs the following steps:

  1. Channel Allocation: The runtime allocates memory for the channel. This memory includes the channel itself and the internal data structures needed to manage the channel's state and the messages it will pass.
  2. Initialization: The runtime initializes the channel's internal data structures. These structures include:
    • A queue for storing sent values (this queue has a capacity for buffered channels; for unbuffered channels, the capacity is effectively zero).
    • Synchronization primitives to manage access to the channel, ensuring that send and receive operations are safe to use across multiple goroutines.
    • Status flags or similar mechanisms to track whether the channel is open or closed.
  3. Setting Zero Capacity: For an unbuffered channel like ch := make(chan int), the channel is set up with zero capacity. This means that send operations will block until another goroutine is ready to receive the value, facilitating direct handoff and synchronization between goroutines.
  4. Returning a Reference: The runtime returns a reference to the newly created channel, which is assigned to the variable ch in your Go program. You use this reference to send and receive values through the channel.

2.3. Internal Data Structures

Although the exact implementation details can vary and may evolve over time, Go typically uses complex data structures to manage channels, including:

  • Send and Receive Queues: To manage goroutines waiting to be sent to or received from the channel.
  • Locks or Atomic Operations: To ensure that concurrent access to the channel by multiple goroutines is safe and does not lead to race conditions.

3. When to use channels in Go?

when-to-use-channel-in-go?

4. Go’s Philosophy on Concurrency

Share memory by communicating; don’t communicate by sharing memory.

This phrase, "Share memory by communicating; don’t communicate by sharing memory," encapsulates a fundamental principle of concurrent programming in Go. It contrasts two approaches to concurrency:

4.1. Communicate by Sharing Memory

This traditional approach involves multiple threads accessing and modifying shared data structures. Synchronization primitives such as mutexes, semaphores, or locks are typically used to prevent race conditions and ensure data consistency. While effective in certain contexts, this model can be error-prone and difficult to reason about, especially as the complexity of the concurrency increases. The challenges include deadlocks, race conditions, and the cognitive load of tracking which parts of the code access shared resources.

You would typically protect the counter with a mutex to prevent simultaneous updates.

var (
    counter int
    mutex   sync.Mutex
)

func Increment() {
    mutex.Lock()
    counter++
    mutex.Unlock()
}

4.2 Share Memory by Communicating

Go advocates for a different model of concurrency where goroutines communicate with each other through channels to pass data. In this model, instead of multiple goroutines accessing shared data, the data is sent from one goroutine to another. This passing of data ensures that only one goroutine can access the particular piece of data at any time. Using channels as the primary means of synchronization and communication reduces the need for explicit locks, and the program becomes easier to understand and maintain.

CounterManager runs in its own goroutine in this model, listening for increment requests. Other goroutines send increment requests through the channel. This design ensures that only one goroutine updates the counter at a time based on messages received, thus "sharing memory by communicating."

var (
    counter int
    ch      = make(chan bool)
)

func CounterManager() {
    for range ch {
        counter++
    }
}

func Increment() {
    ch <- true
}

Go’s philosophy on concurrency can be summed up like this: aim for simplicity, use channels when possible, and treat goroutines like a free resource.

@japananh japananh self-assigned this Feb 19, 2024
@japananh japananh changed the title How channels are created in Go? Channels in Go Feb 19, 2024
@japananh japananh changed the title Channels in Go Channels in Golang Feb 19, 2024
@japananh japananh changed the title Channels in Golang Channels in Go Feb 19, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant