2

Background

I'm unmarshalling JSON data from an HTTP API to the following Golang structs:

type ResponseBody struct {
    Version string `json:"jsonrpc"`
    Result  Result `json:"result"`
    Error   Error  `json:"error"`
    Id      int    `json:"id"`
}

type Result struct {
    Random struct {
        Data           interface{} `json:"data"`
        CompletionTime string      `json:"completionTime"`
    } `json:"random"`
    BitsUsed      int `json:"bitsUsed"`
    BitsLeft      int `json:"bitsLeft"`
    RequestsLeft  int `json:"requestsLeft"`
    AdvisoryDelay int `json:"advisoryDelay"`
}

type Error struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
    Data    []int  `json:"data,omitempty"`
}

and I've implemented the error interface for Error as follows:

func (e Error) Error() string {
    return fmt.Sprintf("Error: %+v", e)
}

Relevant code so far:

func Request(method string, params interface{}) (Result, error) {
    // `error` in return types is _not_ a typo

    body, err := json.Marshal(RequestBody{Version: "2.0", Params: params, Method: method, Id: 1})
    if err != nil {
        return Result{}, err
    }

    resp, err := http.Post(endpoint, "application/json-rpc", bytes.NewReader(body))
    if err != nil {
        return Result{}, fmt.Errorf("Request failed, error was %s", err)
    }
    defer resp.Body.Close()

    text, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return Result{}, fmt.Errorf("Failed to read response into memory, error was: %s", err)
    }

    if resp.StatusCode != 200 {
        var errorMessage Error
        if err := json.Unmarshal(text, &errorMessage); err != nil {
            return Result{}, Error{
                Code:    409,
                Message: fmt.Sprintf("Client could not decode JSON error response, received %s, error was: %s", text, err),
                Data:    []int{},
            }
        }
        return Result{}, errorMessage
    }

    response := ResponseBody{}
    if err := json.Unmarshal(text, &response); err != nil {
        return Result{}, fmt.Errorf("Failed to JSON-decode response body, received %s from source, error was: %s", text, err)
    }

    return response.Result, response.Error
}

Issue

The following code hangs indefinitely on a successful call without panicking:

// `body` here is just any old struct
result, err := Request("generateIntegers", body)
if err != nil {
    return []int{}, err  // hangs here
}

In fact, the code always hangs when I invoke err. No panic is raised and no error is returned - it is simply frozen[1].

[1] Strictly speaking, it causes a stack overflow error, but that's because the function never returns and so the deferred resp.Body.Close() in Request never gets called.

It gets weirder. Adding the following debugging lines:

response := ResponseBody{}
if err := json.Unmarshal(text, &response); err != nil {
    return Result{}, fmt.Errorf("Failed to JSON-decode response body, received %s from source, error was: %s", text, err)
}

fmt.Println(response.Result.BitsUsed)
fmt.Println(response.Result) // this prints!

return response.Result, response.Error

works, but changing these lines to just

response := ResponseBody{}
if err := json.Unmarshal(text, &response); err != nil {
    return Result{}, fmt.Errorf("Failed to JSON-decode response body, received %s from source, error was: %s", text, err)
}

fmt.Println(response.Result) // this no longer prints! :O

return response.Result, response.Error

causes Request function itself to hang at this debug statement.

Things I've Tried

  • Running a trace with go test -trace to view what's being called. This fails because go tool trace cannot parse the generated trace file.
  • Converting my signature to return *error instead of regular error. This did not help.

Why is this code snippet hanging?

Note: I am running Go 1.7

Akshat Mahajan
  • 9,543
  • 4
  • 35
  • 44
  • Related questions: https://stackoverflow.com/questions/27928744/an-infinite-loop-produced-by-fmt-sprinte-inside-the-error-method https://stackoverflow.com/questions/27474907/error-infinite-loop – Charlie Tumahai Dec 29 '17 at 17:06

2 Answers2

6

The problem is with the definition of Error interface. The function Error() string on Error type is recursively calling itself when you try to fmt.Sprintf the value e:

func (e Error) Error() string {
    return fmt.Sprintf("Error: %+v", e) // calls e.Error again
}

Try to return error by accessing Error type's members explicitely:

func (e Error) Error() string {
    return fmt.Sprintf("Error: %+v", e.Message)
}
abhink
  • 8,740
  • 1
  • 36
  • 48
  • Wow, I can't believe I didn't see that. But [the Go docs say](https://golang.org/pkg/fmt/#hdr-Printing) that `%+v` only prints the field names and the values. Why would the `Error` method be called as well? Are methods fields too? – Akshat Mahajan Dec 29 '17 at 10:52
  • 1
    @AkshatMahajan because standard formatting `%v` checks where methods like `String` or `Error` are implemented and then call them to format. And `%+v` is detailed formatting: it does not use those methods and constructs output on it own. The same with `%#v`. But using them is some sort of hack - you lose ability to use standard methods and short representation. – Eugene Lisitsky Dec 29 '17 at 11:00
  • @EugeneLisitsky Thanks for the great explanation! Do you know if this is officially documented anywhere? Would be awesome to know where I should have looked next time. – Akshat Mahajan Dec 29 '17 at 11:04
  • 1
    The behaviour is documented after all verbs have been listed. The section explains special formatting considerations that apply for operants implementing certain interfaces. It is contained in the [fmt](https://golang.org/pkg/fmt/) documentation and can be found by searching for "formatting considerations". The 4th entry in the list is the one relevant to this question. – Leon Dec 29 '17 at 11:10
2

The problem is recursion because fmt calls Error() what calls fmt again.

Classical solutions is to convert your error type into string and then use standard mechanics. This way you have all the formatting tools. Like:

type E string

func (e E) Error() string {
    return fmt.Sprintf(string(e))
}

Playground: https://play.golang.org/p/NI5JL3H4g7Y

Excerpt from fmt documentation (unfortunatelly cannot make direct link to the paragraph):

  1. If an operand implements the error interface, the Error method will be invoked to convert the object to a string, which will then be formatted as required by the verb (if any).

  2. If an operand implements method String() string, that method will be invoked to convert the object to a string, which will then be formatted as required by the verb (if any).

To avoid recursion in cases such as

type X string 

func (x X) String() string { return Sprintf("<%s>", x) }

convert the value before recurring:

func (x X) String() string { return Sprintf("<%s>", string(x)) }

Infinite recursion can also be triggered by self-referential data structures, such as a slice that contains itself as an element, if that type has a String method. Such pathologies are rare, however, and the package does not protect against them.

When printing a struct, fmt cannot and therefore does not invoke formatting methods such as Error or String on unexported fields.

Community
  • 1
  • 1
Eugene Lisitsky
  • 12,113
  • 5
  • 38
  • 59
  • Ah, this is great. Unfortunately I had to give the accepted answer to abhink because he answered first, but perhaps you can help me understand why `fmt` calls `Error()` internally. I thought `Sprintf("%+v", e)` only operates on the field names and values. Is this behaviour documented somewhere? – Akshat Mahajan Dec 29 '17 at 11:00
  • I've described it above: https://stackoverflow.com/questions/48021232/accessing-golang-struct-that-implements-error-interface-causes-function-to-never/48021499?noredirect=1#comment83012103_48021386 – Eugene Lisitsky Dec 29 '17 at 11:02