Question

Read an io.Reader into a String in Go

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.

Take Quiz

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:

  1. resp.Body is an io.ReadCloser that supplies the response bytes.
  2. defer resp.Body.Close() schedules cleanup for the moment readBody returns, including returns caused by an error.
  3. io.ReadAll(resp.Body) repeatedly calls Read internally until it gets io.EOF, indicating the stream ended.
  4. The collected bytes are stored in data, a []byte.
  5. If reading failed because of a network or decoding problem, the function returns an empty string and that error.
  6. string(data) creates a string from the bytes.
  7. The function returns the text; then the deferred Close call 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.

Take Quiz

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

ApproachBest forMemory behavior
io.ReadAll(r) then string(data)Small text content you need all at onceStores the full content in memory
json.NewDecoder(r).Decode(&v)JSON you want as a Go valueProcesses input as it decodes
io.Copy(dst, r)Downloading or forwarding filesStreams data without retaining it all
bufio.ScannerSmall-to-medium line-based textReads one token at a time; has token-size limits
bufio.ReaderCustom incremental text or byte readsBuffered 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.

Take Quiz

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.ReadAll for small, bounded content.
  • io.ReadAll returns []byte, not string.
  • Check the returned error.
  • Close an io.ReadCloser when you own it; http.Response.Body should be closed.
  • A reader is usually consumed once.
  • Use io.LimitReader when the input size is not trusted.
  • Use io.Copy or a decoder for large content or structured data.
  • Prefer io.ReadAll over deprecated ioutil.ReadAll in 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.

Take Quiz

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.

Take Quiz

Keep learning

Related questions

View more