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

~~~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM