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



背景

我将JSON数据从HTTP API删除到以下Golang结构:

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"`
}

我已经实现了Error的错误接口,如下所示:

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

到目前为止相关代码:

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
}

问题

以下代码无限期地悬挂在成功的呼叫中而不会感到恐慌:

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

实际上,当我调用err时,代码总是悬挂。没有恐慌会引起且没有返回错误 - 它只是冷冻[1]。

[1]严格地说,它会导致堆栈溢出错误,但这是因为该功能永远不会返回,因此Request中的递延resp.Body.Close()从未被调用。

它变得奇怪。添加以下调试线:

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

有效,但将这些行更改为

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

使Request功能本身悬挂在此调试语句中。

我尝试过的东西

  • 使用go test -trace运行跟踪以查看所谓的内容。由于go tool trace无法解析生成的跟踪文件。
  • 将我的签名转换为返回*error而不是常规error。这没有帮助。

为什么此代码段悬挂?

注意:我正在运行1.7

问题是Error接口的定义。当您尝试fmt.Sprintfe时,Error上的函数Error() string正在递归地调用自己:

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

尝试通过明确访问Error的构件来返回错误:

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

问题是递归,因为fmt调用Error()再次称为FMT。

经典解决方案是将您的错误类型转换为string,然后使用标准力学。这样,您就拥有所有格式化工具。喜欢:

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

游乐场:https://play.golang.org/p/ni5jl3h4g7y

摘自fmt文档(不幸的是无法直接链接到段落):

  1. 如果操作数实现了错误接口,则将调用错误方法将对象转换为字符串,然后将其按照动词的要求(如果有)进行格式。

  2. 如果操作数实现方法字符串()字符串,则将调用该方法将对象转换为字符串,然后根据动词的要求(如果有)将其格式化。

在诸如

的情况下避免递归
type X string 
func (x X) String() string { return Sprintf("<%s>", x) }

重复之前转换值:

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

无限递归也可以通过自指数据触发结构,例如包含自己作为元素的切片,如果该类型具有字符串方法。但是,这种病理很少包装不能防止它们。

打印结构时,FMT不能,因此不会调用格式化方法,例如错误字段上的错误或字符串。

最新更新