Go語言函數的多返回值

2020-07-16 10:05:08
在Go語言中一個函數能夠返回不止一個結果,我們之前已經見過標準包內的許多函數返回兩個值,一個期望得到的計算結果與一個錯誤值,或者一個表示函數呼叫是否成功的布林值,下面來看看怎樣寫一個這樣的函數。

下面程式中的 findLinks 函數可以自己傳送 HTTP 請求,因為 HTTP 請求和解析操作可能會失敗,所以 findLinks 宣告了兩個結果,一個是發現的連結列表,另一個是錯誤資訊。

另外,HTML 的解析一般能夠修正錯誤的輸入以及構造一個存在錯誤節點的文件,所以 Parse 很少失敗,通常情況下,岀錯都是由基本的 I/O 錯誤引起的。
package main

import (
    "fmt"
    "golang.org/x/net/html"
    "net/http"
    "os"
)

func main() {
    for _, url := range os.Args[1:] {
        fmt.Println(os.Args[1:])
        links, err := findLinks(url)
        if err != nil {
            fmt.Fprintf(os.Stderr, "findlinks2: %vn", err)
            continue
        }
        for _, link := range links {
            fmt.Println(link)
        }
    }
}

// findLinks發起一個HTTP的GET請求,解析返回的HTML頁面,並返回所有連結
func findLinks(url string) ([]string, error) {
    resp, err := http.Get(url)
    if err != nil {
        return nil, err
    }
    if resp.StatusCode != http.StatusOK {
        resp.Body.Close()
        return nil, fmt.Errorf("getting %s: %s", url, resp.Status)
    }
    doc, err := html.Parse(resp.Body)
    resp.Body.Close()
    if err != nil {
        return nil, fmt.Errorf("parsing %s as HTML: %v", url, err)
    }
    return visit(nil, doc), nil
}

// 將節點 n 中的每個連結新增到結果中
func visit(links []string, n *html.Node) []string {
    if n == nil {
        return links
    }
    if n.Type == html.ElementNode && n.Data == "a" {
        for _, a := range n.Attr {
            if a.Key == "href" {
                links = append(links, a.Val)
            }
        }
    }
    // 可怕的遞迴,非常不好理解。
    return visit(visit(links, n.FirstChild), n.NextSibling)
}
findLinks 函數有 4 個返回語句,每一個語句返回一對值,前 3 個返回語句將函數從 http 和 html 包中獲得的錯誤資訊傳遞給呼叫者,第一個返回語句中,錯誤直接返回,第二個返回語句和第三個返回語句則使用 fmt.Errorf 格式化處理過的附加上下文資訊,如果 findLinks 呼叫成功,最後一個返回語句將返回連結的 slice,且 error 為空。

我們必須保證 resp.Body 正確關閉使得網路資源正常釋放,即使在發生錯誤的情況下也必須釋放資源,Go語言的垃圾回收機制將回收未使用的記憶體,但不能指望它會釋放未使用的作業系統資源,比如開啟的檔案以及網路連線必須顯式地關閉它們。

呼叫一個多值計算的函數會返回一組值,如果要使用這些返回值,則必須顯式地將返回值賦給變數。

links, err := findLinks(url)

忽略其中一個返回值可以將它賦給一個空識別符號_

links, _ := findLinks(url) // 忽略的錯誤

一個含有多個值的函數返回值可以是呼叫另一個含有多個返回值的函數得到的,就像下面的函數,這個函數的行為和 findLinks 類似,只是多了一個記錄引數的動作。

func findLinksLog(url string) ([]string, error) {
    log.Printf("findLinks %s", url)
    return findLinks(url)
}

一個含有多個返回值的函數可以作為單獨的實參傳遞給擁有多個形參的函數中,儘管很少在生產環境使用,但是這個特性有的時候可以方便偵錯,它使得我們僅僅使用一條語句就可以輸出所有的結果,下面兩個輸出語句的效果是一致的。

log.Println(findLinks(url))

links, err := findLinks(url)
log.Println(links, err)

良好的名稱可以使得返回值更加有意義,尤其在一個函數返回多個結果且型別相同時,名字的選擇更加重要,比如:

func Size(rect image.Rectangle) (width, height int)
func Split(path string) (dir, file string)
func HourMinSec(t time.Time) (hour, minute, second int)

但不必始終為每個返回值單獨命名,比如,習慣上,最後的一個布林返回值表示成功與否,一個 error 結果通常都不需要特別說明。

一個函數如果有命名的返回值,可以省略 return 語句的運算元,這稱為裸返回。
package main

import (
    "fmt"
    "golang.org/x/net/html"
    "net/http"
    "os"
    "strings"
)

func main() {
    words, images, _ := CountWordsAndImages(os.Args[1])
    fmt.Printf("文字:%d,圖片:%d n", words, images)
}

// CountWordsAndImages 傳送一個 HTTP GET 請求,並且獲取文件的
// 字數與圖片數量
func CountWordsAndImages(url string) (words, images int, err error) {
    resp, err := http.Get(url)
    if err != nil {
        return
    }
    doc, err := html.Parse(resp.Body)
    resp.Body.Close()
    if err != nil {
        err = fmt.Errorf("parsing HTML: %s", err)
        return
    }
    words, images = countWordsAndImages(doc)
    //bare return
    return
}
func countWordsAndImages(n *html.Node) (words, images int) {

    texts, images := visit3(nil, 0, n)
    for _, v := range texts {
        v = strings.Trim(strings.TrimSpace(v), "rn")
        if v == "" {
            continue
        }
        words += strings.Count(v, "")
    }
    //bare return
    return
}

//遞迴迴圈html
func visit3(texts []string, imgs int, n *html.Node) ([]string, int) {
    //文字
    if n.Type == html.TextNode {
        texts = append(texts, n.Data)
    }
    //圖片
    if n.Type == html.ElementNode && (n.Data == "img") {
        imgs++
    }
    for c := n.FirstChild; c != nil; c = c.NextSibling {
        if c.Data == "script" || c.Data == "style" {
            continue
        }

        texts, imgs = visit3(texts, imgs, c)
    }
    //多返回值
    return texts, imgs
}
裸返回是將每個命名返回結果按照順序返回的快捷方法,所以在上面的函數中,每個 return 語句都等同於:

return words, images, err

函數中存在多個返回語句且有多個返回結果時,裸返回可以消除重複程式碼,但是並不能使程式碼更加易於理解,對於這種方式,在第一眼看來,不能直觀地看出 return 語句返回的具體結果,鑑於這個原因,應保守使用裸返回。