四时宝库

程序员的知识宝库

Go的一等公民:函数(javascript 一等公民)

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

Go 函数与函数声明

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

我们看到一个 Go 函数的声明由五部分组成:

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

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

第三部分是参数列表。参数列表中声明了我们将要在函数体中使用的各个参数。参数列表紧接在函数名的后面,并用一个括号包裹。它使用逗号作为参数间的分隔符,而且每个参数的参数名在前,参数类型在后,这和变量声明中变量名与类型的排列方式是一致的。另外,Go 函数支持变长参数,也就是一个形式参数可以对应数量不定的实际参数。Fprintf 就是一个支持变长参数的函数,你可以看到它第三个形式参数 a 就是一个变长参数,而且变长参数与普通参数在声明时的不同点,就在于它会在类型前面增加了一个“…”符号。

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

最后,放在一对大括号内的是函数体,函数的具体实现都放在这里。不过,函数声明中的函数体是可选的。如果没有函数体,说明这个函数可能是在 Go 语言之外实现的,比如使用汇编语言实现,然后通过链接器将实现与声明中的函数名链接到一起。

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

函数参数

由于函数分为声明与使用两个阶段,在不同阶段,参数的称谓也有不同。在函数声明阶段,我们把参数列表中的参数叫做形式参数(Parameter,简称形参),在函数体中,我们使用的都是形参;而在函数实际调用时传入的参数被称为实际参数(Argument,简称实参)。


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

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

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

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

在 Go 中,变长参数实际上是通过切片来实现的。所以,我们在函数体中,就可以使用切片支持的所有操作来操作变长参数,这会大大简化了变长参数的使用复杂度。

函数支持多返回值

和其他主流静态类型语言,比如 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 函数可以存储在变量中。

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

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

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

发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言
    友情链接