@@ -1,58 +0,0 @@ 06.认识函数 | 凤凰涅槃进阶之路

06.认识函数

Abel sun2023年1月6日约 5777 字大约 19 分钟

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")
}

可能函数不太容易看,那么可以通过画图来一一解析:

m_256a8666dcd097572d1a2bf54d0d67fb_r

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")
}

通过画图将运行流程解析一下。

m_ba101555cb25011557392f832a169670_r

使用文字详细表述一下执行流程,注意文字表述比画图的步骤稍有精简,其实是把有些步骤合起来了。

  • 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}
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.9.1