Question

Convert a Byte Slice to io.Reader in Go

Question

I have a Go HTTP response body that I read into a byte slice:

defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
    log.Println("StatusCode=" + strconv.Itoa(resp.StatusCode))
    return
}

respByte, err := ioutil.ReadAll(resp.Body)
if err != nil {
    log.Println("failed to read response data")
    return
}

This works, but later I want to use that response data as an io.Reader again. How can I convert a []byte into an io.Reader in Go?

I tried using NewReader and NewWriter, but I was not successful.

Short Answer

By the end of this page, you will understand how to turn a []byte into an io.Reader in Go, when to use bytes.NewReader vs bytes.NewBuffer vs strings.NewReader, and why this is useful when reusing HTTP response data after calling ReadAll. You will also see common mistakes, practical examples, and a small project that ties the idea together.

Concept

A []byte in Go is raw data stored in memory. An io.Reader is not the data itself—it is something that provides data when another function asks for it.

That distinction is important:

  • []byte = the actual bytes
  • io.Reader = a stream-like interface for reading bytes

In Go, many APIs accept an io.Reader because it gives them a flexible way to read from many sources:

  • files
  • network connections
  • buffers in memory
  • HTTP bodies
  • strings and byte slices wrapped as readers

When you call:

respByte, err := ioutil.ReadAll(resp.Body)

you consume the original resp.Body stream and store all data in memory as a []byte.

If you later need to pass that same data to something expecting an io.Reader, you do not "convert" the slice directly. Instead, you wrap the byte slice in a reader type from the bytes package.

The most common choices are:

  • bytes.NewReader(data)
  • bytes.NewBuffer(data)
  • bytes.NewBufferString(text) for strings

For a plain []byte, bytes.NewReader is usually the clearest choice.

Why this matters in real programming:

  • You may want to log or inspect a response body and then parse it again.
  • You may need to retry sending the same payload in an HTTP request.
  • You may pass in-memory data to functions that only accept io.Reader.
  • You may want to test code that reads from a stream without using real files or network calls.

Mental Model

Think of []byte as a container full of water and io.Reader as a tap attached to that container.

  • The container holds the data.
  • The tap lets other code read the data gradually.

A byte slice by itself is just stored data. It does not know how to behave like a stream. bytes.NewReader attaches the tap.

So instead of asking, "How do I change bytes into a reader?" think:

"How do I wrap these bytes in something that can read them?"

That is exactly what bytes.NewReader does.

Take Quiz

Syntax and Examples

The core syntax is:

reader := bytes.NewReader(data)

Here, data is a []byte, and reader can be used anywhere an io.Reader is needed.

Basic example

package main

import (
    "bytes"
    "fmt"
    "io"
)

func main() {
    data := []byte("hello, Go")

    reader := bytes.NewReader(data)

    output, err := io.ReadAll(reader)
    if err != nil {
        panic(err)
    }

    fmt.Println(string(output))
}

Output:

hello, Go

What happened?

  • data stores the bytes.
  • bytes.NewReader(data) wraps those bytes in a reader.
  • io.ReadAll(reader) reads from that reader.

Your HTTP example

package main

import (
    "bytes"
    "io"
    "log"
    "net/http"
    "strconv"
)

func handleResponse(resp *http.Response) {
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        log.Println("StatusCode=" + strconv.Itoa(resp.StatusCode))
        return
    }

    respBytes, err := io.ReadAll(resp.Body)
    if err != nil {
        log.Println("failed to read response data")
        return
    }

    reader := bytes.NewReader(respBytes)

    copied, err := io.ReadAll(reader)
    if err != nil {
        log.Println("failed to read from byte reader")
        return
    }

    log.Println(string(copied))
}

Using bytes.NewBuffer

buf := bytes.NewBuffer(respBytes)

This also works as an io.Reader. It is often used when you may also want write-related buffer behavior.

For simple read-only reuse of a byte slice, bytes.NewReader is usually the better fit.

Step by Step Execution

Consider this example:

package main

import (
    "bytes"
    "fmt"
    "io"
)

func main() {
    data := []byte("ABC")
    reader := bytes.NewReader(data)

    chunk := make([]byte, 2)

    n1, _ := reader.Read(chunk)
    fmt.Println(n1, string(chunk[:n1]))

    n2, _ := reader.Read(chunk)
    fmt.Println(n2, string(chunk[:n2]))

    n3, err := reader.Read(chunk)
    fmt.Println(n3, err == io.EOF)
}

Step by step

  1. data := []byte("ABC")

    • A byte slice is created in memory.
  2. reader := bytes.NewReader(data)

    • A reader is created that will read from data.
  3. chunk := make([]byte, 2)

    • We prepare a destination slice that can hold 2 bytes at a time.
  4. First Read

    • Reads A and B
    • n1 becomes 2
    • output is AB
  5. Second Read

    • Only C remains
    • n2 becomes 1
    • output is C
  6. Third Read

    • No data remains
    • n3 becomes 0
    • error becomes io.EOF

This shows an important point: readers have a position. Once read, they move forward. If you want to read again from the beginning, create a new reader or reset the position if the type supports it.

Example with reset using bytes.Reader:

reader := bytes.NewReader([]byte("hello"))
io.ReadAll(reader)
reader.Seek(0, io.SeekStart)
again, _ := io.ReadAll(reader)
fmt.Println(string(again))

Real World Use Cases

Here are common situations where wrapping a []byte as an io.Reader is useful:

Reusing an HTTP response body

You read the body once for logging or validation, then need to pass it to a JSON decoder or another function.

bodyBytes, _ := io.ReadAll(resp.Body)
reader := bytes.NewReader(bodyBytes)

Sending a request body from memory

Many HTTP client methods accept an io.Reader:

payload := []byte(`{"name":"Alice"}`)
resp, err := http.Post(url, "application/json", bytes.NewReader(payload))

Testing functions that read from streams

If your function takes an io.Reader, you can feed test data from a byte slice.

func parse(r io.Reader) error {
    // parse data
    return nil
}

err := parse(bytes.NewReader([]byte("test input")))

Working with libraries that expect io.Reader

Compression, archiving, hashing, image decoding, and CSV/JSON/XML parsing often accept readers.

decoder := json.NewDecoder(bytes.NewReader(bodyBytes))

Replaying the same data multiple times

You may need to parse the same payload more than once in middleware, validation, or retry logic.

Take Quiz

Real Codebase Usage

In real Go codebases, developers often use this pattern in a few predictable ways.

1. Read once, reuse many times

bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
    return err
}

log.Printf("response: %s", string(bodyBytes))

decoder := json.NewDecoder(bytes.NewReader(bodyBytes))
if err := decoder.Decode(&result); err != nil {
    return err
}

This is useful when you want both raw access and structured parsing.

2. Restoring an HTTP body after reading it

In middleware or transport code, developers sometimes read the body and then put it back.

bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
    return err
}
resp.Body.Close()
resp.Body = io.NopCloser(bytes.NewReader(bodyBytes))

This is common if later code still expects resp.Body to be readable.

3. Guard clauses for invalid input

if len(bodyBytes) == 0 {
    return errors.New("empty body")
}

reader := bytes.NewReader(bodyBytes)

Small checks like this make stream-related bugs easier to diagnose.

4. Validation before processing

bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
    return err
}

if !json.Valid(bodyBytes) {
    return errors.New("invalid JSON")
}

return json.NewDecoder(bytes.NewReader(bodyBytes)).Decode(&dst)

5. Retryable request bodies

If a request body must be sent more than once, storing it in bytes and rebuilding a reader is a common pattern.

payload := []byte("retryable content")
for i := 0; i < 3; i++ {
    req, _ := http.NewRequest("POST", url, bytes.NewReader(payload))
    _, err := client.Do(req)
    if err == nil {
        break
    }
}

Common Mistakes

Mistake 1: Trying to read resp.Body twice without saving it

Broken example:

body1, _ := io.ReadAll(resp.Body)
body2, _ := io.ReadAll(resp.Body)

Why it fails:

  • resp.Body is a stream.
  • After the first read, it is exhausted.
  • The second read returns no data.

Fix:

body, _ := io.ReadAll(resp.Body)
reader1 := bytes.NewReader(body)
reader2 := bytes.NewReader(body)

Mistake 2: Confusing []byte with io.Reader

Broken example:

func process(r io.Reader) {}

data := []byte("hello")
process(data)

Why it fails:

  • A byte slice does not implement io.Reader.

Fix:

process(bytes.NewReader(data))

Mistake 3: Using the wrong wrapper type without understanding it

bytes.NewBuffer and bytes.NewReader can both act as readers, but they are not identical.

  • bytes.NewReader is better for read-only access to existing bytes.
  • bytes.NewBuffer is useful when you also want buffer-style operations.

Mistake 4: Forgetting that readers have state

Broken example:

r := bytes.NewReader([]byte("hello"))
io.ReadAll(r)
again, _ := io.ReadAll(r)
fmt.Println(string(again))

The second read prints an empty string because the reader is already at the end.

Fix:

r := bytes.NewReader([]byte("hello"))
io.ReadAll(r)
r.Seek(0, io.SeekStart)
again, _ := io.ReadAll(r)

Or simply create a new reader.

Mistake 5: Forgetting to wrap with io.NopCloser when assigning back to resp.Body

Broken example:

resp.Body = bytes.NewReader(bodyBytes)

Why it fails:

  • resp.Body requires an io.ReadCloser, not just an io.Reader.

Fix:

resp.Body = io.NopCloser(bytes.NewReader(bodyBytes))

Comparisons

OptionInput typeImplements io.ReaderBest use caseNotes
bytes.NewReader(data)[]byteYesRead existing byte sliceCan also seek with Seek
bytes.NewBuffer(data)[]byteYesRead and buffer operationsUseful if you also want writes
strings.NewReader(text)stringYesRead from a stringBest when source data is a string
raw []byte[]byteNoStore bytes onlyMust be wrapped first

bytes.NewReader vs bytes.NewBuffer

Featurebytes.NewReaderbytes.NewBuffer
Read from existing bytesYesYes
Write additional bytesNoYes
Seek supportYesNo
Good for read-only reuseYesUsually
Good for building data dynamicallyNoYes

io.Reader vs io.ReadCloser

InterfaceWhat it does
io.ReaderReads bytes
io.ReadCloserReads bytes and can be closed

resp.Body is an io.ReadCloser, so if you rebuild it from bytes, use:

resp.Body = io.NopCloser(bytes.NewReader(bodyBytes))
Take Quiz

Cheat Sheet

// Convert []byte to io.Reader
r := bytes.NewReader(data)

// Also works, but more buffer-oriented
r := bytes.NewBuffer(data)

// Convert string to io.Reader
r := strings.NewReader(text)

// Read HTTP body into []byte
bodyBytes, err := io.ReadAll(resp.Body)

// Restore resp.Body after reading it
resp.Body = io.NopCloser(bytes.NewReader(bodyBytes))

Rules to remember

  • []byte is data, not a reader.
  • Wrap []byte with bytes.NewReader to get an io.Reader.
  • Reading from a reader moves its position forward.
  • If you need to read again, create a new reader or reset with Seek.
  • resp.Body is usually consumed once.
  • To replace resp.Body, use io.NopCloser(...).

Good default choices

  • Have []byte and need io.Reader? Use bytes.NewReader.
  • Have string and need io.Reader? Use strings.NewReader.
  • Need a writable buffer too? Use bytes.NewBuffer.

FAQ

How do I convert []byte to io.Reader in Go?

Use bytes.NewReader(data) where data is a []byte.

Can I use bytes.NewBuffer instead?

Yes. It also implements io.Reader. For simple read-only access, bytes.NewReader is often clearer.

Why can I not pass a []byte directly where io.Reader is expected?

Because []byte is just raw data and does not implement the Read method required by io.Reader.

How do I reuse resp.Body after calling io.ReadAll?

Read it into bytes, then assign it back like this:

bodyBytes, _ := io.ReadAll(resp.Body)
resp.Body = io.NopCloser(bytes.NewReader(bodyBytes))

What is the difference between io.Reader and io.ReadCloser?

io.Reader can read data. io.ReadCloser can read data and also be closed.

Is ioutil.ReadAll still recommended?

In modern Go, prefer io.ReadAll. ioutil has been deprecated.

Can I read from the same bytes.Reader twice?

Yes, but after the first full read you must reset it with Seek(0, io.SeekStart) or create a new reader.

Take Quiz

Mini Project

Description

Build a small Go utility that fetches an HTTP response, reads the body into memory, logs the raw text, then restores the body so it can be parsed again. This demonstrates the practical difference between raw bytes and readers, and shows how to reuse consumed stream data safely.

Goal

Create a program that reads an HTTP response body once, converts the bytes back into a readable stream, and decodes the same content again.

Requirements

  • Make an HTTP GET request to a public JSON endpoint.
  • Read the response body into a []byte.
  • Print the raw response body as text.
  • Rebuild the response body so it can be read again.
  • Decode the rebuilt body into a Go struct.
Take Quiz

Keep learning

Related questions

View more