访问实现错误接口的Golang结构会导致函数永不返回

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

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)
}

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.