Go 语言学习笔记(二):函数
目录
函数
函数定义
函数签名和匿名函数
defer
闭包
概念
panic 和 recover
函数
函数是程序执行的一个基本语法结构,Go 语言的很多特性是基于函数这个基础实现的,比如命名类型的方法本质上是一个函数,类型方法是 Go 面向对象的实现基础;接口的底层同样是通过指针和函数将接口和接口实例连接起来的。甚至 Go 并发语法糖 go 后面跟的也是函数。可见函数在 Go 中就是中流砾柱,既能起到"胶水"的作用,也为其他语言特性起到底层支撑的作用。
Go 不是一门纯函数式的编程语言,但是函数在 Go 中是 "第一公民" 表现在 :
(1) 函数是一种类型,函数类型变量可以像其他类型变量一样使用,可以作为其他函数的参数或返回值,也可以直接调用执行。
(2) 函数支持多值返回。
(3) 支持闭包。
(4) 函数支持可变参数。
函数定义
函数是 Go 程序源代码的基本构造单位,一个函数的定义包括如下几个部分:函数声明关键字 func、函数名、参数列表、返回列表和函数体。函数名遵循标识符的命名规则,首字母的大小写决定该函数在其他包的可见性:大写时其他包可见,小写时只有相同的包可以访问;函数的参数和返回值需要使用 " () " 包裹,如果只有一个返回值,而且使用的是非命名的参数 ,则返回参数的 " () " 可以省略。函数体使用 " {} " 包裹,并且 " { " 必须位于函数返回值同行的行尾。
func funcName (param-list) (result- list) {function - body
}
函数的特点
(1) 函数可以没有输入参数,也可以没有返回值。
(2) 多个相邻的相同类型的参数可以使用简写模式。
func add( a,b int) int { // a int, b int 简写为 a , b intreturn a + b
}
(3) 支持有名的返回值,参数名就相当于函数体内最外层的局部变量,命名返回值变量会被初始化为类型零值,最后的 return 可以不带参数名直接返回。
// sum 相当于函数内的局部变量,被初始化为零
func add (a, b int) (sum int) {sum = a + breturn // return sum 的简写模式// sum := a + b // 如果是 sum := a + b,则相当于新声明一个 sum 变量命名返回变量 sum 覆盖// return sum // 最后需要显式地调用 return sum
}
(4) 不支持默认值参数。
(5) 不支持函数重载。
(6) 不支持函数嵌套,严格地说是不支持命名函数的嵌套定义,但支持嵌套匿名函数。
func add (a , b int) (sum int) {anonynous := func(x , y int) int {return x + y}return anonymous(a , b)
}
多值返回
Go 函数支持多值返回,定义多值返回的返回参数列表时要使用 " () " 包裹,支持命名参数的返回。
func swap (a, b int) (int, int) {return b , a
}
习惯用法:如果多值返回值有错误类型,则一般将错误类型作为最后一个返回值。
实参到形参的传递
Go 函数实参到形参的传递永远是值拷贝,有时函数调用后实参指向的值发生了变化,那是因为参数传递的是指针值的拷贝,实参是一个指针变量,传递给形参的是这个指针变量的副本,二者指向同一地址,本质上参数传递仍然是值拷贝。例如:
package main
import "fmt"func chvalue(a int) int {a = a + 1return a
}func chpointer(a *int) {*a = *a + 1return
}func main() {a := 10chvalue(a)fmt.Println(a)chpointer(&a)fmt.Println(a)
}
不定参数
Go 函数支持不定数目的形式参数,不定参数声明使用 param ... type 的语法格式。
函数的不定参数有如下几个特点:
(1) 所有的不定参数类型必须是相同的。
(2) 不定参数必须是函数的最后一个参数。
(3) 不定参数名在函数体内相当于切片,对切片的操作同样适合对不定参数的操作。例如:
func sum(arr ... int) (sum int) {for _, v := range arr { // 此时 arr 就相当于切片,可以使用 range 访问sum += v}return
}
(4) 切片可以作为参数传递给不定参数,切片名后要加上 " ... " 例如:
func sum(arr ... int) (sum int) {for _, v := range arr {sum += v}return
}func main() {slice := []int{1, 2 , 3 , 4}array := [...]int {1 , 2 , 3 , 4}// 数组不可以作为实参传递给不定参数的函数sum (slice ...)
}
(5) 形参为不定参数的函数和形参为切片的函数类型不相同。
函数签名和匿名函数
函数签名
函数类型又叫函数签名,一个函数的类型就是函数定义首行去掉函数名、参数名和 {,可以使用 fmt.Printf 的 "%T" 格式化参数打印函数的类型。
package main
improt "fmt"func add(a, b int) int {return a + b
}func main() {fmt.Printf("%T\n", add) // func(int, int) int
}
两个函数类型相同的条件是:拥有相同的形参列表和返回值列表(列表元素的次序、个数和类型都相同),形参名可以不同。以下 2 个函数的函数类型完全一样:
func add(a,b int) int { return a+b}
func sub (x int, y int) (c int) { c=x- y ; return c }
可以使用 type 定义函数类型,函数类型变量可以作为函数的参数或返回值。
package main
import "fmt"func add(a, b int) int {return a + b
}func sub(a, b int) int {return a - b
}type Op func(int, int) int // 定义一个函数类型,输入的是两个int类型,返回值是一个int类型func do(f Op, a, b int) int {return f(a, b) // 函数类型变量可以直接用来进行函数调用
}func main() {a := do(add, 1, 2)fmt.Println(a)s := do(sub, 1, 2)fmt.Println(s)
}
函数类型和 map、slice、chan 一样,实际函数类型变量和函数名都可以当作指针变量,该指针指向函数代码的开始位置。通常说函数类型变量是一种引用类型,未初始化的函数类型的变量的默认值是 nil。
Go 中函数是 "第一公民"。有名函数的函数名可以看作函数类型的常量,可以直接使用函数名调用函数,也可以直接赋值给函数类型变量,后续通过该变量来调用该函数。
package main
func sum(a, b int) int {return a + b
}func main() {sum(3, 4) // 直接调用f := sum // 有名函数可以直接赋值给变量f(1, 2)
}
匿名函数
Go 提供两种函数:有名函数和匿名函数。匿名函数可以看作函数字面量,所有直接使用函数类型变量的地方都可以由匿名函数代替。匿名函数可以直接赋值给函数变量,可以当作实参,也可以作为返回值,还可以直接被调用。
package main
import "fmt"
// 匿名函数被直接赋值函数变量
var sum = func(a, b int) int {return a + b
}func doinput(f func(int, int) int, a, b int) int {return f(a, b)
}
// 匿名函数作为返回值
func wrap(op string) func(int, int) int {switch op {case "add":return func(a, b int) int {return a + b}case "sub":return func(a, b int) int {return a - b}default:return nil}
}func main() {// 匿名函数直接被调用defer func() {if err := recover(); err != nil {fmt.Println(err)}}()sum(1, 2)// 匿名函数作为实参doinput(func(x, y int) int {return x + y}, 1, 2)opFunc := wrap("add")re := opFunc(2, 3)fmt.Printf("%d\n", re)
}
defer
Go 函数里提供了 defer 关键字,可以注册多个延迟调用( defer 后面的函数在 defer 语句所在的函数执行结束的时候会被调用 ),这些调用以先进后出( FILO )的顺序在函数返回前被执行。这有点类似于 Java 语言中异常处理中的 finaly 子句。 defer 常用于保证一些资源最终一定能够得到回收和释放。
package main
func main() {defer func() {println("first")}()defer func() {println("second")}()println("function body")
}输出:
function body
second
first
defer 后面必须是函数或方法的调用,不能是语句,否则会报 expression in defer must be function call 错误。
defer 函数的实参在注册时通过值拷贝传递进去。下面示例代码中,实参 a 的值在 defer 注册时通过值拷贝传递进去,后续语句 a++ 并不会影响 defer 语句最后的输出结果。
func f() int {a := 0defer func(i int) {println("defer i = ", i)}(a)a++return a
}打印结果:defer i = 0
defer 语句必须先注册后才能执行,如果 defer 位于 return 之后,则 defer 因为没有注册,不会执行。
package mainfunc main() {defer func() {println("first")}()a := 0println(a)return defer func() {println("second")}()
}输出:0
主动调用 os.Exit(int) 退出进程时, defer 将不再被执行(即使 defer 已经提前注册) 。
package main
import "os"func main() {defer func() {println("defer")}()println("func body")os.Exit(1)
}输出:
func body
exit status 1
defer 的好处是可以在一定程度上避免资源泄漏,特别是在有很多 return 语句,有多个资源需要关闭的场景中,很容易漏掉资源的关闭操作。例如:
func CopyFile(dst, src string) (w int64, err error) {src, err := os.Open(src)if err != nil {return }dst, err := os.Create(dst)if err != nil {src.Close() // src很容易忘记关闭return }w, err = io.Copy(dst, src)dst.Close()dst.Close()return
}
使用 defer 改写后,在打开资源无报错后直接调用 defer 关闭资源,一旦养成这样的编程习惯,则很难会忘记资源的释放。例如:
func CopyFile(dst, src string) (w int64, err error) {src, err := os.Open(src)if err != nil {return }defer src.Close()dst, err := os.Create(dst)if err != nil {return}defer dst.Close()w, err = io.Copy(dst, src)return
}
defer 语句的位置不当,有可能导致 panic ,一般 defer 语句放在错误检查语句之后。defer 也有明显的副作用:defer 会推迟资源的释放,defer 尽量不要放到循环语句里面,将大函数内部的 defer 语句单独拆分成一个小函数是一种很好的实践方式。另外,defer 相对于普通的函数调用需要间接的数据结构的支持,相对于普通函数调用有一定的性能损耗。
闭包
概念
闭包是由函数及其相关引用环境组合而成的实体,一般通过在匿名函数中引用外部函数的局部变量或包全局变量构成。
闭包 = 函数 + 引用环境
闭包对闭包外的环境引入是直接引用,编译器检测到闭包,会将闭包引用的外部变量分配到堆上。
如果函数返回的闭包引用了该函数的局部变量(参数或函数内部变量):
(1) 多次调用该函数,返回的多个闭包所引用的外部变量是多个副本,原因是每次调用函数都会为局部变量分配内存 。
(2) 用一个闭包函数多次,如果该闭包修改了其引用的外部变量,则每一次调用该闭包对该外部变量都有影响,因为闭包函数共享外部引用。
package mainfunc fa(a int) func(i int) int {return func(i, int) int {println(&a, a)a = a + ireturn a}
}func main() {f := fa(1) // f 引用的外部的闭包环境包括本次函数调用的形参 a 的值 1g := fa(1) // g 引用的外部的闭包环境包括本次函数调用的形参 a 的值 1// 此时 f、g 引用的闭包环境中的 a 的值并不是同一个,而是两次函数调用产生的副本println(f(1))// 多次调用 f 引用的是同一个副本 aprintln(f(1))// g 中 a 的值仍然是 1println(g(1))println(g(1))
}
f 和 g 引用的是不同的 a。如果一个函数调用返回的闭包引用修改了全局变量,则每次调用都会影响全局变量。如果函数返回的闭包引用的是全局变量 a,则多次调用该函数返回的多个闭包引用的都是同一个 a 。同理,调用一个闭包多次引用的也是同一个 a。此时如果闭包中修改了 a 值的逻辑,则每次闭包调用都会影响全局变量 a 的值。使用闭包是为了减少全局变量,所以闭包引用全局变量不是好的编程方式。
package mainvar (a = 0
)func fa() func(i, int) int {return func(i int) int {println(&a, a)a = a + ireturn a}
}func main() {f := fa() // f 引用的外部的闭包环境包括全局变量 ag := fa() // f 引用的外部的闭包环境包括全局变量 a// 此时,f、g 引用的闭包环境中的 a 的值是同一个println(f(1)) println(g(1))println(g(1))println(g(1))
}
同一个函数返回的多个闭包共享该函数的局部变量。例如:
package main
func fa(base int) (func(int) int, func(int) int) {println(&base, base)add := func(i int) int {base += iprintln(&base, base)return base}sub := func(i int) int {base -= iprintln(&base, base)return base}return add, sub
}func main() {// f、g 闭包引用的 base 是同一个, 是 fa 函数调用传递过来的实参值f, g := fa(0) // s、k 闭包引用的 base 是同一个, 是 fa 函数调用传递过来的实参值s, k := fa(0)// f、g 和 s、k 引用不同的闭包变量,这是由于 fa 每次调用都要重新分配形参println(f(1), g(2))println(s(1), k(2))
}
闭包的价值
闭包最初的目的是减少全局变量,在函数调用的过程中隐式地传递共享变量,有其有用的一面;但是这种隐秘的共享变量的方式带来的坏处是不够直接,不够清晰,除非是非常有价值的地方,一般不建议使用闭包。
对象是附有行为的数据,而闭包是附有数据的行为,类在定义时已经显式地集中定义了行为,但是闭包中的数据没有显式地集中声明的地方,这种数据和行为耦合的模型不是一种推荐的编程模型,闭包仅仅是锦上添花的东西,不是不可缺少的。
panic 和 recover
panic 和 recover 两个内置函数用来处理 Go 的运行时错误( runtime errors )。panic 用来主动抛出错误,recover 用来捕获 panic 抛出的错误。
基本概念
panic 和 recover 的函数签名如下:
panic(i interface{})
revover()interface{}
引发 panic 有两种情况,一种是程序主动调用 panic 函数,另一种是程序产生运行时错误,由运行时检测井抛出。
发生 panic 后,程序会从调用 panic 的函数位置或发生 panic 的地方立即返回,逐层向上执行函数的 defer 语句,然后逐层打印函数调用堆栈,直到被 recover 捕获或运行到最外层函数而退出。
panic 的参数是一个空接口类型 interface{},所以任意类型的变量都可以传递给 panic。调用 panic 的方法非常简单:panic(xxx)。
panic 不但可以在函数正常流程中抛出,在 defer 逻辑里也可以再次调用 panic 或抛出 panic。defer 里面的 panic 能够被后续执行的 defer 捕获。
recover() 用来捕获 panic,阻止 panic 继续向上传递。recover() 和 defer 一起使用,但是 recover() 只有在 defer 后面的函数体内被直接调用才能捕获 panic 终止异常,否则返回 nil,异常继续向外传递。
// 这个会捕获失败
defer recover()// 这个会捕获失败
defer fmt.Println(recover())// 这个嵌套两层也会捕获失败
defer func() {func() {println("defer inner")recover() //无效}()
}()// 如下场景会捕获成功
defer func() {println("defer inner" )recover()
}()
func except() {recover()
}
func test() {defer except()panic("test panic")
}
可以有连续多个 panic 被抛出,连续多个 panic 的场景只能出现在延迟调用里面,否则不会出现多个 panic 被抛出的场景。但只有最后一次 panic 能被捕获。例如:
panic
package main
import "fmt"func main() {defer func() {if err := recover(); err != nil {fmt.Println(err)}}()// 只有最后一次 panic 调用能够捕获defer func() {panic("first defer panic")}()defer func() {panic("second defer panic")}()panic("main body panic")
}
包中 init 函数引发的 panic 只能在 init 函数中捕获,在 main 中无法被捕获,原因是 init 函数先于 main 执行。函数并不能捕获内部新启动的 goroutine 所抛出的 panic。例如:
package main
import ("fmt""time"
)func do() {// 这里并不能捕获 da 函数中的 panicdefer func() {if err := recover(); err != nil {fmt.Println(err)}}()go da()go db()time.Sleep(3 * time.Second)
}func da() {panic("panic da")for i := 0; i < 10; i++ {fmt.Println(i)}
}func db() {for i:= 0; i < 10; i++ {fmt.Println(i)}
}
使用场景一般有两种情况:
(1) 程序遇到了无法正常执行下去的错误,主动调用 panic 函数结束程序运行 。
(2) 在调试程序时,通过主动调用 panic 实现快速退出,panic 打印出的堆栈能够更快地定位错误。为了保证程序的健壮性,需要主动在程序的分支流程上使用 recover() 拦截运行时错误。
Go 提供了两种处理错误的方式,一种是借助 panic 和 recover 的抛出捕获机制,另一种是使用 error 错误类型。
Go 语言学习笔记(二):函数相关推荐
- C语言学习笔记-7.函数
一.函数的使用 1.每个函数在使用之前必须先定义 例:void function(); //有分号 int main() {-} void add() {-} //无分号 2. ...
- C 语言学习笔记(二):编程基础
目录 一.冯诺依曼模型 二.程序语言发展历史 三.进制 3.1 二进制 3.2 八进制 3.3 十六进制 3.4 进制转换:二进制.八进制.十六进制.十进制之间的转换 3.4.1 将二进制.八进制.十 ...
- C语言学习笔记---浮点函数modf()和fmod()
modf函数可以提取出浮点数的整数部分和小数部分.fmod函数可以返回两个浮点数相除的余数.它们的函数原型如下: double __cdecl modf(double _X,double *_Y) ...
- C语言学习笔记---abs()函数和div()函数
C语言库中提供了许多函数,这样需要计算的时候,可以直接借助库函数,而不用自己重新编写函数.今天就来看一下C语言标准库函数里面的整型函数. int __cdecl abs(int _X);long ...
- C语言学习笔记---断言函数assert()
在C语言库函数中提供了一个辅助调试程序的小型库,它是由assert()宏组成,接收一个整形表达式作为参数.如果表达式的值为假(非零),则assert()宏就在标准错误流(stderr)中写入一条错 ...
- C语言学习笔记---指向函数的指针
在内存中函数的存放也是一段连续的内存,函数名就是指向改内存中的首地址,所以也可以将这个函数的首地址赋给一个指针变量,这样通过指针变量就可以访问改函数. 那么为什么要通过指针来访问函数呢?下面通 ...
- C语言学习笔记---打印函数printf()和sprintf()函数
printf()函数 在C语言中使用最多的打印函数就是printf(),它可以将各种类型的数据转换为字符串输出. int main(int argc, char *argv[]) {char na ...
- C语言学习笔记——调用函数时提示警告
[代码] int main() { abc(); abc3(); return 0; } void abc() { int a=65135; double i=8256.67; a=(int)i+a; ...
- C语言学习笔记(二): 简单的C程序设计
数据的表现形式 常量 在C语言中常量有以下几种: 整型常量: 0,-1,100 实型常量: 小数形式(12.12):指数形式(12.1e3= 12.1 × 1 0 3 12.1\times 10^3 ...
最新文章
- Spring in Action 4th 学习笔记 之 AOP
- FreeRTOS(三)——资源管理
- 博主新书:《大数据日知录:架构与算法》目录
- .NET Core开发实战(第34课:MediatR:轻松实现命令查询职责分离模式(CQRS))--学习笔记(上)...
- 7. Deep Learning From Scratch
- NeurIPS’20 | 长尾问题太严重?半监督和自监督就可以有效缓解!
- android 集成x5内核时 本地没有,腾讯浏览服务-接入文档
- 单元测试工具cmocka英文教程,非常漂亮
- 细品慢酌QuickTest关键视图(1)
- [python] 为 pip 更换国内源
- Maven环境变量配置及是否成功
- java 中的radix_Java Scanner radix()方法与示例
- Redis Master/Salve Replication(主从复制模式)
- Python脚本调用谷歌浏览器的谷歌翻译
- aws cloudfront
- Java之throw和throws的区别
- Spring Boot保姆级入门,还不会过来胖我
- 揭秘5G+AI时代的机器人世界!【附报告下载】
- 2021 ICPC 沈阳站总结
- 基于MATLAB的神经网络训练的车牌识别系统