Question

How to Read an HTTP Response as a String in Go

gonetwork-programming

Question

I want to parse the response from an HTTP request in Go, but I am having trouble accessing the body as a string.

Here is my code:

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "reflect"
)

func main() {
    resp, err := http.Get("http://google.hu/")
    if err != nil {
        // handle error
        return
    }
    defer resp.Body.Close()

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        // handle error
        return
    }

    ioutil.WriteFile("dump", body, 0600)

    for i := 0; i < len(body); i++ {
        fmt.Println(body[i]) // This prints uint8 values as numbers
    }

    fmt.Println(reflect.TypeOf(body))
    fmt.Println("done")
}

ioutil.WriteFile correctly writes the response to a file, but when I print the body in a loop, I only get numbers because each element appears to be a uint8.

How can I access the HTTP response body as a string in Go?

Short Answer

By the end of this page, you will understand why an HTTP response body is read as a []byte in Go, how to convert it into a string, and when to work with bytes versus strings. You will also learn the most common patterns used in real Go code when reading HTTP responses.

Concept

In Go, the body of an HTTP response is usually read as raw bytes, not directly as text.

When you call:

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

body has the type:

a[]byte

A []byte is a slice of bytes. Each byte is a number from 0 to 255, which is why this code:

fmt.Println(body[i])

prints numbers.

To view the entire response as text, you convert the byte slice into a string:

text := string(body)
fmt.Println(text)

This works because Go strings are sequences of bytes too, but they are intended for textual data.

Why this matters:

  • HTTP responses are transmitted as bytes.
  • Some responses are text, such as HTML, JSON, XML, or plain text.
  • Some responses are binary, such as images, ZIP files, or PDFs.
  • Go gives you the raw bytes first so you can decide how to interpret them.

If the response is textual, converting []byte to string is the normal approach. If the response is binary, you should keep it as bytes.

Mental Model

Think of []byte as a box of raw Lego pieces and string as the finished word or sentence made from those pieces.

  • []byte = raw data, one byte at a time
  • string = human-readable text built from those bytes

When you loop through body[i], you are inspecting each individual Lego piece, so you see numbers. When you do string(body), you assemble those pieces into readable text.

Another way to think about it:

  • resp.Body is a stream of incoming data
  • ioutil.ReadAll reads the stream into memory as bytes
  • string(body) tells Go: "Interpret these bytes as text"
Take Quiz

Syntax and Examples

The basic pattern is:

resp, err := http.Get("https://example.com")
if err != nil {
    return
}
defer resp.Body.Close()

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

text := string(body)
fmt.Println(text)

Example: print the response body as a string

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
)

func main() {
    resp, err := http.Get("https://example.com")
    if err != nil {
        fmt.Println("request error:", err)
        return
    }
    defer resp.Body.Close()

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Println("read error:", err)
        return
    }

    fmt.Println(string(body))
}

Example: print bytes versus string

package main

import "fmt"

func main() {
    data := []byte("Hello")

    fmt.Println(data)         // [72 101 108 108 111]
    fmt.Println(data[0])      // 72
    fmt.Println(string(data)) // Hello
}

Explanation:

  • data is a byte slice.
  • data[0] is the first byte, which is the ASCII/UTF-8 value for H.
  • string(data) converts the whole byte slice into readable text.

Modern note

In newer Go code, io.ReadAll is preferred over ioutil.ReadAll:

body, err := io.ReadAll(resp.Body)

The idea is exactly the same.

Step by Step Execution

Consider this example:

package main

import (
    "fmt"
)

func main() {
    body := []byte("Go")

    fmt.Println(body)
    fmt.Println(body[0])
    fmt.Println(body[1])
    fmt.Println(string(body))
}

Step by step:

  1. body := []byte("Go")

    • Go creates a byte slice containing the UTF-8 bytes for G and o.
    • Internally, this is roughly [71 111].
  2. fmt.Println(body)

    • Prints the slice values.
    • Output: [71 111]
  3. fmt.Println(body[0])

    • Prints the first byte.
    • Output: 71
  4. fmt.Println(body[1])

    • Prints the second byte.
    • Output: 111
  5. fmt.Println(string(body))

    • Converts the whole byte slice to a string.
    • Output: Go

Now apply the same idea to an HTTP response:

body, err := ioutil.ReadAll(resp.Body)
fmt.Println(string(body))

You first read the body as bytes, then convert those bytes into text.

Real World Use Cases

Converting an HTTP response to a string is common when the response contains text-based formats.

HTML pages

html := string(body)
fmt.Println(html)

Used for:

  • scraping pages
  • checking page contents
  • debugging server responses

JSON APIs

jsonText := string(body)
fmt.Println(jsonText)

Useful for:

  • logging API responses during development
  • inspecting returned JSON before unmarshaling

Plain text services

Some endpoints return plain text such as status messages or generated tokens.

message := string(body)
fmt.Println(message)

Error responses

If an API returns status 400 or 500, the body may still contain a useful text explanation.

if resp.StatusCode >= 400 {
    fmt.Println("server error:", string(body))
}

Command-line utilities

Go scripts and CLI tools often fetch URLs and print the text output directly to the terminal.

Take Quiz

Real Codebase Usage

In real Go projects, developers usually do more than just convert the response body to a string.

1. Check the status code first

resp, err := http.Get(url)
if err != nil {
    return err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
    body, _ := ioutil.ReadAll(resp.Body)
    return fmt.Errorf("request failed: %s - %s", resp.Status, string(body))
}

This is a common validation pattern.

2. Read once, then reuse the data

resp.Body is a stream. Once read, it is consumed.

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

text := string(body)
fmt.Println(text)

Developers often store the body in a variable, then:

  • log it
  • parse it
  • validate it

3. Unmarshal JSON directly from bytes

In production code, if the body is JSON, developers usually keep it as []byte and unmarshal it.

var result map[string]interface{}
err = json.Unmarshal(body, &result)

The string conversion may still be useful for debugging.

4. Use guard clauses for errors

Go code commonly exits early when errors happen:

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

This keeps the code simple and readable.

5. Avoid reading huge responses into memory unnecessarily

For small responses, ReadAll is fine. For very large responses, developers may stream or decode incrementally instead of converting everything to a string at once.

Common Mistakes

Mistake 1: Printing each byte and expecting text

Broken example:

for i := 0; i < len(body); i++ {
    fmt.Println(body[i])
}

Why it happens:

  • body[i] is a single byte
  • a byte is a number, not a full string

Fix:

fmt.Println(string(body))

Mistake 2: Forgetting to handle the read error

Broken example:

body, err := ioutil.ReadAll(resp.Body)
fmt.Println(string(body))

If err is not checked, you may print incomplete or invalid data.

Fix:

body, err := ioutil.ReadAll(resp.Body)
if err != nil {
    fmt.Println("read error:", err)
    return
}
fmt.Println(string(body))

Mistake 3: Reading the body twice

Broken example:

body1, _ := ioutil.ReadAll(resp.Body)
body2, _ := ioutil.ReadAll(resp.Body)
fmt.Println(string(body1))
fmt.Println(string(body2))

Why it fails:

  • the second read gets nothing because the stream is already consumed

Fix:

body, _ := ioutil.ReadAll(resp.Body)
text := string(body)
fmt.Println(text)

Mistake 4: Treating all responses as text

Broken assumption:

fmt.Println(string(body))

This is only meaningful if the response actually contains text.

For binary files like images or PDFs, keep the data as bytes.

Mistake 5: Ignoring character encoding issues

Most modern web responses use UTF-8, which works well with string(body). But if a response uses a different encoding, the text may look wrong after conversion.

In that case, the bytes are still valid, but you may need decoding logic for that specific encoding.

Comparisons

ConceptWhat it isBest use
[]byteRaw bytesHTTP bodies, files, binary data, JSON decoding
stringTextPrinting, searching, displaying, text processing
body[i]One byte from the sliceInspecting individual bytes
string(body)Converts all bytes into textViewing a text response

[]byte vs string

data := []byte("Hello")
text := string(data)
  • Use []byte when working with raw data.
  • Use string when working with human-readable text.

ioutil.ReadAll vs io.ReadAll

FunctionStatusNotes
ioutil.ReadAllOlder styleWorks, but ioutil has been deprecated in newer Go versions
io.ReadAllPreferredModern replacement

Printing bytes one by one vs printing the whole string

ApproachOutput style
fmt.Println(body[i])Numeric byte values
fmt.Println(string(body))Human-readable text

If your goal is to inspect text, convert the full slice to a string.

Take Quiz

Cheat Sheet

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

text := string(body)
fmt.Println(text)

Key facts

  • resp.Body is a stream.
  • ioutil.ReadAll(resp.Body) returns []byte.
  • body[i] is a single byte, so it prints as a number.
  • string(body) converts the full byte slice to text.
  • Read the body only once.
  • Call defer resp.Body.Close() after a successful request.

Modern equivalent

body, err := io.ReadAll(resp.Body)

Useful pattern

resp, err := http.Get(url)
if err != nil {
    return
}
defer resp.Body.Close()

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

fmt.Println(string(body))

When not to use string(body)

Avoid converting to string if the response is binary, such as:

  • images
  • ZIP files
  • PDFs
  • audio/video data

FAQ

How do I convert an HTTP response body to a string in Go?

Read the body into a []byte, then convert it with string(body).

Why does body[i] print numbers instead of characters?

Because body[i] is a single byte of type uint8. Go prints its numeric value.

Is []byte the same as string in Go?

No. They are different types. A []byte holds raw bytes, while a string represents text.

Can I print the whole HTTP response body directly?

Yes:

fmt.Println(string(body))

Should I use ioutil.ReadAll or io.ReadAll?

In newer Go versions, prefer io.ReadAll. Older examples often use ioutil.ReadAll.

Can I read resp.Body multiple times?

Not directly. It is a stream and is consumed when read. Store the result if you need to reuse it.

What if the response is JSON?

You can print it as a string for debugging, but in real code you often unmarshal the []byte into a Go struct or map.

Why does writing to a file work even though printing does not?

Writing bytes to a file preserves the raw data exactly. Printing one byte at a time shows numeric byte values rather than the full text.

Take Quiz

Mini Project

Description

Build a small Go program that fetches a web page and prints both the raw byte length and the text content. This demonstrates the difference between []byte and string, which is one of the most common beginner questions when working with HTTP in Go.

Goal

Create a program that sends an HTTP request, reads the response body, converts it to a string, and prints the result safely.

Requirements

  • Make an HTTP GET request to a public URL.
  • Read the response body into a byte slice.
  • Print the number of bytes received.
  • Convert the byte slice to a string and print the text.
  • Handle request and read errors properly.
Take Quiz

Keep learning

Related questions

View more