本文記錄了下自己之前在做項目的時候遇到的函數或方法傳遞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) } }
~~~