目录

函数

函数定义

函数签名和匿名函数

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 语言学习笔记(二):函数相关推荐

  1. C语言学习笔记-7.函数

    一.函数的使用 1.每个函数在使用之前必须先定义 例:void function();       //有分号 int main() {-} void add() {-}       //无分号 2. ...

  2. C 语言学习笔记(二):编程基础

    目录 一.冯诺依曼模型 二.程序语言发展历史 三.进制 3.1 二进制 3.2 八进制 3.3 十六进制 3.4 进制转换:二进制.八进制.十六进制.十进制之间的转换 3.4.1 将二进制.八进制.十 ...

  3. C语言学习笔记---浮点函数modf()和fmod()

      modf函数可以提取出浮点数的整数部分和小数部分.fmod函数可以返回两个浮点数相除的余数.它们的函数原型如下: double __cdecl modf(double _X,double *_Y) ...

  4. C语言学习笔记---abs()函数和div()函数

      C语言库中提供了许多函数,这样需要计算的时候,可以直接借助库函数,而不用自己重新编写函数.今天就来看一下C语言标准库函数里面的整型函数. int __cdecl abs(int _X);long ...

  5. C语言学习笔记---断言函数assert()

      在C语言库函数中提供了一个辅助调试程序的小型库,它是由assert()宏组成,接收一个整形表达式作为参数.如果表达式的值为假(非零),则assert()宏就在标准错误流(stderr)中写入一条错 ...

  6. C语言学习笔记---指向函数的指针

      在内存中函数的存放也是一段连续的内存,函数名就是指向改内存中的首地址,所以也可以将这个函数的首地址赋给一个指针变量,这样通过指针变量就可以访问改函数.   那么为什么要通过指针来访问函数呢?下面通 ...

  7. C语言学习笔记---打印函数printf()和sprintf()函数

    printf()函数   在C语言中使用最多的打印函数就是printf(),它可以将各种类型的数据转换为字符串输出. int main(int argc, char *argv[]) {char na ...

  8. C语言学习笔记——调用函数时提示警告

    [代码] int main() { abc(); abc3(); return 0; } void abc() { int a=65135; double i=8256.67; a=(int)i+a; ...

  9. C语言学习笔记(二): 简单的C程序设计

    数据的表现形式 常量 在C语言中常量有以下几种: 整型常量: 0,-1,100 实型常量: 小数形式(12.12):指数形式(12.1e3= 12.1 × 1 0 3 12.1\times 10^3 ...

最新文章

  1. Spring in Action 4th 学习笔记 之 AOP
  2. FreeRTOS(三)——资源管理
  3. 博主新书:《大数据日知录:架构与算法》目录
  4. .NET Core开发实战(第34课:MediatR:轻松实现命令查询职责分离模式(CQRS))--学习笔记(上)...
  5. 7. Deep Learning From Scratch
  6. NeurIPS’20 | 长尾问题太严重?半监督和自监督就可以有效缓解!
  7. android 集成x5内核时 本地没有,腾讯浏览服务-接入文档
  8. 单元测试工具cmocka英文教程,非常漂亮
  9. 细品慢酌QuickTest关键视图(1)
  10. [python] 为 pip 更换国内源
  11. Maven环境变量配置及是否成功
  12. java 中的radix_Java Scanner radix()方法与示例
  13. Redis Master/Salve Replication(主从复制模式)
  14. Python脚本调用谷歌浏览器的谷歌翻译
  15. aws cloudfront
  16. Java之throw和throws的区别
  17. Spring Boot保姆级入门,还不会过来胖我
  18. 揭秘5G+AI时代的机器人世界!【附报告下载】
  19. 2021 ICPC 沈阳站总结
  20. 基于MATLAB的神经网络训练的车牌识别系统

热门文章

  1. _cdecl和_stdcal的区别
  2. WebSoket 的广泛应用
  3. Linux网络编程 | 高性能定时器 :时间轮、时间堆
  4. Python 实例方法,类方法和静态方法的区别
  5. Spring Boot 静态资源处理,原来如此!
  6. OS- -文件系统(一)
  7. 大话ion系列(一)
  8. 对话实录 | 看华为云如何使能AI计算行业创新
  9. LiveVideoStack线上交流分享 (十七) —— AV1编码器优化与实用落地演进之路
  10. 全球计算机视觉顶会CVPR 2019论文出炉:腾讯优图25篇论文入选