Go
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions and Methods
-
Section 4: Concurrency
-
Section 5: Packages and Tooling
-
Section 6: More Standard Library
-
Section 7: Building Services
-
Section 8: Advanced Go
-
Section 9: Go in the Cloud-Native Ecosystem
-
Section 10: Data Structures and Algorithms in Go
-
Section 11: Testing and Deployment
-
Section 12: Practical Projects
-
Section 13: More Standard Library Practice
-
Section 14: More Practice Projects
-
Section 15: Design Patterns in Go
-
Section 16: Interview Practice
-
Section 17: Package fmt In Depth
-
Section 18: Package strings and strconv
-
Section 19: Package os and io
-
Section 20: Package time
-
Section 21: Package sort and container
-
Section 22: Package encoding
-
Section 23: Package net/http In Depth
-
Section 24: Package context
-
Section 25: Package regexp and bytes
-
Section 26: Package errors In Depth
-
Section 27: Package crypto and hash
-
Section 28: Package flag and log
-
Section 29: Package sync In Depth
-
Section 30: More Practice Exercises
-
Section 31: Go Modules and Workspaces In Depth
-
Section 32: Generics Deep Dive (Go 1.18+)
-
Section 33: Testing Package In Depth
-
Section 34: More Interview and Whiteboard Practice
-
Section 35: Package math and unicode
-
Section 36: Package path and filepath
-
Section 37: Package database/sql
-
Section 38: Package text/template and html/template
-
Section 39: Package archive and compress
-
Section 40: Lower-Level net Package
-
Section 41: Package runtime and reflect
-
Section 42: Package embed and io/fs
-
Section 43: Go Assembly and CGO Basics
-
Section 44: Building CLIs and TUIs
-
Section 45: Go Performance Tuning
-
Section 46: More Real-World Projects
-
Section 47: Go in Production
-
Section 48: Go Design Patterns
-
Section 49: Go Interfaces Deep Dive
-
Section 50: Final Practice and Review
104: Implementing Data Structures Idiomatically in Go
Should I just export my internal map or slice, or wrap it in a struct?
I see this a lot when people coming from Python or JavaScript start with Go. They'll just use a map[string]bool to represent a Set and pass it around. While that works for a tiny script, it's a nightmare for a real project. Why? Because you have zero control over how that map is manipulated. Any part of your code can accidentally clear it, put in a nil value, or—worst of all—cause a panic by writing to it from multiple goroutines.
The idiomatic way is to wrap your data in a struct and keep the internal storage private (lowercase). This lets you define a strict API for how the data is accessed. Here is how I usually set up a simple Set:
type Set struct {
elements map[string]struct{}
}
func NewSet() *Set {
return &Set{
elements: make(map[string]struct{}),
}
}
func (s *Set) Add(item string) {
s.elements[item] = struct{}{}
}
func (s *Set) Contains(item string) bool {
_, exists := s.elements[item]
return exists
}
Notice I used struct{} as the map value. It's a Go trick: an empty struct consumes zero bytes of memory, making it the most efficient way to implement a set. If you used bool, you'd be wasting a byte for every single entry.
Do I always need pointer receivers for my data structure methods?
In short: yes, almost always. If your method needs to modify the internal state of the structure—like adding an element to our Set—you must use a pointer receiver (*Set). If you use a value receiver, Go creates a copy of the entire struct every time you call the method, and you'll be modifying a copy that disappears the moment the function returns. It's a classic "why isn't my data updating?" bug.
But there's a second reason to use pointers: consistency. I've seen developers use value receivers for "read-only" methods (like Contains) and pointer receivers for "write" methods (like Add). Don't do that. It's confusing. Pick one and stick to it for the entire type. Since your data structures will almost certainly have at least one method that modifies state, make them all pointer receivers.
How do I make my data structure flexible without using classes or inheritance?
Since Go doesn't have inheritance, you'll be tempted to try and "mimic" it. Resist that urge. Instead, focus on interfaces. If you're building a data structure, don't force the rest of your app to depend on the concrete Set struct. Instead, define an interface that describes what the structure does.
For example, if you have a system that needs to track unique visitors, don't pass around a *Set. Pass around a UniqueTracker interface:
type UniqueTracker interface {
Add(id string)
Contains(id string) bool
}
// Now this function doesn't care if you're using a Map-based Set,
// a Redis-backed Set, or a Bloom Filter.
func ProcessVisitors(tracker UniqueTracker, ids []string) {
for _, id := range ids {
if !tracker.Contains(id) {
tracker.Add(id)
fmt.Println("New visitor discovered:", id)
}
}
}
This approach decouples your storage implementation from your business logic. I've spent hours refactoring old Go code where the developer tied everything to a specific struct, only to realize later they needed to switch to a more performant data structure. Using interfaces saves you from that pain.
📋 Practical Task
Exercise: Building a Thread-Safe Unique User Registry
In a real production environment, your data structures are often accessed by multiple goroutines simultaneously. A standard map in Go is not thread-safe and will crash your program with a concurrent map write panic if you're not careful.
Your Task: Modify the Set implementation from the lesson to create a UserRegistry. Your implementation must meet the following requirements:
- Use a
sync.RWMutexto ensure that multiple goroutines can read from the registry, but only one can write to it at a time. - Implement a method
Add(userId string)that safely adds a user. - Implement a method
HasUser(userId string) boolthat safely checks for a user's existence using a read lock (RLock) for better performance. - Implement a method
Remove(userId string)that safely deletes a user from the registry. - Ensure the internal map is private and cannot be accessed from outside the package.
Test your implementation by launching 100 goroutines that concurrently add and remove users from the same registry instance to ensure no panics occur.
There are no comments for now.