06.认识函数
06.认识函数
函数构成代码执行的逻辑结构。在Go语言中,函数的基本组成为:关键字func,函数名,参数列表,返回值,函数体和返回语句。
Go语言函数定义格式如下:
func FuncName(/*参数列表*/) (01 type1, 02 type2/*返回类型*/) {
//函数体
return v1, v2 //返回多个值
}
函数定义说明:
- func:函数有关键字func声明。
- FuncName:函数名称,根据约定,函数名首字母小写即为private,大写即为public。
- 参数列表:函数可以有0个或多个参数列表,参数格式为:变量名 类型,如果有多个参数,通过逗号分隔,不支持默认参数
- 返回类型:
- 上面返回值声明了两个变量名01和02(命令返回参数),这个不是必须,可以只有类型没有变量名。
- 如果只有一个返回值且不声明返回值变量,那么你可以省略,包括返回的括号。
- 如果没有返回值,那么就直接省略最后的返回信息。
- 如果有返回值,那么必须在函数的内部添加return语句。
1,无参数无返回值
package main
import "fmt"
//无参无返回值函数的的定义
func MyFunc() {
a := 666
fmt.Println("a =", a)
}
//被调用函数放在上边或者下边都是一样的,程序从main函数进入,然后调用MyFunc函数结束。
func main() {
//无参无返回值函数的调用:函数名()
MyFunc()
}
2,有参无返回值
1,普通参数列表
package main
import "fmt"
/*
有参无返回值函数的定义,普通参数列表
定义函数时,在函数名后面()定义的参数叫形参
参数传递,只能由实参传递给形参,不能反过来,这是一个单项传递
*/
func MyFunc01(a int) {
fmt.Println("a =", a)
}
func MyFunc02(a int, b int) {
fmt.Printf("a = %d, b = %d\n", a, b)
}
//或者写成
func MyFunc03(a, b int) {
fmt.Printf("a = %d, b = %d\n", a, b)
}
func MyFunc04(a int, b string, c float64) {
}
func MyFunc05(a, b string, c float64, d, e int) {
}
//推荐如下这种写法,比较清晰好认。
func MyFunc06(a string, b string, c float64, d int, e int) {
}
func main() {
//有参无返回值函数调用方式: 函数名(所需参数)
//点用函数传递的参数叫实参
MyFunc01(666)
MyFunc02(666, 777)
}
2,不定参数类型
package main
import "fmt"
//之前定义的,都是普通参数,或者叫做固定参数,示例如下:
func MyFunc01(a int, b int) {
}
//接下来是不定参数类型
//...int 类似这样的类型,就是不定参数类型
//注意:不定参数,一定(只能)是作为形参中最后一个参数存在
func MyFunc02(args ...int) { //传递的参数可以是0个或多个
fmt.Printf("len(args) = %d\n", len(args)) //表示获取用户传递参数的个数
//如何打印用户输入的参数呢,可以通过for循环来执行
// for i := 0; i < len(args); i++ {
// fmt.Printf("args[%d] = %d\n", i, args[i])
// }
//或者使用range进行打印,代码如下
for i, date := range args { //表示遍历参数列表
fmt.Printf("args[%d] = %d\n", i, date)
}
}
// func MyFunc03(a int,b int, test ...int){
// //这个是正确的演示
// }
// func MyFunc04(a int, test ... int, b int){
// //这个是错误的演示,无论是放中间,还是放在开头,都是不允许的
// }
func main() {
MyFunc02()
MyFunc02(1)
MyFunc02(1, 2, 3)
}
3,不定参数的传递
package main
import "fmt"
/*
1,先通过主函数传递了四个值给test函数中的不定参数args。
*/
func myfunc(tmp ...int) {
for _, date := range tmp {
fmt.Printf("date = %d\n", date)
}
}
func test(args ...int) {
//如果把全部元素传递给myfunc,可用如下方式
//myfunc(args...)
//只想把某两个元素传递下去,如何写
myfunc(args[2:]...) //表示把args[0]~args[2](不包括args[2])对应的元素传递过去
myfunc(args[:2]...) //表示从args[2](包括args[2]本身)开始,后边的都传递过去
}
func main() {
test(1, 2, 3, 4)
}
3,无参有返回值
1,单个返回值
package main
import "fmt"
//无参有返回值,一个返回值的示例
//有返回值的函数需要通过return关键字中断函数,通过return返回
func myfunc() int {
return 666
}
func MyFunc01() (test int) {
return 777
}
//给返回值定义一个名为test的变量,再通过给变量赋值,然后return出去,是go推荐写法,这种写法比较常用
func MyFunc02() (test int) {
test = 888
return
}
func main() {
//标准写法
var a int
a = myfunc()
fmt.Println("a =", a)
//也可以用自动推到类型的写法
b := myfunc()
fmt.Println("b =", b)
c := MyFunc01()
fmt.Println("c =", c)
d := MyFunc02()
fmt.Println("d =", d)
}
2,多个返回值
package main
import "fmt"
func MyFunc01() (int, int, int) {
return 1, 2, 3
}
//常用写法
func MyFunc02() (a, b, c int) {
a, b, c = 4, 5, 6
return
}
func main() {
a, b, c := MyFunc02()
fmt.Printf("a = %d,b = %d, c = %d\n", a, b, c)
}
4,有参有返回值
package main
import "fmt"
//函数定义
func MaxAndMin(a, b int) (max, min int) {
if a > b {
max = a
min = b
} else {
max = b
min = a
}
return //有返回值的函数,必须通过return关键字返回
}
func main() {
//函数调用。
max, min := MaxAndMin(10, 20)
fmt.Printf("max = %d, min = %d\n", max, min)
//通过匿名变量丢弃某个值
a, _ := MaxAndMin(5, 2)
fmt.Println("a =", a)
}
5,普通函数调用流程
在函数当中,程序执行的先后顺序是,先调用,后返回。
package main
import "fmt"
func funcc(c int) {
fmt.Println("c =", c)
}
func funcb(b int) {
funcc(b - 1)
fmt.Println("b =", b)
}
func funca(a int) {
funcb(a - 1)
fmt.Println("a =", a)
}
func main() {
funca(3)
fmt.Println("main")
}
可能函数不太容易看,那么可以通过画图来一一解析:
6,递归函数调用流程
1,认识递归函数
递归函数是指函数可以直接或者间接调用自身。
递归函数通常有相同的结构:一个跳出条件和一个递归体。所谓跳出条件就是根据传入的参数判断是否要终止递归,而递归体则是函数自身所做的一些处理。
package main
import "fmt"
//递归,就是在运行过程中调用自己
func test(a int) {
//必须要设置退出条件,否则将会进入无限循环
if a == 1 {
fmt.Println("a =", a)
return //终止函数调用
}
//函数调用自身
test(a - 1)
fmt.Println("a =", a)
}
func main() {
test(3)
fmt.Println("main")
}
通过画图将运行流程解析一下。
使用文字详细表述一下执行流程,注意文字表述比画图的步骤稍有精简,其实是把有些步骤合起来了。
- 1,通过main入口函数进入程序。
- 2,调用函数test,并赋值变量a为3.
- 3,程序来到函数test,此时a=3,不符合判断条件,所以往下走,调用函数test,并赋值变量a为(a-1),其实就是3-1
- 4,程序再次来到函数test,此时a=2,不符合判断条件,所以往下走,调用函数test,并赋值变量a为(a-1),其实就是2-1
- 5,程序再次来到函数test,此时a=1,符合判断条件,所以进入到判断中,打印此时a的值,为1,然后return跳出。
- 6,跳出后,函数回到第4步,然后打印此时a的值,为2,完成本次运算,跳出。
- 7,跳出后,函数回到第3步,然后打印此时a的值,为3,完成本次运算,跳出。
- 8,跳出后,函数回到第2步,然后打印一个字符串main。
其中针对第6,7两步,再简单分析如下:
前边的5步,都还比较容易理解,但是来到第6步,自己就有点晕了,不知道为什么还会返回去进行打印,后来又翻上去,看了看上边普通函数调用的例子,就有一点点明白了,其实到第6步(也就是第五步的完成),相当于程序完成了第4步的函数调用,结合先调用,后返回的理论,那么此时刚好调用结束了,该进行打印了。同理,到第7步(也就是第6步的完成),相当于程序完成了第3步的函数调用,完成之后,打印彼时的值,然后同理往后推,直到程序完成最终的逻辑规划。
2,计算数字累加和
昨天的时候,曾经使用过for循环来实现,现在可以不回头看文档,自己动手敲一遍for循环的累加和。
package main
import "fmt"
func main() {
sum := 0
for i := 1; i <= 100; i++ {
sum = sum + i
}
fmt.Println("1加到100的和是:", sum)
}
现在来理解,大概就不是十分困难了,这是计算的从1加到100的和,还可以写一下从100加到1的和,如下:
package main
import "fmt"
func main() {
sum := 0
for i := 100; i >= 1; i-- {
sum = sum + i
}
fmt.Println("100加到1的和是:", sum)
}
那么,现在可以应该可以尝试使用递归函数来实现一下了,还是从1加到100开始。
package main
import "fmt"
func test(i int) int {
if i == 100 {
return 100
}
return i + test(i+1)
}
func main() {
var sum int
sum = test(1)
fmt.Println("1加到100的和是:", sum)
}
如果不借助于画图,先来读一读函数所执行的流程。
1,通过main入口函数进入程序。 2,定义一个变量sum,让他等于函数test(),且传递参数使i=1。 3,来到函数test中,此时i=1,首然后进入if判断,显然不等于100,接着往下,得到i+test(i+1),实际上就是1+test(1+1),此时1保留,后边的test(1+1)再次进入函数。 4,再次来到函数test中,此时i=2,首然后进入if判断,显然不等于100,接着往下,得到i+test(i+1),实际上就是2+test(2+1),此时2保留,后边的test(2+1)再次进入函数。 5,再次来到函数test中,此时i=3,首然后进入if判断,显然不等于100,接着往下,得到i+test(i+1),实际上就是3+test(3+1),此时3保留,后边的test(3+1)再次进入函数。 6,再次来到函数test中,此时i=4,首然后进入if判断,显然不等于100,接着往下,得到i+test(i+1),实际上就是4+test(4+1),此时4保留,后边的test(4+1)再次进入函数。 ...... 如此往复循环99次,直到最后一次。 7,再次来到函数test中,此时i=100,首然后进入if判断,等于100,然后return一个100,而前边那么多次,已经累积为1+2+3...+99,再加上这100,就等于最终结果了。
接下来再尝试一下从100加到1的累加和。
package main
import "fmt"
func test01(i int) int {
if i == 1 {
return 1
}
return i + test01(i-1)
}
func main() {
var sum int
sum = test01(100)
fmt.Println("100加到1的和是:", sum)
}
这个相加的过程以及调用,与上边基本上是一致的。
7,函数类型
1,认识函数类型
在go语言中,函数也是一种数据类型,可以用过type来定义它,它的类型就是所拥有的相同的参数,相同的返回值的一种类型。
package main
import "fmt"
//两数相加
func Add(a, b int) int {
return a + b
}
//两数相减
func Minus(a, b int) int {
return a - b
}
//函数也是一种数据类型,通过type可以给函数起一个名字,然后通过这个名字来对函数进行调用。
//FuncType是一个函数类型
type FuncType func(int, int) int //没有函数名字,没有{}
func main() {
//传统方式调用函数
var result int
result = Add(8, 5)
fmt.Println("result =", result)
//现在通过刚刚定义的函数类型名称来进行调用
var ftest FuncType //定义一个变量ftest,其类型是刚刚定义的FuncType
ftest = Add //然后可以将函数名赋给变量ftest,从而让ftest具有对应名称的函数能力
result = ftest(10, 20) //等价于 restult = Add(10,20)
fmt.Println("result1 =", result)
//同样,也可以将另外一个函数名赋给一个变量。
ftest = Minus
result = ftest(1, 1) //等价于 restult = Minus(1,1)
fmt.Println("result2 =", result)
}
如上是一种传统定义以及调用的例子,还可以使用下边的方式进行定义与调用。
package main
import "fmt"
type FuncType func(int, int) int //声明一个函数类型
//函数中有一个参数类型为函数类型:f FuncType
func Calc(a, b int, f FuncType) (result int) {
result = f(a, b)
return
}
func Add(a, b int) int {
return a + b
}
func main() {
//函数调用,第三个参数为函数名字,此函数的参数,返回值必须和FuncType类型一致
a := Calc(3, 3, Add)
fmt.Println("a=", a)
}
那么这种函数类型,究竟有什么用呢,直接调用难道不是更简便么。接下来,就能看到它的作用了。
2,多态
函数类型可以应用于多态当中,在进入正式的示例之前,先来看一个普通的加法计算的代码。
package main
import "fmt"
//两数相加
func Add(a, b int) int {
return a + b
}
//主程序函数调用加法函数
func Calc(a, b int) (result int) {
fmt.Println("Calc")
result = Add(a, b)
return
}
func main() {
a := Calc(3, 3)
fmt.Println("a=", a)
}
接着再来看另外一种写法:
package main
import "fmt"
type FuncType func(int, int) int
//两数相加
func Add(a, b int) int {
return a + b
}
//两数相减
func Minus(a, b int) int {
return a - b
}
//两数相乘
func Mul(a, b int) int {
return a * b
}
/*
计算器,可以进行四则运算
回调函数,函数有一个参数是函数类型,这个函数就是回调函数。在下边的例子中,参数ftest的类型就是上边定义的函数类型(FuncType)
多态,多种形态,调用同一个接口,不同的表现,可以实现不同的结果。
*/
func Calc(a, b int, ftest FuncType) (result int) {
fmt.Println("Calc")
result = ftest(a, b)
return
}
func main() {
a := Calc(3, 3, Mul)
fmt.Println("a=", a)
}
简单说明。
一开始也不是很能理解,但是练习了几个例子,稍微发现了其中的一些奥妙。从main入口处定义开始理解,Calc(3, 3, Mul)套到函数上,注意后边的Mul,等同于ftest的位置,那么替换一下之后,就会发现,与上边刚刚写的那个加法的例子,其实基本上一样了。
8,匿名函数与闭包
所谓闭包就是一个函数“捕获”了和它在同一作用域的其他常量和变量。这就意味着当闭包被调用的时候,不管在程序什么地方调用,闭包能够使用这些常量或者变量。它不关心这些捕获了的变量和常量是否已经超出了作用域,所以只有闭包还在使用它,这些变量就还会存在。
在Go语言里,所有的匿名函数(Go语言规范中称之为函数字面量)都是闭包。匿名函数是指不需要定义函数名的一种函数实现方式。
1,认识匿名函数与闭包
package main
import "fmt"
func main() {
a := 10
str := "mike"
//匿名函数,没有函数名字,以下只是函数的定义
f1 := func() { //自动推导类型,将匿名函数运算赋值给f1
fmt.Printf("a =%d, str =%s\n", a, str)
}
f1() //使用这种方式来进行调用。
//给一个匿名函数类型起个别名,这种写法不常用。
type FuncType func() //函数没有参数,没有返回值
var f2 FuncType
f2 = f1
f2()
//定义匿名函数的同时并调用
func() {
fmt.Printf("a =%d, str =%s\n", a, str)
}() //后面的()表示调用这个匿名函数
//带参数的匿名函数
f3 := func(i, j int) {
fmt.Printf("i =%d, j =%d\n", i, j)
}
f3(3, 5)
//另外一种写法
func(i, j int) {
fmt.Printf("i =%d, j =%d\n", i, j)
}(5, 3)
//匿名函数,有参有返回值
x, y := func(i, j int) (max, min int) {
if i > j {
max = i
min = j
} else {
max = j
min = i
}
return
}(6, 8)
fmt.Printf("x = %d, y = %d\n", x, y)
}
2,闭包捕获外部变量的特点
package main
import "fmt"
func main() {
a := 10
str := "mike"
func() {
//闭包以引用的方式捕获外部的变量
a = 666
str = "go"
fmt.Printf("a = %d, str = %s\n", a, str)
}()
fmt.Printf("a = %d, str = %s\n", a, str)
}
结果输出:
a = 666, str = go
a = 666, str = go
相当于在匿名函数中,更改了变量的值,那么到匿名函数外部之后,值也随之更改。
为了验证这一特点,现在可以做一个对比。
先通过一个普通的函数类型看看情况:
package main
import "fmt"
func test01() int {
var a int //当一个变量没有初始化时,默认是0
a++
return a * a //函数调用完毕,a自动释放。
}
func main() {
fmt.Println(test01())
fmt.Println(test01())
fmt.Println(test01())
fmt.Println(test01())
fmt.Println(test01())
}
输出结果为:
$ go run 15_闭包的特点.go
1
1
1
1
1
注意上边那句话,调用完成,变量的值自动释放,才会得到如上结果。
再来看另外一个例子:
package main
import "fmt"
//函数的返回值是一个匿名函数,返回一个函数类型
func test02() func() int {
var a int //当一个变量没有初始化时,默认是0
return func() int {
a++
return a * a
}
}
func main() {
//上边的返回值为一个匿名函数,返回一个函数类型,因此这里通过一个变量f来调用返回的匿名函数(闭包函数)
//此时,它不关心这些捕获了的变量或常量是否已经超出了作用域,只要闭包还在使用它,这些变量就会一直存在。
f := test02()
fmt.Println(f())
fmt.Println(f())
fmt.Println(f())
fmt.Println(f())
fmt.Println(f())
}
输出结果:
$ go run 15_闭包的特点.go
1
4
9
16
25
同时也要注意上边的话,每一次调用闭包,那么上次的值都会存下来,正如结果所示的,每次调用,a的初始值都是上次经过闭包之后所得到的新值。
9,延迟调用之defer
1,了解defer
关键字defer用于延迟一个函数或者方法(或者当前所创建的匿名函数)的执行,延迟到函数的最后。注意,defer语句只能出现在函数或者方法的内部
。
package main
import "fmt"
func main() {
//defer延迟调用,表示在main函数结束之前调用,也就是放在最后执行
defer fmt.Println("aaaaaaa")
fmt.Println("bbbbbbb")
}
defer语句经常被用于处理那些成对的操作,比如打开、关闭,连接、断开连接,加锁、释放锁,,等,通过defer,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放,释放资源的defer语句,应该直接跟在请求资源的语句后。
2,多个defer的执行顺序
如果一个函数中有多个defer语句,他们会以先进后出
(也就是先定义的,后执行)的顺序执行,哪怕函数或某个延迟调用会发生错误,这些调用依旧会被执行。
package main
import "fmt"
func test(x int) {
fmt.Println(100 / x) //x为0时,产生异常
}
func main() {
defer fmt.Println("aaaaaaa")
defer fmt.Println("bbbbbbb")
defer test(0)
defer fmt.Println("ccccccc")
}
结果如下:
$ go run 17_多个defer执行顺序.go
ccccccc
bbbbbbb
aaaaaaa
panic: runtime error: integer divide by zero
goroutine 1 [running]:
main.test(0x0)
C:/Users/Administrator/Desktop/gocode/src/day2/17_多个defer执行顺序.go:6 +0xa9
main.main()
C:/Users/Administrator/Desktop/gocode/src/day2/17_多个defer执行顺序.go:15 +0x15e
exit status 2
3,defer与匿名函数结合使用
先看一个没有参数的例子。
package main
import "fmt"
func main() {
a := 10
b := 20
defer func() {
fmt.Printf("a = %d, b = %d\n", a, b)
}()
a = 100
b = 200
fmt.Printf("外部:a = %d, b = %d\n", a, b)
}
结果输出:
$ go run 18_defer和匿名函数结合使用.go
外部:a = 100, b = 200
a = 100, b = 200
接着给匿名函数定义两个参数,看看结果。
package main
import "fmt"
func main() {
a, b := 10, 20
defer func(a, b int) {
fmt.Printf("a = %d, b = %d\n", a, b)
}(a, b) //函数执行时,到达defer语句处,虽然暂不执行,但是参数已经传递,结果到最后输出。
a = 100
b = 200
fmt.Printf("外部:a = %d, b = %d\n", a, b)
}
结果输出:
$ go run 18_defer和匿名函数结合使用.go
外部:a = 100, b = 200
a = 10, b = 20
10,获取命令行参数
package main
import (
"fmt"
"os"
)
func main() {
list := os.Args
n := len(list)
fmt.Println("参数个数为:", n)
}
执行一下看看:
$ go run 19_获取命令行参数.go 1 2 3
参数个数为: 4
可以分别打印一下这些参数:
package main
import (
"fmt"
"os"
)
func main() {
list := os.Args
n := len(list)
fmt.Println("参数个数为:", n)
for i := 0; i < n; i++ {
fmt.Printf("list[%d] = %s\n", i, list[i])
}
//迭代打印
for i, data := range list {
fmt.Printf("list[%d] = %s\n", i, data)
}
}
运行结果:
$ go build 19_获取命令行参数.go
$ ./19_获取命令行参数.exe 1 2 3
参数个数为: 4
list[0] = C:\Users\Administrator\Desktop\gocode\src\day2\19_获取命令行参数.exe
list[1] = 1
list[2] = 2
list[3] = 3
list[0] = C:\Users\Administrator\Desktop\gocode\src\day2\19_获取命令行参数.exe
list[1] = 1
list[2] = 2
list[3] = 3
11,作用域
1,局部变量
package main
import "fmt"
func test() {
a := 10
fmt.Println("a =", a)
}
/*
作用域:就是变量产生作用的范围。
定义在{}里面的变量就是局部变量,同时也只在{}里面有效。
执行到定义变量那句话,才开始给变量分配空间,离开作用域之后自动释放
*/
func main() {
fmt.Println("a =", a)
{
i := 10
fmt.Println("i = ", i)
}
fmt.Println("i = ", i)
if b := 3; b == 3 {
fmt.Println("b = ", b)
}
fmt.Println("b = ", b)
}
2,全局变量
package main
import "fmt"
//全局变量是指定义在函数外部的量
//全局变量在任何地方都能使用
var a int
func main() {
a = 10
fmt.Println("a =", a)
test()
}
func test() {
fmt.Println("test a =", a)
}
3,不同作用域的同名变量
package main
import "fmt"
var a byte //全局变量
//1,不同作用域,允许定义同名变量
//2,使用变量的原则,就近原则。
func main() {
var a int //局部变量
fmt.Printf("1: %T\n", a)
{
var a float64
fmt.Printf("2: %T\n", a)
}
test()
}
func test() {
fmt.Printf("3: %T\n", a)
}
12,可变参数
//可变参数
func add(a...int) int { //0个或多个可变参数
}
func add(a int, arg...int) int { //1个或多个可变参数
}
func add(a, b int, arg...int) int { //2个或多个可变参数
}
//注意:其中arg是一个slice,可通过arg[index]来获取对应参数,通过len(arg)来判断参数的个数
可变参数的应用:
package main
import "fmt"
// 函数可变参数
func f(a ...interface{}) {
// a 是一个切片
fmt.Printf("type:%T value:%#v\n", a, a)
}
func main() {
var s = []interface{}{1, 3, 5, 7, 9}
f(s) // 表示把s这个切片当成一个整体,作为函数参数a这个切片的第一个元素
f(s...) //表示把s这个切片中的数据展开,来逐个作为函数参数a这个切片的元素
}
如上代码输出结果:
type:[]interface {} value:[]interface {}{[]interface {}{1, 3, 5, 7, 9}}
type:[]interface {} value:[]interface {}{1, 3, 5, 7, 9}