Notes About Concepts in Go.

Aranya Dutta • 16 May 2026

Notes About Concepts in Go That I Learned While Building a Project

I recently built a real-time anonymous chat application in Go using WebSockets. It was my first serious Go project and also my first time working with real-time systems.

While building it, I came across many backend concepts that I had only heard about before. These are some simple notes about what I learned.


1. Goroutines

Goroutines are lightweight threads managed by Go.

They allow multiple tasks to run at the same time without manually managing operating system threads.

Example:

go sendMessage()

This starts sendMessage() in the background while the rest of the program continues running.

In my project, goroutines were used for:

One thing I learned quickly is that goroutines are easy to create, but once many of them run together, debugging becomes much harder.


2. Channels

Channels are used for communication between goroutines.

Instead of directly sharing memory, goroutines can pass data through channels.

Sending data:

messages <- "hello"

Receiving data:

msg := <-messages

In the chat project, channels were heavily used inside the WebSocket hub.

Examples:

Channels made the flow of communication cleaner and easier to reason about.


3. Mutexes

When multiple goroutines access shared data at the same time, race conditions can happen.

Mutexes help prevent this by allowing only one goroutine to modify shared state at a time.

Example:

mu.Lock()
users[id] = user
mu.Unlock()

I used mutexes around shared structures like:

At first I used mutexes almost everywhere. Later I realized that too many locks can make code difficult to maintain.


4. WebSocket Hub Architecture

This was one of the most confusing concepts initially.

I learned that many WebSocket applications use a central Hub to manage all active connections.

The hub is responsible for:

Instead of every part of the application touching WebSocket connections directly, everything communicates through the hub.

I also learned why WebSocket systems usually separate reading and writing into two loops:

The read loop handles incoming messages.

The write loop handles outgoing messages.

This avoids concurrent writes to the same WebSocket connection.


5. Event-Driven Systems

While building the project, I realized almost everything could be treated as an event.

Examples:

join_queue
send_message
match_found
next_partner
chat_ended

The frontend sends events to the backend.

The backend processes them and sends new events back.

This approach made the system easier to extend because new features could be added as new event types instead of tightly coupling everything together.


6. Concurrency Handling

Concurrency is not only about running tasks in parallel.

It is also about managing shared state safely.

Even simple situations became complicated once multiple users interacted with the system simultaneously.

Examples:

This forced me to think carefully about which part of the code owns which data.


7. Cleanup Workers

In-memory systems can slowly collect stale data if cleanup is ignored.

I added background workers that periodically checked:

Example:

for {
    time.Sleep(time.Minute)
    cleanupInactiveUsers()
}

Without cleanup workers, memory usage and stale state would continue growing over time.


8. Avoiding Circular Dependencies

At one point my packages became tightly connected.

Example:

server -> websocket
websocket -> server

This caused circular dependency problems.

I learned that transport layers should not contain business logic.

So I separated responsibilities more clearly:

This made the architecture much easier to understand and maintain.


9. State Machines

Even though I did not explicitly design a formal state machine, the application naturally behaved like one.

Users moved through states like:

offline
idle
in_queue
in_chat

Certain actions were only valid in certain states.

Examples:

Thinking in terms of states helped prevent invalid behavior.


10. In-Memory Realtime Systems

The entire system currently runs in memory.

This means:

all exist only inside the running server process.

This makes the application very fast because no database queries are needed for most operations.

But it also means all state disappears if the server restarts.

This taught me an important tradeoff between speed and persistence.


Final Thoughts

This project taught me much more than just Go syntax.

It helped me understand how real-time systems behave internally and why backend architecture matters even in relatively small projects.

Many concepts that looked confusing initially only started making sense after debugging real problems during development.