Go语言学习7-函数类型

举报
Huazie 发表于 2024/02/28 14:32:48 2024/02/28
【摘要】 本篇 Huazie 向大家介绍 Go 语言的函数类型

引言

上篇我们了解了Go语言的字典类型,本篇主要了解函数和方法。主要如下:

主要内容

在Go语言中,函数类型是一等类型,可以把函数当做一个值来传递和使用。函数类型的值(简称为函数值)既可以作为其他函数的参数,也可以作为其他函数的结果(之一)。

1. 类型表示法

函数类型指代了所有可以接受若干参数并能够返回若干结果的函数。

声明一个函数类型总会以关键字 func 作为开始,紧跟在关键字 func 之后的应该是这个函数的签名,包括了参数声明列表(在左边)和结果声明列表(在右边),两者用空格分隔。参数声明列表必须由圆括号括起来,多个参数声明之间需用逗号分隔。

参数声明是参数名称在前,参数类型在后,中间以空格分隔。如果有一个参数列表,除了一个名称为 name、类型为 string 的参数之外,还包括一个名称为 age 、类型为 int 的参数。参数列表如下:

(name string, age int) 

注意:在同一个参数声明列表中的所有参数名称都必须是唯一的。

如果相邻的两个参数属于同一数据类型,那么我们只需要写一次参数类型。在上面的参数类型中添加一个名称为 level 、类型为 int 的参数:

(name string, age, level int)

这个就相当于:

(name string, age int, level int)

当然,这里甚至可以省略所有参数的名称。但是强烈不推荐这种做法,它的可读性很差。

现在向参数声明中添加一个名称为 informations 、类型为 …string 的可变长参数:

(name string, age int, level int, informations ...string)

注意:可变参数必须是参数列表中的最后一个。

函数类型声明的结果声明列表中一般包含若干个结果声明。结果声明列表的编写规则与参数声明基本一致。不过有两点区别:

  1. 只存在可变长参数的声明而不存在可变长结果的声明;

  2. 如果结果声明列表中只有一个结果声明且这个结果声明中并不包含结果的名称,那么就可以忽略它的圆括号。

如下, bool 就是这个函数类型的唯一结果的类型声明。该结果声明独自组成了该函数类型的结果声明列表。

func (name string, age int, level int, informations ...string) bool

如果我们需要命名这个结果为 done,可以如下编写:

func (name string, age int, level int, informations ...string) (done bool)

注意:这时的结果声明列表必须被圆括号括起来了。命名的结果其名称可以作为附属于该函数类型声明的文档的一部分,方便其他阅读的人员了解其含义。

一个函数类型可以有一个结果声明的列表,这是因为Go语言的函数类型可以有多个结果,这是Go语言的先进特性之一。如下函数类型声明:

func (name string, age int, level int, informations ...string) (effected uint, err error)

为函数声明多个结果可以让每个结果的职责更加单一,这既易于理解又方便使用。如上可以利用这一特性将错误值作为结果(之一)返回给调用它的代码,而不是包错误抛出来,然后再不得不在调用它的地方编写若干代码来抓住这个错误。(有关Go语言的错误处理机制后续博文会详细讨论)

函数类型的多个结果声明可以从不同的角度来体现函数的内部操作的结果。例如:

func (name string, age int, level int, informations ...string) (done bool, id uint, synchronized bool)

假设上面声明的函数类型专门用于保存某项数据,它的3个结果的作用如下:

  1. done : 用于表示数据是否被成功保存。

  2. id : 数据被保存后的ID,此ID可以被用来检索数据。

  3. synchronized : 用于表示数据是否被同步到相关系统中。

这样该函数的调用法会更加清晰明了地获知具体的操作结果,处理这些操作的结果的代码也会更加简单和扁平化。

2. 值表示法

函数类型的零值是nil。未初始化的函数类型的变量的值就是nil。函数类型的值分为两类:命名函数值匿名函数值。在Go语言中,很多时候通常称命名函数值命名函数,称匿名函数值匿名函数,但是它们都是的一种。

命名函数

命名函数的声明一般由关键字func函数名称函数签名(由参数声明列表和结果声明列表组成)和函数体组成。如果在函数的签名中包含了结果声明列表,那么在该函数的函数体中的任何可到达的流程分支的最后一条语句都必须是终止语句终止语句有很多种,比如以关键字returngoto开始的语句、仅包含针对内建函数panic(用于产生一个运行时恐慌)的调用表达式的语句。

定义了一个用于取模运算的 Module 函数:

func Module(x, y int) int {
	return x % y
}

注意:在关键字 return 右边的结果必须在数量上与该函数的结果声明列表中的内容完全一致,且在对应位置的结果的类型上存在可赋予的关系,否则将不能通过编译。

Module 函数的结果命名,例如:

func Module(x, y int) (result int){
	return x % y
}

为函数的结果命名会使它们能过以常规变量的形式存在,就像函数的参数那样。当结果被命名,它们在函数被调用时就会被初始化为对应的数据类型的零值。如果这样的函数的函数体中有一条不带任何参数的 return 语句,那么在执行到这条 return 语句的时候,作为结果的变量的当前值就会被返回给函数调用方。例如:

func Module(x, y int) (result int){
	result = x % y
	return 
}

如上面 Module 函数被调用时,变量 result 被初始化为 int 类型的零值 0。当该函数的函数体中的第一条语句被执行时,变量 result 被赋予了表达式 x % y 的结果值。当该函数体中的无参数的 return 语句被执行时,result 的当前值就会作为结果被返回给函数调用方。

知识点: Go语言命名函数的声明还可以省略掉函数体。这意味着,该函数会由外部程序(如汇编语言程序)实现,而不会由Go语言程序实现。

匿名函数

匿名函数由函数字面量表示。函数字面量也是表达式的一种。在声明的内容上,匿名函数与命名函数的区别也只是少了一个函数名称。如下匿名函数:

func (x, y int) (result int){
	result = x % y
	return 
}

函数字面量也可以看做是对某个函数类型的即时实现,它比函数类型声明多了一个函数体。一个函数字面量可以被赋给一个变量,也可以被直接调用。

3. 属性和基本操作

函数作为Go语言的数据类型之一,可以把函数作为一个变量的类型。例如声明一个变量:

var recorder func (name string, age int, level int)(done bool)

声明过后,所有符合这个函数类型的实现都可以被赋给变量 recorder,如下:

recorder = func (name string, age int, level int) (done bool) {
	//省略若干实现语句
	return
}

注意:被赋给变量 recorder 的函数字面量必须与 recorder 的类型拥有相同的函数签名

可以在一个函数类型的变量上直接应用函数表达式来调用它,例如:

done := recorder("Huazie", 23, 1)

注意:被赋值的变量在数量上必须与函数的结果声明列表中的内容完全一致,且对应位置的变量和结果的类型上存在可赋予的关系。同样适用于对命名函数进行调用并赋值的情况。

在函数字面量被编写出来的时候直接调用它,例如:

recorder = func (name string, age int, level int) (done bool) {
	//省略若干实现语句
	return
}(“Huazie”, 23, 1)

如上所示函数既然可以作为变量的值,那么也就可以像其他值一样在函数之间传递(即作为其他函数的参数或其他函数的结果)。

现在举出一个例子,现在要声明一个可以对一段文本进行加密的函数,同时,要求可以根据不同的应用场景实时地、频繁地对加密算法进行变更。如上,我们应该声明一个能够生成加密函数的函数,然后在程序运行期间,根据不同的要求使用这个函数来生成需要的加密函数。此外,所有用于封装加密算法的函数都应该是同一个函数类型的,这有利于加密算法的无缝替换。

首先声明一个如下的函数类型:

type Encipher func(plaintext string) []byte

如上Encipher是函数类型 func(plaintext string) []byte 的别名,这个函数接收一个 string 类型的参数,并且返回一个元素类型为 byte 的切片类型的结果,这分别代表了一类比较通用的加密算法的输入数据和输出数据。

有了这个用于封装加密算法的函数类型之后,如下声明可以生成加密函数的函数:

func GenEncryptionFunc(encrypt Encipher) func(string) (ciphertext string) {
	return func(plaintext string) string {
		return fmt.Sprintf("%x", encrypt(plaintext))
	}
}

如上看着比较复杂的函数 GenEncryptionFunc 的签名中包括了一个参数声明和一个结果声明。其中,参数声明中的参数类型就是之前定义的用于封装加密算法的函数类型,结果声明表示了一个函数类型的结果。而这个函数类型正是 GenEncryptionFunc 函数所生成的加密函数的类型,它接收一个 string 类型的明文作为参考,并返回一个 string 类型的密文作为结果。

GenEncryptionFunc 函数的函数体内直接返回了复合加密函数类型的匿名函数。这个匿名函数的函数体内这一条语句首先调用了名称为 encrypt 的函数,对匿名函数的参数的明文加密;然后,它使用了标准库代码包 fmt 中的 Sprintf 函数,把 encrypt 函数的调用结果转换为字符串。该字符串的内容实际上是用十六进制数表示的加密结果,而这个加密结果实际上是 []byte 类型的。

每一次调用 GenEncryptionFunc 函数时,传递给他的那个加密算法函数都会一直被对应的加密函数引用这。只要生成的加密函数还可以被访问,其中的加密算法函数就会一直存在,而不会被Go语言的垃圾回收器回收。
理解 GenEncryptionFunc 函数所涉及到的一些概念:

image.png

知识点: 闭包这个词源自于通过“捕获”自由变量的绑定对函数文本执行的“闭合”动作。

只有当函数类型是一等类型并且其值可以作为其他函数的参数或结果的时候,才能够编写出实现闭包的代码。函数类型是Go语言支持函数式编程范式的重要体现,也就是我们编写函数式风格代码的主要手段。函数还可以附属于任何自定义的数据类型,或者与接口类型和结构体类型相结合作为针对某个或某些数据类型的操作方法。

4. 方法

方法就是附属于某个自定义的数据类型的函数。一个方法就是一个与某个接受者关联的函数。方法的声明中包含了关键字func接收者声明方法名称参数声明列表结果声明列表方法体。其中的接收者声明、参数声明列表和结果声明列表统称为方法签名,而方法体可以在某些情况下被忽略。例如:

type MyIntSlice []int
func (self MyIntSlice) Max() (result int) {
	//省略若干实现语句
	return
}

如上,我们首先自定义了一个数据类型MyIntSlice,可以看做 []int 的别名类型。同时,这里还声明了一个方法。在这个名称为 Max 的方法中,接收者声明为**(self MyIntSlice)**。右边的标识符表示该方法所属的数据类型,即 MyIntSlice ; 左边的接收者标识符则代表了 MyIntSlice 类型的值在方法 Max 中的名称。

方法声明中的接收者声明有关的几条编写规则:

  1. 接收者声明中的类型必须是某个自定义的数据类型,或者是一个与某个自定义数据类型对应的指针类型。但不论接收者的类型是哪一种,接收者的基本类型都会是那个自定义数据类型。接收者的基本类型既不能是一个指针类型,也不能是一个接口类型。例如, 方法声明:

    func (self *MyIntSlice) Min() (result int)//接收者的类是*MyIntSlice,而其基本类型是MyIntSlice.
    
  2. 接收者声明中的类型必须由非限定标识符代表。方法所属的数据类型的声明必须与该方法声明处在同一个代码包内。

  3. 接收者标识符不能是空标识符“_”, 并且必须在其所在的方法签名中是唯一的。

  4. 如果接收者的值(由接收者标识符代表)未在当前方法的方法体内被引用,那么我们就可以将这个接收者标识符从当前方法的接收者声明中删除掉。注意,这条不建议这么做,原因和函数声明中的参数声明类似,会使代码的可读性变差。

在Go语言中,常常把接收者类型是某个自定义数据类型的方法叫做该数据类型的值方法,而把接收者类型是某个自定义数据类型对应的指针类型的方法叫作该数据类型的指针方法

对于一个接收者的基本类型来说,它所包含的方法的名称之间不能有重复。如果这个接收者的基本类型是一个结构体类型,还需要保证它包含的字段和方法的名称之间不能出现重复。

定义一个方法:

func (self *MyIntSlice) Min() (result int)

该方法的类型:

func Min() (self *MyIntSlice, result int)

注意:形如上述方法的类型表示的函数的值只能算是一个函数,而不能叫作方法。这样的函数并没有与任何自定义数据类型相关联。

在接收者的基本类型确定的情况下,如何在值方法和指针方法做出选择:

  1. 在某个自定义数据类型的值上,只能够调用与这个数据类型相关联的值方法,而在指向这个值的指针值上,却能够调用与其数据类型关联的值方法和指针方法。虽然自定义数据类型的方法集合中不包含与它关联的指针类型,但是我们仍能够通过这个类型的值调用它的指针方法,这里需要使用取地址符**&**。

  2. 在指针方法中一定能够改变接收者的值。而在值方法中,对接收者的值的改变对于该方法之外一般是无效的。以接收者标识符代表的接收者的值实际上也是当前方法所属的数据类型的当前值的一个复制品。对于值方法来说,由于这个接收者的值就是一个当前值的复制品,所以对它的改变并不会影响到当前值。而对于指针方法来说,这个接收者的值则是一个当前值的指针的复制品。依据这个指针对当前值修改,就等于直接对该值进行了改变。不过有个例外,当接收者的类型如果是引用类型的别名类型,那么在该类型值的值方法中对该值的改变也是对外有效的。

结语

本篇就聊到这里,下篇继续未完的Go语言数据类型…

最后附上知名的Go语言开源框架(每篇更新一个):

Docker: 一个软件部署解决方案,也是一个轻量级的应用容器框架。使用 Docker,我们可以轻松地打包、发布和运行任何应用。现在,Docker 已经成为了名副其实的 Go 语言杀手级应用框架。其官网:http://www.docker.com。非官方的中文网站 : http://www.docker.org.cn

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。