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
82: Building a REST API with Go
When you first start building APIs in Go, the temptation is to lean heavily on the net/http package's simplicity. It's a great package, but there's a very common trap I see developers fall into: they treat the handler function as the place where everything happens. I call this the "God Handler" pattern, and while it feels fast at first, it becomes a maintenance nightmare the moment your project grows beyond a single endpoint.
The Trap of the All-in-One Handler
Imagine we're building a simple Book Inventory API. In a naive implementation, you'd probably write a handler that parses the request, connects to the database, performs the logic, and writes the JSON response all in one block of code. It looks something like this:
func handleCreateBook(w http.ResponseWriter, r *http.Request) {
var b Book
if err := json.NewDecoder(r.Body).Decode(&b); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Directly calling a global DB variable
if err := db.SaveBook(b); err != nil {
http.Error(w, "Database error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(b)
}
On the surface, this works. But look closer. This function is doing three different jobs: it's handling HTTP transport (parsing JSON/writing headers), it's managing business logic, and it's talking to the data layer. If you want to change your database to something else, or if you want to write a unit test for the "save book" logic without starting a real HTTP server, you're stuck. You can't test the logic without triggering the whole HTTP cycle.
Decoupling with a Server Struct
The better way—the way we actually do this in production—is to separate the transport (HTTP) from the logic (the Service). Instead of using global variables and standalone functions, I prefer to wrap my dependencies in a Server struct. This allows us to "inject" our database or logger, making the code modular and testable.
By moving the data logic into a separate BookStore interface, the handler becomes a thin wrapper. Its only job is to translate an HTTP request into a Go object and translate the result back into an HTTP response. I've found that when you treat your handler as a "translator," your code becomes significantly cleaner.
type BookStore interface {
Save(Book) error
}
type Server struct {
Store BookStore
}
func (s *Server) handleCreateBook(w http.ResponseWriter, r *http.Request) {
var b Book
if err := json.NewDecoder(r.Body).Decode(&b); err != nil {
s.respondWithError(w, http.StatusBadRequest, "Invalid request payload")
return
}
if err := s.Store.Save(b); err != nil {
s.respondWithError(w, http.StatusInternalServerError, "Failed to save book")
return
}
s.respondWithJSON(w, http.StatusCreated, b)
}
The Cost of Abstraction vs. The Cost of Rigidity
Now, you might look at this and think, "I've just added more code to do the same thing." You're not wrong. We've added a struct, an interface, and a couple of helper methods like respondWithJSON. In a tiny script, this is overkill.
But here is the trade-off: the naive way is cheap to start but expensive to change. The structured way is slightly more expensive to start but nearly free to change. If we decide to switch from a PostgreSQL database to a MongoDB instance, we only change the implementation of the BookStore. We don't touch a single line of the HTTP handler code. More importantly, we can now write a "MockStore" for our tests, allowing us to test our API logic in milliseconds without needing a database running in the background.
I usually recommend sticking to this pattern from day one. It forces you to think about the boundaries of your application. When you separate how a request arrives (HTTP) from what the application does (Save Book), you're building a system that can actually survive a year of feature requests.
📋 Practical Task
Exercise: Refactoring the Library Management API
You have been handed a legacy piece of code for a "Library Management" API. Currently, it uses a global slice for storage and a monolithic handler that manages both the data and the HTTP response. Your goal is to refactor this into a professional structure.
Requirements:
- Create a
LibraryStoreinterface with aGetBook(id string) (Book, error)method. - Implement a
Serverstruct that holds aLibraryStore. - Refactor the
GetBookHandlerso it no longer accesses a global slice directly, but instead calls theLibraryStoreinterface via theServerstruct. - Implement a helper method on the
Serverstruct calledsendJSONto handle the repetitive task of setting theContent-Typeheader and encoding the JSON response.
Success Criteria: The API should still return the correct book when requested via GET /books/{id}, but the handler function should contain no direct database/slice manipulation logic.
There are no comments for now.