Notes — Module 13: Web Services and APIs¶
These are your personal study notes. Write freely and honestly. Incomplete notes are fine — they show where your understanding still needs work. Return to this file to add insights as they develop over time.
Module: [[go/13. Web Services and APIs]] Topic: [[go]] Date started: YYYY-MM-DD Status: In progress
Concept Map¶
Sketch how the concepts in this module relate to each other. Fill in the Mermaid diagram.
mindmap
root((Web Services))
net/http server
http.Handler interface
http.HandlerFunc adapter
http.ServeMux registration
ListenAndServe
Go 1.22 routing
method + path patterns
r.PathValue path params
wildcard rest...
precedence rules
JSON APIs
encoding/json encode/decode
MaxBytesReader body limit
Content-Type header order
structured error responses
status code choices
Middleware
handler wrapping pattern
Logger / Recoverer / RequestID
chain function
context.WithValue for values
Context in handlers
r.Context propagation
WithTimeout per handler
client disconnect detection
links to Advanced Concurrency
Server config and shutdown
ReadTimeout WriteTimeout IdleTimeout
signal.NotifyContext
srv.Shutdown graceful drain
http.Client
reuse one client
NewRequestWithContext
defer resp.Body.Close
Service structure
ItemStore interface
constructor injection
RegisterRoutes pattern
no global state
Testing
httptest.NewRecorder
httptest.NewServer
fakeStore test double
table-driven handler tests
Alternative: draw this on paper, photo it, and link the image here.
Key Insights¶
The "aha moments" — the things that, once understood, made the rest clear. Be specific: "I finally understood X because Y" is more useful than "X makes sense".
- Middleware is just a function from Handler to Handler: Once I realised that
func(http.Handler) http.Handleris all a middleware is, I could write any middleware I wanted. The pattern is always the same: return a new HandlerFunc that calls next.ServeHTTP inside it. - Headers must be set before any write: I initially assumed I could set Content-Type at the end of a handler as cleanup. It doesn't work — as soon as you write bytes to the ResponseWriter, the HTTP headers are flushed. Set them first.
- Add insights as you discover them
My Understanding¶
Explain the core concepts in your own words, as if teaching them to someone else. If you can't explain it simply, you don't understand it well enough yet.
net/http and ServeMux¶
Your explanation here
What I'm still unsure about: (e.g., exactly when does Go's mux decide that one pattern is more specific than another?)
Go 1.22 routing patterns¶
Your explanation here
What I'm still unsure about: (e.g., what happens when both a method-qualified and a method-unqualified pattern match the same path?)
JSON encoding/decoding in handlers¶
Your explanation here
What I'm still unsure about: (e.g., what does DisallowUnknownFields do when the JSON has extra nested objects?)
Middleware chaining¶
Your explanation here
What I'm still unsure about: (e.g., how do I capture the response status code in a logging middleware without a custom ResponseWriter wrapper?)
Graceful shutdown¶
Your explanation here
What I'm still unsure about: (e.g., what happens to WebSocket connections during Shutdown — are they forcibly closed?)
Connections to Other Topics¶
How does this module connect to things you already know?
| This module's concept | Connects to | How |
|---|---|---|
| JSON error responses | [[go/8. Error Handling]] | errors.As unwrapping *http.MaxBytesError is the same pattern used for custom error types |
r.Context() propagation |
[[go/12. Advanced Concurrency Patterns]] | The context cancellation model is identical; requests are just another source of cancellable contexts |
httptest table-driven tests |
[[go/11. Testing and Benchmarking]] | httptest.NewRecorder replaces a real ResponseWriter but t.Run and the table pattern are unchanged |
ItemStore interface injection |
[[go/6. Methods and Interfaces]] | Dependency injection via interface is the same structural typing pattern; no implements keyword needed |
Questions That Arose¶
Log questions as they appear. Don't stop to answer them now — just capture them. Then move the serious ones to QUESTIONS.md.
- Does the Go 1.22 mux match trailing slash vs no trailing slash differently? → added to QUESTIONS.md as Q001
- Is there a performance difference between
json.NewEncoder(w).Encode(v)andjson.Marshal+w.Write? → might be answered by benchmarking - What is the correct way to capture the response status code in logging middleware? → needs a custom ResponseWriter wrapper study
Code Snippets Worth Remembering¶
Patterns, idioms, or examples that captured something important.
The writeJSON / writeError helpers¶
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(v)
}
func writeError(w http.ResponseWriter, status int, msg string) {
writeJSON(w, status, map[string]string{"error": msg})
}
Why I'm saving this: These two helpers appear in virtually every Go JSON API. Setting Content-Type before WriteHeader is the key ordering constraint.
Graceful shutdown skeleton¶
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("server: %v", err)
}
}()
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
srv.Shutdown(shutdownCtx)
Why I'm saving this: Every production Go HTTP service needs this pattern. The key insight is that http.ErrServerClosed is the expected return after Shutdown — it is not an error.
Middleware wrapping pattern¶
func MyMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// before
next.ServeHTTP(w, r)
// after
})
}
Why I'm saving this: The shape is always the same. Fill in before/after with whatever the middleware needs to do.
http.Client with context¶
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
Why I'm saving this: The two rules are always: use NewRequestWithContext (not NewRequest) and always defer resp.Body.Close().
What Tripped Me Up¶
Mistakes I made, misconceptions I had, things that confused me more than they should have. Being honest here helps you later.
- Headers after WriteHeader — I initially thought you could set headers anywhere in a handler. The HTTP spec and Go's implementation both flush headers on the first write. Set
Content-Typefirst, always. - Not returning after writeError — Writing an error response and forgetting
returncauses the handler to continue running and attempt a second write. The compiler doesn't catch this — it's a logic error.
Summary in My Own Words¶
Write a 3–5 sentence summary of this entire module without looking at any notes. If you can't do this, you need more study time.
Write your summary here after completing the module.
Last updated: YYYY-MM-DD