Go语言中的函数和方法!
Go语言中的函数和方法!
月伴飞鱼函数声明
1 | func funcName(params) result { |
这就是一个函数的签名定义,它包含以下几个部分:
关键字 func
函数名字 funcName
函数的参数 params,用来定义形参的变量名和类型
- 可以有一个参数,也可以有多个,也可以没有
result 是返回的函数值,用于定义返回值的类型
- 如果没有返回值,省略即可,也可以有多个返回值
body 就是函数体,可以在这里写函数的代码逻辑
1 | func sum(a int,b int) int{ |
函数中形参的定义和定义变量是一样的,都是变量名称在前,变量类型在后
- 只不过在函数里,变量名称叫作参数名称
- 也就是函数的形参,形参只能在该函数体内使用。
函数形参的值由调用者提供,这个值也称为函数的实参
- 现在传递实参给 sum 函数
演示函数的调用,如下面的代码所示:
1 | func main() { |
自定义的 sum 函数,在 main 函数中直接调用
- 调用的时候需要提供真实的参数,也就是实参 1 和 2。
函数的返回值被赋值给变量 result,然后把这个结果打印出来。
- 在以上函数定义中,a 和 b 形参的类型是一样的
这个时候可以省略其中一个类型的声明,如下所示:
1 | func sum(a, b int) int { |
像这样使用逗号分隔变量,后面统一使用 int 类型
- 这和变量的声明是一样的,多个相同类型的变量都可以这么声明。
多值返回
在 Go 语言的标准库中,可以看到很多这样的函数:
- 第一个值返回函数的结果,第二个值返回函数出错的信息,这种就是多值返回的经典应用。
对于
sum
函数,假设不允许提供的实参是负数,可以这样改造:
- 在实参是负数的时候,通过多值返回,返回函数的错误信息
如下面的代码所示:
1 | func sum(a, b int) (int,error){ |
这里需要注意的是,如果函数有多个返回值
- 返回值部分的类型定义需要使用小括号括起来,也就是 (
int,error
)。这代表函数 sum 有两个返回值,第一个是 int 类型,第二个是 error 类型
- 在函数体中使用 return 返回结果的时候,也要符合这个类型顺序。
在函数体中,可以使用 return 返回多个值,返回的多个值通过逗号分隔即可
- 返回多个值的类型顺序要和函数声明的返回类型顺序一致
比如下面的例子:
1 | return 0,errors.New("a或者b不能是负数") |
返回的第一个值 0 是 int 类型,第二个值是 error 类型
和函数定义的返回类型完全一致。
定义好了多值返回的函数
现在我们用如下代码尝试调用
1 | func main() { |
函数有多值返回的时候,需要有多个变量接收它的值
- 示例中使用 result 和 err 变量,使用逗号分开。
如果有的函数的返回值不需要,可以使用下划线
_
丢弃它
- 这种方式我在
for range
循环那节课里也使用过如下所示:
1 | result,_ := sum(1, 2) |
这样即可忽略函数 sum 返回的错误信息,也不用再做判断。
提示:这里使用的 error 是 Go 语言内置的一个接口
- 用于表示程序的错误信息。
命名返回参数
不止函数的参数可以有变量名称,函数的返回值也可以
- 也就是说你可以为每个返回值都起一个名字
- 这个名字可以像参数一样在函数体内使用。
1 | func sum(a, b int) (sum int,err error){ |
返回值的命名和参数、变量都是一样的,名称在前,类型在后。
以上示例中,命名的两个返回值名称,一个是 sum,一个是 err
- 这样就可以在函数体中使用它们了。
通过下面示例中的这种方式直接为命名返回参数赋值,也就等于函数有了返回值
- 所以就可以忽略 return 的返回值了,也就是说
示例中只有一个 return,return 后没有要返回的值。
1 | sum=a+b |
通过命名返回参数的赋值方式,和直接使用 return 返回值的方式结果是一样的
- 所以调用以上 sum 函数,返回的结果也一样。
虽然 Go 语言支持函数返回值命名,但是并不是太常用
- 根据自己的需求情况,酌情选择是否对函数返回值命名。
可变参数
可变参数,就是函数的参数数量是可变的
- 比如最常见的
fmt.Println
函数。同样一个函数,可以不传参数,也可以传递一个参数,也可以两个参数,也可以是多个等等
- 这种函数就是具有可变参数的函数
如下所示:
1 | fmt.Println() |
下面所演示的是 Println 函数的声明
从中可以看到,定义可变参数,只要在参数类型前加三个点
…
即可
1 | func Println(a ...interface{}) (n int, err error) |
现在也可以定义自己的可变参数的函数了。
还是以 sum 函数为例,在下面的代码中,我通过可变参数的方式
- 计算调用者传递的所有实参的和
1 | func sum1(params ...int) int { |
为了便于和 sum 函数区分,我定义了函数 sum1,该函数的参数是一个可变参数
- 然后通过
for range
循环来计算这些参数之和。可变参数的类型其实就是切片,比如示例中 params 参数的类型是
[]int
- 所以可以使用
for range
进行循环。
1 | fmt.Println(sum1(1,2)) |
这里需要注意,如果你定义的函数中既有普通参数,又有可变参数
那么可变参数一定要放在参数列表的最后一个
比如
sum1(tip string,params …int)
- params 可变参数一定要放在最末尾。
包级函数
不管是自定义的函数 sum、sum1,还是我们使用到的函数 Println
- 都会从属于一个包,也就是 package。
sum 函数属于 main 包,Println 函数属于 fmt 包。
同一个包中的函数哪怕是私有的(函数名称首字母小写)也可以被调用。
如果不同包的函数要被调用,那么函数的作用域必须是公有的
也就是函数名称的首字母要大写,比如 Println。
函数名称首字母小写代表私有函数,只有在同一个包中才可以被调用
函数名称首字母大写代表公有函数,不同的包也可以调用
任何一个函数都会从属于一个包。
Go 语言没有用
public、private
这样的修饰符来修饰函数是公有还是私有
- 而是通过函数名称的大小写来代表,这样省略了烦琐的修饰符,更简洁。
匿名函数和闭包
匿名函数就是没有名字的函数,这是它和正常函数的主要区别。
在下面的示例中,变量 sum2 所对应的值就是一个匿名函数。
- 需要注意的是,这里的 sum2 只是一个函数类型的变量,并不是函数的名字。
1 | func main() { |
通过 sum2,我们可以对匿名函数进行调用
- 以上示例算出的结果是 3,和使用正常的函数一样。
有了匿名函数,就可以在函数中再定义函数(函数嵌套)
- 定义的这个匿名函数,也可以称为内部函数。
更重要的是,在函数内定义的内部函数
- 可以使用外部函数的变量等,这种方式也称为闭包。
1 | func main() { |
运行这个代码,你会看到输出打印的结果是:
1 | 1 |
这都得益于匿名函数闭包的能力,让我们自定义的 colsure 函数
- 可以返回一个匿名函数,并且持有外部函数 colsure 的变量 i。
因而在 main 函数中,每调用一次
cl()
,i 的值就会加 1。
在 Go 语言中,函数也是一种类型:
它也可以被用来声明函数类型的变量、参数或者作为另一个函数的返回值类型。
不同于函数的方法
在 Go 语言中,方法和函数是两个概念,但又非常相似
不同点在于方法必须要有一个接收者,这个接收者是一个类型
这样方法就和这个类型绑定在一起,称为这个类型的方法。
在下面的示例中,
type Age uint
表示定义一个新类型 Age
- 该类型等价于 uint,可以理解为类型 uint 的重命名。
其中 type 是 Go 语言关键字
- 表示定义一个类型。
1 | type Age uint |
示例中方法 String() 就是类型 Age 的方法
- 类型 Age 是方法 String() 的接收者。
和函数不同,定义方法时会在关键字 func 和方法名 String 之间加一个接收者 (age Age)
- 接收者使用小括号包围。
接收者的定义和普通变量、函数参数等一样
- 前面是变量名,后面是接收者类型。
现在方法 String() 就和类型 Age 绑定在一起了,String() 是类型 Age 的方法。
- 定义了接收者的方法后,就可以通过点操作符调用方法
如下面的代码所示:
1 | func main() { |
运行这段代码,可以看到如下输出:
1 | the age is 25 |
接收者就是函数和方法的最大不同
- 此外,上面所讲到的函数具备的能力,方法也都具备。
提示:因为 25 也是 unit 类型,unit 类型等价于我定义的 Age 类型
- 所以 25 可以强制转换为 Age 类型。
值类型接收者和指针类型接收者
方法的接收者除了可以是值类型,也可以是指针类型。
定义的方法的接收者类型是指针
- 所以对指针的修改是有效的,如果不是指针,修改就没有效果
如下所示:
1 | func (age *Age) Modify(){ |
调用一次 Modify 方法后,再调用 String 方法查看结果,会发现已经变成了 30
- 说明基于指针的修改有效
如下所示:
1 | age:=Age(25) |
提示:
在调用方法的时候,传递的接收者本质上都是副本
- 只不过一个是这个值副本,一是指向这个值指针的副本。
指针具有指向原有值的特性,所以修改了指针指向的值,也就修改了原有的值。
我们可以简单地理解为值接收者使用的是值的副本来调用方法
- 而指针接收者使用实际的值来调用方法。
示例中调用指针接收者方法的时候,使用的是一个值类型的变量,并不是一个指针类型
- 其实这里使用指针变量调用也是可以的
如下面的代码所示:
1 | (&age).Modify() |
这就是 Go 语言编译器帮我们自动做的事情:
如果使用一个值类型变量调用指针类型接收者的方法
- Go 语言编译器会自动帮我们取指针调用,以满足指针接收者的要求。
同样的原理,如果使用一个指针类型变量调用值类型接收者的方法
- Go 语言编译器会自动帮我们解引用调用,以满足值类型接收者的要求。
总之,方法的调用者,既可以是值也可以是指针,不用太关注这些
- Go 语言会帮我们自动转义,大大提高开发效率,同时避免因不小心造成的 Bug。
不管是使用值类型接收者,还是指针类型接收者,要先确定你的需求:
在对类型进行操作的时候是要改变当前接收者的值,还是要创建一个新值进行返回?
这些就可以决定使用哪种接收者。