本文记录了下自己之前在做项目的时候遇到的函数或方法传递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参数的位置,肯定会报错的!
参考资料
解决方案
既然知道了原因,那么其实我们有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 }
所以,构建的请求体数据是*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) } }
~~~