函数和方法是我们迈向代码复用,多人协作开发的第一步。通过函数,可以把开发任务分解成一个个小的单元,这些小单元可以被其他单元复用,进而提升开发效率、降低代码重复度。再加上现成的函数已经被充分测试和使用过,所以其他函数在使用这个函数时也更安全,相较自己重新写一个相似功能的函数,Bug率也更低。
本章将会详细讲解 Go 语言的函数和方法,了解它们的声明、使用和区别。虽然在 Go 语言中函数和方法是两种概念,但他木讷的相似度非常高,只是所属的对象不同。我们先从函数开始了解。
函数
函数初探
在前面的内容中,我们已经见到了 Go 语言中一个非常重要的函数:main 函数,它是一个 Go 语言程序的入口函数。
我们来看一个 main 函数的例子:
func main() {
}
它由几部分组成:
- 任何一个函数的定义都有一个
func
关键字,用于声明一个函数,就像使用var
关键字声明一个变量一样。 - 然后紧跟的
main
是函数的名字,命名符合 Go 语言的规范即可。 main
函数名字后面的一对括号是不能省略的,括号中可以定义函数使用的参数,这里的main
函数没有参数,所以是空括号。- 括号后面还可以有函数的返回值,因为
main
函数没有返回值,所以这里没有定义。 - 最后就是大括号({})函数体了,你可以在函数体内书写代码,写该函数自己的业务逻辑。
函数的声明
经过上一小节的介绍,相信你已经对Go语言函数的构成有了一个比较清晰的了解,现在让我们一起总结函数的声明格式,如下面的代码所示:
func funcName(params) result {
body
}
这就是一个函数的定义,它包含以下几个部分:
- 关键字
func
。 - 函数名字
funcName
。 - 函数的参数
params
,用于定义形参的变量名和类型,可以有一个参数,也可以有多个,也可以没有。 result
用于定义函数的返回值,如果没有返回值,省略即可。也可以有多个返回值,需要放到哦同一个括号内。可以给返回值指定名字。body
是函数体,可以在这里写函数的代码逻辑。
现在,我们根据上面的函数声明格式,自定义一个函数:
func sum(a int, b int) int {
return a+b
}
这是一个计算两数之和的函数,函数的名字是 sum,它有两个参数 a、b,参数的类型都是 int。sum 函数的返回值也是 int 类型,函数体部分就是把 a 和 b 相加,然后通过 return
关键字返回,如果函数没有返回值,就可以不使用 return
关键字。
函数中形参的定义和我们定义变量是一样的,都是变量名称在前,变量类型在后,只不过在函数中,变量名称叫做参数名称,也就是函数的形参,形参只能在该函数体内使用。函数形参的值由点用着提供,这个值也称为函数实参。
在声明函数参数是,相同类型的形参可以省去多余的类型,只留一个即可。
func sum(a, b int) int {
return a+b
}
像这样使用逗号分隔变量,后面统一使用 int 类型,这与变量的声明是一样的,多个相同类型的变量都可以这样声明。
多值返回
与有的编程语言不一样的是,Go语言的函数可以返回多个值,也就是多值返回。在 Go语言的标准库中,你可以看到很多这样的函数:第一个值返回函数的结果,第二个值返回函数出错的信息,这就是多值返回的经典应用。
对于 sum 函数,假设我们不允许提供的是负数,可以这样写:
func sum(a, b int) (int, error) {
if a < 0 || b < 0 {
return 0, errors.New("a 或者 b 不能是负数")
}
return a+b, nil
}
这里需要注意的是,如果函数有多个返回值,返回值部分的类型定义需要使用小括号括起来,也就是 (int,error)
。这代表函数 sum 有两个返回值,第一个是 int 类型,第二个是 error 类型,我们在函数体中使用 return
返回结果的时候,也要符合这个类型顺序。
在函数体中,可以使用 return
返回多个值,返回的多个值通过逗号分隔即可,返回多个值的类型顺序要与函数声明的返回类型顺序一致。
函数有多值返回的时候,需要有多个变量接收它的值。
如果有的函数返回值不需要,可以使用**下划线(_)**丢弃它。
返回值命名
在Go语言中,不止函数的参数可以有变量的名称,函数的返回值也可以,也就是说你可以为每个返回值起一个名字,这个名字就可以像参数一样在函数体内使用了。
func sum(a, b int) (sum int, err error) {
if a < 0 || b < 0 {
return 0, errors.New("a 或者 b 不能是负数")
}
sum = a + b
err = nil
return
}
返回值的命名与参数、变量命名一样,名称在前,类型在后。在以上示例中,对两个返回值进行了命名,一个是 sum,一个是 err,这样就可以在函数体中使用它们了。
你可能注意到了,上面的实例中的 return
后面什么也没有。直接为命名的返回值赋值,也就等于函数有了返回值,所以可以忽略 return
的返回值。
虽然 Go语言支持函数返回值命名,但这并不是太常用,可以根据自己的需求情况,酌情选择是否对函数返回值命名。
可变参数
可变参数就是函数的参数数量是可变的,比如常见的 fmt.Println
函数。
同样一个函数,可以不传参数,可以传一个参数,可以传两个参数,还可以传多个参数,等等,这种函数就是具有可变参数的函数。
fmt.Println()
fmt.Println("hello")
fmt.Println("hello", "world")
如何定义?下面以 Println
函数为例子。
func Println(a ...interface{}) (n int, err error)
现在我们也可以定义自己的带可变参数的函数了。还是以sum函数为例,在下面的代码中,我们通过可变参数的方式,计算调用者传递的所有实参的和.
func (params ...int) int {
sum := 0
for _, i := range params {
sum += i
}
return sum
}
该函数的参数是一个可变参数,然后通过 for range
循环来计算这些参数之和。
讲到这里,相信你也看明白了,可变参数的类型其实就是切片,比如示例中 params
参数的类型是 []int
,所以可以使用 for range
进行循环。函数有了可变参数,就可以灵活使用它了。
这里需要注意的是,如果你定义的函数中既有普通参数,又有可变参数,那么可变参数一定要放在参数列表的最末尾。
包级函数
不管是自定义的函数sum,还是我们前面多次使用过的函数 Println
,都会从属于一个包(package)。sum函数属于 main
包,Println
函数属于 fmt
包。
同一个包中的函数哪怕是私有的(函数名称首字母小写)也可以被调用。如果不同包的函数要被调用,那么函数的作用域必须是公有的,也就是函数名称的首字母要大写,比如Println
。
在后面的一些章节中,我会对包、作用域和模块化做详细讲解,这里可以先记住:
- 函数名称首字母小写代表私有函数,只有在同一个包中才可以被调用。
- 函数名称首字母大写代表公有函数,在不同的包中也可以被调用。
- 任何一个函数都会从属于一个包。
Go语言没有用public、private这样的修饰符来修饰函数是公有还是私有,而是通过函数名称的首字母大小写来代表,这样省略了烦琐的修饰符,使之更简洁。
匿名函数和闭包
顾名思义,匿名函数就是没有名字的函数,这是它与正常函数的主要区别。
func main() {
sum := func(a, b int) int {
return a+b
}
fmt.Println(sum(1,2))
}
在上面的实例中,变量 sum 所对应的值就是一个匿名函数。需要注意的是,这里的 sum 只是一个函数类型的变量,并不是函数的名字。
有了匿名函数,就可以在函数中再定义函数(函数嵌套),定义的这个匿名函数也可以被称为内部函数。更重要的是,在函数内定义的内部函数,可以使用外部函数的变量等,这种方式也成为闭包。
func main() {
cl:=colsure()
fmt.Println(cl())
fmt.Println(cl())
fmt.Println(cl())
}
func colsure() func() int {
i:=0
return func() int {
i++
return i
}
}
运行这个代码,你会看到输出打印的结果是:
1
2
3
这都得益于匿名函数闭包的能力,让我们自定义的colsure
函数可以返回一个匿名函数,并且该匿名函数持有外部函数colsure
的变量 i。因而在 main函数中,每调用一次cl()
,i 的值都会加1。
在Go语言中,函数也是一种类型,它也可以被用来声明函数类型的变量、参数或者作为另一个函数的返回值类型。
方法
不同于函数的方法
在 Go语言中,方法和函数是两个概念,但又非常相似,不同点在于方法必须要有一个接收者,这个接受这是一个类型,这样方法就与这个类型绑定在一起了,成为这个类型的方法。
// 定义一个新的类型 Age,等价于 uint
type Age uint
// 定义 Age 类型的方法,接收者为 Age
func (age Age) String() {
fmt.Println("the age is ", age)
}
与函数不同,定义方法时会在关键字func和方法名String之间加一个接收者(age Age),接收者使用小括号包围。
接收者的定义和普通变量、函数参数等一样,前面是变量名,后面是接收者类型。
现在方法String()就和类型Age绑定在一起了,String()是类型Age的方法。定义了接收者的方法后,就可以通过点操作符(.)来调用方法。
age := Age(25)
age.String()
运行这段代码,可以看到如下输出:
the age is 25
接收者就是函数和方法的最大不同,此外,上面所讲到的函数具备的能力,方法也都具备。
值类型接收者和指针类型接收者
方法的接收者除了可以是值类型(比如上一小节的示例),也可以是指针类型。
如果定义的方法的接收者类型是指针,我们对指针的修改就是有效的,如果不是指针,修改就没有效果。
func (age *Age) Modify() {
*age = Age(30)
}
调用一次Modify方法后,再调用String方法查看结果,会发现已经变成30了,说明基于指针的修改有效。
在调用方法的时候,传递的接收者本质上都是副本,只不过一个是这个值的副本,一个是指向这个值的指针的副本。指针具有指向原有值的特性,所以修改了指针指向的值,也就修改了原有的值。我们可以简单地理解为值接收者使用的是值的副本来调用方法,而指针接收者使用实际的值来调用方法。
示例中调用指针接收者方法的时候,使用的是一个值类型的变量,并不是一个指针类型,其实这里使用指针变量调用也是可以的.
这是因为Go语言编译器帮我们自动做了如下事情:如果使用一个值类型变量调用指针类型接收者的方法,Go语言编译器会自动帮我们取指针调用,以满足指针接收者的要求。
同样的原理,如果使用一个指针类型变量调用值类型接收者的方法,Go语言编译器会自动帮我们解引用调用,以满足值类型接收者的要求。
总之,方法的调用者既可以是值也可以是指针,不用太关注这些,Go语言会帮我们自动转义,这大大提高了开发效率,同时避免因不小心造成的Bug。
不管是使用值类型接收者,还是指针类型接收者,应先确定你的需求:在对类型进行操作的时候是要改变当前接收者的值,还是要创建一个新值进行返回?在明确了需求之后,就可以决定使用哪种接收者了。
总结
在Go语言中,虽然存在函数和方法两个概念,但是它们基本相同,不同的是所属的对象。函数属于一个包,方法属于一个类型,所以方法也可以简单地理解为与一个类型关联的函数。
不管是函数还是方法,它们都是代码复用的第一步,也是代码职责分离的基础。掌握好函数和方法,可以让你写出职责清晰、任务明确、可复用的代码,提高开发效率、降低Bug率。
有关于匿名函数、闭包以及方法接受者等特性的详细分析,后面会有单独的文章进行分析讲解,这里只需要先记住它的特点。
这部分的内容虽然不是面试的常问考点,但是一旦面试官问了这方面的问题,你就必须要给出正确的答案,有错误的话基本上这场面试也就拜拜了,所以还是非常重要的。
标签:函数,int,sum,入门,接收者,类型,Go,返回值,语言 From: https://blog.csdn.net/2301_76655656/article/details/145041076