3. Channels Pt.2

In the previous lesson, we saw how we can receive values from channels like this:

v := <-ch

However, sometimes we don't care what is passed through a channel. We only care when and if something is passed. In that situation, we can block and wait until something is sent on a channel using the following syntax.

<-ch

This will block until it pops a single item off the channel, then continue, discarding the item.

In cases like this, Empty structsarrow-up-right are often used as a unaryarrow-up-right value so that the sender communicates that this is only a "signal" and not some data that is meant to be captured and used by the receiver.

Here is an example:

func downloadData() chan struct{} {
	downloadDoneCh := make(chan struct{})

	go func() {
		fmt.Println("Downloading data file...")
		time.Sleep(2 * time.Second) // simulate download time

		// after the download is done, send a "signal" to the channel
		downloadDoneCh <- struct{}{}
	}()

	return downloadDoneCh
}

func processData(downloadDoneCh chan struct{}) {
	// any code here can run normally
	fmt.Println("Preparing to process data...")

	// block until `downloadData` sends the signal that it's done
	<-downloadDoneCh

	// any code here can assume that data download is complete
	fmt.Println("Data download complete, starting data processing...")
}

processData(downloadData())
// Preparing to process data...
// Downloading data file...
// Data download complete, starting data processing...

Channels - Signaling Without Data

Receiving Without Storing

Normal Receive (Store the Value)

Signal Receive (Discard the Value)

This blocks until something is sent, then continues without storing the value.

Why Use This?

When you only care that something happened, not what was sent.

Examples:

  • "Download is done"

  • "Task completed"

  • "Ready to proceed"

Empty Struct: struct{}

The empty struct takes up zero bytes of memory:

Perfect for signaling because:

  • No data needed

  • Zero memory cost

  • Clear intent: "this is just a signal"

Creating and Sending Empty Struct

Create channel:

Send empty struct:

Receive (wait for signal):

Complete Example Breakdown

Output:

Flow Diagram

Real-World Example: Multiple Workers

Comparison: With vs Without Data

With Data (Need the Result)

Without Data (Just Need to Know It's Done)

Understanding struct{}{}

Breakdown:

  • struct{} = the type (empty struct)

  • {} = create an instance of it

Other examples:

Common Patterns

Pattern 1: Wait for Single Task

Pattern 2: Wait for Multiple Tasks

Pattern 3: Notify When Ready

Why Not Just Use bool?

You could, but struct{} is better:

Using bool:

Using struct{}:

Benefits:

  • Zero memory

  • Clear intent

  • Can't accidentally interpret the value

Key Takeaways

1

Receive-and-discard

<-ch without assignment receives and discards the value β€” use it to wait for a signal.

2

Empty struct is zero bytes

struct{}{} is an empty struct value that occupies 0 bytes.

3

Signal channels

chan struct{} is the common pattern for channels used purely for signaling.

4

Use case

Use signal channels to indicate completion or readiness, not to pass data.

5

Blocking behavior

Receiving from the channel blocks until a sender provides a signal.

Quick Reference

Operation
Syntax
Purpose

Create signal channel

make(chan struct{})

Channel for signaling

Send signal

ch <- struct{}{}

"Something happened"

Wait for signal

<-ch

Block until signal

Check type size

unsafe.Sizeof(struct{}{})

0 bytes

Remember: Use struct{} channels when you care that something happened, not what happened!

Assignment

Our Textio server isn't able to boot up until it receives the signal that its databases are all online, and it learns about them being online by waiting for tokens (empty structs) on a channel.

Run the code. It never exits! The channel passed to waitForDBs stays blocked, because it's only popping the first value off the channel.

Fix the waitForDBs function. It should pause execution until it receives a token for every database from the dbChan channel. Each time waitForDBs reads a token, the getDBsChannel goroutine will print a message to the console for you. The succinctly named numDBs input is the total number of databases. Look at the test code to see how these functions are used so you can understand the control flow.

Solution

The Problem

Currently, it only waits for the first database, then returns. But you need to wait for all databases.

The Solution

You need to receive numDBs times:

Why This Works

Example with 3 databases:

Step-by-Step Flow

Expected Output

(Assuming numDBs = 3)

Why Your Original Code Blocked Forever

The goroutine tries to send all signals, but after the first one, there's no receiver, so it blocks forever.

Complete Solution

That's it! The key is looping numDBs times to receive all the signals.