是时候开始研究函数和方法了。。。
函数
通过函数,可以把开发任务分解成一个个小的单元,这些小单元可以被其他单元复用,进而提高开发效率、降低代码重合度。
1. 函数声明
func funcName(params) result {
body
}
- 关键字 func
- 函数名字 funcName
- 函数的参数 params,用来定义形参的变量名和类型
- result 是返回的函数值,用于定义返回值的类型,如果没有可以省略
- body 就是函数体,可以在这里写函数的代码逻辑
写一个计算两数相加的函数:
package main
import (
"fmt"
)
// 计算两值之和
// 变量名称在前,变量类型在后
// 变量名称叫做参数名称,也就是函数的形参
func addTwoNum(a int, b int) int{
return a + b
}
func main(){
fmt.Println(addTwoNum(12, 21))
}
2. 多值返回
Go 语言的函数可以返回多个值,也就是多值返回
第一个值返回函数的结果,第二个值返回函数出错的信息
package main
import (
"errors"
"fmt"
)
// 计算两值之和,如果为负数就返回错误
func addTwoNum(a int, b int) (int, error) {
if a < 0 || b < 0 {
return 0, errors.New("a或者b不能为负数")
}
return a + b, nil
}
func main() {
// 获取结果和错误信息
a, err := addTwoNum(-12, 21)
if err != nil {
fmt.Println(err)
} else {
fmt.Println("计算结果为:", a)
}
}
3.命名返回参数
函数的返回值也可以有变量名称,这个并不常用,了解一下
改造函数为:
package main
import (
"errors"
"fmt"
)
// 计算两值之和,如果为负数就返回错误
func addTwoNum(a int, b int) (sum int, err error) {
if a < 0 || b < 0 {
// 这里按照正常进行返回
return 0, errors.New("a或者b不能为负数")
}
// 这里按照返回值给相关参数赋值,return后面不需要任何参数
sum = a + b
err = nil
return
}
func main() {
// 获取结果和错误信息
a, err := addTwoNum(-12, 21)
if err != nil {
fmt.Println(err)
} else {
fmt.Println("计算结果为:", a)
}
}
4. 可变参数
可变参数,就是函数的参数数量是可变的
如果函数中既有普通参数又有可变参数,那么可变参数一定要放到参数列表的末尾
主要就是在参数类型前面添加三个点
例如:
// 没有参数
fmt.Println()
// 一个参数
fmt.Println("zhouzhaodong")
// 两个参数
fmt.Println("zhouzhaodong","xiaoqiang")
我们写一个计算所有数字之和的函数:
package main
import (
"fmt"
)
// 计算所有值之和
func addAllNum(params ...int) int {
sum := 0
for _, i := range params {
sum += i
}
return sum
}
func main() {
fmt.Println("计算结果为:", addAllNum(1, 2, 3, 3, 3, 4, 5, 9))
}
5. 包级函数
不管是自定义的函数
还是我们使用到的函数,都会从属一个包
也就是 package
不同包的函数要被调用,那么函数的作用域必须是公有的,也就是函数名称的首字母要大写
- 函数名称首字母小写代表私有函数,只有在同一个包中才可以被调用
- 函数名称首字母大写代表公有函数,不同的包也可以调用
- 任何一个函数都会从属于一个包
6. 匿名函数和闭包
匿名函数就是没有名字的函数
package main
import (
"fmt"
)
func main() {
sum := func(a, b int) int {
return a + b
}
fmt.Println("计算结果为:", sum(1, 2))
}
在函数中再定义函数(函数嵌套),定义的这个匿名函数,也可以称为内部函数
更重要的是,在函数内定义的内部函数,可以使用外部函数的变量等,这种方式也称为闭包
方法
方法必须要有一个接收者,这个接收者是一个类型
这样方法就和这个类型绑定在一起,成为这个类型的方法
接收者的定义和普通变量、函数参数等一样
前面是变量名,后面是接收者类型
package main
import (
"fmt"
)
// 定义一个新的类型,该类型等价于 uint
// 可以理解为 uint 的重命名
type Age uint
// 定义一个方法,参数就是Age
func (age Age) String() {
fmt.Println("the age is", age)
}
func main() {
age := Age(25)
age.String()
}
接收者就是函数和方法最大的不同
1. 值类型接收者和指针类型接收者
定义的方法的接收者类型是指针,所以我们对指针的修改是有效的,如果不是指针,修改就没有效果,如下所示:
package main
import (
"fmt"
)
// 定义一个新的类型,该类型等价于 uint
// 可以理解为 uint 的重命名
type Age uint
// 定义一个方法,参数就是Age
func (age Age) String() {
fmt.Println("the age is", age)
}
// 定义一个方法,参数就是Age指针
func (age *Age) Modify() {
*age = Age(30)
}
func main() {
age := Age(25)
age.String()
// 修改age的值
age.Modify()
age.String()
}
提示:在调用方法的时候,传递的接收者本质上都是副本,只不过一个是这个值副本,一是指向这个值指针的副本。指针具有指向原有值的特性,所以修改了指针指向的值,也就修改了原有的值。我们可以简单地理解为值接收者使用的是值的副本来调用方法,而指针接收者使用实际的值来调用方法。
这就是 Go 语言编译器帮我们自动做的事情:
- 如果使用一个值类型变量调用指针类型接收者的方法,Go 语言编译器会自动帮我们取指针调用,以满足指针接收者的要求。
- 如果使用一个指针类型变量调用值类型接收者的方法,Go 语言编译器会自动帮我们解引用调用,以满足值类型接收者的要求。
总之,方法的调用者,既可以是值也可以是指针,不用太关注这些,Go 语言会帮我们自动转义,大大提高开发效率,同时避免因不小心造成的 Bug。
不管是使用值类型接收者,还是指针类型接收者,要先确定你的需求:在对类型进行操作的时候是要改变当前接收者的值,还是要创建一个新值进行返回?这些就可以决定使用哪种接收者。