Go 的函数

152 阅读7分钟

「这是我参与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:因为没有声明函数,随机取的函数名