当前位置: 首页 > news >正文

【GoLang #4】:Go 语言 函数详述(定义调用 | 匿名函数 闭包 | defer)

函数定义与调用:函数的基本定义,参数传递,返回值。

golang 函数特点如下

  • 无需声明原型。
  • 支持 不定变参 / 多返回值 / 命名返回参数 / 匿名函数和闭包
  • 函数也是一种类型,一个函数可以赋值给变量。
  • 不支持 嵌套 (nested) 一个包不能有两个名字一样的函数。
  • 不支持 重载 (overload) / 默认参数 (default parameter)。

1. 函数定义和调用

函数声明包含一个函数名,参数列表, 返回值列表和函数体。如果函数没有返回值,则返回列表可以省略。函数从第一条语句开始执行,直到执行return语句或者执行函数的最后一条语句。

  • 函数可以没有参数或接受多个参数。
  • 注意类型在变量名之后 。
  • 当两个或多个连续的函数命名参数是同一类型,则除了最后一个类型之外,其他都可以省略。
  • 函数可以返回任意数量的返回值。

使用关键字 func 定义函数,左大括号依旧不能另起一行。

1.1 基本函数定义

有以下三种函数定义的情况:

// 无参数无返回值
func sayHello() {fmt.Println("Hello, Go!")
}// 带参数无返回值
func greet(name string) {fmt.Printf("Hello, %s!\n", name)
}// 带参数和返回值
func add(a, b int) int {return a + b
}// 函数调用示例
func main() {sayHello()           // Hello, Go!greet("Alice")       // Hello, Alice!result := add(5, 7)  // 12
}

函数是第一类对象,可作为参数传递。建议将复杂签名定义为函数类型,以便于阅读。

func test(fn func() int) int {return fn()
}
// 定义函数类型。
type FormatFunc func(s string, x, y int) string func format(fn FormatFunc, s string, x, y int) string {return fn(s, x, y)
}

有返回值的函数,必须有明确的终止语句,否则会引发编译错误。

1.2 函数参数

函数定义时指出,函数定义时有参数,该变量可称为函数的形参。形参就像定义在函数体内的局部变量。

但当调用函数,传递过来的变量就是函数的实参,函数可以通过两种方式来传递参数:

  • 值传递:指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
  • 引用传递:是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数

但是 Go 语言只有值传递,但通过指针可以实现类似引用传递的效果:

// 值传递(操作副本)
func doubleValue(n int) {n *= 2
}// 指针传递(操作原值)
func doublePointer(n *int) {*n *= 2
}func main() {x := 5doubleValue(x)   // x 不变(仍为5)doublePointer(&x) // x 变为10
}

注意

  1. 在默认情况下,Go 语言使用的是值传递,即在调用过程中不会影响到实际参数。
  2. 无论是值传递,还是引用传递,传递给函数的都是变量的副本,不过,值传递是值的拷贝。引用传递是地址的拷贝,一般来说,地址拷贝更为高效。而值拷贝取决于拷贝的对象大小,对象越大,则性能越低。
  3. map、slice、chan、指针、interface默认以 引用 的方式传递。
1.3 返回值

Go 语言的特色功能 多返回值机制,特别适合错误处理

如下是 多返回值 常见的两个例子:

// 返回商和余数
func div(a, b int) (int, int) {return a / b, a % b
}// 返回值命名
func calc(a, b int) (sum int, diff int) {sum = a + bdiff = a - breturn // 隐式返回命名返回值
}func main() {q, r := div(10, 3) // q=3, r=1s, d := calc(8, 5) // s=13, d=3x, _ := calc(1, 3)
}

上面我们用到了 带名字的返回值,这个又是什么呢?

Go 的返回值可被命名,它们会被视作定义在函数顶部的变量。

  • 返回值的命名应当能反应其含义,它可以作为文档使用。

  • 没有参数的 return 语句会直接返回已命名的返回值,也就是「裸」返回值。

  • 注意:裸返回语句应当仅用在上面这样的短函数中。在长的函数中它们会影响代码的可读

注意:Golang返回值不能用容器对象接收多返回值。只能用多个变量,或 "_" 忽略。

错误处理模式 的应用,举个例子:

func readFile(filename string) ([]byte, error) {data, err := os.ReadFile(filename)if err != nil {return nil, fmt.Errorf("读取文件失败: %w", err)}return data, nil
}func main() {content, err := readFile("config.yaml")if err != nil {log.Fatal(err)}fmt.Println(string(content))
}

2. 匿名函数和闭包

2.1 匿名函数

定义

  • 匿名函数是指 不需要定义函数名的一种函数实现方式。1958年LISP首先采用匿名函数。
  • 在Go里面,函数可以像普通变量一样被传递或使用,Go语言支持随时在代码里定义匿名函数。
  • 匿名函数由一个不带函数名的函数声明和函数体组成。匿名函数的优越性在于可以直接使用函数内的变量,不必申明。

举个最简单的例子:

func main() {// 1. 最简单的匿名函数 func() {fmt.Println("这是一个匿名函数")}()  // 立即执行 -- IIFE// 2. 带参数的匿名函数func(name string) {fmt.Printf("Hello, %s!\n", name)}("张三")// 3. 带返回值的匿名函数 -- 赋值给变量后调用result := func(a, b int) int {return a + b}(3, 5)fmt.Printf("3 + 5 = %d\n", result)
}
  • 初始化该变量时和之前的变量初始化有些不同,使用了func,func是定义函数的,可是这个函数和上面说的函数最大不同就是没有函数名,也就是匿名函数。

同样,我们也可以把一个函数当做 变量/参数传递 一样的操作。

func main() {// 1. 将匿名函数赋值给变量add := func(a, b int) int {return a + b}fmt.Printf("add(10, 20) = %d\n", add(10, 20))// 2. 匿名函数作为参数传递operate := func(fn func(int, int) int, x, y int) int {return fn(x, y)}multiply := func(a, b int) int {return a * b}result2 := operate(multiply, 4, 6)fmt.Printf("operate(multiply, 4, 6) = %d\n", result2)// 3. 作为结构体fns := [](func(x int) int){func(x int) int { return x + 1 },func(x int) int { return x + 2 },}println(fns[0](100)) // 101
}

还可以作为回调函数(和上面匿名函数作为参数传递其实大体相似),如下:

func main(){processNumbers([]int{1, 2, 3}, func(n int) {fmt.Println(n * 2)})
}func processNumbers(nums []int, callback func(int)) {for _, n := range nums {callback(n)}
}
2.2 闭包(词法作用域)

闭包的应该都听过,但到底什么是闭包呢?

  • 闭包是由 函数及其相关引用环境组合而成的实体(即:闭包=函数+引用环境),能够访问其外部作用域变量的函数
  • “官方”的解释是:所谓“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。
  • 维基百科讲,闭包(Closure),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。

看着上面的描述,会发现闭包和匿名函数似乎有些像。可是可能还是有些云里雾里的。因为跳过闭包的创建过程直接理解闭包的定义是非常困难的。

目前在JavaScript、Go、PHP、Scala、Scheme、Common Lisp、Smalltalk、Groovy、Ruby、 Python、Lua、objective c、Swift 以及Java8以上等语言中都能找到对闭包不同程度的支持。通过支持闭包的语法可以发现一个特点,他们都有垃圾回收(GC)机制。

javascript应该是普及度比较高的编程语言了,通过这个来举例应该好理解写。看下面的代码,只要关注script里方法的定义和调用就可以了。

<!DOCTYPE html>
<html lang="zh">
<head><title></title>
</head>
<body> 
</body>
</html>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.2.6/jquery.min.js" type="text/javascript"></script>
<script>
function a(){var i=0;function b(){console.log(++i);document.write("<h1>"+i+"</h1>");}return b;
}$(function(){var c1=a();c1();c1();c1();//a(); //不会有信息输出document.write("<h1>=============</h1>");var c2=a();c2();c2();
});</script>

这段代码有两个特点:

  • 函数b嵌套在函数a内部 函数a返回函数b 这样在执行完 var c1=a() 后,变量c实际上是指向了函数b(),再执行函数 c1() 后就会显示i的值,第一次为1,第二次为2,第三次为3,以此类推。 其实,这段代码就创建了一个闭包。

  • 因为函数a()外的变量 c1引用了函数a()内的函数b(),就是说:

    • 当函数 a() 的内部函数 b() 被函数a()外的一个变量引用的时候,就创建了一个闭包。 在上面的例子中,由于闭包的存在使得函数a()返回后,a中的i始终存在,这样每次执行 c1(),i都是自加1后的值。 从上面可以看出闭包的作用就是在 a() 执行完并返回后,闭包使得Javascript的垃圾回收机制GC不会收回a()所占用的资源,因为a()的内部函数b()的执行需要依赖 a() 中的变量i。

    • 在给定函数被多次调用的过程中,这些私有变量能够保持其持久性。变量的作用域仅限于包含它们的函数,因此无法从其它程序代码部分进行访问。不过,变量的生存期是可以很长,在一次函数调用期间所创建所生成的值在下次函数调用时仍然存在。正因为这一特点,闭包可以用来完成信息隐藏,并进而应用于需要状态表达的某些编程范型中。 下面来想象另一种情况,如果 a() 返回的不是函数 b(),情况就完全不同了。因为 a() 执行完后,b()没有被返回给 a() 的外界,只是被 a() 所引用,而此时 a() 也只会被 b() 引 用,因此函数a()和b()互相引用但又不被外界打扰(被外界引用),函数a和b就会被GC回收。所以直接调用 a();是页面并没有信息输出。

下面来说闭包的另一要素引用环境。c1() 跟c2()引用的是不同的环境,在调用i++时修改的不是同一个i,因此两次的输出都是1。函数a()每进入一次,就形成了一个新的环境,对应的闭包中,函数都是同一个函数,环境却是引用不同的环境。这和 c1()c2() 的调用顺序都是无关的。

下面我来将之前的JavaScript的闭包例子用Go来实现,代码示例

func counter() func() int {count := 0return func() int {count++return count}
}func main() {// 1. 简单闭包示例 -- 变量捕获x := 10closure := func() {fmt.Printf("闭包访问外部变量 x = %d\n", x)  // 访问外部变量}closure()// // 2. 闭包捕获变量 -- 匿名函数形式// counter := func() func() int {//     count := 0  // 外部变量//     return func() int {//         count++  // 修改外部变量//         return count//     }// }// 创建两个独立的计数器c1 := counter()c2 := counter()fmt.Println("计数器1:")fmt.Println(c1())  // 1fmt.Println(c1())  // 2fmt.Println("计数器2:")fmt.Println(c2())  // 1(独立作用域)fmt.Println("再次调用计数器1:")fmt.Println(c1())  // 4
}

可以发现,输出和之前的JavaScript的代码是一致的。具体的原因和上面的也是一样的,这说明Go语言是支持闭包的。

闭包复制的是原对象指针,这就很容易解释延迟引用现象。

func test() func() {x := 100fmt.Printf("x (%p) = %d\n", &x, x)return func() {fmt.Printf("x (%p) = %d\n", &x, x)}
}func main() {f := test()f()
}// 输出
x (0xc42007c008) = 100
x (0xc42007c008) = 100

在汇编层 ,test 实际返回的是 FuncVal 对象,其中包含了匿名函数地址、闭包对象指针。当调 匿名函数时,只需以某个寄存器传递该对象即可。

FuncVal { func_address, closure_var_pointer ... }

① 外部引用函数参数局部变量

func add(base int) func(int) int {return func(i int) int {base += ireturn base}
}func main() {tmp1 := add(10)fmt.Println(tmp1(1), tmp1(2))// 此时tmp1和tmp2不是一个实体了tmp2 := add(100)fmt.Println(tmp2(1), tmp2(2))
}

② 变量共享

func main() {count := 0counter := func() func() {return func() {count++fmt.Printf("share: %d\n", count)return}}c1 := counter()c2 := counter()c1()c1()c2()
}

③ 返回2个闭包

// 返回2个函数类型的返回值
func test(base int) (func(int) int, func(int) int) {// 定义2个函数,并返回// 相加add := func(i int) int {base += ireturn base}// 相减sub := func(i int) int {base -= ireturn base}// 返回return add, sub
}func main() {f1, f2 := test(10)// base一直是没有消fmt.Println(f1(1), f2(2))// 此时base是9fmt.Println(f1(3), f2(4))
}
2.3 递归函数

递归,就是在运行的过程中调用自己。 一个函数调用自己,就叫做递归函数。

构成递归需具备的条件:

  1. 子问题须与原始问题为同样的事,且更为简单。
  2. 不能无限制地调用本身,须有个出口,化简为非递归状况处理

场景一:数字阶乘

func factorial(i int) int {if i <= 1 {return 1}return i * factorial(i-1)
}func main() {var i int = 7fmt.Printf("Factorial of %d is %d\n", i, factorial(i))
}

场景二:斐波那契数列

func fibonaci(i int) int {if i == 0 {return 0}if i == 1 {return 1}return fibonaci(i-1) + fibonaci(i-2)
}func main() {var i intfor i = 0; i < 10; i++ {fmt.Printf("%d ", fibonaci(i))}
}

3. defer 延迟执行

特点如下

  1. 关键字 defer 用于注册延迟调用,这些调用直到 return 前才被执行。因此可用来做资源清理。
    • 理解:会将函数推迟到外层函数返回之后执行,推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都 不会被调用
  2. 多个defer语句,按 先进后出 的方式执行。
  3. defer语句中的变量,在defer声明时就决定了。

用途:关闭文件句柄、锁资源释放、数据库连接释放

举个最简单的例子,如下:

func main() {defer fmt.Println("world")fmt.Println("hello")
}// 输出
hello
world

defer 栈:推迟调用的函数调用会被压入一个栈中。 当外层函数返回时,被推迟的调用会按照 后进先出 的顺序调用

  • 毕竟后面的语句会依赖前面的资源,因此如果先前面的资源先释放了,后面的语句就没法执行了
func main() {fmt.Println("counting")for i := 0; i < 10; i++ {defer fmt.Printf("%d ", i)}fmt.Println("done")
}// 输出
counting
done
9 8 7 6 5 4 3 2 1 0 

defer 闭包

func main() {var whatever [5]struct{}for i := range whatever {defer func() { fmt.Print("%d ", i) }() // 4 3 2 1 0}
}

但是在 之前版本的 Go 中,输出的 i 可能全是 4,因为 闭包用到的变量 i 在执行的时候已经变成4

defer f.Close

type Test struct {name string
}func (t *Test) Close() {fmt.Println(t.name, " closed")
}
func main() {ts := []Test{{"a"}, {"b"}, {"c"}}for _, t := range ts {// 在之前版本可能会出现调用的全是 c  closed 的情况, 因为闭包执行的时候最后刚好是 c// 之前的解决办法// t2 := t// defer t2.Close() defer t.Close() // 现在不用直接这样写就行}
}

4. 函数高级特性

4.1 可变参数

不定参数传值 就是函数的参数不是固定的,后面的类型是固定的。(可变参数)

  • Golang 可变参数本质上就是 slice。只能有一个,且必须是最后一个。
  • 在参数赋值时可以不用用一个一个的赋值,可以直接传递一个数组或者切片,特别注意的是在参数后加上 即可。
func myfunc(args ...int) {    //0个或多个参数
}func add(a int, args…int) int {    //1个或多个参数
}func add(a int, b int, args…int) int {    //2个或多个参数
}

注意:其中args是一个slice,我们可以通过 arg[index] 依次访问所有参数,通过 len(arg) 来判断传递参数的个数.

  • 任意类型的不定参数: 就是函数的参数和每个参数的类型都不是固定的。
  • interface{} 传递任意类型数据是Go语言的惯例用法,而且 interface{}类型安全 的。
func myfunc(args ...interface{}) {
}

代码

func sum(numbers ...int) int {total := 0for _, num := range numbers {total += num}return total
}func main() {fmt.Println(sum(1, 2, 3))       // 6fmt.Println(sum([]int{4, 5, 6}...)) // 15 slice... 展开slice
}

注意:使用 slice 对象做变参时,必须展开。(slice...)

4.2 函数作为参数和返回值

多返回值可直接作为其他函数调用实参。

type Operation func(int, int) intfunc calculate(a, b int, op Operation) int {return op(a, b)
}func getOperator(op string) Operation {switch op {case "add":return func(a, b int) int { return a + b }case "multiply":return func(a, b int) int { return a * b }default:return nil}
}func main() {add := getOperator("add")fmt.Println(calculate(3, 4, add)) // 7
}

命名返回参数

命名返回参数可看做与形参类似的局部变量,最后由 return 隐式返回

func add(x, y int) (z int) {z = x + yreturn
}func main() {println(add(1, 2)) // 3
}

注意:命名返回参数可被同名局部变量遮蔽,此时需要显式返回。如下:

func add(x, y int) (z int) {{ // 不能在一个级别,引发 "z redeclared in this block" 错误。var z = x + y// return   // Error: z is shadowed during returnreturn z // 必须显式返回。}
}

同样,命名返回参数允许 defer 延迟调用通过闭包读取和修改

func add(x, y int) (z int) {defer func() {z += 100}()z = x + yreturn
}func main() {println(add(1, 2)) 
}

注意:显式 return 返回前,会先修改命名返回参数

func add(x, y int) (z int) {defer func() {println(z) // 输出: 203}()z = x + yreturn z + 200 // 执行顺序: (z = z + 200) -> (call defer) -> (return)
}func main() {println(add(1, 2)) // 输出: 203
}
4.3 nil 函数调用
var fn func()func main() {if fn == nil {fmt.Println("函数未初始化")}fn() // panic: call of nil function
}

在这里插入图片描述

http://www.dtcms.com/a/303453.html

相关文章:

  • windows clion远程连接ubuntu运行调试nginx-1.22.1版本
  • 【优先级高,先补充】基于文本增强跨模态特征交互注意网络的多模态情感分析
  • SVN与GIT的区别,分别使用与哪些管理场景?
  • 《汇编语言:基于X86处理器》第10章 结构和宏(2)
  • Linux——线程池的模拟实现
  • 解决c++静态成员编译报错:‘xxx‘ is not a member of ‘xxx‘ 问题
  • 第五届先进算法与神经网络国际学术会议(AANN 2025)
  • vue项目进首页不加载全部资源
  • 【数据结构初阶】--二叉树(三)
  • ICDC自动化部署方案概述
  • 如何规范化项目执行
  • 2024年7月19日全国青少年信息素养大赛图形化(Scratch)编程小学低年级组复赛真题+答案解析
  • KubeSphere离线部署Kubernetes集群
  • “量子通信”
  • 系统远程配置
  • 概率有限自动机定义与示例
  • 智慧社区项目开发(二)——基于 JWT 的登录验证功能实现详解
  • 吃透 lambda 表达式(匿名函数)
  • mysql详细知识点
  • python中类变量 __slots__ 解析
  • Matplotlib(三)- 图表辅助元素
  • Vue3判断对象是否为空方法
  • 飞鹤困局:增长神话的裂痕
  • 嵌软面试——通信协议
  • 7.项目起步(1)
  • 1.vue体验
  • 快速构建基于React.js的用户注册与登录的Web应用程序
  • vue element 封装表单
  • 代码随想录算法训练营第三十三天
  • 7.28PBR技术