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. 定义函数类型
- 我们可以使用
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.") // 程序继续执行
}