Calling Another Service
Every HTTP lesson so far has been about the server side of a request —
routing, handlers, middleware. Real services also make outbound calls to
other services, and http.Get(url) is the function that looks like the
obvious way to do it. It's also missing two things a production call
needs: a timeout, and a way to cancel it early.
http.DefaultClient has no deadline
resp, err := http.Get(url) // uses http.DefaultClient, Timeout: 0 — unbounded
http.Get is shorthand for http.DefaultClient.Get, and DefaultClient's
Timeout field is its zero value: 0, which means no timeout at all. If
the other service hangs — a slow database on their end, a network partition,
a process that accepted the connection and then never responded — this call
blocks forever. Not "a long time." Forever, until something outside Go
kills the process. One dependency having a bad day becomes every goroutine
that called it stuck permanently, which is how a single slow downstream
takes an entire service down.
Requests carry a context, not just a URL
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return 0, fmt.Errorf("build request: %w", err)
}
resp, err := client.Do(req)
if err != nil {
return 0, fmt.Errorf("call %s: %w", url, err)
}
defer resp.Body.Close()
http.NewRequestWithContext — not http.NewRequest, not http.Get — is
how a caller's own deadline or cancellation actually reaches the network
call. The same ctx idiom this book has used for goroutines and channels
applies here without any new mechanism: if the caller's context is
cancelled, client.Do returns promptly instead of waiting out whatever
timeout the *http.Client itself has configured, or lack of one.
Close the body. Every time.
defer resp.Body.Close() goes immediately after the error check on Do,
even in a function that never reads the response body — which is
FetchStatus's exact situation here, since it only wants the status code.
Skipping Close because you didn't call Read is a real, common mistake:
the underlying TCP connection can't be returned to the client's connection
pool for reuse until the body is closed, so a program that "forgets" this
under load eventually can't open new connections at all — it has quietly
exhausted its own connection pool one un-closed response at a time. The
fix costs one line and must run on every path, success or error, which is
exactly what defer right after the check gives you for free.
A client, not the client
Notice the exercise takes a *http.Client as a parameter rather than
reaching for http.DefaultClient directly. A caller who owns the client can
configure its Timeout, swap in a custom Transport for testing, or point
it at a different server entirely — none of which is possible if the
function hard-codes which client it uses. The hidden test relies on exactly
this: it passes a client pointed at a local test server, which is the only
reason this exercise is gradeable without a real network at all.
Try it
Implement FetchStatus(ctx context.Context, client *http.Client, url string) (int, error).
It builds a GET request against url using ctx, sends it with client,
closes the response body, and returns the status code.
Practice
1. Call Another Service
IntermediateImplement
FetchStatus(ctx context.Context, client *http.Client, url string) (int, error).- Build a
GETrequest againsturl, carryingctx. - Send it with
client. - Close the response body.
- Return the response's status code.
- Wrap any failure (building the request, sending it) with
%w.
package main import ( "context" "fmt" "net/http" ) // FetchStatus GETs url using client and ctx, and returns the response's // status code. func FetchStatus(ctx context.Context, client *http.Client, url string) (int, error) { // TODO: implement return 0, fmt.Errorf("not implemented") } func main() {}Sign in to write and run your solution in the browser.
Sign in for hints and the solution.
- Build a