在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: %v\n", 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语言的垃圾回收机制将回收未使用的内存,但不能指望它会释放未使用的操作系统资源,比如打开的文件以及网络连接必须显式地关闭它们。
调用一个多值计算的函数会返回一组值,如果要使用这些返回值,则必须显式地将返回值赋给变量。
忽略其中一个返回值可以将它赋给一个空标识符_。
一个含有多个值的函数返回值可以是调用另一个含有多个返回值的函数得到的,就像下面的函数,这个函数的行为和 findLinks 类似,只是多了一个记录参数的动作。
一个含有多个返回值的函数可以作为单独的实参传递给拥有多个形参的函数中,尽管很少在生产环境使用,但是这个特性有的时候可以方便调试,它使得我们仅仅使用一条语句就可以输出所有的结果,下面两个输出语句的效果是一致的。
良好的名称可以使得返回值更加有意义,尤其在一个函数返回多个结果且类型相同时,名字的选择更加重要,比如:
但不必始终为每个返回值单独命名,比如,习惯上,最后的一个布尔返回值表示成功与否,一个 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), "\r\n")
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 语句返回的具体结果,鉴于这个原因,应保守使用裸返回。