前端时间的繁忙,未曾更新go语言系列。由于函数非常重要,为此将本篇往前提一提,另外补充一些有关go新版本前面遗漏的部分。
需要恭喜你的事情是本篇学完,go语言中基础部分已经学完一半,这意味着你可以使用go语言去解决大部分的Leetcode
的题,为此后面的1篇,将带领大家去巩固go语言的学习成果
函数min
和max
在go 1.21
已经被实现,不过由于其建立在go1.18
泛型实现的基础上,需要在后面提到泛型的时候讲到。
从go1.1
时代一直这样一个坑或者面试题是这样的:
题目1.
s := []int{1, 2, 3, 4}
var prints []func()
for _, v := range s {
prints = append(prints, func() { fmt.Println(v) })
}
for _, print := range prints {
print()
}
或者是
题目2
s := []int{1, 2, 3, 4}
var ps []*int
for _, v := range s {
ps = append(ps,&v)
}
for _, v := range ps {
fmt.Println(*v)
}
或者是
题目3
var prints []func()
for i:= 1; i <= 4; i++{
prints = append(prints, func() { fmt.Println(i) })
}
for _, print := range prints {
print()
}
题目1,2,3的答案都不是1,2,3,4
,而分别是反直觉的4个4
,4个4
和4个5
在go1.21
时代中,你加入环境变量GOEXPERIMENT=loopvar
,这些题目的答案就统一变为1,2,3,4
。
这里需要提醒面试官们更新自己的面试题了,对于各位面试者来说,这个知识点反杀面试官,想想是不是很帅呢?
等会,面试官也不知道这个知识把你刷了,那么这样的公司不去也罢(这家公司根本不关注语言的更新,你想这个公司能给你多少成长?)。
panic在go1.21
后函数panic
的参数时nil
,那么recover
不会再受到nil
,而是类型为*runtime.PanicNilError
的错误。
defer func() {
if err := recover(); err != nil {
fmt.Println("error:", err)
}
fmt.Println("end")
}()
panic(nil)
后面为了简单起见,我指导AI完成这方面的写作,如果你觉得这些知识较为简单,可以略过不看。
Go语言的函数结构声明简单明了,由关键字func
、函数名、参数列表、返回类型以及函数体组成。下面是一个基本的Go语言函数的结构示例:
func functionName(parameter1 type1, parameter2 type2) returnType {
// 函数体
// 执行语句
return value
}
让我们来详细了解一下每个部分的作用:
func
:这是Go语言中定义函数的关键字,它表示接下来要定义一个函数。functionName
:这是函数的名称,你可以根据自己的需求为函数命名。函数名应该具有描述性,能够清晰地表达函数的功能。parameter1, parameter2
:这些是函数的参数,你可以根据需要定义任意数量的参数。每个参数都有一个名称和一个类型。在函数体内,你可以通过这些参数名称来访问传递给函数的值。type1, type2
:这些参数的类型,指定了传递给函数的值的类型。Go语言是一种静态类型语言,因此在定义函数时,你需要明确指定每个参数的类型。returnType
:这是函数的返回类型,表示函数执行完成后返回的数据类型。如果函数不需要返回任何值,则返回类型可以为空。return value
:这是函数的返回语句,用于返回函数的执行结果。返回值的类型必须与函数的返回类型相匹配。这是一个简单的Go语言函数示例,用于计算两个整数的和:
func add(a int, b int) int {
sum := a + b
return sum
}
在这个示例中,add
是函数的名称,它接受两个整数类型的参数a
和b
,并返回一个整数类型的结果。函数体内将a
和b
相加,并将结果赋值给变量sum
,然后通过return
语句返回sum
的值。
在Go语言中,函数可以返回多个值。这是Go语言的一项非常强大的功能,使得函数能够更灵活地处理多种情况并返回多个相关的信息。
要在函数中返回多个值,只需在函数签名中指定多个返回类型,并在函数体中使用逗号分隔的值列表来返回这些值。下面是一个简单的示例:
func calculate(a, b int) (int, int) {
sum := a + b
diff := a - b
return sum, diff
}
在上面的示例中,calculate
函数接受两个整数参数a
和b
,并返回它们的和与差。函数签名中指定了两个返回类型int
,分别对应和与差的结果。在函数体内,我们计算了和与差,并使用return
语句返回这两个值。
要调用返回多个值的函数,可以使用多个变量来接收返回值。例如:
result1, result2 := calculate(10, 5)
fmt.Println(result1) // 输出:15
fmt.Println(result2) // 输出:5
在上面的代码中,我们调用了calculate
函数,并使用两个变量result1
和result2
来接收返回的和与差。然后,我们可以根据需要使用这些返回值。
多返回值功能使得函数能够更灵活地处理多种情况,并返回多个相关的信息。这在很多情况下都非常有用,比如同时获取某个操作的结果和状态码,或者同时获取多个计算结果等。
在Go语言中,函数的参数列表可以使用省略号(...)来表示接受可变数量的参数。这样的函数被称为可变形参函数。
可变形参函数可以接受任意数量的参数,包括零个参数。这些参数在函数内部可以通过一个切片来访问,切片的长度等于传递给函数的参数数量。
下面是一个简单的示例,演示了如何在Go语言中定义和使用可变形参函数:
func sum(numbers ...int) int {
total := 0
for _, num := range numbers {
total += num
}
return total
}
在上面的示例中,sum
函数接受一个可变数量的整数参数,并返回它们的和。函数签名中的省略号表示参数列表是可变的。在函数内部,我们使用一个切片numbers
来访问传递给函数的参数。然后,我们遍历切片中的每个元素,并将它们累加到total
变量中。最后,我们返回total
的值作为函数的结果。
要调用可变形参函数,可以在函数调用时使用逗号分隔的参数列表,或者直接传递一个切片。下面是两种调用方式的示例:
// 使用逗号分隔的参数列表调用函数
result := sum(1, 2, 3, 4, 5)
fmt.Println(result) // 输出:15
// 使用切片调用函数
numbers := []int{6, 7, 8, 9, 10}
result = sum(numbers...)
fmt.Println(result) // 输出:40
在第一个示例中,我们使用逗号分隔的参数列表调用了sum
函数,并传递了5个整数参数。在第二个示例中,我们首先创建了一个包含5个整数的切片numbers
,然后使用切片调用了sum
函数。注意,在传递切片给可变形参函数时,需要使用省略号来展开切片中的元素。
可变形参函数在Go语言中非常有用,特别是当你不确定要传递多少个参数给函数时。它们使得函数更加灵活和通用化。
在Go语言中,可以使用匿名函数(也称为闭包函数)来创建没有名称的函数。匿名函数可以直接赋值给变量,或者作为参数传递给其他函数,或者作为函数的返回值。
下面是一个简单的示例,演示了如何在Go语言中定义和使用匿名函数:
// 定义一个匿名函数,并将其赋值给变量add
add := func(a, b int) int {
return a + b
}
// 调用匿名函数
result := add(3, 4)
fmt.Println(result) // 输出:7
在上面的示例中,我们定义了一个匿名函数,并将其赋值给变量add
。匿名函数接受两个整数参数a
和b
,并返回它们的和。然后,我们调用了匿名函数,并将结果赋值给变量result
。最后,我们打印了result
的值,可以看到输出结果为7。
匿名函数可以作为参数传递给其他函数。例如,你可以将匿名函数传递给排序函数,以便自定义排序逻辑。下面是一个示例:
// 定义一个切片
numbers := []int{5, 2, 4, 6, 1, 3}
// 使用匿名函数作为参数传递给排序函数
sort.Slice(numbers, func(i, j int) bool {
return numbers[i] < numbers[j]
})
// 打印排序后的切片
fmt.Println(numbers) // 输出:[1 2 3 4 5 6]
在上面的示例中,我们定义了一个切片numbers
,然后使用匿名函数作为参数传递给了sort.Slice
函数。匿名函数定义了排序逻辑,根据切片元素的大小进行比较。最后,我们打印了排序后的切片。
匿名函数还可以作为函数的返回值。例如,你可以定义一个函数,它返回一个匿名函数,以便在需要时动态创建函数。下面是一个示例:
// 定义一个函数,返回一个匿名函数
func createAdder(x int) func(int) int {
return func(y int) int {
return x + y
}
}
// 调用createAdder函数,获取一个加法器函数
adder := createAdder(5)
// 使用加法器函数进行计算
result := adder(3)
fmt.Println(result) // 输出:8
在上面的示例中,我们定义了一个函数createAdder
,它接受一个整数参数x
,并返回一个匿名函数。匿名函数接受一个整数参数y
,并返回x+y
的结果。然后,我们调用了createAdder
函数,获取了一个加法器函数adder
。最后,我们使用了加法器函数进行计算,并将结果打印出来。
在Go语言中,闭包(Closure)是指一个函数值(function value),它引用了自己函数体之外的变量。换句话说,闭包是由函数及其相关的引用环境组合而成的实体。
闭包在Go语言中有很多实际的应用场景,比如在并发编程中常用的goroutine和匿名函数等。闭包可以让函数访问并操作其词法环境中的变量,即使函数是在其定义的词法环境之外调用的。
下面是一个简单的Go语言闭包示例:
func main() {
// 外部函数
outer := func() {
// 内部函数引用了外部函数的变量
count := 0
inner := func() {
count++
fmt.Println(count)
}
// 调用内部函数
inner()
}
// 调用外部函数
outer() // 输出:1
}
在上面的示例中,outer
函数是一个闭包,它包含了一个内部函数inner
。inner
函数引用了outer
函数中的count
变量。在outer
函数被调用时,会创建一个新的count
变量,并在inner
函数中对其进行操作。每次调用outer
函数时,都会创建一个新的闭包实例,并且每个闭包实例都有自己的count
变量。在上述代码中,我们只调用了一次outer
函数,所以只有一个闭包实例,并且输出为1。
闭包在Go语言中有很多用途,比如在并发编程中可以使用闭包来创建goroutine,以便在每个goroutine中执行不同的任务。闭包还可以用于实现函数工厂、回调函数、高阶函数等功能。由于闭包可以捕获其外部环境的变量,因此它们也是一种非常有用的工具,可以在不改变外部变量的情况下对其进行操作。
defer
是Go语言中的一个关键字,用于延迟(defer)一个函数的执行,直到包含它的函数(也称为外部函数)执行完毕之前。defer
语句会将函数的执行推迟到外部函数返回之前,无论外部函数是通过正常返回还是由于发生panic异常而返回。
defer
语句的语法形式如下:
defer function_call
其中,function_call
是一个函数调用表达式,可以是任意的函数调用,包括内置函数、用户自定义函数或方法调用等。
当包含defer
语句的函数执行到其定义的末尾时,被defer
的函数会被推迟执行。推迟执行的函数可以访问其外部函数的变量,即使外部函数已经返回。这意味着defer
语句可以用于释放资源、关闭文件、解锁互斥锁等操作,以确保在函数返回之前这些操作一定会执行。
下面是一个简单的示例,演示了defer
语句的用法:
func main() {
fmt.Println("Start")
defer fmt.Println("Middle")
fmt.Println("End")
}
在上面的示例中,当main
函数执行到defer fmt.Println("Middle")
时,fmt.Println("Middle")
函数的执行会被推迟。然后,程序会继续执行fmt.Println("End")
,最后当main
函数执行完毕之前,被推迟的fmt.Println("Middle")
函数会被执行。因此,上述代码的输出结果为:
Start
End
Middle
可以看到,defer
语句改变了函数的执行顺序,使得被推迟的函数在外部函数返回之前执行。
除了用于释放资源和执行清理操作之外,defer
语句还可以用于实现一些高级功能,比如错误处理和恢复(panic/recover)机制等。通过使用defer
语句,可以更方便地处理错误和异常情况。
panic
和recover
是Go语言中的两个内置函数,用于处理异常情况。它们一起构成了Go语言的异常处理机制。
panic
函数用于引发一个异常,它会中断当前的程序执行流程,并向上层调用栈传播panic,直到被捕获或程序终止。panic
函数接受一个任意类型的参数,该参数会被传递给捕获异常的代码,通常用于传递错误信息。
recover
函数用于捕获并处理异常。它只能在defer
函数中调用,并且通常与panic
函数配合使用。当一个异常被引发时,程序执行流程会被中断,但在中断之前,Go语言会执行所有尚未执行的defer
函数。在defer
函数中调用recover
函数可以捕获异常,并返回传递给panic
函数的值。如果没有异常发生,或者recover
函数不是在defer
函数中调用的,那么recover
函数会返回nil。
下面是一个简单的示例,演示了panic
和recover
函数的用法:
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println("Recovered:", err)
}
}()
panic("an error occurred")
}
在上面的示例中,我们使用了一个匿名函数作为defer
函数的参数,并在匿名函数中调用了recover
函数。当程序执行到panic("an error occurred")
时,会引发一个异常,程序执行流程会被中断,但在中断之前,Go语言会执行尚未执行的defer
函数。在defer
函数中,我们调用了recover
函数来捕获异常,并打印出传递给panic
函数的错误信息。因此,上述代码的输出结果为:
makefile复制代码
Recovered: an error occurred
可以看到,通过使用panic
和recover
函数,我们可以实现异常处理机制,以便在发生错误时优雅地处理异常情况。
defer
语句在Go语言中的性能问题是一个经常被讨论的话题。由于defer
语句会将函数的执行推迟到外部函数返回之前,这意味着在外部函数执行期间,被defer
的函数会一直保持在调用栈中,这可能会增加内存占用和执行时间。
问题其实早在go1.14
中已经得到了完美解决。该版本能保证defer
在绝大多数场景下的开销几乎为0,这就意味着无论什么情况下,我们都可以使用defer
一些清理操作,比如关闭文件、释放锁等。
回顾其优化的历史,Go语言最早在go1.8
对defer
进行了优化处理,另外在go1.13
和go1.14
连续两个版本提升defer
的性能,彻底解决了defer
的性能问题。
回到go语言不优雅的错误处理这边,其实我想说的是函数多返回值事实上是无奈之举,go语言没有像Java/C++那样的异常捕获机制,使得其错误处理显得很不优雅,这个可能是go语言本身支持多携程的一种妥协,利用多返回值就可以返回错误和函数结果来帮助进行错误处理。
至于panic/recover作为一种异常处理机制,PostgreSQL 数据库交互的第三方包github.com/lib/pq
就利用了这点,但是需要注意的是并不是所有的错误都能通过recover恢复,也就是说recover并不是万能的。
在Go语言中,recover
函数只能用于捕获并处理由panic
函数引发的异常,它不能恢复由其他错误或异常情况导致的程序中断。
recover
函数只能在defer
函数中调用,并且通常与panic
函数配合使用。当一个异常被引发时,程序执行流程会被中断,但在中断之前,Go语言会执行所有尚未执行的defer
函数。在defer
函数中调用recover
函数可以捕获异常,并返回传递给panic
函数的值。然后,程序可以继续执行,就好像没有发生异常一样。
然而,如果程序是由于其他原因而中断的,比如运行时错误、内存溢出、无效的指针引用等,那么recover
函数就无法恢复程序的执行。在这些情况下,程序会立即终止,不会执行任何尚未执行的defer
函数。
此外,即使在defer
函数中调用了recover
函数,它也只能捕获并处理当前goroutine中的异常。如果其他goroutine中发生了异常,那么该goroutine的执行会被中断,但不会影响当前goroutine的执行。
因此,在编写Go程序时,应该谨慎使用panic
和recover
函数,并确保它们只用于处理可预见的异常情况。对于不可预见的错误或异常情况,应该使用其他错误处理机制来处理,比如返回错误码、使用错误类型等。
使用go语言刷Leetcode题