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!")
}
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')
}
如果不满足下面的类型,那就不能够传入进去参数
函数还可以作为返回值
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)
}
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个 作用:接收外来的数据,后续处理。
实际参数列表:实际传入的数据
3、返回值列表:函数的返回值类型应该写在这个列表中
返回0个:
返回值1个:
返回值多个:
补充阅读理解:
- 函数名:见名知意。由字母、数字、下划线组成。但函数名的第一个字母不能是数字。在同一个包内,函数名也称不能重名(包的概念详见后文)。
- 形参列表:类似于一个占位,参数由参数变量和参数变量的类型(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)
}
内存分析:
当我们运行Go语言时,会向内存申请一块空间,供Go语言运行起来的程序来用。
随后进行逻辑划分,就是分成三个部分。栈、堆、代码区。
基本情况下,栈是用来存放基本数据类型的,如int、string、bool等;堆是用来存放引用数据类型、复杂数据类型的;代码区就是用来存放代码。(再次强调一下,这是一般情况,特殊情况可能堆栈存放的数据会变化)
1、运行代码时,首先是入口main函数,一旦运行main函数,就会在栈里面独自创建出一块区域让函数来存放函数自身的变量等,这块区域被称作为栈帧。
func main() {
2、在main函数中执行声明变量语句,即声明num1,num2;随后在终端输出第一句话
var num1 int = 10
var num2 int = 20
fmt.Printf("交换前的两个数:num1=%v,num2=%v\n", num1, num2)
3、随后调用函数exchangeNum,内存中就会创建exchangeNum栈帧。
exchangeNum(num1, num2)
4、随后进行exchangeNum函数中的第一行语句,开始声明变量num1,num2,并从main函数中继承数值,说白了就是拷贝一份数据过去。
func exchangeNum(num1 int, num2 int) {
5、随后进入函数体,声明了t函数,开辟相应的内存空间
var t int
6、随后num1的值被传入到t,t此时被赋值
t = num1 //t = 10
7、再之后,num1的值被替换成20
num1 = num2 //num1 = 20
8、再之后,num2的值会变成10,之后结束函数运行。
num2 = t //num2 = 10
}
9、当函数运行结束后,会消除掉栈帧,即exchangeNum函数会被销毁。所以exchangeNum函数仅仅只是完成了自身形参的转换罢了,对main函数内部的变量没有任何影响。再之后进行打印,还是这两个数值。
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
}
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)
}
05 值拷贝
基本数据类型和数组默认都是值传递的,即进行值拷贝。在函数内修改,不会影响到原来的值。
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值
}
07 函数也是一种数据类型
在Go语言中,函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量了,通过该变量可以对函数调用。
上面这句话后面的部分,一句一句解释:
func test(a int) {
fmt.Println(a)
}
func main() {
b := test
//函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量了
fmt.Printf("test函数的类型为:%T\nb的类型为:%T", test, b)
//通过该变量可以对函数调用
b(10)
}
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')
}
如果不满足下面的类型,那就不能够传入进去参数。
函数还可以作为返回值
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)
}
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中编译是别的时候
//仍然认为两者的数据类型不是一样的
}
那么怎么才能让其输出正确?给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 支持对函数返回值命名
做一个改动:
特点
函数和函数之间是并列关系,不影响
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!")
}
【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!")
}
【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"
}
所以,顺序是:1、外部的包中init 2、main中的init 3、main函数。为什么呢?因为导入包的时候,就会调用包中的init函数
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))
}
感受:匿名函数中引用的那个变量会一直保存在内存中,可以一直使用
【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、函数内部的变量,外部是访问不到的。
代码块作用域
高阶函数
函数也是一种类型,它可以作为参数,也可以作为返回值
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!