函数是组织好的、可重复使用的、用于执行指定任务的代码块。Go语言中支持函数、匿名函数和闭包,并且函数在Go语言中属于“一等公民”。
函数定义:
func 函数名 (参数1 数据类型, 参数2 ...数据类型) (返回值1 数据类型, 返回值2 数据类型) {
函数内部代码
...
deder 延迟执行语句
return 变量名或者数值
}
其中:
- 函数名:由字母、数字、下划线组成。但函数名的第一个字母不能是数字。在同一个包内,函数名也称不能重名(包的概念详见后文)。
- 参数:参数由参数变量和参数变量的类型组成,多个参数之间使用
,
分隔。 - 返回值:返回值由返回值变量和其变量类型组成,也可以只写返回值的类型,多个返回值必须用
()
包裹,并用,
分隔。 - 函数体:实现指定功能的代码块。
函数的调用
使用函数名()
调用函数,有返回值的函数可以不接收返回值。
函数参数
参数:参数支持简写func 函数名 (参数1,参数2 数据类型, 参数3 ...数据类型) {}
。参数和数据类型中间隔一个空格。...数据类型
叫可变参数,也有人叫解包
。可变参数一定要放在固定参数的末尾,也就是说如果需要可变参数,那么它一定是在最后一个。一个函数仅支持一个可变参数。可变参数在函数内部是一个切片。看下列:
func add(a ...int) {
fmt.Printf("可变参数类型:[%T] \n", a)
fmt.Printf("%#v", a)
}
func main() {
add(1, 2, 3, 4, 5)
}
调用函数add()
后运行结果如下:
可变参数类型:[[]int]
[]int{1, 2, 3, 4, 5}
函数返回值
返回值:使用return
关键字向外输出返回值,一个函数可以没有返回值。在声明函数时命名过的返回值在函数内部可以直接使用,不需要在函数内部再次声明。当一个返回一个切片时,nil
可以看做是一个有效的切片,没必要返回一个长度为0的切片[]int{}
。
func someFunc(x string) []int {
if x == "" {
return nil // 没必要返回[]int{}
}
...
}
变量左右域
变量作用域–全局变量是定义在函数(func main()
也是一个函数)之外的变量,他在整个运行周期都有效。在函数中可以访问到全局变量。如果全局变量和函数内部的变量重名,将优先使用函数内部的变量。另外函数访问不到其他函数内部的变量。
//全局变量
var a = 8
var c = 99
//函数f2
func f2(f2 int) {
//变量f2在这里
}
//函数f1
func f1(a, b int) {
// fmt.Printf(" f2:%v \n", f2)
fmt.Printf("a:%v b:%v c:%v \n", a, b, c)
}
func main() {
//调用函数f1。其中参数a=1 b=2
f1(1, 2)
}
运行效果如下:
a:1 b:2 c:99
同一级别并列的{}
的变量不能互相识别,但是包含在{}
内部的{}
内的变量从内向外识别,入下例if
,for
,switch
中的变量不能互相识别,但是可以识别到函数变量或者全局变量。
//全局变量
var quanju1 = 8
var quanju2 = 99
//函数f1
func f1(a int) {
//例一:for内部可识别范围
for b := 0; b < 3; b++ {
for_a := 99
fmt.Println(quanju1, a, b, for_a)
}
//if内部不能识别for内部变量'for_a'
if a == 1 {
if_a := 66
fmt.Println(quanju1, a, if_a)
// fmt.Println(for_a)
// fmt.Println(b)
}
//if从内到外识别
for i := 0; i < 3; i++ {
for_b := 998
if a == 1 {
fmt.Println(quanju1, a, for_b)
// fmt.Println(if_a)
// fmt.Println(for_a)
}
}
}
func main() {
//调用函数f1。其中参数a=1
f1(1)
}
defer延迟执行
defer
延迟时机,defer
后面的语句会在函数执行完毕真正返回之前执行。一个函数可以有多个defer
语句,多个defer
语句按照先进后出执行。如下例:
func main() {
fmt.Println("start")
defer fmt.Println(1)
defer fmt.Println(2)
defer fmt.Println(3)
fmt.Println("end")
}
运行结果如下:
start
end
3
2
1
在Go语言的函数中return
语句在底层并不是原子操作,它分为给返回值赋值和RET
指令两步。而defer
语句执行的时机就在返回值赋值操作后,RET
指令执行前。具体看下图:
具体可以看下例:
func f1() int {
x := 5
defer func() {
x++
}()
return x
}
上面代码先看函数的返回值,这是一个未命名返回值的函数,可以把他的返回值想象成z
,然后看函数体x := 5
,接下来就是一个defer
,跳过defer
看返回语句return x
,这句可以理解成z = x; defer; return
,现在返回值z
变成5
了。回过头再来看那一句defer
语句里面有个x++
,返回值z
已经是5
了,那个x++
和返回值z
没关系了。在下一句就是就是return
了,所以这个函数返回的是5。在看下面的例子:
func f2() (x int) {
defer func() {
x++
}()
return 5
}
这个函数是命名返回值的函数,记住他的返回值是x
。然后看函数体没其它语句,上来就是一个defer
,先跳过看最后的return 5
,一样先翻译一下x = 5; defer; return
,这样比较容易理解,现在的返回值是x
,再来看defer
语句,x++
也就是6
了,现在的返回值x
是6昂。在然后就是真的的return
了,所以这个函数返回6。下面是全部代码,懒得打字了:
func f1() int {
x := 5
defer func() {
x++
}()
return x
}
func f2() (x int) {
defer func() {
x++
}()
return 5
}
func f3() (y int) {
x := 5
defer func() {
x++
}()
return x
}
func f4() (x int) {
defer func(x int) {
x++
}(x)
return 5
}
func main() {
fmt.Println(f1())
fmt.Println(f2())
fmt.Println(f3())
fmt.Println(f4())
}
还要记住一点,函数传参数,穿的是副本。修改传到函数内部的参数,并不会影响之前的本体。
函数类型
函数类型:函数也是一种数据类型
func add(a, b int) {
fmt.Println("hello world")
}
func main() {
a := add
fmt.Printf("%T \n", a)
}
运行结果显示,变量a
的数据类型是func(int, int)
。
定义函数类型
定义函数类型:使用关键字type
定义函数类型
type hanshu func(参数类型1,参数类型2) 返回值类型
来声明一个类型hanshu
,他有两个int
参数和一个int
返回值。
type hanshu func(int, int) int
使用函数类型
使用函数类型:要使用hanshu
类型首先要有符和它的函数
func add(x, y int) int {
return x + y
}
然后声明一个hanshu
类型的变量c
var c hanshu
为变量c
赋值一个函数,这个函数要符号hanshu
类型的要求,两个int
参数和一个int
返回值
c = add
接下来就该使用这个hanshu
类型的变量c
了,使用它和使用函数的方法一样,看起来只是把函数的名字换成了变量名。额~~~饶了一大圈,并没什么卵用啊~~~
c(2,3)
这里查看一下变量c
的数据类型显示为
main.hanshu
fmt.Printf("%T \n", c)
下面是全部代码:
//定义一个类型'hanshu'它有两个'int'参数和一个'int'返回值
type hanshu func(int, int) int
//add函数符合hanshu类型
func add(x, y int) int {
return x + y
}
//sub函数符合hanshu类型
func sub(x, y int) int {
return x - y
}
func main() {
//声明一个类型为'hanshu'的变量'c'
var c hanshu
//为变量c赋值一个符合类型'hanshu'的函数add
c = add
//输出并使用变量变量c,c的类型是'hanshu'
fmt.Println(c(2, 3)) //看起来好像只是把'add'函数改了个名字~~~
fmt.Println("--------------------")
//查看变量'c'的数据类型,显示为'main.hanshu'
fmt.Printf("%T \n", c)
//再把变量c赋值一个sub函数
c = sub
fmt.Println(c(2, 3))
}
如果配合switch
倒是能少写点代码~~~~~~
函数类型可以作为函数参数
函数类型可以作为参数使用,在声明函数的时候写明要传进来的函数具体类型,入下例第三个参数运行传入一个函数,要求传入的这个函数有两个int
参数和一个int
返回值。
//第三个参数'z'允许传入一个函数。
func add(x, y int, z func(int, int) int) int {
//使用当做参数传进来的函数。
z(x, y)
return x + y + z(x, y)
}
func sub(x, y int) int {
return x - y
}
func main() {
//直接把函数'sub'函数当做参数
fmt.Println(add(9, 5, sub))
}
函数类型作为函数返回值
也可以把函数当做返回值来使用:
//定义该函数的返回值是函数类型
func add() func(string) {
fmt.Println("我是add")
//返回函数'dayin()'
return dayin
}
func dayin(s string) {
if s == "" {
s = "打印"
} else {
s = "打印" + s
}
fmt.Println(s)
}
func main() {
//调用函数'dayin()'
dayin("")
//调用函数'add()'后在调用它返回的函数
add()("add()()")
}
匿名函数
匿名函数,匿名函数就是一个没有名字的函数,一般用在函数内部。一般来说在函数内部不能声明一个正常的函数,但是可以调用函数,既然不能声明正常函数,那就是说可以声明不正常的函数,也就是匿名函数。
func main() {
//在函数内部声明函数的办法
var ff = func(s string) { fmt.Printf("%s \n", s) }
//调用在函数内部声明的函数
ff("匿名函数")
fmt.Printf("变量ff的类型:%T \n", ff)
}
上面代码就这main()
函数内部声明了一个函数,并成功的调用了这个函数。这个函数可以多次调用,当然也可以定义一个只用一次的函数,用一次就不再使用了,就销毁了。
func main() {
//在函数内部声明函数的办法,这个函数立即执行,只能使用一次
func(s string) { fmt.Printf("%s \n", s) }("立即执行的匿名函数,只能使用一次。")
}
闭包
先看下面这个列子,这个函数返回一个没有参数没有返回值的函数。如果仅调用这个函数add()
只会执行fmt.Println("add")
这一行代码,也就是说只会输出add
。因为他的返回值是一个函数,所以要想执行fmt.Println("aaa")
的话,必须这样调用add()()
。或者先用一个变量接收他的返回值add_fanhui
,然后在用add_fanhui()
的方式输出aaa
func add() func() {
fmt.Println("add")
return func() {
fmt.Println("aaa")
}
}
func main() {
add_fanhui := add()
fmt.Printf("add_fanhui数据类型:%T \n", add_fanhui)
add_fanhui()
}
可能会觉得上面这种写法不好理解,如果把函数add()
返回的那个函数重新写成一个函数,并起一个民那个字,return xxx
会更直观更好理解。其实这样写是迫不得已,看下面的例子:
func add() func() {
s := "add"
fmt.Printf("%s \n", s)
return func() {
b := "add2" + s
fmt.Printf("%s \n", b)
}
}
func main() {
add()()
}
如果分成两个函数写的话,变量b
就拿不到变量s
的数据了。想想函数变量的作用域。两个函数属于并列关系,都拿不到对方的变量数据,而现在这种不好理解的写法,属于包含关系,所以返回的那个函数可以拿到属于函数add()
中变量s
的数据。
这个函数返回一个没有参数没有返回值的函数
如果仅调用这个函数add()
只会执行fmt.Println("add")
这一行代码,也就是说只会输出add
因为他的返回值是一个函数,所以要想执行fmt.Println("aaa")
的话,必须这样调用add()()
。或者先用一个变量接收他的返回值add_fanhui
,然后在用add_fanhui()
的方式输出aaa
func add() func() {
fmt.Println("add")
return func() {
fmt.Println("aaa")
}
}
func main() {
add_fanhui := add()
fmt.Printf("add_fanhui数据类型:%T \n", add_fanhui)
add_fanhui()
}
可能会觉得上面这种写法不好理解,如果把函数add()
返回的那个函数重新写成一个函数,并起一个民那个字,return xxx
会更直观更好理解。其实这样写是迫不得已,看下面的例子:
func add() func() {
s := "add"
fmt.Printf("%s \n", s)
return func() {
b := "add2" + s
fmt.Printf("%s \n", b)
}
}
func main() {
add()()
}
如果分成两个函数写的话,变量b
就拿不到变量s
的数据了。想想函数变量的作用域。两个函数属于并列关系,都拿不到对方的变量数据,而现在这种不好理解的写法,属于包含关系,所以返回的那个函数可以拿到属于函数add()
中变量s
的数据。还是上面的代码稍微改一下,改成下面这种,原理一样只是改成带参数。
func add(a int) func(int) int {
// a = 0
fmt.Printf("add函数内变量a:[%d] a地址:[%v] \n", a, &a)
return func(y int) int {
fmt.Printf("返回的匿名函数内a:[%d] a的地址[%v]--1 \n", a, &a)
a += y
fmt.Printf("返回的匿名函数内a:[%d] a的地址[%v]--2 \n", a, &a)
return a
}
}
func main() {
aa := add(1) //0
// fmt.Printf("%T", aa)
fmt.Println(aa(1)) //0----1-[1]
add(9) //0
fmt.Println(aa(5)) //1----6-[6]
add(1) //0
fmt.Println(aa(0)) //6----6-[6]
fmt.Println(add(0)(9)) //这种方式调用匿名函数会重置变量'a'的数据
fmt.Println(add(1)(3))
}
我们会发现,返回的这个匿名函数永远会包括这个变量a
,仔细观察每次输出的变量a
的内存地址,只有第一次匿名函数拿到的变量a
的内存地址和add()
函数内部变量a
的内存地址一样,后面在调用add()
的话,add内的变量a会换另一个内存地址。我也不知道怎么表达了,总计一下就是,匿名函数包括函数体和他第一次拿到的那个变量a
,但是不包括下一次调用add()
时的变量a
。其主要原因在于aa := add(1)
这一句,aa
拿到的是add()
返回的匿名函数的内存地址,这个返回的匿名函数的内存地址包括了add()
内变量a
,所以只要aa
拿到的这个匿名函数的内存地址不变,那个a
变量的数据也就不会变。或者可以使用add()()
这种调用匿名函数的方式来重置变量a
的数据。
下面在来一个高难度的,下面这个ff函数能输出钢蛋?基佬
,中间的?
号由参数y func()
来控制。并且这个函数不允许修改。
func ff(y func()) {
fmt.Print("钢蛋")
y()
fmt.Print("基佬 \n")
}
下面这个fd函数能输出是
或者不是
,至于具体输出什么由参数s
控制,这个函数也不允许修改。
func fd(s string) {
fmt.Print(s)
}
现在的要求是要用函数ff()
调用函数fd()
来实现输出钢蛋是基佬
或者钢蛋不是基佬
,至于是
或者不是
由函数fd()
计算后得出。这里的两个函数ff()
和fd()
都不允许修改。来看看怎么实现的。看下面这个函数
//自己写的封装函数
func yesORno(s string) func() {
return func() {
fd(s)
}
}
这个封装函数接受一个字符串变量s
,返回一个没有参数没有返回值的的你卖匿名函数。
//这个函数不允许修改
func ff(y func()) {
fmt.Print("钢蛋")
y()
fmt.Print("基佬 \n")
}
//这个函数同样不允许修改
func fd(s string) {
fmt.Print(s)
}
//自己写的封装函数
func yesORno(s string) func() {
return func() {
fd(s)
}
}
func main() {
ff(yesORno("==>"))
}
实例面试题
管于defer的一个面试题:
func calc(index string, a, b int) int {
ret := a + b
fmt.Println(index, a, b, ret)
return ret
}
func main() {
x := 1
y := 2
defer calc("AA", x, calc("A", x, y))
x = 10
defer calc("BB", x, calc("B", x, y))
y = 20
}
defer语句后面的语句,程序会先别确切的参数或者数据算出来(仅包括变量的值,函数的参数等。不包括不包括函数内部代码也就是不包括{}
内的代码。),然后在圧栈,具体流程如下:
- x=1
- y=2
- defer calc(“AA”,1,calc(“A”,1,2))
输出:”A” 1 2 3
defer calc(“AA”,1,3) - x=10
- defer calc(“BB”,10,calc(“B”,10,2))
输出:”B” 10 2 12
defer calc(“BB”,10,12) - y=20
- 输出:”BB” 10 12 22
- 输出:”AA” 1 3 4
分金币练习
你有50枚金币,需要分配给以下几个人:Matthew,Sarah,Augustus,Heidi,Emilie,Peter,Giana,Adriano,Aaron,Elizabeth。
分配规则如下:
- 名字中每包含1个
e
或E
分1枚金币 - 名字中每包含1个
i
或I
分2枚金币 - 名字中每包含1个
o
或O
分3枚金币 - 名字中每包含1个
u
或U
分4枚金币
写一个程序,计算每个用户分到多少金币,以及最后剩余多少金币?
程序结构如下,请实现 dispatchCoin
函数
package main
import "fmt"
var (
coins = 50
users = []string{
"Matthew", "Sarah", "Augustus", "Heidi", "Emilie", "Peter", "Giana", "Adriano", "Aaron", "Elizabeth",
}
distribution = make(map[string]int, len(users))
)
func dispatchCoin() int {
// 字符串切片导入map
for _, v := range users {
//遍历users字符
for _, x := range v {
//具体计算获得金边数量
if x == 'e' || x == 'E' {
distribution[v]++
coins--
} else if x == 'i' || x == 'I' {
distribution[v] += 2
coins -= 2
} else if x == 'o' || x == 'O' {
distribution[v] += 3
coins -= 3
} else if x == 'u' || x == 'U' {
distribution[v] += 4
coins -= 4
}
}
}
//返回金边剩余数量
return coins
}
func main() {
left := dispatchCoin()
fmt.Println("剩下:", left)
for k, v := range distribution {
fmt.Printf("%s分得%d枚金边 \n", k, v)
}
}
递归函数
递归函数:递归函数就是自己调用自己的函数,递归函数一定要有一个明确的退出条件,比如阶乘:
比如5的阶乘是:
3*2*1
4*3*2*1
5*4*3*2*1
6*5*4*3*2*1
6的阶乘是6乘以5的阶乘
5的阶乘是5乘以4的阶乘n的阶乘是n乘以n-1的阶乘
,所以~~~
package main
import "fmt"
var s = "-"
//递归函数练习
func digui(a int) int {
//无实际作用,仅作为辅助理解递归函数运行模式使用
fmt.Printf("%sif之前a[%d] ", s, a)
if a <= 1 {
//无实际作用,仅作为辅助理解递归函数运行模式使用
fmt.Printf("-if内,准备返回a[%d] \n", a)
s += "-"
return a
}
//无实际作用,仅作为辅助理解递归函数运行模式使用
fmt.Printf("-if之后a[%d]这里返回的是%d * digui(%d-1) \n", a, a, a)
s += "-"
return a * digui(a-1)
}
func main() {
fmt.Println(digui(3))
}
运行结果如下:
-if之前a[3] -if之后a[3]这里返回的是
3 * digui(3-1)
这里程序digui(3)
运行结束,准备返回3*digui(3-1)
时,要先算出digui(3-1)
返回的具体数字。–if之前a[2] -if之后a[2]这里返回的是
2 * digui(2-1)
和上一步一样,在运行3*digui(3-1)
时,准备返回2*digui(2-1)
时,需要先算出这个digui(2-1)
的具体数字。—if之前a[1] -if内,准备返回a[1]
- 这里是程序
digui(2-1)
运行结束时,程序明确的知道要返回1
- 所以第二步的返回值
2*digui(2-1)
实际运行的就是2*1
,返回值就是明确的数字2
- 所以第一步
3*digui(3-1)
实际运行的就是3*2
,返回值是6
- 这里是程序
6
所以main
函数中digui(3)
的返回值就是6
乘法表
九九乘法表,支持正序或倒序输出。源代码如下,自行编译去吧。
package main
import "fmt"
var shuru = 0
/*
打印九九乘法表
作用:根据选项o正三角或倒三角输出九九乘法表。
参数:o bool类型。true以正三角输出九九乘法表。false倒三角输出九九乘法表。
返回值:无
实例:chengfabiao(true)
*/
func chengfabiao(o bool) {
if o {
//正三角输出九九乘法表
for i := 1; i <= 9; i++ {
for j := 1; j <= i; j++ {
fmt.Printf("%d*%d=%d \t", i, j, i*j)
}
fmt.Println()
}
} else {
//倒三角输出九九乘法表
for i := 9; i >= 1; i-- {
for j := 1; j <= i; j++ {
fmt.Printf("%d*%d=%d \t", j, i, j*i)
}
fmt.Println()
}
}
}
//菜单函数
func menu(shuru int) {
//菜单功能
switch shuru {
//用户选1,正序输出
case 1:
chengfabiao(true)
// 用户选择2,倒序输出
case 2:
chengfabiao(false)
//不要胡乱操作
default:
fmt.Println("如需要退出,请按[3]后回车,致谢~")
}
}
func main() {
for {
//欢迎辞
fmt.Println(`----------九九乘法表----------
1:正序输出 2:倒序输出 3:退出或者Ctrl + c
--------------------建议横屏获得最佳显示效果-------------------`)
//获取用户输入
fmt.Scanln(&shuru)
//调用menu函数或者退出
if shuru != 3 {
menu(shuru)
} else {
break
}
}
}