Golang函数或方法传递nil值的一个坑

本文记录了下自己之前在做项目的时候遇到的函数或方法传递nil值的一个坑,后面会附上说明与解决方案。

错误示范

下面这个BaseRequestString函数主要实现的功能是:分别处理GET或POST请求,requestBody参数在GET请求时传nil,POST请求如果请求体里有数据的话需要处理一下请求体的数据传入。

下面是一个错误的示范:

package t13_niu_error

import (
    "bytes"
    "fmt"
    "io/ioutil"
    "net/http"
    "testing"
)

// 将请求获取的数据转为string,支持GET或POST请求
func BaseRequestString(requestMethod, url string, requestBody *bytes.Reader) (string, error) {
    client := &http.Client{
    }
    var req *http.Request
    var err error
    // 特别注意这里,即使外边requestBody传nil的话
    // 但是它的类型type是 *bytes.Reader,传给http.NewRequest方法的最后一个参数不是需要的io.Reader类型的!会报错
    req, err = http.NewRequest(requestMethod, url, requestBody)

    if err != nil {
        return "", err
    }
    res, err := client.Do(req)
    if err != nil {
        return "", err
    }
    defer res.Body.Close()
    body, err := ioutil.ReadAll(res.Body)
    if err != nil {
        return "", err
    }
    return string(body), nil

}

// 测试
func TestNilError(t *testing.T) {
    // GET
    if res, err := BaseRequestString("GET", "https://www.baidu.com", nil); err != nil {
        fmt.Println("err>> ", err.Error())
    } else {
        fmt.Println("res>> ", res)
    }
}

如果按照这种实现的方式,程序会上报一个错误:

panic: runtime error: invalid memory address or nil pointer dereference [recovered]

原因分析

问题解释

其实错误就出在了nil的类型不一样。

我们可以看到,在函数定义阶段,requestBody定义的是*bytes.Reader类型的,也就是说外部传入的nil也是*bytes.Reader类型的。

但是我们再在http.NewRequest方法中看一下它的源码:

// NewRequest wraps NewRequestWithContext using the background context.
func NewRequest(method, url string, body io.Reader) (*Request, error) {
    return NewRequestWithContext(context.Background(), method, url, body)
}

即使传nil的话,源码里面body也是需要 io.Reader类型的nil!

出处

在Go语言中,变量总是被一个定义明确的值初始化,即使接口类型也不例外。对于一个接口的零值就是它的类型和值的部分都是nil:

这就很明白了:两个nil值的value虽然都是nil,但是它们的type不同!所以将bytes.Reader类型的nil传入需要io.Reader类型的nil参数的位置,肯定会报错的!

参考资料

GO语言圣经-7.5. 接口值

解决方案

既然知道了原因,那么其实我们有2种解决方案:一种是判断一下传入的requestBody值,针对不同的值传入不同的参数,另外一个就是直接将最后一个参数的类型改成io.Reader。

我个人倾向与前者,虽然第一种解决方案看起来麻烦一点,但是实际上更加灵活,因为我们在构建POST请求参数的时候,往往会将结果做成*bytes.Reader类型的:

这里是我自己封装的一个生成POST请求体参数的例子:

func (a *AppleTask) getPostBody(startDate, endDate string, offset int32) (*bytes.Reader, *model.AppError) {
    // POST请求的请求体构建
    info := make(map[string]interface{})
    // 1、构建 orderBy 的筛选
    orderDic := map[string]string{
        "field":     "localSpend",
        "sortOrder": "ASCENDING",
    }
    orderLst := []interface{}{orderDic}
    // 2、构建 conditions 的筛选
    values1 := []interface{}{"false", "true"}
    conditionDic1 := map[string]interface{}{
        "field":    "deleted",
        "operator": "IN",
        "values":   values1,
    }
    conditionLst := []interface{}{
        conditionDic1,
    }
    // 3、构建分页的筛选
    paginationMap := map[string]interface{}{
        "offset": offset,
        "limit":  200,
    }
    // 4、selector的数据
    SelectorObj := map[string]interface{}{
        "pagination": paginationMap,
        "orderBy":    orderLst,
        "conditions": conditionLst,
    }
    // selector
    info["selector"] = SelectorObj
    info["startTime"] = startDate
    info["endTime"] = endDate
    info["timeZone"] = "UTC"
    // info["returnRecordsWithNoMetrics"] = true
    //info["returnRowTotals"] = true
    //info["returnGrandTotals"] = true
    info["granularity"] = "DAILY"
    bytesData, err := json.Marshal(info)
    if err != nil {
        return nil, model.NewAppError("getPostBody", "Marshal.error", err.Error(), nil)
    }
    reader := bytes.NewReader(bytesData)
    return reader, nil
}
构建POST请求体的参数

所以,构建的请求体数据是*bytes.Reader类型的,没有必要非得跟http.NewRequest保持一致!

所以我自己的解决方案如下:

package t13_niu_error

import (
    "bytes"
    "fmt"
    "io/ioutil"
    "net/http"
    "testing"
)

// 将请求获取的数据转为string,支持GET或POST请求
func BaseRequestString(requestMethod, url string, requestBody *bytes.Reader) (string, error) {
    client := &http.Client{
    }
    var req *http.Request
    var err error
    // GET请求
    if requestBody == nil {
        req, err = http.NewRequest(requestMethod, url, nil)
        // POST请求
    } else {
        // 验证:
        fmt.Println("requestBody没传nil>>> ", requestBody)
        req, err = http.NewRequest(requestMethod, url, requestBody)
    }
    if err != nil {
        return "", err
    }
    res, err := client.Do(req)
    if err != nil {
        return "", err
    }
    defer res.Body.Close()
    body, err := ioutil.ReadAll(res.Body)
    if err != nil {
        return "", err
    }
    return string(body), nil

}

// 测试
func TestNilError(t *testing.T) {
    // GET
    if res, err := BaseRequestString("GET", "https://www.baidu.com", nil); err != nil {
        fmt.Println("err>> ", err.Error())
    } else {
        fmt.Println("res>> ", res)
    }
}

~~~

posted on 2021-01-20 11:48  江湖乄夜雨  阅读(1624)  评论(0编辑  收藏  举报