Searches such as golang readcloser, io.readcloser golang, go io.readcloser, io readcloser, io.readcloser to string, golang readcloser to string, io.nopcloser, nopcloser, and golang io reader example all point at the same small idea: io.ReadCloser combines io.Reader with io.Closer, and the standard library gives you io.NopCloser when you only have a reader but something expects a closer. This article shows the interface, io.NopCloser as a golang io reader example bridge, a hand-rolled ReadCloser, and the common readcloser-to-string pattern with io.ReadAll.
Tested with Go 1.24 on Linux.
What is io.ReadCloser?
io.ReadCloser embeds Reader and Closer:
type ReadCloser interface {
Reader
Closer
}Callers can read until EOF or an error, then call Close() to release sockets, file descriptors, or other resources. Many APIs return a ReadCloser even when the underlying data lives in memory so the same contract works everywhere.
io.NopCloser (nopcloser)
If you only have an io.Reader but a function needs an io.ReadCloser, wrap it with io.NopCloser (available since Go 1.16). The returned value’s Close does nothing useful; it exists so the static type matches ReadCloser. Older tutorials mention ioutil.NopCloser; ioutil is deprecated for this—use io.NopCloser in new code.
package main
import (
"bytes"
"fmt"
"io"
"strings"
)
func main() {
rc := io.NopCloser(strings.NewReader("Hello GoLinuxCloud Members!"))
fmt.Printf("Type of readCloser is %T\n", rc)
buf := new(bytes.Buffer)
n, err := buf.ReadFrom(rc)
if err != nil {
panic(err)
}
_ = rc.Close()
fmt.Printf("Read: %d bytes, content is: %q\n", n, buf.String())
}On Go 1.24 you will often see the concrete type name io.nopCloserWriterTo because strings.Reader implements io.WriterTo and the wrapper forwards that optimization. For printing types in general, see print type in Go.
Implementing ReadCloser yourself
Any type that already embeds or wraps something with Read can add Close. Here ClosingBuffer embeds *bytes.Buffer (which provides Read) and defines Close:
package main
import (
"bytes"
"fmt"
"io"
)
type ClosingBuffer struct {
*bytes.Buffer
}
func (cb *ClosingBuffer) Close() error {
return nil
}
func main() {
cb := &ClosingBuffer{bytes.NewBufferString("Hi GoCloudMember!")}
var rc io.ReadCloser = cb
buf := new(bytes.Buffer)
n, err := buf.ReadFrom(rc)
if err != nil {
panic(err)
}
fmt.Printf("Read: %d bytes, content is: %q\n", n, buf.String())
fmt.Printf("Type of cb is %T\n", cb)
fmt.Printf("Type of rc is %T\n", rc)
_ = rc.Close()
}Running it prints the read line with Hi GoCloudMember!, then two lines showing *main.ClosingBuffer for both cb and rc.
io.ReadCloser to string (io.ReadAll)
For io.readcloser to string and golang readcloser to string, read the full body with io.ReadAll (replaces the old ioutil.ReadAll pattern), convert with string(b), and defer resp.Body.Close() on HTTP responses so the connection can be reused. Never call ReadAll and forget Close on a live http.Response.
The program below uses httptest so one go run exercises a JSON body without a second manual server:
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/http/httptest"
)
func main() {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]string{
"message": "Hello GoLinuxCloud Members!",
"time": "2022-10-26",
})
}))
defer srv.Close()
resp, err := http.Get(srv.URL)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
fmt.Printf("Body type: %T\n", resp.Body)
b, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(b))
}Body type: *http.bodyEOFSignal
{"message":"Hello GoLinuxCloud Members!","time":"2022-10-26"}For broader HTTP client patterns, see HTTP in Go. For small inline handlers, anonymous functions are often enough.
Summary
io.ReadCloser golang code paths all come back to read-then-close: the interface is Reader plus Closer, io.NopCloser upgrades a bare Reader to a ReadCloser when close is a no-op, and golang readcloser to string means io.ReadAll then string(bytes), with Close deferred on real transports like http.Response.Body. Knowing nopcloser and the io.nopCloserWriterTo concrete type name helps when you debug types or follow stack traces.
References
io.ReadCloserdocumentationio.NopCloserdocumentationio.ReadAlldocumentationnet/http/httptestdocumentation

