Go语言学习14-函数(超级重点)

Go语言中支持函数、匿名函数和闭包,并且函数在Go语言中属于“一等公民”。

0x00 return关键字

return的作用1:结束当前函数,跳出整个函数。所以连后面的It's over!都没有输出出来。

package main

import "fmt"

func main() {
	for i := 0; i <= 10; i++ {
		fmt.Println(i)
		if i == 6 {
			return
		}
	}
	fmt.Println("It's over!")
}

image-20220213212808841

0x01 函数引入理解

为什么要使用函数?

假设我现在需要一个求和的功能,那么就是下面这段代码。如果我还想求和其他的数,一遍一遍写就太麻烦了。

提高代码的复用,减少代码的冗余,代码的维护性也提高了。说白了,下次用这个功能,直接调用这个函数即可。

例子查看

package main

import "fmt"

func main() {
	var num1 int = 100
	var num2 int = 200
	var sum int = 0
	sum += num1
	sum += num2

	fmt.Println(sum)
}

0x02 函数详解

简单的函数

可以看到下段代码,我们在main()里面引用了f1函数,可以帮助我们快速打印出很多行代码。但是思考一下,如果我要让它打印出不同的结果怎么办?

func f1() {
	fmt.Println("111111111111")
	fmt.Println("111111111111")
    fmt.Println("111111111111")
    fmt.Println("111111111111")
}

func main() {
	f1()
}

传参,实现定制化的操作

func f2(x string) {
	fmt.Println("Hello!", x)
}
func main() {
	f2("北京")
	f2("上海")
}

带返回值的

思考下面函数会输出语句么?不会,是因为f3(100,200)仅仅是调用函数。调用函数和输出语句没有任何关系。如果想要让其输出,加一个输出即可。

func f3(x int, y int) (sum int, sub int) {
	sum = x + y
	sub = x - y
	return
}
func main() {
	f3(100, 200)
}

参数类型简写

x和y都是整型,所以直接输出即可

//参数类型简写
func f3(x, y int) (sum int, sub int) {
	sum = x + y
	sub = x - y
	return
}

多个参数变种

还记得...吧,...代表若干个,如果类型一样,可以用...代替,注意,返回值不支持这么写!

//可变参数(多个参数)
func f4(x int, y ...string) {
	fmt.Println(x, y)
}

函数中不支持再添加一个函数,匿名函数可以

func f2()(x int){
	defer func(){
		x++
	}()
	return 5
}

高阶函数:函数作为形参或者返回值

函数既然是一种数据类型,因此在Go中,函数可以作为形参,并且调用,把函数本身当作一种数据类型。

可以看到虽然a和b都是函数,但是他们的函数类型是不一样的。

func f1() {
	fmt.Println("Hellow shahe")
}
func f2(x int) int {
	// fmt.Println("hellow")
	return x
}
func main() {
	a := f1
	b := f2
	fmt.Printf("%T\n %T", a, b)
}

一般情况下我们在函数里面定义参数类型如切片,就是[]rune

func fa(slice []rune){
    fmt.Println('a')
}
如果不满足下面的类型,那就不能够传入进去参数

image-20220216140403125

函数还可以作为返回值
func f5(x func() int) func(int, int) int {
	return ff
}
func ff(a, b int) int {
	return a + b
}
func test(num int) {
	fmt.Println(num)
}
func test02(num1 int, num2 float32, testFunc func(int)) {
	fmt.Println("----test02")
}
func main() {
	a := test
	fmt.Printf("a的类型是:%T, test函数的类型是%T \n", a, test)
	a(10)

	//调用test02函数
	test02(10, 3.14, test)	//因为能够输出----test02,所以证明能够成功将test传入test02进行使用
	test02(10, 3.14, a)
}

image-20220214223843474

01 基本语法

func 函数名(形参列表)(返回值类型列表){
    执行语句...
    return + 返回值列表
}

例子:使用函数来定义两数相加

func sum(x int, y int) int {
	return x + y	//返回x+y后的结果,跳出函数
}
func main() {
	a := 10
	b := 20
    c := sum(a, b)	//函数的调用,a就是x,b就是y a和b使用sum函数进行处理后的结果即return出来的结果,相当于sum(a,b)用return来替换了
	fmt.Println(c)
}

1、函数名:

  • 遵循标识符命名规范:见名知意addNum,驼峰命名addNum
  • 首字母不能是数字
  • 首字母大写该函数可以被本包文件和其他包文件使用(类似public)
  • 首字母小写只能被本包文件使用,其它包文件不能使用(类似private)

2、形参列表与参数列表

形式参数列表:个数可以是0、1、n个 作用:接收外来的数据,后续处理。

实际参数列表:实际传入的数据

image-20220214100059688

3、返回值列表:函数的返回值类型应该写在这个列表中

返回0个:

image-20220214101414768

返回值1个:

image-20220214101618795

返回值多个:

image-20220214113921989

补充阅读理解:

  • 函数名:见名知意。由字母、数字、下划线组成。但函数名的第一个字母不能是数字。在同一个包内,函数名也称不能重名(包的概念详见后文)。
  • 形参列表:类似于一个占位,参数由参数变量和参数变量的类型(Go语言为强类型,必须定义)组成,多个参数之间使用,分隔。
  • 返回值:返回值由返回值变量和其变量类型组成,也可以只写返回值的类型,多个返回值必须用()包裹,并用,分隔。
  • 函数体:实现指定功能的代码块。

02 通过例题进行内存分析

首先程序运行会进入到main函数,因为main()是入口函数,第一个需要进行执行的函数,优先级高。

在main里面定义了两个变量,正常输出num1,num2=10,20没毛病;

到了调用exchangeNumb函数的时候,交换两个数的数值,再回到main函数中,按理说应该换了数值呀,为什么没有换??

package main

import "fmt"

func exchangeNum(num1 int, num2 int) {
	var t int
	t = num1
	num1 = num2
	num2 = t

}

func main() {
	var num1 int = 10
	var num2 int = 20
	fmt.Printf("交换前的两个数:num1=%v,num2=%v\n", num1, num2)
	exchangeNum(num1, num2)
	fmt.Printf("交换后的两个数:num1=%v,num2=%v\n", num1, num2)
}

image-20220214211127500

内存分析:

当我们运行Go语言时,会向内存申请一块空间,供Go语言运行起来的程序来用。

随后进行逻辑划分,就是分成三个部分。栈、堆、代码区。

基本情况下,栈是用来存放基本数据类型的,如int、string、bool等;堆是用来存放引用数据类型、复杂数据类型的;代码区就是用来存放代码。(再次强调一下,这是一般情况,特殊情况可能堆栈存放的数据会变化)

image-20220214204523493

1、运行代码时,首先是入口main函数,一旦运行main函数,就会在栈里面独自创建出一块区域让函数来存放函数自身的变量等,这块区域被称作为栈帧。

func main() {

image-20220214204804092

2、在main函数中执行声明变量语句,即声明num1,num2;随后在终端输出第一句话

var num1 int = 10
var num2 int = 20
fmt.Printf("交换前的两个数:num1=%v,num2=%v\n", num1, num2)

image-20220214204938556

3、随后调用函数exchangeNum,内存中就会创建exchangeNum栈帧。

exchangeNum(num1, num2)

image-20220214205910795

4、随后进行exchangeNum函数中的第一行语句,开始声明变量num1,num2,并从main函数中继承数值,说白了就是拷贝一份数据过去。

func exchangeNum(num1 int, num2 int) {

image-20220214205844178

5、随后进入函数体,声明了t函数,开辟相应的内存空间

var t int

image-20220214205700326

6、随后num1的值被传入到t,t此时被赋值

t = num1	//t = 10

image-20220214205638843

7、再之后,num1的值被替换成20

num1 = num2	//num1 = 20

image-20220214205608974

8、再之后,num2的值会变成10,之后结束函数运行。

num2 = t	//num2 = 10
}

image-20220214211551672

9、当函数运行结束后,会消除掉栈帧,即exchangeNum函数会被销毁。所以exchangeNum函数仅仅只是完成了自身形参的转换罢了,对main函数内部的变量没有任何影响。再之后进行打印,还是这两个数值。

image-20220214210715640

03 函数不支持重载

重载:函数名相同,形参列表相同。可以看到报错了,不支持函数重新声明(redeclared)。不过可以使用匿名函数。

func exchangeNum(num1 int, num2 int) {
	var t int
	t = num1
	num1 = num2
	num2 = t
}

func exchangeNum(num1 int) {
	var t int
	t = num1
	num1 = num2
	num2 = t
}

image-20220214212124325

04 函数支持可变参数(如果你希望函数带有可变数量的参数)

可变参数是什么意思?就是能够变化的参数,一般来说我们可能会去传多个参数,有时候传0,有时候传2,等等,但是吧,不稳定,我们希望这些参数是可以变化的。

什么东西都没有返还,什么东西都没有使用,所以是支持可变参数的。但是问题来了,怎么处理里面的参数?函数内部处理可变参数的时候,将可变参数当作切片来处理。

package main

//定义一个函数,函数的参数为:可变参数 ...参数的数量可变
func test(args ...int) { //args英文是多个参数的意思,不是关键词,args...代表可以
	//传入任意多个数量的int类型的数据 传入0个,1个...n个
}
func main() {
	test()                                                  //传0个
	test(1)                                                 //传1个
	test(1, 2, 3, 4, 5, 45, 2, 3, 23, 12, 312, 3, 23, 1, 1) //传n个

}

遍历切片就好说了:

func test(args ...int) { //args英文是多个参数的意思,不是关键词,args...代表可以
	//传入任意多个数量的int类型的数据 传入0个,1个...n个
	for i, v := range args {
		fmt.Println(i, v)
	}
}
func main() {
	test()                                                  
	test(1)                                                 
	test(1, 2, 3, 4, 5, 45, 2, 3, 23, 12, 312, 3, 23, 1, 1) 
}

image-20220214214825372

05 值拷贝

基本数据类型和数组默认都是值传递的,即进行值拷贝。在函数内修改,不会影响到原来的值。

image-20220214220608776

06 函数内变量修改函数外变量

以值传递方式的数据类型,如果希望在函数内能够修改函数外的变量,可以传入变量的地址&,函数内以指针的方式操作变量,从效果来看类似引用传递。

package main

import "fmt"

func test(num *int) {	//5、test函数新建指针变量接收num指针
	*num = 30			//6、num指针对应的内存值改为30,函数结束,删除栈帧
}

func main() {		//1、进入main函数
	var num int = 10	//2、声明变量num=10
	fmt.Println(&num)	//3、打印num变量的指针
	test(&num)		//4、将num变量的指针传入test函数
	fmt.Println(num)	//7、输出num值
}

image-20220214221845752

07 函数也是一种数据类型

在Go语言中,函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量了,通过该变量可以对函数调用。

上面这句话后面的部分,一句一句解释:

func test(a int) {
	fmt.Println(a)
}
func main() {

	b := test
	//函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量了
	fmt.Printf("test函数的类型为:%T\nb的类型为:%T", test, b)
	//通过该变量可以对函数调用
	b(10)
}

image-20220214223136699

08 函数作为形参

函数既然是一种数据类型,因此在Go中,函数可以作为形参,并且调用,把函数本身当作一种数据类型。

可以看到虽然a和b都是函数,但是他们的函数类型是不一样的。

func f1() {
	fmt.Println("Hellow shahe")
}
func f2(x int) int {
	// fmt.Println("hellow")
	return x
}
func main() {
	a := f1
	b := f2
	fmt.Printf("%T\n %T", a, b)
}

一般情况下我们在函数里面定义参数类型如切片,就是[]rune

func fa(slice []rune){
    fmt.Println('a')
}

如果不满足下面的类型,那就不能够传入进去参数。

image-20220216140403125

函数还可以作为返回值

func f5(x func() int) func(int, int) int {
	return ff
}
func ff(a, b int) int {
	return a + b
}
func test(num int) {
	fmt.Println(num)
}
func test02(num1 int, num2 float32, testFunc func(int)) {
	fmt.Println("----test02")
}
func main() {
	a := test
	fmt.Printf("a的类型是:%T, test函数的类型是%T \n", a, test)
	a(10)

	//调用test02函数
	test02(10, 3.14, test)	//因为能够输出----test02,所以证明能够成功将test传入test02进行使用
	test02(10, 3.14, a)
}

image-20220214223843474

09 Go支持自定义数据类型

为了简化数据类型定义,Go支持自定义数据类型。

  • 基本语法:type 自定义数据类型名 数据类型
  • 可以理解为:相当于起了一个别名
  • 例如:type mylnt int ----->这时mylnt就等价int来使用了
  • 例如:type mySum func(int,int) int-------------------------------------->这时mySum就等价一个函数类型func(int,int) int
func main() {
	type myInt int
	var num1 myInt = 30
	fmt.Println("num1", num1)

	var num2 int = 30
	num2 = num1 //虽然是别名,但是在go中编译是别的时候
	//仍然认为两者的数据类型不是一样的
}

image-20220215162333057

那么怎么才能让其输出正确?给num1强制转换一下就好了。

func main() {
	type myInt int
	var num1 myInt = 30
	fmt.Println("num1", num1)

	var num2 int = 30
	num2 = int(num1) 
	fmt.Println("num2:", num2)
}

10 支持对函数返回值命名

image-20220215163231542

做一个改动:

image-20220215163449473

特点

函数和函数之间是并列关系,不影响

0x03 匿名函数

定义

匿名函数就是没有名字的函数,很简单。就完了。在函数内部使用。我们都知道函数内部不能声明其他函数,但是匿名函数可以。

func(x int, y int) int {		
	ret := x + y
	return ret
}

匿名函数的调用

第一种写法:定义在外面,前面加个变量,就可以定义名字了。

var f1 = func(x int, y int) int {
	ret := x + y
	return ret
}

func main() {
	fuck := f1(10, 20)
	fmt.Println(fuck)
}

第二种写法:在函数里面进行定义和调用

func main() {
	a()
	//函数内部调用匿名函数,相当于用一个变量声明匿名函数,随后调用
	f1 := func() {
		fmt.Println("helkad")
	}
    f1()
}

第三种写法:立即调用匿名函数

func main(){
//如果只是调用一次,可以简写称立即执行函数
	func() {
		fmt.Println("立即执行匿名函数")
	}() //这里就是立即执行
}

第四种写法:立即调用带有参数的匿名函数

func main() {
	a()
	//函数内部调用匿名函数,相当于用一个变量声明匿名函数,随后调用
	f1 := func() {
		fmt.Println("helkad")
	}
	f1()
	//如果只是调用一次,可以简写称立即执行函数
	func() {
		fmt.Println("立即执行匿名函数")
	}() //这里就是立即执行

	//立即调用带有参数的匿名函数
	func(x int, y int) {
		ret := x + y
		fmt.Println(ret)
	}(10, 20)
}

0x04 init函数

【1】定义

init函数:初始化函数,可以用来进行一些初始化的操作。每一个源文件都可以包含一个init函数,该函数会在main函数执行前,被Go运行框架调用

func main() {
	fmt.Println("main will be the 1st!")
}

func init() {
	fmt.Println("init will be the first!")
}

image-20220216221321446

【2】全局变量定义,init函数,main函数的执行流程?

1、全局变量定义 2、init函数 3、main函数

var x int = test()

func test() int {
	fmt.Println("test函数被调用!")
	return 10
}

func main() {
	fmt.Println("main will be the 1st!")
}

func init() {
	fmt.Println("init will be the first!")
}

image-20220216221906611

【3】多个源文件都有init函数的时候,如何执行?

main.go

package main

import (
	"Study_GO/studygo/day06/init/testutils"
	"fmt"
)

var x int = test()

func test() int {
	fmt.Println("test函数被调用!")
	return 10
}

func main() {
	fmt.Println("main will be the 1st!")
	fmt.Println("Name=", testutils.Name, "Gender=", testutils.Gender, "Age=", testutils.Age)
}

func init() {
	fmt.Println("main中的init被执行了")
}

testutils.go

package testutils

import "fmt"

var Name string
var Age int
var Gender string

func init() {
	fmt.Println("test中的init函数被执行")
	Name = "你好"
	Age = 18
	Gender = "boy"
}

image-20220216223609399

所以,顺序是:1、外部的包中init 2、main中的init 3、main函数。为什么呢?因为导入包的时候,就会调用包中的init函数

image-20220216223840717

image-20220216224016618

0x05 闭包

【1】什么是闭包?

闭包就是一个函数与其相关的引用环境组合的一个整体。

func getsum() func(int) int {
	var sum int = 0
	return func(num int) int {
		sum = sum + num
		return sum
	}
}
//闭包:返回的匿名函数+匿名函数以外的变量num
func main() {
	f := getsum()
	fmt.Println(f(1))
	fmt.Println(f(1))
	fmt.Println(f(1))
	fmt.Println(f(1))

}

感受:匿名函数中引用的那个变量会一直保存在内存中,可以一直使用

image-20220216232127739

【3】闭包的本质:

闭包本质依旧是一个匿名函数,只是这个函数引入外界的变量/参数

匿名函数+引用的变量/参数 = 闭包

【4】特点:

(1)返回的是一个匿名函数,但是这个匿名函数引用到函数外的变量/参数,因此这个匿名函数就和变量/参数形成一个整体,构成闭包。

(2)闭包中使用的变量/参数会一直保存在内存中,所以会一直使用------------->意味着闭包不可滥用(对内存消耗很大!)

【5】不适用闭包可以嘛?可以是可以,但是很麻烦,我们需要每次将结果都传一遍参数才可以,这是十分狗屎的。

func main() {
	fmt.Println("==============我是一条优美的分割线===============")
	// fuck := sum()
	fmt.Println(sum(0, 1))
	fmt.Println(sum(1, 1))
	fmt.Println(sum(2, 1))
}

//不使用闭包来实现一个累加的效果可以吗?
func sum(shu1, shu2 int) int {
	shu1 = shu1 + shu2
	return shu1
}

【6】总结

1、不使用闭包的时候:我想保留的值,不可以反复使用

2、闭包应用场景:闭包可以保留上次引用的某个值,我们传入一次就可以反复使用了。即实现一个累加的场景。

0x06 函数再总结

函数的定义

参数的格式

无参数的函数

有参数的函数

参数类型简写

可变参数

返回值的格式

有返回值

多返回值

命名返回值

变量的作用域

全局作用域

函数作用域

​ 查找变量的顺序:

​ 1、先在函数内部寻找变量,找不到往外找。

​ 2、函数内部的变量,外部是访问不到的。

代码块作用域

高阶函数

函数也是一种类型,它可以作为参数,也可以作为返回值

posted @ 2022-02-22 21:14  sukusec不觉水流  阅读(12)  评论(0编辑  收藏  举报