Question
Given an io.ReadCloser, such as the Body from an http.Response, what is the most efficient and idiomatic way in Go to read the entire stream into a string?
For example:
resp, err := http.Get("https://example.com")
if err != nil {
// handle error
}
// How can resp.Body be converted to a string?
Short Answer
You will learn how to consume an io.Reader in Go with io.ReadAll, convert the resulting bytes to a string, close io.ReadCloser values such as HTTP response bodies, and decide when reading an entire stream is appropriate.
Concept
An io.Reader represents a source of bytes that can be read progressively. Files, HTTP response bodies, compressed data, and network connections often implement this interface.
An io.ReadCloser is an io.Reader with an additional Close method. http.Response.Body is an io.ReadCloser because its underlying network resources must be released when you are finished with it.
When you truly need the entire content as text, use io.ReadAll:
bytes, err := io.ReadAll(reader)
text := string(bytes)
io.ReadAll keeps reading until it reaches the end of the stream or encounters an error. It returns a []byte; converting that byte slice with string(bytes) gives you a Go string.
This is idiomatic, but it is only efficient when the input has a reasonable, known size. Reading the whole stream means all content must fit in memory. For a very large or untrusted response, process it incrementally or enforce a size limit instead.
Mental Model
Think of an io.Reader as a drinking straw connected to a container of data. You normally sip bytes through it a little at a time.
io.ReadAll says: “Keep sipping until the container is empty, then give me everything collected in one bucket.” Converting the bucket of bytes to a string gives you text.
An io.ReadCloser also has a valve. Calling Close shuts the valve and releases the resource, such as the HTTP connection. Even after you have read all data, closing it is still the correct habit.
Syntax and Examples
The standard pattern for a readable stream is:
contents, err := io.ReadAll(reader)
if err != nil {
return err
}
text := string(contents)
For an HTTP response body, defer Close immediately after successfully receiving the response:
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
resp, err := http.Get("https://example.com")
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
panic(err)
}
text := string(body)
fmt.Println(text)
}
io.ReadAll returns bytes because readers are not necessarily textual: they may contain an image, a ZIP file, or arbitrary binary data. Convert to string only when the bytes represent text in the expected encoding, commonly UTF-8.
In modern Go, prefer io.ReadAll. The older ioutil.ReadAll still works in existing code but the io package version is the current API.
Step by Step Execution
Consider this function:
func readBody(resp *http.Response) (string, error) {
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(data), nil
}
Execution proceeds as follows:
resp.Bodyis anio.ReadCloserthat supplies the response bytes.defer resp.Body.Close()schedules cleanup for the momentreadBodyreturns, including returns caused by an error.io.ReadAll(resp.Body)repeatedly callsReadinternally until it getsio.EOF, indicating the stream ended.- The collected bytes are stored in
data, a[]byte. - If reading failed because of a network or decoding problem, the function returns an empty string and that error.
string(data)creates a string from the bytes.- The function returns the text; then the deferred
Closecall runs and releases the response body resources.
A response body can normally be consumed only once. After io.ReadAll reaches its end, another read returns no more body content.
Real World Use Cases
Reading a complete stream into a string is useful when the content is small enough to fit comfortably in memory:
- Reading a JSON error response from an HTTP API for logging or diagnostics.
- Loading a small configuration file, template, or SQL script.
- Fetching a plain-text endpoint such as a health-check message.
- Reading the output of a command when the expected output is small.
- Writing tests that compare a response body with an expected string.
For JSON API responses, you often do not need an intermediate string. Decode directly from the body instead:
err := json.NewDecoder(resp.Body).Decode(&result)
Direct decoding avoids storing a separate full copy of the JSON response.
Real Codebase Usage
In production code, developers usually combine full reads with resource cleanup, status validation, and size limits.
Check the HTTP status before treating the body as success data
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
data, readErr := io.ReadAll(resp.Body)
if readErr != nil {
return fmt.Errorf("read error response: %w", readErr)
}
return fmt.Errorf("request failed: %s: %s", resp.Status, string(data))
}
Limit data from an untrusted source
An external server might return an unexpectedly large response. io.LimitReader ensures your program reads no more than a chosen maximum:
const maxBodySize = 1 << 20 // 1 MiB
limited := io.LimitReader(resp.Body, maxBodySize)
data, err := io.ReadAll(limited)
if err != nil {
return err
}
If you must detect that a limit was exceeded, read one extra byte or use an http.MaxBytesReader in HTTP server handlers.
Stream rather than collect
For large files, copy directly to a destination:
_, err := io.Copy(outputFile, resp.Body)
For line-oriented text, use bufio.Scanner with an appropriate buffer limit, or bufio.Reader when you need more control. These approaches use bounded memory rather than retaining the entire input.
Common Mistakes
Forgetting to close an HTTP response body
Broken:
resp, err := http.Get(url)
if err != nil {
return err
}
data, err := io.ReadAll(resp.Body)
Fix: close it after confirming http.Get succeeded.
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
Failing to close bodies can prevent HTTP connections from being reused and can exhaust resources over time.
Ignoring read errors
Broken:
data, _ := io.ReadAll(resp.Body)
text := string(data)
A network stream can fail partway through reading. Always check the error; otherwise you may use incomplete data.
Reading unlimited untrusted input
Broken:
data, err := io.ReadAll(resp.Body)
This may allocate a very large amount of memory if the server sends a huge body. Apply a limit or stream the content when response size is unknown.
Assuming every byte stream is text
text := string(data)
This conversion is valid Go, but it does not make binary data meaningful text. Use bytes directly for binary formats, or decode with the appropriate format-specific package.
Trying to read the body twice
first, _ := io.ReadAll(resp.Body)
second, _ := io.ReadAll(resp.Body) // usually empty
Readers move forward. Save first if you need to inspect the content more than once, or replace resp.Body with a new reader only when that is genuinely necessary.
Comparisons
| Approach | Best for | Memory behavior |
|---|---|---|
io.ReadAll(r) then string(data) | Small text content you need all at once | Stores the full content in memory |
json.NewDecoder(r).Decode(&v) | JSON you want as a Go value | Processes input as it decodes |
io.Copy(dst, r) | Downloading or forwarding files | Streams data without retaining it all |
bufio.Scanner | Small-to-medium line-based text | Reads one token at a time; has token-size limits |
bufio.Reader | Custom incremental text or byte reads | Buffered streaming |
io.Reader is the minimal reading interface:
type Reader interface {
Read(p []byte) (n int, err error)
}
io.ReadCloser adds cleanup:
type ReadCloser interface {
Reader
Close() error
}
Use io.Reader in function parameters when your function only needs to read. Require io.ReadCloser only when the function itself is responsible for closing the input.
Cheat Sheet
import "io"
// Read all bytes from any io.Reader.
data, err := io.ReadAll(r)
// Convert textual bytes to a Go string.
text := string(data)
// Typical HTTP response pattern.
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
- Use
io.ReadAllfor small, bounded content. io.ReadAllreturns[]byte, notstring.- Check the returned error.
- Close an
io.ReadCloserwhen you own it;http.Response.Bodyshould be closed. - A reader is usually consumed once.
- Use
io.LimitReaderwhen the input size is not trusted. - Use
io.Copyor a decoder for large content or structured data. - Prefer
io.ReadAllover deprecatedioutil.ReadAllin new Go code.
FAQ
How do I convert an io.Reader to a string in Go?
Call io.ReadAll(reader), check the error, and convert the returned bytes with string(data).
Do I need to close resp.Body after calling io.ReadAll?
Yes. io.ReadAll consumes the content but does not call Close. Use defer resp.Body.Close() after a successful HTTP request.
Is io.ReadAll efficient?
It is idiomatic and suitable for small or size-limited input. It is not suitable for arbitrarily large streams because it stores the full content in memory.
Should I use ioutil.ReadAll or io.ReadAll?
Use io.ReadAll in new code. ioutil.ReadAll remains for compatibility with older code but is deprecated.
Can I read an HTTP response body more than once?
Not directly. Reading advances through the stream. Keep the bytes you read, or create a new reader from those bytes if another component must read them.
What if the response is JSON?
When you need a Go struct or map, use json.NewDecoder(resp.Body).Decode(&value) instead of reading into a string first.
What character encoding does string(data) use?
It converts bytes to a string without validating or transcoding them. Most Go HTTP text is UTF-8, but if another encoding is used, decode that encoding explicitly.
Mini Project
Description
Build a small function that fetches a text endpoint safely. It demonstrates the standard HTTP body lifecycle: make a request, check the status, limit the amount of data read, convert a successful body to text, and always close the body.
Goal
Create a function that returns the text from a successful HTTP response while rejecting non-successful statuses and oversized responses.
Requirements
Use http.Get to request a URL.
Close the response body on every path after a response is received.
Accept only HTTP status codes from 200 through 299.
Read no more than 1 MiB of response data.
Return the successful body as a string and propagate meaningful errors.
Keep learning
Related questions
Are Go Maps Passed by Value or Reference?
Learn Go map parameter and return semantics: maps are passed by value, while copies share underlying data. Know when map pointers are useful.
Authentication for Go REST APIs: Sessions, Tokens, and Middleware
Learn practical authentication patterns for Go REST APIs, including password hashing, server sessions, bearer tokens, middleware, and security rules.
Automatic Build Versioning in Go: Embed Incrementing Build Numbers
Learn how to add automatic build versioning in Go using linker flags, build metadata, CI counters, and Git-based version values.