前言

对于任何语言而言,函数都是结构化编程中的重要一环,通过函数机制,可以把一个大的模块分解成多个小任务,让代码结构更清晰,可复用性大大提高。

本文将介绍如何定义,调用函数,如何定义和接收函数错误,以及 golang 特有的可变参数,闭包,defer,panic 和 recover 等机制。

文章目录

  • 前言
  • 函数定义
  • 可变参数
  • 函数错误处理
  • 闭包 (closures)
  • defer 函数
  • panic 和 recover
    • panic
    • recover

函数定义

Golang 的函数基本组成为:函数名,入参列表,函数体,返回参数列表。

func function_name( [parameter list] ) [return_types] {// 函数体
}// 如:
// func find_max(num1, num2 int) int {}
// func swap(x, y string) (string, string) {}

如果一个函数所有的返回值都有显式的变量名,那么该函数的return语句可以省略操作数。这称之为 bare return,但是不建议使用这种方法,会导致程序的可读性变差。

func CountWordsAndImages(url string) (words, images int, err error) {// 函数体return          // 等价于 return words, images, err
}words, images, err := CountWordsAndImages("/test/test.go")
//...

注意:

  1. 每一次函数调用都必须按照声明顺序为所有参数提供实参(参数值)。在函数调用时,Golang 没有默认参数值,也没有任何方法可以通过参数名指定形参。
  2. 默认情况下,Golang 使用的是值传递,因此函数的形参是实参的拷贝, 在调用过程中对形参进行修改不会影响实参。 如果需要在函数中修改变量,需要传入变量的指针。
  3. 跟 Golang 的变量命名规则一致,如果函数名的首字母为大写,函数为可导出函数,可以在包外被调用,如果为小写,则只能在本包内调用。
  4. Golang 的函数可以有多个返回值,调用时需要依次接收,如果有某个回参不需要被使用,必须要在对应位置使用 blank identifier _ 接收。
  5. 函数作为 Golang 中的 first class values,拥有类型,并且可以被赋值给其他变量,本身也可以作为函数的入参和回参。

可变参数

参数数量可变的函数称为为可变参数函数。例如 fmt.Printf() 函数,首先接收一个必备的参数,之后接收任意个数的后续参数。

func Printf(format string, a ...interface{}) (n int, err error) {return Fprintf(os.Stdout, format, a...)
}

在声明可变参数函数时,只能把可变参数作为参数列表的最后一个参数,在参数类型之前加上省略符号 ...,这表示该函数会接收任意数量的该类型参数。如:

func sum(vals...int) int {total := 0for _, val := range vals {total += val}return total
}fmt.Println(sum()) // 0
fmt.Println(sum(3)) // 3
fmt.Println(sum(1, 2, 3, 4)) // 10fmt.Printf("%T", sum) // func(...int)

实际上,可变参数会被处理为一个slice传递给函数,如果原始参数本身就是一个 slice,则需要使用 ... 先将切片展开,然后再作为参数传递,如:

values := []int{1, 2, 3, 4}
fmt.Println(sum(values...)) // "10"

函数错误处理

内置的 error 类型是接口类型,可能是 nil 或 non-nil,nil 意味着函数运行成功,non-nil 代表运行失败。对于non-nil的error类型, 我们可以通过调用error的Error函数或者输出函数获得字符串类型的错误信息。golang 推荐在任何有可能出现错误的函数中都添加 error 回参来增加程序的鲁棒性,比如 db 读写,接口调用等。

错误相关的方法都为于 errors 包中,在 go 语言的编码风格中,为了程序逻辑的连贯性,通常会将处理失败的逻辑代码放在处理成功的代码之前,并且不需要将正常逻辑放在 else 中。

func f() error {}err := f()
if err != nil {// 错误处理
}// 正常逻辑

当函数出现错误时,通常在子程序中将错误返回给调用层并打印日志,调用层再进行相应的处理。也可以在发生错误时采取重试,回滚等操作。

闭包 (closures)

使用函数字面量(function literal)可以在任何表达式中声明一个函数并使用,这种方法声明的函数没有函数名,称为匿名函数(anonymous function),通常在 defer、go 等情况使用,如:

defer func() {// do something...
}()

在一个函数中定义内部匿名函数时,匿名函数可以访问该函数完整的词法环境 (lexical environment),这意味着在函数中定义的内部函数可以引用该函数的变,如:

// squares 函数的入参为空,返回值为一个匿名函数,该匿名函数的返回值为 int
func squares() func() int {var x int return func() int {x++return x * x}
}func main() {f := squares()fmt.Println(f())       // 1fmt.Println(f())        // 4fmt.Println(f())        // 9g := squares()fmt.Println(g())     // 1fmt.Println(g())        // 4fmt.Println(g())        // 9
}

每次调用 squares 会生成一个局部变量 x 并返回一个匿名函数,每次调用该匿名函数时,函数都会使 x 的值加 1,这意味着匿名内部函数可以引用并修改 squares 的局部变量,也就是说匿名函数和squares中,存在变量引用,这种方式称为闭包。

通过这个例子,可以看出变量的生命周期不由作用域决定,squares 返回后,x 仍隐式的存在于 f 中。

defer 函数

在某些需要释放资源的函数中(如关闭文件,关闭 db,处理互斥锁等),为了保证资源被释放,需要在每一个逻辑中进行处理,如下伪代码:

func demo() {file, err := os.Open(filename)if err != nil {return}操作。。。if err1 != nil {file.Close()return}继续操作。。。if err2 != nil {file.Close()return}正常结束。。。file.Close()return
}

为了确保在所有执行路径下(即使函数运行失败)都释放了资源,在所有的节点都要进行 file.Close() 操作,随着函数变得复杂,需要处理的错误也变多,维护清理逻辑变得越来越困难。而 Go 语言独有的 defer 机制可以很好的解决这个问题。

defer 语句经常被用于处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁,释放资源的defer应该直接跟在请求资源的语句后。通过 defer 机制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放。

当defer语句被执行时,跟在defer后面的函数会被延迟执行,直到包含该 defer 语句的函数执行完毕时,defer 的函数才会被执行,不论包含defer语句的函数是通过return正常结束,还是由于panic导致的异常结束。

使用 defer 修改后的示例如下:

func demo() {file, err := os.Open(filename)if err != nil {return}defer file.Close()操作。。。if err1 != nil {return}继续操作。。。if err2 != nil {return}正常结束。。。return
}

可以在一个函数中执行多条defer语句,它们的执行顺序与声明顺序相反,先 defer 的函数后执行,类似一个栈。

要注意的是尽量不要在循环体中直接加入 defer,否则在所有文件都被处理完之后,defer 才会被执行,在这个过程中,不会有任何一个文件被成功关闭,可能导致系统的文件描述符被耗尽。如:

for _, filename := range filenames {f, err := os.Open(filename)if err ! = nil {return err}defer f.Close()    // 风险操作,可能导致文件描述符耗尽
}

正确的做法是将循环体中的defer语句移至另外一个函数。在每次循环时,调用这个函数。如下:

for _, filename := range filenames {if err := doFile(filename); err ! = nil {return err}
}func doFile(filename string) error {f, err := os.Open(filename)if err ! = nil {return err}defer f.Close()   // defer 在每一次 doFile 执行完毕后都会运行
}

需要注意的是 defer 后面跟着的应该是一个函数调用,而不是函数声明,所以要特别注意括号的使用,尤其是在 defer 中使用匿名函数时,如:

func add(x, y int) int {defer func() {fmt.Println("defer")}()                  // 这个括号不能忘记return x + y
}

panic 和 recover

panic

有些错误在编译时无法被发现,只会在运行时引发,如数组越界、空指针引用等。这些运行时错误会引起 painc 异常,中断程序并且立即执行该 goroutine 中的 defer 函数,随后,程序崩溃并输出日志信息,包括 panic value 和函数调用的堆栈跟踪信息。

也可以手动调用内置的panic函数引发panic异常,如:

if err != nil {panic(fmt.Printf("error: %s", err))
}

但是要注意,由于 panic 会引起程序的崩溃,所以除非是非常严重的错误,否则尽量避免使用 Panic 。对于大部分漏洞,我们应该使用Go提供的错误机制来处理异常,而不是panic,以避免程序的崩溃。在健壮的程序中,任何可以预料到的错误,如不正确的输入、错误的配置或是失败的 I/O 操作都应该被优雅的处理

recover

使用 recover 可以帮助程序从 panic 异常中恢复。或者至少在程序崩溃前做一些处理,例如当web服务器遇到不可预料的严重问题时,在崩溃前将所有的连接关闭,以免客户端一直处于等待状态。

如果在 defer 函数中调用了内置函数recover,并且定义该 defer 语句的函数发生了 panic 异常,recover 会使程序从 panic 中恢复,并返回 panic value,导致panic异常的函数不会继续运行,但能正常返回。

在未发生 panic 时调用 recover,recover 会返回 nil。使用示例如下:

func Parse(input string) (s *Syntax, err error) {defer func() {if p := recover(); p != nil {err = fmt.Errorf("internal error: %v", p)}}()// ...parser...
}

defer 函数帮助 Parse 从 panic 中恢复。在 defer 函数内部,panic value 被附加到错误信息中,并用 err 变量接收错误信息,返回给调用者。

然而,在实际使用中不推荐使用 recover 函数,否则很有可能不能及时的结束已经出错的函数,并导致更加严重的后果。

Golang function 函数详解相关推荐

  1. Java8之function函数详解

    1. function 我们来看下function这个函数式接口里面都有什么方法 下面,我们来仔细研究下以下的方法 1.R apply(T t) 按照以往的逻辑,我们先来看下原始接口是怎么说的 翻译过 ...

  2. 【ES6】Generator函数详解

    [ES6]Generator函数详解 一.Generator函数简介 基本概念 函数写法 yield关键字介绍 二.next方法的参数 三.for...of循环 四.关于普通throw()与Gener ...

  3. MATLAB的iptcheckinput函数详解

    图像处理开发需求.图像处理接私活挣零花钱,请加微信/QQ 2487872782 图像处理开发资料.图像处理技术交流请加QQ群,群号 271891601 iptcheckinput函数详解,这个函数在看 ...

  4. MATLAB的iptchecknargin函数详解

    iptchecknargin函数详解,这个函数在看别人写的M函数时很有用! ipt是image process tools,即图像处理工具的缩写,MALAB里边有一个图像像处理工具箱,里边有一组函数的 ...

  5. QT:常用函数详解--常用操作记录(个人笔记)

    QT:常用函数详解(个人笔记) PS:一下内容个人笔记,要求自己看懂,随笔,阅读体验会很差很差! Qt setContentsMargins()函数 函数原型:void QLayout::setCon ...

  6. [zz]jQuery.extend 函数详解

    JQuery的extend扩展方法:       Jquery的扩展方法extend是我们在写插件的过程中常用的方法,该方法有一些重载原型,在此,我们一起去了解了解.       一.Jquery的扩 ...

  7. python3 内置函数详解

    内置函数详解 abs(x) 返回数字的绝对值,参数可以是整数或浮点数,如果参数是复数,则返回其大小. # 如果参数是复数,则返回其大小.>>> abs(-25) 25>> ...

  8. 在oracle中游标的操作,Oracle中的游标和函数详解

    Oracle中的游标和函数详解 1.游标 游标是一种 PL/SQL 控制结构:可以对 SQL 语句的处理进行显示控制,便于对表的行数据 逐条进行处理. 游标并不是一个数据库对象,只是存留在内存中. 操 ...

  9. Javascript 函数详解

    Javascript 函数详解 1)函数声明: 通过关键字function定义,把函数作为变量来声明 函数声明后不会立即执行,会在我们需要的时候调用到. <script>function ...

最新文章

  1. Paper8:Spatial Pyramid Pooling in Deep Convolutional Networks for Visual Recognition
  2. 在C语言中,double、long、unsigned、int、char类型数据所占字节数
  3. 「基本功」不可不说的Java“锁”事
  4. Oracle database server 安装tips
  5. nginx 禁止通过IP,未绑定域名访问服务器
  6. [html]说说页面中字体渲染规则是怎样的?会有哪些因素影响字体的渲染?
  7. 无心剑中译叶芝诗17首
  8. java怎么给框架添加背景图,java里怎么给Container添加背景图片
  9. 使用Monitor调试Unity3D Android程序日志输出(非DDMS和ADB)
  10. Android(java)学习笔记155:中文乱码的问题处理(qq登录案例)
  11. opencv 图像融合
  12. configure: error: You requested LIBYUV but not found...die
  13. qt 的进程间共享内存 QSharedMemory
  14. 小程序支付一定要后台服务器,2.字节跳动小程序支付配置
  15. linux中dpkg找不到命令_Linux中tree命令的使用
  16. 2022 阿里 java 面经
  17. html中五号字体是多少像素,5号字是多大(字体尺寸对照表mm)
  18. 导出微信添加的自定义表情(动图)
  19. pdf文档动态插入水印,45度角,位于文档中央,可插入中文
  20. PMP项目管理-项目成本管理(3)

热门文章

  1. Photoshop设计Christmas posters
  2. 他山之玉 可以攻石——碧玉太湖石挂件赏
  3. 空心圆圈里的数字,实现
  4. jquery 特效 地址
  5. python excel计算_python计算excel平均值和标准差
  6. 总结QueueUserWorkItem传参的几种方式
  7. 《肖申克的救赎》经典台词,句句触动人心泪流满面
  8. 数据库——男或女性别约束
  9. 龙之信条服务器维护,《龙之信条》终于通关了,说点大家感兴趣的问题吧
  10. 100句恶搞有趣的QQ签名微博语录