Convert a Go Byte Slice to io.Reader (and Reader Back to Bytes)

Tech reviewed: Deepak Prasad
Convert a Go Byte Slice to io.Reader (and Reader Back to Bytes)

If you need a golang io reader example for in-memory data, the usual pattern is to turn a []byte (sometimes called a byte array in other languages) into something that implements io.Reader so helpers like io.Copy, io.ReadAll, or parsers that accept a reader work unchanged. For golang bytes to reader or golang byte to io.Reader, use bytes.NewReader; for text in a string, strings.NewReader is the usual choice. The reverse—golang io reader from bytes in the sense of “I have a reader and want a []byte”—is typically io.ReadAll or reading into a bytes.Buffer.

Tested with Go 1.24 on Linux.


Byte slice, bytes.Reader, and io.Reader

bytes.NewReader: golang bytes reader from []byte

bytes.NewReader(b []byte) returns a *bytes.Reader, a small golang bytes reader type backed by your slice. It implements io.Reader and also supports Seek, which is handy when the same bytes are parsed more than once. The reader does not copy the slice contents into a new allocation; it reads from the slice you passed in (so avoid mutating that slice while something else is still reading unless you intend that sharing).

Run the program: it should print the first 10 bytes as text (this is so).

go
package main

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

func main() {
	testData := []byte("this is some random data in a small example!")

	reader := bytes.NewReader(testData)

	buf := make([]byte, 10)
	if _, err := io.ReadFull(reader, buf); err != nil {
		log.Fatal(err)
	}
	fmt.Println(string(buf))
}
Output

String to reader (strings.NewReader)

If the data starts as a string, the same golang byte reader style APIs apply: anything that takes io.Reader accepts strings.NewReader without you converting the string to []byte first.

go
package main

import (
	"fmt"
	"io"
	"log"
	"strings"
)

func main() {
	r := strings.NewReader("hello from strings.Reader")
	out, err := io.ReadAll(r)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(string(out))
}
Output

You should see hello from strings.Reader printed.

Fixed-size array: slice the array, then NewReader

For a [N]byte array, slice to []byte and use the same helper:

go
package main

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

func main() {
	var arr [4]byte{'a', 'b', 'c', 'd'}
	r := bytes.NewReader(arr[:])
	b, err := io.ReadAll(r)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(string(b))
}
Output

You should see abcd.


From io.Reader back to []byte or string

Prefer io.ReadAll for “read everything”

For golang io reader from bytes in reverse (reader → contiguous bytes), io.ReadAll(r) is the most direct API since Go 1.16: one call returns a new []byte or an error.

go
package main

import (
	"fmt"
	"io"
	"log"
	"strings"
)

func main() {
	r := strings.NewReader("this is some random data in a small example!")
	data, err := io.ReadAll(r)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(string(data[:10]))
}
Output

You should see this is so.

bytes.Buffer with ReadFrom (streaming into a buffer)

bytes.Buffer is useful when you read in chunks, write into the buffer from multiple sources, or already need a growable byte sink. ReadFrom copies from the reader until EOF.

go
package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"strings"
)

func main() {
	r := strings.NewReader("this is some random data in a small example!")
	var buf bytes.Buffer
	if _, err := buf.ReadFrom(r); err != nil {
		log.Fatal(err)
	}
	data := buf.Bytes()
	fmt.Println(string(data[:10]))
}
Output

Same first-line outcome as the io.ReadAll example: this is so.

Reader to string (Buffer.String or ReadAll + convert)

Either read into a buffer and call String, or use string(io.ReadAll(r)) after checking the error. strings.Builder with io.Copy is a good fit when you are assembling text and want to minimize copying from other writers:

go
package main

import (
	"fmt"
	"io"
	"log"
	"strings"
)

func main() {
	r := strings.NewReader("this is some random data in a small example!")
	var b strings.Builder
	if _, err := io.Copy(&b, r); err != nil {
		log.Fatal(err)
	}
	fmt.Println(b.String())
}
Output

You should see the full sentence printed on one line.


Choosing helpers and io.ReadCloser

  • Use bytes.NewReader when you have a []byte and only need read and optional seek.
  • Use strings.NewReader when you have a string.
  • Use io.ReadAll when you want the whole stream as []byte.
  • Use bytes.Buffer when you need a mutable buffer or incremental reads/writes.
  • If a function requires io.ReadCloser but your data is memory-only, wrap with io.NopCloser(bytes.NewReader(b)) so Close succeeds without doing extra work.

For background on why io.Reader shows up everywhere, see interfaces in Go and the language tour’s reader examples.


Summary

This article is a practical golang io reader example path for in-memory data: bytes.NewReader gives an io.Reader from a byte slice (or from arr[:] when you started from a byte array), strings.NewReader does the same for strings, and io.ReadAll or bytes.Buffer.ReadFrom bring you back from a reader to []byte (or Buffer.String / strings.Builder for text). Pick bytes.Reader when you want a read-only, seekable view of existing bytes; pick Buffer or ReadAll when you are draining an arbitrary stream.


References


Frequently Asked Questions

1. How do I get an io.Reader from a byte slice in Go?

Use bytes.NewReader(b). It returns a *bytes.Reader that implements io.Reader (and also io.Seeker and io.ReaderAt). If you already have a string, strings.NewReader(s) is the usual choice.

2. What is the difference between bytes.NewReader and bytes.NewBuffer?

bytes.NewReader wraps an existing []byte for reading only and supports seeking. bytes.NewBuffer (or NewBufferString) builds a growable buffer you can also write to; both satisfy io.Reader for consumers that only need Read.

3. How do I read all bytes from an io.Reader into a slice?

Prefer io.ReadAll(r) from the standard library: it returns a new []byte with the full stream contents or an error. bytes.Buffer with ReadFrom is still fine when you already need a buffer for incremental processing.

4. Can I convert a byte array to a reader the same way as a slice?

In Go, a byte array [N]byte can be sliced as b[:] to get a []byte, then passed to bytes.NewReader(b[:]). The reader reads from that same backing array; it does not allocate a fresh copy of the byte data (only the small Reader value itself is new).

5. When should I use io.NopCloser with bytes.NewReader?

When an API expects io.ReadCloser (for example some HTTP helpers) but you only have in-memory bytes, wrap with io.NopCloser(bytes.NewReader(b)) so Close is a no-op after reads finish.
Tuan Nguyen

Data Scientist

Proficient in Golang, Python, Java, MongoDB, Selenium, Spring Boot, Kubernetes, Scrapy, API development, Docker, Data Scraping, PrimeFaces, Linux, Data Structures, and Data Mining. With expertise spanning these technologies, he develops …

  • Deep Learning with TensorFlow
  • Machine Learning with Python
  • Go (programming language)
  • Python (programming language)
  • Java (programming language)
  • MongoDB