首页 Go语言中的函数
文章
取消

Go语言中的函数

本文基于Go 1.19版本。

声明

Go语言中,函数的声明由名字、形参列表、返回列表(可选)和函数体构成,注意Go对代码格式要求比较严格:

1
2
3
func name(parameter-list) (result-list) {
    body
}

声明的组成成分

函数的名字和其他名字有着同样的规则,由字母或下划线开头,后面可以跟任意数量的字符、数字和下划线,并且区分大小写。

函数的形参列表由一组变量的参数名和参数类型构成。

函数的形参变量由函数的实参的值进行初始化,为函数最外层作用域的局部变量。

函数的返回列表制定了返回值的类型和名字(可选)。如果函数的没有返回值或只有一个未命名的返回值是,返回列表的圆括号可以省略。

命名的返回值会根据变量类型初始化为相应的0值,和形参变量一样,同为函数最外层作用域的局部变量。

存在返回列表时,无论返回值有没有得到命名,函数必须显式地以return结束。如下,return不能省略:

1
2
3
4
func sub(x int, y int) (z int) {
    z = x - y
    return
}

相同类型简写

当多个形参或返回值的类型相同时,可以采用简写的方式,只写一次类型。如下的两个声明完全等价:

1
2
3
4
5
6
7
func f(i, j, k, int, s, t string ) {
    /* ... */
}

func f(i int, j int, k int, s string, t string) {
    /* ... */
}

函数签名及其等价性

函数的类型被称为函数签名,如果两个函数的形参列表和返回列表相同(形参和返回值的名称不影响),则认为两个函数的类型是相同的。如下的两个函数签名即是相同的:

1
2
3
4
5
6
7
8
func add(a int, b int) int {
    return a + b
}

func sub(x int, y int) (z int) {
    z = x - y
    return
}

无默认参数

Go语言中没有默认参数值,也不能指定实参名,因此GO语言中需要提供实参来对应函数中的每个形参,并保持调用顺序的一致。以下在Python中的语法,Go中不存在:

1
2
3
4
5
6
7
8
9
10
def func(a=2, b=3, c=5):
    return a + 2*b + 3*c


# 2+2*3+3*5 = 23
print(func())
# 1+2*3+3*5 = 22
print(func(1))
# 2+2*4+3*1 = 13
print(func(c=1, b=4, a=2))

按值传递

和C语言类似的,Go语言的实参也是按值传递的,修改函数的形参变量并不会影响被调用者提供的实参。当然,可以通过指针、slice等特定形参变量间接修改实参的变量。

无函数体的声明

Go语言中存在没有函数体的函数声明,这样的函数是由其他语言实现的:

1
2
3
package math

func Sin(x float64) float64 // 使用汇编语言实现

递归

和许多语言类似,Go支持递归,递归过程中也会有函数调用栈的入栈和出栈。但与固定长度大小的函数调用栈的语言不同,Go同时还支持可变长度的函数调用栈,函数调用栈的大小随着使用增加可达到1GB,因此通常情况下不用担心函数递归时常会担心的栈溢出问题。

支持多返回值的return语句

return语句用法

Go语言中的函数能返回多个结果,一个常见的用法具备两个返回值,一个为期望的计算结果,另一个为错误值。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
func main() {
	for _, url := range 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 performs an HTTP GET request for url, parses the
// response as HTML, and extracts and returns the links.
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
}

注意Go语言虽然能够通过垃圾回收机制回收内存,但不能通过此机制释放未使用的操作系统资源(如打开文件和网络连接),无论是否发生错误,都必须显示将其关闭,防止资源泄露。

当然返回一个同样多返回值的函数的调用也是允许的行为,如下所示:

1
2
3
4
5
6
7
func complexF1() (re float64, im float64) {
	return -7.0, -4.0
}

func complexF2() (re float64, im float64) {
	return complexF1()
}

命名返回值和裸返回

函数的返回值(部分或全部)可以命名。命名的返回值可以带来更好的可读性,同时(返回值都被命名时)也带来了裸返回的新返回方法。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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)
    return
}

在上面的例子中,return语句都等同于:

1
    return words, images, err

虽然裸返回可以消除重复代码,但降低了代码可读性,应当保守使用。

注意在实现中,一个编译器如果发现和返回值名字相同的不同实体(entity)被定义,可能会禁止此处使用裸返回。如下所示:

1
2
3
4
5
6
func f(n int) (res int, err error) {
	if _, err := f(n-1); err != nil {
		return  // invalid return statement: err is shadowed
	}
	return
}

在上述示例中,if语句中_, err := f(n-1)重新定义了新的名为err的实体,导致err名字对应的是if语句中新的err实体,而非函数f的返回值err。在这种情况下,编译器可以拒绝在if语句的作用域内使用裸返回。

使用或忽略返回值

如果需要函数的返回值,必须将其显示赋值给变量,如果其中有部分返回值不需要,则可以将其赋值给空标识符。

1
2
3
links, err := findLinks(url)
// ignore error
links, _ := findLinks(url)

错误处理

基于返回值的错误处理

Go语言采用返回值的方式进行错误处理,这类似于C语言的处理方式。函数将错误信息返回给调用者,由调用者决定如何处理,很多情况下调用者也不清楚应该如何处理该错误,那么往往会再将该错误返回值传递下去,通过层层返回到逻辑上便于处理的层级进行处理。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func findLinks(url string) ([]string, error) {
    resp, err := http.Get()
    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.Errof("parsing %s as HTML: %v", url, err)
    }
    return visit(nil, doc), nil
}

从例子中可以看到Go语言中关于错误处理的几个通用做法:

  1. 错误为最后1个返回值,且为error类型。
  2. 当函数允许成功时,错误返回为nil。
  3. 对于子函数的错误,如果不好处理,会返回给上级函数。

不过也并非所有函数的错误类型均为error类型,如果错误情况只有一种,则通常用布尔类型进行表示。如下所示:

1
2
3
4
value, ok := cache.Lookup(key)
if !ok {
    // ...cache[key]不存在
}

其中返回值ok即为布尔型,正确代表无错误发生。

错误处理不是异常机制,只处理通常错误

Go语言并未采用类似try-catch的异常机制来处理通常的错误,因为Go语言的设计者认为try-catch的异常机制将导致栈跟踪信息难以理解。

但这并不代表Go语言没有类似异常的机制,Go语言存在panic-recover机制,不过这套机制只应被用于处理程序Bug导致的预料外的结果,对于可预见的错误还是应该通过通常的错误处理方式进行处理。

标识错误的返回值和error接口类

和C进行比较的话,C语言的返回值往往既能表示正确的值又能表示错误的值,光靠返回值有时不足以表明其具体错误类型,因此可能还需要通过其他方式将错误信息传递出来,比如常见的通过errno传递错误信息。

1
2
3
4
if (somecall() == -1) {
    printf("somecall() failed\n");
    if (errno == ...) { ... }
}

而Go语言中,多返回值的特性,本身就使得错误返回值能够只需表达错误情况,而不需要传递程序正常运行所需要的返回值。error类型本身也可以传递字符串作为错误标识,其表达力足够清晰表达大部分错误。error是接口类型,其包含返回错误信息的方法:

1
2
3
type error interface {
    Error() string
}

error包也很简单,只有4行。如下所示:

1
2
3
4
package errors
func New(text string) error { return &errorString{text} }
type errorString struct { text string }
func (e *errorString) Error() string { return e.text }

其中errorString对string做了简单封装,目的是避免后续布局变更。而满足error接口的是*errorString指针,这样就能使得每次New分配的error实例都不相等。

1
fmt.Println(errors.New("EOF") == errors.New("EOF")) // "false"

函数变量

函数是一等公民

匿名函数

变长函数

宕机

参考

本文由作者按照 CC BY 4.0 进行授权