函数是组织好的、可重复使用的、用于执行指定任务的代码块。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
}
}
}