Go语言入门-函数-function
定义
Go语言中函数面向过程编程中最小的模块单元,具备输入、输出的功能,完成从输入到输出的转换,主要是把业务逻辑或者算法分解成具备单一功能,独立解耦的程序块。
函数签名
函数的签名包括:关键字 “func”、函数名称、函数参数、函数返回值。
Go语言的函数定义格式:
//多个返回值,需要用括号包裹起来
func functionName([parameterList]) ([returnTypes]) {
...
body
...
}
//返回值只有一个可以省略返回值需要包裹的括号
func functionName([parameterList]) returnType {
...
body
...
}
- 关键字func用来标示函数的定义声明
- functionName-函数名:首字母不是能数字,同一个包里不能出现重复(即使参数返回值不同)
- parameterList-参数: 在定义时, 用来限定函数的输入的占位符,需要指定输入参数的类型,也可以指定参数的默认变量(形参),调用时可以直接传入变量(实际参数),参数是可选的
- returnTypes-返回值:在定义时,用来限定函数的输出,需要指定输出参数的类型,在调用时主要做右值,赋值给调用方输出结果,当函数没有返回值时可以不需要returnTypes
- body-函数块:用来处理代码逻辑块。
函数参数
-
参数类型简写
当两个或多个连续的函数命名参数是同一类型,则除了最后一个类型之外,其他都可以省略。 -
普通函数定义
//普通函数定义 a,b 形式参数
func testAdd(a int, b int) {
}
func main() {
var c, d int
//普通函数调用 c,d实际参数
testAdd(c, d)
}
a int, b int 为形式参数列表
var c, d int 为形式参数
- 参数类型简写
//省略a的类型
func testAdd(a, b int) {
}
//等价于
func testAdd(a int, b int) {
}
- 变长形参
当函数的输入不固定时,通过在最后一个参数名增加 [… + 参数类型] 来定义改函数可以接收不定长的参数。
//strs ...string 相当于是形式切片
func parseParams(strs ...string) {
fmt.Printf("strs Type %T\n", strs)
for i, str:= range strs {
fmt.Printf("param [%d] = [%s]\n", i, str)
}
fmt.Println("parseParams done")
}
func main() {
parseParams("a", "b", "c")
parseParams("a", "b", "c", "d")
parseParams("a", "b", "c", "d", "e")
}
/**
output:
strs Type []string
param [0] = [a]
param [1] = [b]
param [2] = [c]
parseParams done
strs Type []string
param [0] = [a]
param [1] = [b]
param [2] = [c]
param [3] = [d]
parseParams done
strs Type []string
param [0] = [a]
param [1] = [b]
param [2] = [c]
param [3] = [d]
param [4] = [e]
parseParams done
*/
strs Type []string 可以看出变长的参数实际上对应参数类型的切片
函数形参
是在定义函数名和函数体的时候使用的参数,目的是用来接收调用该函数时传入的参数.形参可以当多函数局部变量。因此不能在函数中在定义同名变量。
func testFuncScope(a int) {
var a int //'a' redeclared in this block --编译报错
fmt.Println(a)
}
函数实参
是在使用函数使用实际要输入的参数,目的是把对应变量的值传递给形式参数。
//普通函数定义 a,b 形式参数
func testAdd(a int, b int) {
}
func main() {
var c, d int
//普通函数调用 c,d实际参数
testAdd(c, d)
}
以上示例中 a、b是形式参数,c、d是实际参数。当函数调用时c、d会传递给函数内部的实际形参变量中。
函数实际参数赋值给形式参数(函数局部变量)都是值传递。也就意味着要函数修改传入的实际参数变量的值,只能通过传递该参数变量的地址才能修改。当然传递进去的地址本质也是值拷贝。虽然引用类型看上去传引用,本质也是传递对应数据接口的指针。
函数返回值
函数返回值,当在函数定义时,定义了返回值的类型和变量,那么意味着在函数定义中不能出现同名变量。可以理解为定义的返回值变量也是局部变量。
单返回值
- 示例1
单个返回值,函数定义的时候没有指定函数返回值变量,那么在调用时直接返回a的副本给调用发。–值传递。
//验证函数返回值
//返回值没有指定1个返回值初始区部变量
func testReturn1() int {
var a int
return a
}
返回值变量,最后可以由 return 隐式返回。
- 示例2
单个返回值,函数定义的时候指定函数返回值变量,那么在函数调用的时候存在局部变量a,return的时候就可以省略 返回值变量。
//返回值指定返回值变量,因此函数在被调用是就会存在区部变量a 当执行return是 就会返回变量a的副本。
func testReturn2() (a int) {
//隐式定义 a 因此编译报错
//var a int //'a' redeclared in this block
a = 10
return
}
- 示例3
返回值变量可以和return后面返回的变量不是同一个,但是类型必须相同,底层会把return后面的变量的值赋值给返回值变量,然后返回返回值变量的副本。
//返回值指定返回值变量,因此函数在被调用是就会存在区部变量a, 当执行return b的时候实际是把b的值赋值给局部变量a,返回a的副本。
func testReturn21() (a int) {
//隐式定义 a 因此编译报错
//var a int //'a' redeclared in this block
a = 10
b := 10
return b
}
多返回值
多返回值和单返回值的使用基本一致,不同的地方是可以返回多个返回值,当两个或多个连续的函数返回值是同一类型,则除了最后一个类型之外,其他都可以省略。
//返回值没有指定2个返回值初始区部变量
func testReturn3()(int, error){
var a int
var e error
return a, e
}
//多返回值只显示返回
//返回值指定返回值变量,因此函数在被调用是就会存在区部变量a e
func testReturn4()(a int, e error){
a = 10
return //可以省略return 后面返回变量,隐式返回 局部变量a和e
}
//多返回值只显示返回
//返回值指定返回值变量,因此函数在被调用是就会存在区部变量a e
func testReturn5()(a int, e error){
a = 10
return a, e // 显示返回a/e
}
//多返回值返回, 返回值类型省略示例
func testReturn6() (a, b int, c, d string) {
return a, b, c, d
}
函数类型
- 函数也是一种类型
- A function type denotes the set of all functions with the same parameter and result types. The value of an uninitialized variable of function type is nil.–只要具备参数类型、返回值类型一致,就是一类函数。声明一个函数类型如果没有初始化,那么值为nil。
- 示例1
演示函数类型
func main() {
//声明一个类型是 func()()的函数变量f,f的特征是没有参数和返回值
var f func()()
//未初始化 值为nil
//(func())(nil)
fmt.Printf("%#v\n", f)
}
/**
output:
(func())(nil)
*/
- 示例2
简单函数变量声明初始化及通过函数变量进行函数调用
func test() {
fmt.Println("test")
}
func main() {
//声明一个类型是 func()()的函数变量f,f的特征是没有参数和返回值
var f func()()
//未初始化 值为nil
//(func())(nil)
fmt.Printf("%#v\n", f)
//f初始化为test
f = test
fmt.Printf("%#v\n", f)
//初始化后f就可以当做函数来使用 调用方式为(函数变量(实际参数列表))
f()
//f() 等价于 test
test()
}
/**
(func())(nil)
(func())(0x49c360)
test
test
*/
- 示例3
定义函数类型,通过函数类型声明对应的函数变量,通过变量保存的不同函数地址,进行灵活的函数调用。
func main() {
//声明一个 myFunc类型的变量没有初始化
var func1 myFunc
fmt.Printf("%#v\n", func1)
//给func1初始化,实际上是把testAdd的地址复制给func1
func1 = testAdd1
//通过func1进行函数调用
result := func1(1, 2)
fmt.Printf("%#v\n", func1)
fmt.Printf("function 's result: %d \n", result)
//给func1初始化,实际上是把testSub的地址复制给func1
func1 = testSub
//调用func1指向的函数testSub
result = func1(1, 2)
fmt.Printf("%#v\n", func1)
fmt.Printf("function 's result: %d \n", result)
}
/**
output:
(main.myFunc)(nil)
testAdd: 3
(main.myFunc)(0x49c360)
function 's result: 3
testSub: -1
(main.myFunc)(0x49c440)
function 's result: -1
*/
函数的作用域
作用域
- 全局变量
全局变量是定义函数外部的变量,再整个程序的生存周期有效,函数可以访问全局变量。
- 局部变量
局部变量是定义在函数、for、select、switch、等语句内部定义的变量。或者在函数中被{}包括的变量定义。
func testVaribleScope() {
{
//0
a := 0
fmt.Printf("\t a = %d\n", a)
}
{
{
//最内侧局部变量
a := 100
fmt.Printf("\t\t a = %d\n", a)
}
a := 2
//2
fmt.Printf("\t a = %d\n", a)
}
//全局变量
fmt.Printf("a = %d\n", a)
a := -1
//局部变量
fmt.Printf("a = %d\n", a)
}
func main() {
//全局变量
fmt.Printf("a = %d\n", a)
testVaribleScope()
}
/**
output:
a = -2
a = 0
a = 100
a = 2
a = -2
a = -1
*/
1. 局部变量中最内层的变量优先使用
2. 全局变量和局部变量同名,优先使用局部变量
函数使用
函数调用
从以上的例子中可以看出定义函数以后,我们可以通过函数名+实际参数列表进行函数调用 或者 定义函数变量并通过实际定义的函数初始化函数类型变量进行函数调用,调用方式为函数变量+实际参数列表
匿名函数
定义及语法
- 定义匿名函数语法:
//多个返回值,需要用括号包裹起来
func ([parameterList]) ([returnTypes]) {
...
body
...
}
- 调用匿名函数语法:
func ([parameterList]) ([returnTypes]) {
...
body
...
}(parameterList)
匿名函数是指没有定义名字符号的函数如下:
- 示例1
定义了个匿名函数并执行,该函数没有参数和返回值。注意函数定义有的()表示执行
func main() {
func(){
fmt.Println("匿名函数调用")
}()
}
/**
output:
匿名函数调用
*/
示例2
定义一个函数变量,并用匿名函数初始化
type MyFunc1 func(int, int)
func main() {
var myfun1 MyFunc1 = func(i int, i2 int) {
fmt.Println("执行匿名函数")
}
myfun1(1, 2)
}
/**
output:
执行匿名函数
*/
示例3
定义一个函数,函数入参是匿名函数类型 ,并且传入具体的匿名函数进行调用
func main() {
//通过函数testFunc调用匿名函数
testFunc(func(a int, b string)(e error){
fmt.Printf("param1=[%d], param2=[%s]\n", a, b)
return nil
}, 10, "2")
//直接调用匿名函数
func(a int, b string)(e error){
fmt.Printf("param1=[%d], param2=[%s]\n", a, b)
return nil
}(10, "2")
//通过函数变量保留匿名函数的地址,然后通过函数变量进行调用
f := func(a int, b string)(e error) {
fmt.Printf("param1=[%d], param2=[%s]\n", a, b)
return nil
}
f(10, "2")
}
/**
output:
param1=[10], param2=[2]
param1=[10], param2=[2]
param1=[10], param2=[2]
*/
以上通过testFunc实现了一个简单的代理类,代理入参是一个int、一个是ring返回值是errors函数。testFunc最后两个参数和匿名函数的参数保持一致。
示例4
匿名函数作为返回值
//匿名函数作为返回值
//MytestFunc2返回一个函数,该函数类型的参数是int,string,返回值是string
func MytestFunc2() func(int, string) (result string) {
return func(o int, s string)(res string){
return fmt.Sprintf("%d%s\n", o, s)
}
}
func main() {
//MytestFunc2()返回匿名函数地址。
//MytestFunc2()(1, "2")匿名函数的调用
//因为MytestFunc2()指向的匿名函数返回一个string, 因此打印匿名函数的结果
fmt.Println(MytestFunc2()(1, "2"))
}
/**
output:
12
*/
将匿名函数赋值给变量,和普通函数提供名字标识符有根本的区别。编译器会为匿名函数生成一个“随机的”符号名
匿名函数是常用的重构手段,可以把大函数分解成多个独立的匿名函数块,实现框架和细节的分离。相比较语句块。匿名函数的作用被隔离(没有闭包的情况下),不会引发外部的污染。较为灵活。没有定义顺序限制。必要时可以抽离、实现干净,清晰的代码层次-------《go语言学习笔记》
至今为止个人还是没有体会到匿名在不用于闭包情况下有何优势。。。这可能就是大佬和彩笔的区别吧
高阶用法-闭包
闭包(Closure)就是一个匿名函数捕获了和它在同一作用域常量或者变量。意味着不管在程序什么地方调用,闭包都能够使用这些常量或者变量,而不需要关注捕获的常量是否已经超过作用域。闭包可以理解为带状态的函数,或者理解为一个特殊的函数,该函数在首次使用时会保留主调函数局部变量中的一个多个变量地。后续在调用闭包函数的时候不需要不需要重新获取主调函数的局部变量(因此携带了状态),直接使用闭包函数保留的变量地址进行相关处理。
- 示例1
简单的闭包演示
func callClosure() func() {
x := 0
return func() {
fmt.Println(x, &x)
x++
}
}
func main() {
//获取返回的匿名函数
f := callClosure()
//打印f的类型
fmt.Printf("f Type %T\n", f)
//执行f指向的匿名函数
f()
f()
}
/**
output:
f Type func()
0 0xc00000a0d8
1 0xc00000a0d8
*/
当初始化变量f(函数变量、函数指针时),就会调用callClosure函数。因此callClosure函数为x分配内存,同时匿名函数是不需要主调函数传递入参就能获取匿名函数同级的变量,也就是能够直接使用主调函数中和匿名函数同级别的变量,当函数callClosure执行结束return的时候会返回匿名函数func() {fmt.Println(x, &x) x++},然后这时候发现x不是在匿名函数中定义的,因此也会一并返回x的地址。当第一次执行因为x已经初始化成0,这是后会打印0,然后增加x的值为2,然后第二次执行匿名函数的时候打印x=2,我们可以发现两次打印的x的地址是相同的。
- 示例2
匿名函数中定义的变量是否带有状态
func callClosure1() func() {
x := 0
return func() {
y := 0
fmt.Println("x =", x, &x)
fmt.Println("y =", y, &y)
x++
}
}
func main() {
//获取返回的匿名函数
f := callClosure1()
//打印f的类型
fmt.Printf("f Type %T\n", f)
//执行f指向的匿名函数
f()
f()
}
/**
f Type func()
x = 0 0xc000072090
y = 0 0xc0000720c0
x = 1 0xc000072090
y = 0 0xc0000720c8
*/
两次打印y的地址不一致,因此y是每次调用闭包函数的时候都会重新分配内存
- 示例3
捕获两个变量的例子
func callClosure3() func() {
x := 0
y := 1000
return func() {
z := y
fmt.Println("x =", x, &x)
fmt.Println("z =", z, &z)
fmt.Println("y =", y, &y)
x++
}
}
func main() {
//获取返回的匿名函数
f := callClosure3()
//打印f的类型
fmt.Printf("f Type %T\n", f)
//执行f指向的匿名函数
f()
f()
}
/**
f Type func()
x = 0 0xc00000a0d8
z = 1000 0xc00000a120
y = 1000 0xc00000a0f0
x = 1 0xc00000a0d8
z = 1000 0xc00000a138
y = 1000 0xc00000a0f0
*/
- 示例4
闭包的延迟求值的特性,多个闭包函数捕获同一个主调函数变量。
func callClosure4() []func() {
var funcList []func()
for i := 0; i < 3; i++ {
//循环三次每次放进去一个闭包函数,该闭包函数捕获了外部变量i,
funcList = append(funcList, func() {
fmt.Println(&i, i)
})
}
return funcList
}
func main() {
for _, f := range callClosure4() {
//循环获取闭包函数,但是闭包函数都捕获的是callClosure4函数中for循环作用域中i,而该i只创建了一次,因此只有一个地址。因此3个闭包函数都是同一个
//i而i的值是3
f()
}
}
/**
output:
0xc00000a0d8 3
0xc00000a0d8 3
0xc00000a0d8 3
*/
示例5
如何解决延迟初始化
func callClosure5() []func() {
var funcList []func()
for i := 0; i < 3; i++ {
//让每次循环捕获的都是新从主调函数中的变量。保证每个闭包函数捕获的变量不是同一个
var x int = i
funcList = append(funcList, func() {
fmt.Println(&x, x)
})
}
return funcList
}
func main() {
for _, f := range callClosure5() {
f()
}
}
/**
output:
0xc00000a0d8 0
0xc00000a0f0 1
0xc00000a0f8 2
*/
保证每一个闭包函数拿到的主调函数的局部变量地址不一样。
避免多个闭包函数同时捕获相同的局部变量。