无法从HTTP响应中解析JSON

So, I'm trying to figure out how to get the following code to properly parse the JSON data from https://api.coinmarketcap.com/v1/ticker/ethereum. It seems to have no problem decoding the JSON data in the response from http://echo.jsontest.com/key1/value1/key2/value2, but only gets empty/zero values when pointed at the CoinMarketCap API.

package main

import(
  "encoding/json"
  "net/http"
  "log"
)

type JsonTest struct {
  Key1  string
  Key2  string
}

type CoinMarketCapData struct {
  Id               string
  Name             string
  Symbol           string
  Rank             int
  PriceUSD         float64
  PriceBTC         float64
  Volume24hUSD     float64
  MarketCapUSD     float64
  AvailableSupply   float64
  TotalSupply       float64
  PercentChange1h   float32
  PercentChange24h float32
  PercentChange7d   float32
}

func getJson(url string, target interface{}) error {
  client := &http.Client{}
  req, _ := http.NewRequest("GET", url, nil)
  req.Header.Set("Content-Type", "application/json")
  r, err := client.Do(req)
  if err != nil {
      return err
  }
  defer r.Body.Close()
  return json.NewDecoder(r.Body).Decode(target)
}

func main() {
  //Test with dummy JSON
  url1 := "http://echo.jsontest.com/key1/value1/key2/value2"
  jsonTest := new(JsonTest)
  getJson(url1, jsonTest)
  log.Printf("jsonTest Key1: %s", jsonTest.Key1)

  //Test with CoinMarketCap JSON
  url2 := "https://api.coinmarketcap.com/v1/ticker/ethereum"
  priceData := new(CoinMarketCapData)
  getJson(url2, priceData)
  //Should print "Ethereum Id: ethereum"
  log.Printf("Ethereum Id: %s", priceData.Id)
}

I suspect it's related to the fact that the JSON at CoinMarketCap is inside a top level JSON array, but I've tried various iterations of things like:

priceData := make([]CoinMarketCapData, 1)

to no avail. Any advice is much appreciated, thanks.

The JSON is an array, so you need to pass an array to the Decode method. Also remember to check the returned error.

package main

import(
  "encoding/json"
  "net/http"
  "log"
)

type CoinMarketCapData struct {
  Id               string
  Name             string
  Symbol           string
  Rank             int
  PriceUSD         float64
  PriceBTC         float64
  Volume24hUSD     float64
  MarketCapUSD     float64
  AvailableSupply   float64
  TotalSupply       float64
  PercentChange1h   float32
  PercentChange24h float32
  PercentChange7d   float32
}

func getJson(url string, target interface{}) error {
  client := &http.Client{}
  req, _ := http.NewRequest("GET", url, nil)
  req.Header.Set("Content-Type", "application/json")
  r, err := client.Do(req)
  if err != nil {
      return err
  }
  defer r.Body.Close()
  return json.NewDecoder(r.Body).Decode(target)
}

func main() {
  //Test with CoinMarketCap JSON
  url2 := "https://api.coinmarketcap.com/v1/ticker/ethereum"
  priceData := make([]CoinMarketCapData, 0)
  err := getJson(url2, &priceData)
  if err != nil {
     log.Printf("Failed to decode json: %v", err)
  } else {
    //Should print "Ethereum Id: ethereum"
    log.Printf("Ethereum Id: %v", priceData[0].Id)
  }
}

running this prints

2016/08/21 17:15:27 Ethereum Id: ethereum

You're right, the top level API response type is a list and that has to be reflected in the unmarshalling process. One way to fix this would be to define your MarketCap response as slice, like this:

package main

import (
    "encoding/json"
    "log"
    "net/http"
)

// curl -sLf "https://api.coinmarketcap.com/v1/ticker/ethereum" | JSONGen
// command line helper: `go get github.com/bemasher/JSONGen`
type MarketCapResponse []struct {
    AvailableSupply  float64 `json:"available_supply"`
    HVolumeUsd       float64 `json:"24h_volume_usd"`
    Id               string  `json:"id"`
    MarketCapUsd     float64 `json:"market_cap_usd"`
    Name             string  `json:"name"`
    PercentChange1h  float64 `json:"percent_change_1h"`
    PercentChange24h float64 `json:"percent_change_24h"`
    PercentChange7d  float64 `json:"percent_change_7d"`
    PriceBtc         float64 `json:"price_btc"`
    PriceUsd         float64 `json:"price_usd"`
    Rank             int64   `json:"rank"`
    Symbol           string  `json:"symbol"`
    TotalSupply      float64 `json:"total_supply"`
}

Then the unmarshaling would work just fine. One thing to note is that pointer to a slice is different from the slice. Especially, the pointer does not support indexing, which is why need to dereference it first to access the first item in the list.

func getAndUnmarshal(url string, target interface{}) error {
    var client = &http.Client{}
    req, _ := http.NewRequest("GET", url, nil)
    req.Header.Set("Content-Type", "application/json")
    r, _ := client.Do(req)
    defer r.Body.Close()
    return json.NewDecoder(r.Body).Decode(target)
}

func main() {
    link := "https://api.coinmarketcap.com/v1/ticker/ethereum"
    resp := new(MarketCapResponse)
    getAndUnmarshal(link, resp)
    log.Printf("Ethereum Id: %s", (*resp)[0].Id)
    // prints:
    // 2016/08/22 02:13:23 Ethereum Id: ethereum
}

Another way would be to define a type for a single MarketCap and then create a slice as a target when needed:

package main

// curl -sLf "https://api.coinmarketcap.com/v1/ticker/ethereum" | jq .[0] | JSONGen
type MarketCap struct {
    AvailableSupply  float64 `json:"available_supply"`
    HVolumeUsd       float64 `json:"24h_volume_usd"`
    Id               string  `json:"id"`
    MarketCapUsd     float64 `json:"market_cap_usd"`
    Name             string  `json:"name"`
    PercentChange1h  float64 `json:"percent_change_1h"`
    PercentChange24h float64 `json:"percent_change_24h"`
    PercentChange7d  float64 `json:"percent_change_7d"`
    PriceBtc         float64 `json:"price_btc"`
    PriceUsd         float64 `json:"price_usd"`
    Rank             int64   `json:"rank"`
    Symbol           string  `json:"symbol"`
    TotalSupply      float64 `json:"total_supply"`
}

func getAndUnmarshal(url string, target interface{}) error {
     ...
}

func main() {
    link := "https://api.coinmarketcap.com/v1/ticker/ethereum"
    resp := make([]MarketCap, 0)
    getAndUnmarshal(link, &resp)
    log.Printf("Ethereum Id: %s", resp[0].Id)
    // 2016/08/22 02:13:23 Ethereum Id: ethereum
}

What is more suitable for you will depend on your use case. If you want the struct to reflect the API response, then the first approach seems more suitable. Is MarketCap a thing and the API is just one way to access it, than the second fits better, I believe.