27.5 Reading and Closing Response Bodies
Right, let’s talk about the part of the HTTP conversation everyone gets wrong at least once: dealing with the response body. You’ve made your request, you got your 200 OK, and now you have a resp.Body sitting there. It’s an io.ReadCloser, which is a fantastic Go interface composition meaning “I am a stream of data you must read from and then explicitly close.” This isn’t a suggestion. It’s the law. Break it, and bad things happen.
The most important thing you will learn in this entire chapter is this: you must close the response body, even if you don’t read from it. Why? Because that Body is your live connection to the remote server. Until it’s closed, that connection cannot be reused by the underlying http.Transport for future requests (it’s kept alive by default, which is good for performance!). If you leak enough of these, you’ll run out of sockets. Your application will grind to a halt. The operating system will look at you with profound disappointment.
So, your first and most sacred pattern is this, using defer to ensure it happens no matter how you exit the function:
resp, err := http.Get("http://example.com/")
if err != nil {
// Handle the error. Maybe the network is down, maybe the URL is malformed.
return err
}
// This line is your lifeline. Write it before you do anything else.
defer resp.Body.Close()
// Now, and only now, can you safely read from the body.
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
// Use `body`...
The defer and Error Handling Dance
You’ll notice I put the defer right after checking the initial error. This is crucial. If http.Get fails, resp will be nil, and calling resp.Body.Close() on a nil response will cause a panic. So we only defer the close if we successfully got a response object. This pattern holds for any HTTP client method.
To Read or Not to Read (It’s Not a Question)
You might think, “I only care about the status code, I don’t need the body.” Tough. You still have to close it. The HTTP protocol requires that the entire body be consumed so the connection can be recycled. If you don’t want the data, read it into a black hole and then close. The io.Copy(io.Discard, resp.Body) function is your friend here. The standard library is smart enough to do this for you under certain conditions (like on redirects), but you should never rely on that. It’s your responsibility.
resp, err := http.Get("http://example.com/api/health")
if err != nil {
return err
}
defer resp.Body.Close()
// We just want the 200 status, but we must consume the body.
_, err = io.Copy(io.Discard, resp.Body)
if err != nil {
return err
}
// Now the connection is truly free.
fmt.Println("Status:", resp.Status)
Resource Management is Your Job
This isn’t a Python “garbage collector will eventually get it” situation. This is Go. You manage resources. The http.Client is incredibly efficient, but it’s not magical. It relies on you, the programmer, to follow the contract. Forgetting to close the body is the most common rookie mistake, and it’s a silent killer. It won’t show up in your unit tests. It’ll show up at 3 AM when your production service making a million requests a minute suddenly stops making any requests at all.
Partial Reads and Early Exits
What if you start reading the body—it’s a massive JSON payload—and halfway through you realize you have an error and want to bail? You still need to close the body. The good news is that a defer is still your best bet. It will run when your function returns, closing the body even if you only read the first 50 bytes. The transport is smart enough to handle this; it will read the remaining bytes off the wire and discard them to properly reuse the connection. You just have to hold up your end of the bargain by calling Close().