go/函数

go/函数

函数定义

c 复制代码
func 函数名(参数)(返回值){
    函数体
}
- 函数名:由字母、数字、下划线组成。但函数名的第一个字母不能是数字。在同一个包内,函数名也称不能重名(包的概念详见后文)。
- 参数:参数由参数变量和参数变量的类型组成,多个参数之间使用`,`分隔。
- 返回值:返回值由返回值变量和其变量类型组成,也可以只写返回值的类型,多个返回值必须用`()`包裹,并用`,`分隔。
- 函数体:实现指定功能的代码块。
c 复制代码
函数可以接收参数,参数类型在参数名称后面指定,可以指定多个参数。

func add(a int, b int) int {
    return a + b
}

// 调用函数
result := add(5, 7)
fmt.Println(result) // 输出: 12

函数可以有返回值,可以有多个返回值。返回值类型在参数列表后面指定。

c 复制代码
func swap(x, y int) (int, int) {
    return y, x
}

a, b := swap(3, 4)
fmt.Println(a, b) // 输出: 4 3

命名返回值

可以为返回值命名,这样可以在函数体内直接使用该名称。

c 复制代码
func divide(x, y float64) (result float64) {
    if y != 0 {
        result = x / y
    }
    return // 使用命名返回值
}

函数可以返回切片(slice)作为返回值,切片是一种动态大小的数组,可以方便地处理集合类型的数据。

c 复制代码
package main

import "fmt"

// 定义一个返回切片的函数
func generateNumbers(n int) []int {
    numbers := make([]int, n) // 创建一个长度为n的切片
    for i := 0; i < n; i++ {
        numbers[i] = i + 1 // 填充切片
    }
    return numbers // 返回切片
}

func main() {
    result := generateNumbers(5) // 调用函数并获取返回的切片
    fmt.Println("Generated Numbers:", result) // 输出: Generated Numbers: [1 2 3 4 5]
}

可变参数函数

可以使用...语法定义可变参数函数,以便接受任意数量的参数。

c 复制代码
func sum(nums ...int) int {
    total := 0
    for _, num := range nums {
        total += num
    }
    return total
}

totalSum := sum(1, 2, 3, 4) // 接受多个参数
fmt.Println(totalSum) // 输出: 10

函数的可变参数实际上是通过切片实现的。

使用...语法来定义可变参数时,Go会将传入的多个参数收集到一个切片中。

从技术上讲,这使得函数内部可以像处理切片一样处理这些参数

传递切片

另外,如果你已经有一个切片,并希望将其作为可变参数传递给函数,可以使用展开操作符...:

c 复制代码
func main() {
    slice := []int{1, 2, 3, 4, 5}
    result := sum(slice...) // 使用 ... 来展开切片
    fmt.Println("Sum:", result) // 输出: Sum: 15
}

函数类型与变量

1. 定义函数类型

  1. 我们可以使用type关键字来定义一个函数类型,具体格式如下:
c 复制代码
package main

import (
    "fmt"
)

// 定义一个函数类型
type IntOperation func(int, int) int

// 定义两个具体的函数符合这个类型
func add(a int, b int) int {
    return a + b
}

func multiply(a int, b int) int {
    return a * b
}
在这个例子中,IntOperation是一个函数类型,它接受两个int参数并返回一个int值

2. 使用函数类型

定义完函数类型后,你可以使用这个类型来声明变量,并将具体的函数赋给这些变量。

c 复制代码
func main() {
    // 声明一个变量,类型为 IntOperation
    var operation IntOperation

    // 将具体的函数赋值给该变量
    operation = add
    fmt.Println("Addition:", operation(5, 3)) // 输出: Addition: 8

    // 重新赋值为另一个函数
    operation = multiply
    fmt.Println("Multiplication:", operation(5, 3)) // 输出: Multiplication: 15
}

3. 函数作为参数

你还可以将你定义的函数类型作为参数传递给其他函数,增加了函数的灵活性。

c 复制代码
func performOperation(a int, b int, op IntOperation) int {
    return op(a, b) // 调用传入的函数
}

func main() {
    result := performOperation(5, 3, add)
    fmt.Println("Result of addition:", result) // 输出: Result of addition: 8

    result = performOperation(5, 3, multiply)
    fmt.Println("Result of multiplication:", result) // 输出: Result of multiplication: 15
}

4. 函数作为返回值

函数类型也可以用作返回值,这样你可以返回一个函数。

c 复制代码
func createOperator(opType string) IntOperation {
    if opType == "add" {
        return add
    }
    return multiply
}

func main() {
    operation := createOperator("add")
    fmt.Println("Result of addition:", operation(5, 3)) // 输出: Result of addition: 8

    operation = createOperator("multiply")
    fmt.Println("Result of multiplication:", operation(5, 3)) // 输出: Result of multiplication: 15
}

匿名函数和闭包

1. 匿名函数 匿名函数多用于实现回调函数和闭包。

匿名函数是指没有名称的函数。它可以被赋值给变量、作为参数传递或作为返回值返回。匿名函数非常适合用于一次性执行的操作。

c 复制代码
package main

import "fmt"

func main() {
    // 定义一个匿名函数并立即调用
    func() {
        fmt.Println("This is an anonymous function.")
    }() // 立即调用

    // 将匿名函数赋值给变量
    greet := func(name string) {
        fmt.Printf("Hello, %s!\n", name)
    }
    greet("Go") // 调用赋值的匿名函数
}

闭包 闭包指的是一个函数和与其相关的引用环境组合而成的实体。简单来说,闭包=函数+引用环境

闭包是一个函数值,它引用了其定义范围内的变量。

当一个函数被嵌套在另一个函数中时,外部函数的作用域(包括其中变量)可以被内部的匿名函数所访问。

闭包可以记住并访问它创建时的环境。

c 复制代码
package main

import "fmt"

// 生成闭包的函数
func makeCounter() func() int {
    count := 0
    // 返回一个匿名函数,形成闭包
    return func() int {
        count++ // 访问外部变量
        return count
    }
}

func main() {
    counter := makeCounter() // 创建一个闭包
    fmt.Println(counter()) // 输出: 1
    fmt.Println(counter()) // 输出: 2
    fmt.Println(counter()) // 输出: 3

    // 每次调用 counter() 时,都能访问并修改 count 变量
}
c 复制代码
func makeSuffixFunc(suffix string) func(string) string {
	return func(name string) string {
		if !strings.HasSuffix(name, suffix) {
			return name + suffix
		}
		return name
	}
}
makeSuffixFunc是一个高阶函数,接受一个字符串类型的参数suffix,并返回一个匿名函数。
返回的匿名函数也接受一个字符串类型的参数name,并检查这个名字是否已经带有指定的后缀。
使用strings.HasSuffix函数来检查name是否以suffix结尾。如果没有结尾,则在name后添加suffix。

func main() {
	jpgFunc := makeSuffixFunc(".jpg")
	txtFunc := makeSuffixFunc(".txt")
	fmt.Println(jpgFunc("test")) //test.jpg
	fmt.Println(txtFunc("test")) //test.txt
}
jpgFunc和txtFunc是通过调用makeSuffixFunc函数创建的两个闭包,分别用于处理.jpg和.txt后缀。
当调用jpgFunc("test")时,它返回了"test.jpg",而调用txtFunc("test")返回了"test.txt"。
这说明这些闭包记住了它们创建时的特定后缀。
c 复制代码
calc函数接受一个整数参数base,并返回两个函数:一个用于加法add,一个用于减法sub。
匿名函数add接收一个参数i,将其添加到base上,并返回新的base值。
匿名函数sub接收一个参数i,从base中减去它,并返回新的base值。
这两个函数都形成闭包,能够访问并修改base变量。

func calc(base int) (func(int) int, func(int) int) {
	add := func(i int) int {
		base += i
		return base
	}

	sub := func(i int) int {
		base -= i
		return base
	}
	return add, sub
}

在main函数中,调用calc(10),并将返回的两个函数赋值给f1和f2。
f1和f2分别用于加法和减法。
当调用f1(1)时,base从10变为11,并返回11。
当调用f2(2)时,base在减去2后变为9,并返回9。
重复的调用显示了base的状态是持续的,第一次调用后base就会更新到新的值,这正是闭包的作用。

func main() {
	f1, f2 := calc(10)
	fmt.Println(f1(1), f2(2)) //11 9
	fmt.Println(f1(3), f2(4)) //12 8
	fmt.Println(f1(5), f2(6)) //13 7
}

defer语句 后进先出(LIFO)

Go语言中的defer语句会将其后面跟随的语句进行延迟处理。

在defer归属的函数即将返回时,将延迟处理的语句按defer定义的逆序进行执行,也就是说,

先被defer的语句最后被执行,最后被defer的语句,最先被执行。

c 复制代码
func main() {
	fmt.Println("start")
	defer fmt.Println(1)
	defer fmt.Println(2)
	defer fmt.Println(3)
	fmt.Println("end")
}
start
end
3
2
1

由于defer语句延迟调用的特性,所以defer语句能非常方便的处理资源释放问题。比如:资源清理、文件关闭、解锁及记录时间等。

当你使用defer关键字时,它会将随后的函数调用推迟到包含它的函数执行完毕后再执行。

无论函数是正常返回还是因错误而提前返回,defer语句所指向的函数都会被调用。

defer与函数参数

defer注册要延迟执行的函数时该函数所有的参数都需要确定其值

c 复制代码
func main() {
    x := 10
    defer fmt.Println("Value of x:", x) // 这里的x会在defer定义时被捕获
    x = 20
    fmt.Println("Main function is executing")
}
Main function is executing
Value of x: 10

例题1

c 复制代码
package main

import "fmt"

// 函数 f1 返回 int 类型
func f1() int {
    x := 5 // 定义局部变量 x,初始化为 5
    // defer 语句,定义一个匿名函数,在 f1 返回之前执行
    defer func() {
        x++ // 这个函数会在 f1 返回后执行,增加 x 的值
    }()
    return x // 返回 x 的当前值,即 5
}

x初始化为5,然后执行defer语句,该匿名函数会在函数执行完毕返回值前被调用。
由于return x语句会在 defer 执行之前完成,所以最终 f1 的返回值是 5。
defer 中的函数会在返回值已确定之后再执行,因此并不会影响函数的返回结果。

// 函数 f2 返回一个命名返回值 x 的 int 类型
func f2() (x int) {
    // defer 语句,定义一个匿名函数,在 f2 返回之前执行
    defer func() {
        x++ // 这个函数会在 f2 返回后执行,增加 x 的值
    }()
    return 5 // 返回值 x 的当前值会被赋为 5
}
这里,x 是一个命名返回值,默认初始化为 0。
当执行 return 5 时,x 的值被赋为 5。然后,defer 中的匿名函数会在 f2 返回之前执行,增加 x 的值。
因此,f2 最终返回 6。


// 函数 f3 返回一个命名返回值 y 的 int 类型
func f3() (y int) {
    x := 5 // 定义局部变量 x,初始化为 5
    // defer 语句,定义一个匿名函数,在 f3 返回之前执行
    defer func() {
        x++ // 这个函数会在 f3 返回后执行,增加 x 的值
    }()
    return x // 返回 x 的当前值,即 5
}
这里,局部变量 x 初始化为 5,而命名返回值 y 默认初始化为 0。
当调用 return x 时,y 将得到 x 的值,也就是 5。
defer 中的函数同样会在 f3 返回之前执行,但它增加的是 x 的值而不是 y,这意味着 y 最终仍然是 5。



// 函数 f4 返回一个命名返回值 x 的 int 类型
func f4() (x int) {
    // defer 语句,定义一个匿名函数,并将 x 作为参数传递
    defer func(x int) {
        x++ // 这个函数会在 f4 返回后执行,增加参数 x 的值
    }(x) // 在这里,传递的是函数开头 x 的初始值
    return 5 // 返回值 x 的当前值会被赋为 5
}
在 f4 中,命名返回值 x 默认初始化为 0,return 5 会把 x 赋值为 5。
然而,在 defer 中传递的是 x 的初始值(在函数开始时的状态),即在 defer 执行时会增加传递的值,而不会影响命名返回值 x。
因此,最终返回的值仍然是 5。

// 主函数
func main() {
    // 输出 f1 的返回值
    fmt.Println(f1()) // 输出: 5
    // 输出 f2 的返回值
    fmt.Println(f2()) // 输出: 6
    // 输出 f3 的返回值
    fmt.Println(f3()) // 输出: 5
    // 输出 f4 的返回值
    fmt.Println(f4()) // 输出: 5
}

例题2

c 复制代码
package main

import "fmt"

// 定义一个计算与输出函数
func calc(index string, a, b int) int {
	ret := a + b // 计算 a + b
	fmt.Println(index, a, b, ret) // 打印当前的 index 和计算结果
	return ret // 返回计算结果
}

func main() {
	x := 1 // 初始化变量 x 为 1
	y := 2 // 初始化变量 y 为 2

	// 使用 defer 语句,调用 calc 函数
	defer calc("AA", x, calc("A", x, y)) 

	x = 10 // 将 x 改为 10

	// 使用 defer 语句,调用 calc 函数
	defer calc("BB", x, calc("B", x, y))

	y = 20 // 将 y 改为 20
}
执行过程
首先,在 main 函数开始时,x 初始化为 1,y 初始化为 2。

第一个 deffered 调用:

defer calc("AA", x, calc("A", x, y)) 被调用时,由于 defer 的特性,它会在 main 函数返回之前执行。
在执行 defer 语句时,calc("A", x, y) 会立即被调用,计算 calc("A", 1, 2):
此时 x=1 和 y=2,计算结果 1 + 2 = 3。
输出: A 1 2 3
函数 calc 返回值 3,因此 defer 语句会变为 defer calc("AA", x, 3)。
修改 x:

接下来,x 被改为 10。当前状态为 x=10 和 y=2。
第二个 deffered 调用:

defer calc("BB", x, calc("B", x, y)) 在此时被调用:
calc("B", x, y) 立即被调用,计算 calc("B", 10, 2):
此时 x=10 和 y=2,计算结果为 10 + 2 = 12。
输出: B 10 2 12
函数 calc 返回值 12,所以第二个 defer 语句变为 defer calc("BB", 10, 12)。
修改 y:

最后,y 被改为 20,但这不会影响已经保存的 defer。
c 复制代码
A 1 2 3
B 10 2 12
BB 10 12 22
AA 1 3 4

异常机制

1. panic

panic 函数用于触发一个运行时错误,导致程序的控制流转向当前执行栈的顶部。

通常,使用 panic 来表示不可恢复的错误,例如数组越界或访问不存在的元素。

c 复制代码
package main

import "fmt"

func causePanic() {
    panic("This is a panic!") // 触发一个 panic
}

func main() {
    fmt.Println("Program started")
    causePanic() // 调用会导致 panic 的函数
    fmt.Println("This line will not be reached") // 这行不会被执行
}
Program started
panic: This is a panic!

在触发 panic 后,程序的控制流会转到调用栈的顶部,并且不会执行 causePanic 后面的任何代码。

2. recover

recover 是用来恢复因 panic 导致的程序崩溃的机制。它通常与 defer 结合使用,可以用于捕获并处理 panic。

当 recover 被调用时,它会取得 panic 传递的信息,并且程序的控制流将恢复到 recover 之后的代码。

c 复制代码
程序在调用 causePanic 时触发了 panic,但随后通过 recover 捕获了异常,使得程序没有崩溃。

package main

import "fmt"

// causePanic 函数定义
func causePanic() {
    panic("This is a panic!") // 触发一个 panic,程序将会崩溃,并输出信息
}

// recoverFromPanic 函数定义
func recoverFromPanic() {
    // 使用 recover 函数捕获 panic,如果没有发生 panic,r 会是 nil
    if r := recover(); r != nil {
        fmt.Println("Recovered from panic:", r) // 如果捕获到 panic,打印出 panic 的信息
    }
}

func main() {
    defer recoverFromPanic() // 使用 defer 注册 recoverFromPanic 函数,以便在 main 函数结束前调用

    fmt.Println("Program started") // 程序开始,输出提示信息
    causePanic() // 调用会导致 panic 的函数,这里会触发 panic

    fmt.Println("This line will not be reached") // 这行不会被执行,因为发生了 panic
}

Program started
Recovered from panic: This is a panic!

3. 使用场景

当遇到不应被恢复的严重错误时,可以使用 panic,如出错状态、调用运行时错误或者不可能完成的操作。

使用 recover 处理 panic,可以防止应用程序崩溃,允许程序继续执行后续的逻辑。通常会在库或复杂的应用程序中用到。

Go语言的异常处理机制通过 panic 和 recover 提供了一种简单而有效的错误处理方式。

使用 panic 可以指示程序发生了严重错误,而使用 recover 则能够在这种情况下允许程序恢复并继续运行。

defer 是管理 recover 非常有效的工具,并使异常处理逻辑更加清晰和可控。

c 复制代码
package main

import "fmt"

func riskyFunction() {
    panic("Something went wrong!") // 模拟某个出错函数
}

func safeFunction() {
    defer recoverFromPanic() // 确保恢复函数从 panic 中恢复
    riskyFunction() // 调用可能触发 panic 的函数
    fmt.Println("This line won't be executed if panic occurs")
}

func recoverFromPanic() {
    if r := recover(); r != nil {
        fmt.Println("Recovered from panic:", r) // 输出 panic 信息
    }
}

func main() {
    safeFunction() // 调用安全函数
    fmt.Println("Program continues to run.") // 程序继续执行
}
相关推荐
biomooc14 分钟前
R 语言 | 绘图的文字格式(绘制上标、下标、斜体、文字标注等)
开发语言·r语言
骇客野人17 分钟前
【JAVA】JAVA接口公共返回体ResponseData封装
java·开发语言
black^sugar19 分钟前
纯前端实现更新检测
开发语言·前端·javascript
404NooFound24 分钟前
Python轻量级NoSQL数据库TinyDB
开发语言·python·nosql
用余生去守护1 小时前
python报错系列(16)--pyinstaller ????????
开发语言·python
数据小爬虫@1 小时前
利用Python爬虫快速获取商品历史价格信息
开发语言·爬虫·python
向宇it1 小时前
【从零开始入门unity游戏开发之——C#篇25】C#面向对象动态多态——virtual、override 和 base 关键字、抽象类和抽象方法
java·开发语言·unity·c#·游戏引擎
莫名其妙小饼干2 小时前
网上球鞋竞拍系统|Java|SSM|VUE| 前后端分离
java·开发语言·maven·mssql
十年一梦实验室2 小时前
【C++】sophus : sim_details.hpp 实现了矩阵函数 W、其导数,以及其逆 (十七)
开发语言·c++·线性代数·矩阵
最爱番茄味2 小时前
Python实例之函数基础打卡篇
开发语言·python