你好,我是Tony Bai。

在前面的几讲中,我们学习了用于对现实世界实体抽象的类型,以及用来实现算法逻辑控制的几种控制结构。从这一讲开始,我们来学习一下Go代码中的基本功能逻辑单元:函数

学到这里,相信你对Go中的函数已经不陌生了,因为我们在前面的示例程序中一直都在使用函数。函数是现代编程语言的基本语法元素,无论是在命令式语言、面向对象语言还是动态脚本语言中,函数都位列C位。

Go语言也不例外。在Go语言中,函数是唯一一种基于特定输入,实现特定任务并可返回任务执行结果的代码块(Go语言中的方法本质上也是函数)。如果忽略Go包在Go代码组织层面的作用,我们可以说Go程序就是一组函数的集合,实际上,我们日常的Go代码编写大多都集中在实现某个函数上。

但“一龙生九子,九子各不同”!虽然各种编程语言都加入了函数这个语法元素,但各个语言中函数的形式与特点又有不同。那么Go语言中函数又有哪些独特之处呢?考虑到函数的重要性,我们会用三节课的时间,全面系统地讲解Go语言的函数。

在这一节课中,我们就先来学习一下函数基础,以及Go函数最与众不同的一大特点。我们先从最基本的函数声明开始说起。

Go函数与函数声明

函数对应的英文单词是Function,Function这个单词原本是功能、职责的意思。编程语言使用Function这个单词,表示将一个大问题分解后而形成的、若干具有特定功能或职责的小任务,可以说十分贴切。函数代表的小任务可以在一个程序中被多次使用,甚至可以在不同程序中被使用,因此函数的出现也提升了整个程序界代码复用的水平

那Go语言中,函数相关的语法形式是怎样的呢?我们先来看最常用的Go函数声明。

在Go中,我们定义一个函数的最常用方式就是使用函数声明。我们以Go标准库fmt包提供的Fprintf函数为例,看一下一个普通Go函数的声明长啥样:

图片

我们看到一个Go函数的声明由五部分组成,我们一个个来拆解一下。

第一部分是关键字func,Go函数声明必须以关键字func开始。

第二部分是函数名。函数名是指代函数定义的标识符,函数声明后,我们会通过函数名这个标识符来使用这个函数。在同一个Go包中,函数名应该是唯一的,并且它也遵守Go标识符的导出规则,也就是我们之前说的,首字母大写的函数名指代的函数是可以在包外使用的,小写的就只在包内可见。

第三部分是参数列表。参数列表中声明了我们将要在函数体中使用的各个参数。参数列表紧接在函数名的后面,并用一个括号包裹。它使用逗号作为参数间的分隔符,而且每个参数的参数名在前,参数类型在后,这和变量声明中变量名与类型的排列方式是一致的。

另外,Go函数支持变长参数,也就是一个形式参数可以对应数量不定的实际参数。Fprintf就是一个支持变长参数的函数,你可以看到它第三个形式参数a就是一个变长参数,而且变长参数与普通参数在声明时的不同点,就在于它会在类型前面增加了一个“…”符号。关于函数对变长参数的支持,我们在后面还会再讲。

第四部分是返回值列表。返回值承载了函数执行后要返回给调用者的结果,返回值列表声明了这些返回值的类型,返回值列表的位置紧接在参数列表后面,两者之间用一个空格隔开。不过,上图中比较特殊,Fprintf函数的返回值列表不仅声明了返回值的类型,还声明了返回值的名称,这种返回值被称为具名返回值。多数情况下,我们不需要这么做,只需声明返回值的类型即可。

最后,放在一对大括号内的是函数体,函数的具体实现都放在这里。不过,函数声明中的函数体是可选的。如果没有函数体,说明这个函数可能是在Go语言之外实现的,比如使用汇编语言实现,然后通过链接器将实现与声明中的函数名链接到一起。没有函数体的函数声明是更高级的话题了,你感兴趣可以自己去了解一下,我们这里还是先打好基础。

看到这里,你可能会问:同为声明,为啥函数声明与之前学过的变量声明在形式上差距这么大呢? 变量声明中的变量名、类型名和初值与上面的函数声明是怎么对应的呢?

为了让更好地理解函数声明,也给我们后续的讲解做铺垫,这里我们就横向对比一下,把上面的函数声明等价转换为变量声明的形式看看:

图片

转换后的代码不仅和之前的函数声明是等价的,而且这也是完全合乎Go语法规则的代码。对照一下这两张图,你是不是有一种豁然开朗的感觉呢?这不就是在声明一个类型为函数类型的变量吗

我们看到,函数声明中的函数名其实就是变量名,函数声明中的func关键字、参数列表和返回值列表共同构成了函数类型。而参数列表与返回值列表的组合也被称为函数签名,它是决定两个函数类型是否相同的决定因素。因此,函数类型也可以看成是由func关键字与函数签名组合而成的。

通常,在表述函数类型时,我们会省略函数签名参数列表中的参数名,以及返回值列表中的返回值变量名。比如上面Fprintf函数的函数类型是:

func(io.Writer, string, ...interface{}) (int, error)

这样,如果两个函数类型的函数签名是相同的,即便参数列表中的参数名,以及返回值列表中的返回值变量名都是不同的,那么这两个函数类型也是相同类型,比如下面两个函数类型:

func (a int, b string) (results []string, err error)
func (c int, d string) (sl []string, err error)

如果我们把这两个函数类型的参数名与返回值变量名省略,那它们都是func (int, string) ([]string, error),因此它们是相同的函数类型。

到这里,我们可以得到这样一个结论:每个函数声明所定义的函数,仅仅是对应的函数类型的一个实例,就像var a int = 13这个变量声明语句中a是int类型的一个实例一样。

如果你还记得前面第17讲中、使用复合类型字面值对结构体类型变量进行显式初始化的内容,你一定会觉得上面这种、用变量声明来声明函数变量的形式,似曾相识,我们把这两种形式都以最简化的样子表现出来,看下面代码:

s := T{}      // 使用复合类型字面值对结构体类型T的变量进行显式初始化
f := func(){} // 使用变量声明形式的函数声明

这里,T{}被称为复合类型字面值,那么处于同样位置的func(){}是什么呢?Go语言也为它准备了一个名字,叫“函数字面值(Function Literal)”。我们可以看到,函数字面值由函数类型与函数体组成,它特别像一个没有函数名的函数声明,因此我们也叫它匿名函数。匿名函数在Go中用途很广,稍后我们会细讲。

讲到这里,你可能会想:既然是等价的,那我以后就用这种变量声明的形式来声明一个函数吧。万万不可!我这里只是为了帮你理解函数声明做了一个等价变换。在Go中的绝大多数情况,我们还是会通过传统的函数声明来声明一个特定函数类型的实例,也就是我们俗称的“定义一个函数”。

好了,横向对比就到此为止了,现在我们继续回到函数声明中来, 详细看看函数声明的重要组成部分——参数。

函数参数的那些事儿

函数参数列表中的参数,是函数声明的、用于函数体实现的局部变量。由于函数分为声明与使用两个阶段,在不同阶段,参数的称谓也有不同。在函数声明阶段,我们把参数列表中的参数叫做形式参数(Parameter,简称形参),在函数体中,我们使用的都是形参;而在函数实际调用时传入的参数被称为实际参数(Argument,简称实参)。为了便于直观理解,我绘制了这张示意图,你可以参考一下:

图片

当我们实际调用函数的时候,实参会传递给函数,并和形式参数逐一绑定,编译器会根据各个形参的类型与数量,来检查传入的实参的类型与数量是否匹配。只有匹配,程序才能继续执行函数调用,否则编译器就会报错。

Go语言中,函数参数传递采用是值传递的方式。所谓“值传递”,就是将实际参数在内存中的表示逐位拷贝(Bitwise Copy)到形式参数中。对于像整型、数组、结构体这类类型,它们的内存表示就是它们自身的数据内容,因此当这些类型作为实参类型时,值传递拷贝的就是它们自身,传递的开销也与它们自身的大小成正比。

但是像string、切片、map这些类型就不是了,它们的内存表示对应的是它们数据内容的“描述符”。当这些类型作为实参类型时,值传递拷贝的也是它们数据内容的“描述符”,不包括数据内容本身,所以这些类型传递的开销是固定的,与数据内容大小无关。这种只拷贝“描述符”,不拷贝实际数据内容的拷贝过程,也被称为“浅拷贝”

不过函数参数的传递也有两个例外,当函数的形参为接口类型,或者形参是变长参数时,简单的值传递就不能满足要求了,这时Go编译器会介入:对于类型为接口类型的形参,Go编译器会把传递的实参赋值给对应的接口类型形参;对于为变长参数的形参,Go编译器会将零个或多个实参按一定形式转换为对应的变长形参。

那么这里,零个或多个传递给变长形式参数的实参,被Go编译器转换为何种形式了呢?我们通过下面示例代码来看一下:

func myAppend(sl []int, elems ...int) []int {
    fmt.Printf("%T\n", elems) // []int
    if len(elems) == 0 {
        println("no elems to append")
        return sl
    }

    sl = append(sl, elems...)
    return sl
}

func main() {
    sl := []int{1, 2, 3}
    sl = myAppend(sl) // no elems to append
    fmt.Println(sl) // [1 2 3]
    sl = myAppend(sl, 4, 5, 6)
    fmt.Println(sl) // [1 2 3 4 5 6]
}

我们重点看一下代码中的myAppend函数,这个函数基于append,实现了向一个整型切片追加数据的功能。它支持变长参数,它的第二个形参elems就是一个变长参数。myAppend函数通过Printf输出了变长参数的类型。执行这段代码,我们将看到变长参数elems的类型为[]int。

这也就说明,在Go中,变长参数实际上是通过切片来实现的。所以,我们在函数体中,就可以使用切片支持的所有操作来操作变长参数,这会大大简化了变长参数的使用复杂度。比如myAppend中,我们使用len函数就可以获取到传给变长参数的实参个数。

到这里,我们已经学习了函数声明的两个部分。接下来,我们再看看函数声明的最后一部分,返回值列表。

函数支持多返回值

和其他主流静态类型语言,比如C、C++和Java不同,Go函数支持多返回值。多返回值可以让函数将更多结果信息返回给它的调用者,Go语言的错误处理机制很大程度就是建立在多返回值的机制之上的,这个我们在后续课程中还会详细讲解。

函数返回值列表从形式上看主要有三种:

func foo()                       // 无返回值
func foo() error                 // 仅有一个返回值
func foo() (int, string, error)  // 有2或2个以上返回值

如果一个函数没有显式返回值,那么我们可以像第一种情况那样,在函数声明中省略返回值列表。而且,如果一个函数仅有一个返回值,那么通常我们在函数声明中,就不需要将返回值用括号括起来,如果是2个或2个以上的返回值,那我们还是需要用括号括起来的。

在函数声明的返回值列表中,我们通常会像上面例子那样,仅列举返回值的类型,但我们也可以像fmt.Fprintf函数的返回值列表那样,为每个返回值声明变量名,这种带有名字的返回值被称为具名返回值(Named Return Value)。这种具名返回值变量可以像函数体中声明的局部变量一样在函数体内使用。

那么在日常编码中,我们究竟该使用普通返回值形式,还是具名返回值形式呢?

Go标准库以及大多数项目代码中的函数,都选择了使用普通的非具名返回值形式。但在一些特定场景下,具名返回值也会得到应用。比如,当函数使用defer,而且还在defer函数中修改外部函数返回值时,具名返回值可以让代码显得更优雅清晰。关于defer的使用,我们会在后面课程中还会细讲。

再比如,当函数的返回值个数较多时,每次显式使用return语句时都会接一长串返回值,这时,我们用具名返回值可以让函数实现的可读性更好一些,比如下面Go标准库time包中的parseNanoseconds函数就是这样:

// $GOROOT/src/time/format.go
func parseNanoseconds(value string, nbytes int) (ns int, rangeErrString string, err error) {
    if !commaOrPeriod(value[0]) {
        err = errBad
        return
    }
    if ns, err = atoi(value[1:nbytes]); err != nil {
        return
    }
    if ns < 0 || 1e9 <= ns {
        rangeErrString = "fractional second"
        return
    }

    scaleDigits := 10 - nbytes
    for i := 0; i < scaleDigits; i++ {
        ns *= 10
    }
    return
}

了解了上面这些有关Go函数的基础知识后,接下来,我们来学习Go函数与众不同的一个特点,这个特点使得Go函数具有更大的灵活性和表达力。

函数是“一等公民”

这个特点就是,函数在Go语言中属于“一等公民(First-Class Citizen)”。要知道,并不是在所有编程语言中函数都是“一等公民”。

那么,什么是编程语言的“一等公民”呢?关于这个名词,业界和教科书都没有给出精准的定义。我们这里可以引用一下wiki发明人、C2站点作者沃德·坎宁安(Ward Cunningham)对“一等公民”的解释

如果一门编程语言对某种语言元素的创建和使用没有限制,我们可以像对待值(value)一样对待这种语法元素,那么我们就称这种语法元素是这门编程语言的“一等公民”。拥有“一等公民”待遇的语法元素可以存储在变量中,可以作为参数传递给函数,可以在函数内部创建并可以作为返回值从函数返回。

基于这个解释,我们来看看Go语言的函数作为“一等公民”,表现出的各种行为特征。

特征一:Go函数可以存储在变量中。

按照沃德·坎宁安对一等公民的解释,身为一等公民的语法元素是可以存储在变量中的。其实,这点我们在前面理解函数声明时已经验证过了,这里我们再用例子简单说明一下:

var (
    myFprintf = func(w io.Writer, format string, a ...interface{}) (int, error) {
        return fmt.Fprintf(w, format, a...)
    }
)

func main() {
    fmt.Printf("%T\n", myFprintf) // func(io.Writer, string, ...interface {}) (int, error)
    myFprintf(os.Stdout, "%s\n", "Hello, Go") // 输出Hello,Go
}

在这个例子中,我们把新创建的一个匿名函数赋值给了一个名为myFprintf的变量,通过这个变量,我们便可以调用刚刚定义的匿名函数。然后我们再通过Printf输出myFprintf变量的类型,也会发现结果与我们预期的函数类型是相符的。

特征二:支持在函数内创建并通过返回值返回。

Go函数不仅可以在函数外创建,还可以在函数内创建。而且由于函数可以存储在变量中,所以函数也可以在创建后,作为函数返回值返回。我们来看下面这个例子:

func setup(task string) func() {
    println("do some setup stuff for", task)
    return func() {
        println("do some teardown stuff for", task)
    }
}

func main() {
    teardown := setup("demo")
    defer teardown()
    println("do some bussiness stuff")
}

这个例子,模拟了执行一些重要逻辑之前的上下文建立(setup),以及之后的上下文拆除(teardown)。在一些单元测试的代码中,我们也经常会在执行某些用例之前,建立此次执行的上下文(setup),并在这些用例执行后拆除上下文(teardown),避免这次执行对后续用例执行的干扰。

在这个例子中,我们在setup函数中创建了这次执行的上下文拆除函数,并通过返回值的形式,将这个拆除函数返回给了setup函数的调用者。setup函数的调用者,在执行完对应这次执行上下文的重要逻辑后,再调用setup函数返回的拆除函数,就可以完成对上下文的拆除了。

从这段代码中我们也可以看到,setup函数中创建的拆除函数也是一个匿名函数,但和前面我们看到的匿名函数有一个不同,这个不同就在于这个匿名函数使用了定义它的函数setup的局部变量task,这样的匿名函数在Go中也被称为闭包(Closure)。

闭包本质上就是一个匿名函数或叫函数字面值,它们可以引用它的包裹函数,也就是创建它们的函数中定义的变量。然后,这些变量在包裹函数和匿名函数之间共享,只要闭包可以被访问,这些共享的变量就会继续存在。显然,Go语言的闭包特性也是建立在“函数是一等公民”特性的基础上的,后面我们还会讲解涉及到闭包的内容。

特征三:作为参数传入函数。

既然函数可以存储在变量中,也可以作为返回值返回,那我们可以理所当然地想到,把函数作为参数传入函数也是可行的。比如我们在日常编码时经常使用、标准库time包的AfterFunc函数,就是一个接受函数类型参数的典型例子。你可以看看下面这行代码,这里通过AfterFunc函数设置了一个2秒的定时器,并传入了时间到了后要执行的函数。这里传入的就是一个匿名函数:

time.AfterFunc(time.Second*2, func() { println("timer fired") })

特征四:拥有自己的类型。

通过我们前面的讲解,你可以知道,作为一等公民的整型值拥有自己的类型int,而这个整型值只是类型int的一个实例,其他作为一等公民的字符串值、布尔值等类型也都拥有自己类型。那函数呢?

在前面讲解函数声明时,我们曾得到过这样一个结论:每个函数声明定义的函数仅仅是对应的函数类型的一个实例,就像var a int = 13这个变量声明语句中的a,只是int类型的一个实例一样。换句话说,每个函数都和整型值、字符串值等一等公民一样,拥有自己的类型,也就是我们讲过的函数类型

我们甚至可以基于函数类型来自定义类型,就像基于整型、字符串类型等类型来自定义类型一样。下面代码中的HandlerFunc、visitFunc就是Go标准库中,基于函数类型进行自定义的类型:

// $GOROOT/src/net/http/server.go
type HandlerFunc func(ResponseWriter, *Request)

// $GOROOT/src/sort/genzfunc.go
type visitFunc func(ast.Node) ast.Visitor

到这里,我们已经可以看到,Go函数确实表现出了沃德·坎宁安诠释中“一等公民”的所有特征:Go函数可以存储在变量中,可以在函数内创建并通过返回值返回,可以作为参数传递给其他函数,可以拥有自己的类型。通过这些分析,你也能感受到,和C/C++等语言中的函数相比,作为“一等公民”的Go函数拥有难得的灵活性。

那么在实际生产中,我们怎么才能发挥出这种灵活性的最大效用,帮助我们写出更加优雅简洁的Go代码呢?接下来,我们就看几个这方面的例子。

函数“一等公民”特性的高效运用

应用一:函数类型的妙用

Go函数是“一等公民”,也就是说,它拥有自己的类型。而且,整型、字符串型等所有类型都可以进行的操作,比如显式转型,也同样可以用在函数类型上面,也就是说,函数也可以被显式转型。并且,这样的转型在特定的领域具有奇妙的作用,一个最为典型的示例就是标准库http包中的HandlerFunc这个类型。我们来看一个使用了这个类型的例子:

func greeting(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Welcome, Gopher!\n")
}                    

func main() {
    http.ListenAndServe(":8080", http.HandlerFunc(greeting))
}

这我们日常最常见的、用Go构建Web Server的例子。它的工作机制也很简单,就是当用户通过浏览器,或者类似curl这样的命令行工具,访问Web server的8080端口时,会收到“Welcome, Gopher!”这样的文字应答。我们在09讲曾讲过使用http包编写web server的方法,但当时我没有进一步讲解其中的原理,这一节课中我们就补上这一点。

我们先来看一下http包的函数ListenAndServe的源码:

// $GOROOT/src/net/http/server.go
func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}

函数ListenAndServe会把来自客户端的http请求,交给它的第二个参数handler处理,而这里handler参数的类型http.Handler,是一个自定义的接口类型,它的源码是这样的:

// $GOROOT/src/net/http/server.go
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

我们还没有系统学习接口类型,你现在只要知道接口是一组方法的集合就好了。这个接口只有一个方法ServeHTTP,他的函数类型是func(http.ResponseWriter, *http.Request)。这和我们自己定义的http请求处理函数greeting的类型是一致的,但是我们没法直接将greeting作为参数值传入,否则编译器会报错:

func(http.ResponseWriter, *http.Request) does not implement http.Handler (missing ServeHTTP method)

这里,编译器提示我们,函数greeting还没有实现接口Handler的方法,无法将它赋值给Handler类型的参数。现在我们再回过头来看下代码,代码中我们也没有直接将greeting传给ListenAndServe函数,而是将http.HandlerFunc(greeting)作为参数传给了ListenAndServe。那这个http.HandlerFunc究竟是什么呢?我们直接来看一下它的源码:

// $GOROOT/src/net/http/server.go

type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
        f(w, r)
}

通过它的源码我们看到,HandlerFunc是一个基于函数类型定义的新类型,它的底层类型为函数类型func(ResponseWriter, *Request)。这个类型有一个方法ServeHTTP,然后实现了Handler接口。也就是说http.HandlerFunc(greeting)这句代码的真正含义,是将函数greeting显式转换为HandlerFunc类型,后者实现了Handler接口,满足ListenAndServe函数第二个参数的要求。

另外,之所以http.HandlerFunc(greeting)这段代码可以通过编译器检查,正是因为HandlerFunc的底层类型是func(ResponseWriter, *Request),与greeting函数的类型是一致的,这和下面整型变量的显式转型原理也是一样的:

type MyInt int
var x int = 5
y := MyInt(x) // MyInt的底层类型为int,类比HandlerFunc的底层类型为func(ResponseWriter, *Request)

应用二:利用闭包简化函数调用。

我们前面讲过,Go闭包是在函数内部创建的匿名函数,这个匿名函数可以访问创建它的函数的参数与局部变量。我们可以利用闭包的这一特性来简化函数调用,这里我们看一个具体例子:

func times(x, y int) int {
	return x * y
}

在上面的代码中,times函数用来进行两个整型数的乘法。我们使用times函数的时候需要传入两个实参,比如:

times(2, 5) // 计算2 x 5
times(3, 5) // 计算3 x 5
times(4, 5) // 计算4 x 5

不过,有些场景存在一些高频使用的乘数,这个时候我们就没必要每次都传入这样的高频乘数了。那我们怎样能省去高频乘数的传入呢? 我们看看下面这个新函数partialTimes:

func partialTimes(x int) func(int) int {
	return func(y int) int {
		return times(x, y)
	}
}

这里,partialTimes的返回值是一个接受单一参数的函数,这个由partialTimes函数生成的匿名函数,使用了partialTimes函数的参数x。按照前面的定义,这个匿名函数就是一个闭包。partialTimes实质上就是用来生成以x为固定乘数的、接受另外一个乘数作为参数的、闭包函数的函数。当程序调用partialTimes(2)时,partialTimes实际上返回了一个调用times(2,y)的函数,这个过程的逻辑类似于下面代码:

timesTwo = func(y int) int {
    return times(2, y)
}

这个时候,我们再看看如何使用partialTimes,分别生成以2、3、4为固定高频乘数的乘法函数,以及这些生成的乘法函数的使用方法:

func main() {
	timesTwo := partialTimes(2)   // 以高频乘数2为固定乘数的乘法函数
	timesThree := partialTimes(3) // 以高频乘数3为固定乘数的乘法函数
	timesFour := partialTimes(4)  // 以高频乘数4为固定乘数的乘法函数
	fmt.Println(timesTwo(5))   // 10,等价于times(2, 5)
	fmt.Println(timesTwo(6))   // 12,等价于times(2, 6)
	fmt.Println(timesThree(5)) // 15,等价于times(3, 5)
	fmt.Println(timesThree(6)) // 18,等价于times(3, 6)
	fmt.Println(timesFour(5))  // 20,等价于times(4, 5)
	fmt.Println(timesFour(6))  // 24,等价于times(4, 6)
}

你可以看到,通过partialTimes,我们生成了三个带有固定乘数的函数。这样,我们在计算乘法时,就可以减少参数的重复输入。你看到这里可能会说,这种简化的程度十分有限啊!

不是的。这里我只是举了一个比较好理解的简单例子,在那些动辄就有5个以上参数的复杂函数中,减少参数的重复输入给开发人员带去的收益,可要比这个简单的例子大得多。

小结

好了,今天的课讲到这里就结束了,现在我们一起来回顾一下吧。

在这一讲中,我们讲解了Go代码中的基本功能逻辑单元:函数。函数这种语法元素的诞生,源于将大问题分解为若干小任务与代码复用。

Go语言中定义一个函数的最常用方式就是使用函数声明。函数声明虽然形式上与我们之前学过的变量声明不同,但本质其实是一致的,我们可以通过一个等价转换,将函数声明转换为一个以函数名为变量名、以函数字面值为初值的函数变量声明形式。这个转换是你深入理解函数的关键。

我们对函数字面值再进行了拆解。函数字面值是由函数类型与函数体组成的,而函数类型则是由func关键字+函数签名组成。再拆解,函数签名又包括函数的参数列表与返回值列表。通常我们说函数签名时,会省去参数名与返回值变量名,只保留各自的类型信息。函数签名相同的两个函数类型就是相同的函数类型。

而且,Go函数采用值传递的方式进行参数传递,对于string、切片、map等类型参数来说,这种传递方式传递的仅是“描述符”信息,是一种“浅拷贝”,这点你一定要牢记。Go函数支持多返回值,Go语言的错误处理机制就是建立在多返回值的基础上的。

最后,与传统的C、C++、Java等静态编程语言中的函数相比,Go函数的最大特点就是它属于Go语言的“一等公民”。Go函数具备一切作为“一等公民”的行为特征,包括函数可以存储在变量中、支持函数内创建并通过返回值返回、支持作为参数传递给函数,以及拥有自己的类型等。这些“一等公民”的特征,让Go函数表现出极大的灵活性。日常编码中,我们也可以利用这些特征进行一些巧妙的代码设计,让代码的实现更简化。

思考题

函数“一等公民”特性的高效运用的例子,显然不限于我们今天提到的这两个,这里我想让你思考一下,你还能列举出其他的高效运用函数“一等公民”特性的例子吗?

欢迎你把这节课分享给更多对Go语言的函数感兴趣的朋友。我是Tony Bai,我们下节课见。