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
58: Practice Exercise: Building a REST API with net/http
By now, you know how to spin up a basic server in Go. But there is a massive gulf between "making a port listen" and building a REST API that doesn't become a maintenance nightmare the moment you add a second endpoint. I've seen a lot of developers start their first Go API by leaning too hard on the net/http package's global functions, and while it feels productive for the first twenty minutes, it usually ends in tears during the first integration test.
The "Quick and Dirty" Global State
When people first start, they usually do something like this. They define a global slice to hold their data—let's say we're building a simple Book Inventory system—and they use http.HandleFunc to map routes. It looks clean because it's concise, but it's a trap.
var books = []Book{{ID: "1", Title: "The Go Programming Language"}}
func bookHandler(w http.ResponseWriter, r *http.Request) {
// Logic to return books...
json.NewEncoder(w).Encode(books)
}
func main() {
http.HandleFunc("/books", bookHandler)
http.ListenAndServe(":8080", nil)
}
Here is where this breaks. Because books is a global variable, your handler is tightly coupled to that specific instance of data. If you want to write a unit test for bookHandler, you can't easily swap out that slice for a mock database or a controlled test set without affecting every other test running in parallel. Even worse, using the default http.DefaultServeMux (which is what happens when you pass nil to ListenAndServe) means any package in your entire dependency tree can register routes to your server. It's a security risk and a debugging headache.
Structuring for Testability and State
The better way—the way we actually do this in production—is to encapsulate your dependencies in a struct. Think of this struct as your "Application Context." Instead of global variables, you put your database connection, logger, or configuration inside this struct, and then you define your handlers as methods on that struct.
I also strongly recommend creating your own http.ServeMux rather than using the global one. It gives you total control over your routing table and prevents external packages from messing with your endpoints.
type BookServer struct {
db []Book // In a real app, this would be *sql.DB
}
func (s *BookServer) handleBooks(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(s.db)
}
func main() {
server := &BookServer{
db: []Book{{ID: "1", Title: "The Go Programming Language"}},
}
mux := http.NewServeMux()
mux.HandleFunc("/books", server.handleBooks)
http.ListenAndServe(":8080", mux)
}
Now, the trade-off is a bit more boilerplate. You have to define a struct and initialize it. But look at what you gained: you can now instantiate BookServer in a test file with a specific set of dummy books, call handleBooks directly, and verify the output without ever actually opening a network port. You've decoupled your business logic from the global state of the running process.
Handling the JSON Plumbing
One last thing that trips people up is how they handle responses. I see a lot of developers manually writing strings into the ResponseWriter or forgetting to set the Content-Type header. In a REST API, consistency is everything. If one endpoint returns JSON and another returns plain text because of a missing header, your frontend developers will hate you.
Instead of scattering json.NewEncoder calls everywhere, I usually suggest creating a small helper method on your server struct. It keeps your handlers focused on the "what" rather than the "how." It turns a three-line ritual of setting headers and encoding into a single, readable function call. It's a small win, but across fifty endpoints, it's the difference between a codebase that feels professional and one that feels like a collection of scripts.
📋 Practical Task
Exercise: Refactoring the Library Catalog API
You have been handed a "prototype" for a Library Catalog API that uses global state and the default serve mux. Your task is to refactor this into a professional structure using a server struct and a custom mux.
Requirements:
- Create a
CatalogServerstruct that holds a slice ofBookobjects (each book should have anIDand aTitle). - Move the logic for the
/booksendpoint into a method ofCatalogServer. - Replace
http.HandleFuncwith a customhttp.NewServeMux(). - Ensure the
Content-Type: application/jsonheader is set for the response. - Initialize the server in
main()with at least two books before starting the listener.
Starter Code:
package main
import (
"encoding/json"
"net/http"
)
type Book struct {
ID string `json:"id"`
Title string `json:"title"`
}
var library = []Book{{ID: "1", Title: "Clean Code"}}
func getBooks(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(library)
}
func main() {
http.HandleFunc("/books", getBooks)
http.ListenAndServe(":8080", nil)
}
There are no comments for now.