「这是我参与2022首次更文挑战的第13天,活动详情查看:2022首次更文挑战」。
什么是函数
函数是执行特定任务的代码块
语法格式
func funcName(parametername type1, parametername type2, ...) (output1 type1, output2 type2, ...) {
//这里是处理逻辑代码
//返回多个值
return value1, value2, ...
}
func
函数由 func 开始声明
funcName
函数名称,函数名和参数列表一起构成了函数签名
parametername type
- parametername:参数名
- type:参数类型
- 参数列表,参数就像一个占位符,当函数被调用时,你可以将值传递给参数,这个值被称为实际参数
- 参数列表指定的是参数类型、顺序、及参数个数
- 参数是可选的,也就是说函数也可以不包含参数
output1 type1
- output1:返回值变量
- type1:返回值的类型
- 返回值是可选的,函数可以不需要返回值,也可以仅指定返回值类型
- 上面返回值声明了两个变量 output1 和 output2,如果不想声明也可以,直接就写两个类型
- 如果只有一个返回值且不声明返回值变量,那么可以省略包括返回值的括号
函数体
函数定义的代码集合,写业务逻辑的地方
最简单的🌰
add 函数接受两个 int 类型的参数,并声明了返回值类型是 int
package main
import "fmt"
func add(x int, y int) int {
return x + y
}
func main() {
fmt.Println(add(42, 13))
}
运行结果
55
改进下参数类型的声明方式
当连续两个或多个函数形参的类型相同时,除最后一个参数需要声明类型以外,其它都可以省略
package main
import "fmt"
func add(x, y int) int {
return x + y
}
func main() {
fmt.Println(add(42, 13))
}
运行结果
55
函数的参数
参数的使用
形式参数: 定义函数时,用于接收外部传入的数据,叫做形式参数,简称形参。
实际参数: 调用函数时,传给形参的实际的数据,叫做实际参数,简称实参。
函数调用:
- 函数名称必须匹配
- 实参与形参必须一一对应:顺序,个数,类型
多个类型的形参的🌰
package main
import (
"fmt"
)
func hello(name string, age int, man bool) {
fmt.Println(name, age, man)
}
func main() {
hello("小菠萝测试", 24, false)
}
运行结果
小菠萝测试 24 false
可变参数
- 和 Python 一样,Go 也有可变参数
- 函数接受的形参数量是不确定的
- 但是 Go 只有可变参数,没有像 Python 那样的默认参数、可选参数、关键字参数....
语法格式
func myfunc(arg ...int) {}
arg ...int告诉Go这个函数接受不定数量的参数- 注意:这些参数的类型全部是int
- 在函数体中,变量 arg 是一个 []int (int 类型的切片,后面会讲切片)
🌰
package main
import (
"fmt"
)
func moreArgs(args ...int) {
fmt.Printf("%T,%v \n", args, args)
for _, arg := range args {
fmt.Println(arg)
}
}
func main() {
moreArgs(1, 2, 3, 4)
hello("小菠萝测试", 24, false)
}
运行结果
[]int,[1 2 3 4]
1
2
3
4
参数传递
和大部分编程语言不一样,Go 只有值传递,但是指针传递能完成引用传递
- 值传递:函数调用时会对参数进行拷贝,被调用方和调用方两者持有不相关的两份数据;
- 指针传递:函数调用时会传递参数的指针,被调用方和调用方两者持有相同的数据,任意一方做出的修改都会影响另一方
值传递
package main
import (
"fmt"
)
func add1(a int) int {
a = a + 1
fmt.Printf("【函数体内】形参 a 的内存地址:%v, 值为:%v \n", &a, a)
return a
}
func main() {
x := 3
fmt.Printf("调用函数【前】,变量 x 的内存地址:%v, 值为:%v \n", &x, x)
x1 := add1(x) // 拷贝的是 x
fmt.Printf("调用函数【后】,变量 x 的内存地址:%v, 值为:%v,x1 的值为:%v\n", &x, x, x1)
}
运行结果
调用函数【前】,变量 x 的内存地址:0xc00001c0b8, 值为:3
【函数体内】形参 a 的内存地址:0xc00001c0c0, 值为:4
调用函数【后】,变量 x 的内存地址:0xc00001c0b8, 值为:3,x1 的值为:4
可以看到形参 a 和变量 x 的内存地址并不不一样,因为拷贝的是 a 变量,所以修改形参 a 的值,并不会影响变量 x
Go 语言的整型和数组类型都是值传递的
需要注意的是如果当前数组的大小非常的大,这种传值的方式会对性能造成比较大的影响。
指针传递
- 引用就涉及到了 Go 的指针,指针具体后面会展开讲
- 变量是存放在内存中,有一个内存地址会指向变量,修改变量实际是修改变量所在的内存地址
- 指针传递时,仍然会拷贝,只不过是拷贝指针,就是变量的内存地址
package main
import (
"fmt"
)
//简单的一个函数,实现了参数+1的操作
// 参数 a 的类型是 int 指针
func add2(a *int) int { // 请注意,
*a = *a + 1 // 修改了 a 的值
fmt.Printf("【函数体内】参数 a 的类型是:%T, %T \n", a, *a)
fmt.Printf("【函数体内】参数 a 的内存地址:%v, 值为:%v \n", a, *a)
return *a // 返回新值
}
func main() {
x := 3
fmt.Printf("调用函数【前】,变量 x 的内存地址:%v, 值为:%v \n", &x, x)
x1 := add2(&x) // 调用 add1(&x),拷贝的是 x 的内存地址
fmt.Printf("调用函数【后】,&x 的类型是:%T, 变量 x 的内存地址:%v, 值为:%v,x1 的值为:%v\n",&x, &x, x, x1)
}
运行结果
调用函数【前】,变量 x 的内存地址:0xc000018030, 值为:3
【函数体内】参数 a 的类型是:*int, int
【函数体内】参数 a 的内存地址:0xc000018030, 值为:4
调用函数【后】,&x 的类型是:*int, 变量 x 的内存地址:0xc000018030, 值为:4,x1 的值为:4
- 参数 a 和变量 x 的内存地址是同一个,所以修改参数 a 的时候会同步修改变量 x 的值
- 注意: 参数 a 的类型是 int 指针,a 的值是内存地址,如果要拿到内存地址存放的值,需要
*a - &x 类型也是 int 指针,值也是内存地址
重点
- 指针传递使得多个函数能操作同一个对象
- 指针传递比较轻量级 (8 bytes),实际是传递(拷贝)内存地址
- 可以用指针传递体积大的结构体
- 如果用参数值传递的话, 在每次拷贝上面就会花费相对较多的系统开销(内存和时间),所以当要传递大的结构体时,用指针是一个明智的选择
- Go 语言中 slice,map 这几种类型的实现机制类似指针,所以可以直接传递,而不用取地址后传递指针,这个后面再详解(注:若函数需改变 slice 的长度,则仍需要取地址传递指针)
函数作为参数
package main
import (
"fmt"
"math"
"reflect"
"runtime"
)
// 函数作为参数
func apply(op func(int, int) int, a, b int) int {
// 获取存放指针的 int
p := reflect.ValueOf(op).Pointer()
fmt.Printf("type is %T, value is %v \n", p, p)
// 获取函数名
funcName := runtime.FuncForPC(p).Name()
fmt.Printf("Calling function %s with args (%d, %d)", funcName, a, b)
return op(a, b)
}
func pow(a, b int) int {
return int(math.Pow(float64(a), float64(b)))
}
func main() {
fmt.Println(apply(pow, 2, 2))
}
运行结果
type is uintptr, value is 4720000
Calling function main.pow with args (2, 2)4
优化代码
因为 Go 是函数式编程,所以可以进一步压缩代码量;直接将函数当参数传递
package main
import (
"fmt"
"math"
"reflect"
"runtime"
)
// 函数作为参数
func apply(op func(int, int) int, a, b int) int {
// 去掉声明变量的步骤,直接函数传参
fmt.Printf("Calling function %s with args (%d, %d)",
runtime.FuncForPC(
reflect.ValueOf(op).Pointer()).Name(),
a, b)
return op(a, b)
func main() {
// 直接函数传参
fmt.Println(apply(func(i int, i2 int) int {
return int(math.Pow(float64(i), float64(i2)))
}, 2, 2))
}
运行结果
Calling function main.main.func1 with args (2, 2)4
main.main.func1
- main:package
- main:外层函数名
- func1:因为没有声明函数,随机取的函数名