Question
How can I read JSON data returned by http.Get in Go? The following program makes a request, but unmarshalling appears to produce an empty result. What is wrong with the struct definitions or JSON decoding code?
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
)
type Tracks struct {
Toptracks []Toptracks_info
}
type Toptracks_info struct {
Track []Track_info
Attr []Attr_info
}
type Track_info struct {
Name string
Duration string
Listeners string
Mbid string
Url string
Streamable []Streamable_info
Artist []Artist_info
Attr []Track_attr_info
}
type Attr_info struct {
Country string
Page string
PerPage string
TotalPages string
Total string
}
type Streamable_info struct {
Text string
Fulltrack string
}
type Artist_info struct {
Name string
Mbid string
Url string
}
type Track_attr_info struct {
Rank string
}
func get_content() {
url := "http://ws.audioscrobbler.com/2.0/?method=geo.gettoptracks&api_key=c1572082105bd40d247836b5c1819623&format=json&country=Netherlands"
res, err := http.Get(url)
if err != nil {
panic(err.Error())
}
body, err := ioutil.ReadAll(res.Body)
if err != nil {
panic(err.Error())
}
var data Tracks
json.Unmarshal(body, &data)
fmt.Printf("Results: %v\n", data)
os.Exit(0)
}
func main() {
get_content()
}
Short Answer
You will learn how Go maps JSON into structs after an HTTP request. The key is to make each Go field match the JSON value type: JSON objects map to structs, JSON arrays map to slices, and unusual JSON keys such as @attr need struct tags. You will also learn to check HTTP and JSON errors instead of silently ignoring them.
Concept
Go's encoding/json package decodes JSON by comparing JSON object keys with exported struct fields or explicit JSON tags.
The most important rule is that the Go type must match the JSON value's shape:
- A JSON object (
{ ... }) maps to a Go struct. - A JSON array (
[ ... ]) maps to a Go slice such as[]Track. - A JSON string maps to
string. - A JSON number maps to a numeric Go type, unless the API sends it in quotes, in which case it is a
string.
For this response shape, the outer JSON is similar to:
{
"toptracks": {
"track": [
{
"name": "Example track",
"artist": { "name": "Example artist" },
"@attr": { "rank": "1" }
}
],
"@attr": { "country": "Netherlands" }
}
}
Therefore, toptracks must be a single struct, not []ToptracksInfo. Likewise, each track's artist, streamable, and @attr values are objects, so they should be structs rather than slices.
JSON fields named @attr cannot be represented by ordinary Go field-name matching. Use a struct tag such as json:"@attr" to tell the decoder exactly which JSON key belongs to the field.
Finally, always handle the error returned by json.Unmarshal or json.NewDecoder(...).Decode(...). Ignoring it can make a type mismatch look like an empty result.
Mental Model
Think of JSON as a collection of containers:
{}is one labeled box, so use one struct.[]is a row of boxes, so use a slice.- A value like
"name": "Song"is an item inside a box, so use a field such asName string.
If you tell Go that a single box is a row of boxes—for example, Toptracks []ToptracksInfo when JSON contains "toptracks": { ... }—Go cannot place the data correctly. Decoding returns an error, and if that error is ignored, the result appears empty.
Struct tags are like translation labels. The Go field Attr is readable Go code, while the tag json:"@attr" tells Go that it corresponds to the JSON label @attr.
Syntax and Examples
Use http.Get to make a request, close the response body, verify the status, and decode JSON directly from the body.
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status: %s", resp.Status)
}
var result Response
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return err
}
A struct should follow the JSON shape:
type Response struct {
TopTracks TopTracks `json:"toptracks"`
}
type TopTracks struct {
Tracks []Track `json:"track"`
Attr Attr `json:"@attr"`
}
type Track struct {
Name string `json:"name"`
Artist Artist `json:"artist"`
}
type Artist struct {
Name string `json:"name"`
}
type Attr struct {
Country string `json:"country"`
}
Tracks []Track is a slice because track is an array. TopTracks, Artist, and Attr are individual structs because their JSON values are objects.
Using json.NewDecoder is convenient for an HTTP response because it decodes directly from the response stream. json.Unmarshal is also valid when you already have JSON stored in a []byte.
Step by Step Execution
Consider this JSON:
{
"toptracks": {
"track": [
{ "name": "First Song", "artist": { "name": "The Band" } }
],
"@attr": { "country": "Netherlands" }
}
}
And these types:
type Response struct {
TopTracks TopTracks `json:"toptracks"`
}
type TopTracks struct {
Tracks []Track `json:"track"`
Attr Attr `json:"@attr"`
}
type Track struct {
Name string `json:"name"`
Artist Artist `json:"artist"`
}
type Artist struct {
Name string `json:"name"`
}
type Attr struct {
Country string `json:"country"`
}
When Go decodes the JSON:
- The root object has a
toptrackskey, so it fillsResponse.TopTracks. toptrackscontainstrack, whose value is[...], so Go creates a[]Trackslice.- The first array item is an object, so it fills one
Trackvalue. namefillsTrack.Namewith"First Song".artistis an object, so Go fillsTrack.Artist.Namewith"The Band".- The
@attrkey matches the explicit tag onTopTracks.Attr, fillingAttr.Country.
After decoding, you can read values normally:
fmt.Println(result.TopTracks.Attr.Country) // Netherlands
fmt.Println(result.TopTracks.Tracks[0].Name) // First Song
Real World Use Cases
JSON HTTP decoding is used whenever a Go program communicates with a web service, including:
- Calling a weather, maps, payment, or music API.
- Reading data from an internal microservice.
- Loading GitHub issues, repository details, or CI results.
- Downloading application configuration from a service.
- Building a command-line tool that fetches and displays remote data.
- Processing webhook payloads in an HTTP server.
In every case, inspect a sample response first, then define only the struct fields your program actually needs. Go safely ignores JSON fields that are not present in your struct.
Real Codebase Usage
In production code, developers usually wrap API calls in a function that returns typed data and an error:
func FetchTopTracks(client *http.Client, url string) (Response, error) {
resp, err := client.Get(url)
if err != nil {
return Response{}, fmt.Errorf("request top tracks: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return Response{}, fmt.Errorf("request top tracks: %s", resp.Status)
}
var data Response
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
return Response{}, fmt.Errorf("decode top tracks response: %w", err)
}
return data, nil
}
Common project patterns include:
- Dependency injection: accept
*http.Clientinstead of always usinghttp.Get. This makes timeouts and tests easier. - Guard clauses: return early when the request, status code, or decoding fails.
- Minimal response models: define only fields the application uses.
- Error context: wrap errors with what the application was trying to do.
- Timeouts: configure an HTTP client timeout so a stalled server does not block forever.
- Validation: check that required decoded fields, such as a track name or ID, are not empty before using them.
Common Mistakes
Using a slice for a JSON object
This is incorrect when toptracks is { ... }:
type Response struct {
TopTracks []TopTracks `json:"toptracks"` // wrong
}
Use one struct instead:
type Response struct {
TopTracks TopTracks `json:"toptracks"`
}
Ignoring decode errors
This hides the useful explanation for a type mismatch:
json.Unmarshal(body, &data) // error discarded
Always check it:
if err := json.Unmarshal(body, &data); err != nil {
return err
}
Forgetting to close the response body
Not closing resp.Body can prevent HTTP connections from being reused efficiently.
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
Forgetting special-key tags
A field named Attr does not automatically match @attr.
type TopTracks struct {
Attr Attr // will not match "@attr"
}
Use a tag:
type TopTracks struct {
Attr Attr `json:"@attr"`
}
Using unexported fields
The JSON package only fills exported fields, which begin with an uppercase letter.
type Track struct {
name string // ignored
}
Use Name string instead.
Assuming every API response is successful JSON
An API may return HTML, plain text, or a different JSON error object for a failed request. Check resp.StatusCode before decoding the expected success format.
Comparisons
| Situation | JSON shape | Appropriate Go type |
|---|---|---|
| One nested record | "artist": { "name": "A" } | Artist |
| Many records | "track": [{...}, {...}] | []Track |
| Text value | "name": "Song" | string |
| Special key | "@attr": { ... } | Attr \json:"@attr"`` |
| Optional nested object | may be null or {...} | *Artist when you need to distinguish null from an empty object |
json.Unmarshal and json.Decoder.Decode both decode JSON into Go values:
| Method | Best when | Input |
|---|---|---|
json.Unmarshal | You already have all JSON bytes | []byte |
json.NewDecoder(r).Decode | Reading from an HTTP body or file | an io.Reader |
For an HTTP response, Decoder.Decode avoids the extra step of reading the entire body into a byte slice first.
Cheat Sheet
// Request and decode JSON from an HTTP response.
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status: %s", resp.Status)
}
var data Response
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
return err
}
- JSON object
{}→ Go struct. - JSON array
[]→ Go slice ([]T). - JSON string →
string. - Struct fields must be exported:
Name, notname. - Use tags for exact, snake_case, or special JSON keys:
json:"@attr". - Check errors from
http.Get, reading, and JSON decoding. - Call
defer resp.Body.Close()immediately after a successful request. - Unknown JSON fields are ignored by default.
- Missing JSON fields leave Go fields at their zero values, such as
"",0,false, ornil.
FAQ
Why is my unmarshalled Go struct empty?
A common cause is ignoring an error from json.Unmarshal or Decode. Print or return that error. It often reports that Go expected an array but received an object, or the reverse.
Do Go struct field names have to exactly match JSON keys?
Not always. Go matches exported field names case-insensitively in many ordinary cases. Use explicit json tags for reliable mappings, especially for names like total_pages, fulltrack, or @attr.
Should I use json.Unmarshal or json.NewDecoder for HTTP responses?
Use json.NewDecoder(resp.Body).Decode(&value) for most HTTP responses. Use json.Unmarshal when you specifically need the full JSON body as []byte first.
Why do I need defer resp.Body.Close()?
It releases the response body and lets Go's HTTP transport reuse the underlying connection. Add it after confirming the request returned a non-nil response without an error.
Can I omit fields from my Go JSON struct?
Yes. Go ignores JSON properties that have no corresponding struct field. This is useful when an API response is large but your program needs only a few values.
What type should I use when an API sometimes returns null?
Use a pointer for an optional object, such as *Artist. A nil pointer means the JSON value was missing or null; a non-nil pointer means an object was supplied.
Why does @attr need a JSON tag?
@attr is not a normal Go identifier and does not correspond to the field name Attr automatically. The tag json:"@attr" provides the exact mapping.
Mini Project
Description
Build a small command-line program that requests a JSON list of public users and prints each user's name and email address. It practices the complete workflow: making an HTTP request, checking the status code, closing the body, decoding an array, and using the resulting structs.
Goal
Fetch a JSON API response and print a readable list of user names and email addresses.
Requirements
Use http.Get to request https://jsonplaceholder.typicode.com/users.
Check that the response status is 200 OK.
Close the response body with defer.
Decode the JSON array into a slice of structs.
Print one name <email> line for every user.
Keep learning
Related questions
Are Go Maps Passed by Value or Reference?
Learn Go map parameter and return semantics: maps are passed by value, while copies share underlying data. Know when map pointers are useful.
Authentication for Go REST APIs: Sessions, Tokens, and Middleware
Learn practical authentication patterns for Go REST APIs, including password hashing, server sessions, bearer tokens, middleware, and security rules.
Automatic Build Versioning in Go: Embed Incrementing Build Numbers
Learn how to add automatic build versioning in Go using linker flags, build metadata, CI counters, and Git-based version values.