5.go语言函数提纲

1 本篇前瞻

前端时间的繁忙,未曾更新go语言系列。由于函数非常重要,为此将本篇往前提一提,另外补充一些有关go新版本前面遗漏的部分。

需要恭喜你的事情是本篇学完,go语言中基础部分已经学完一半,这意味着你可以使用go语言去解决大部分的Leetcode的题,为此后面的1篇,将带领大家去巩固go语言的学习成果

2 版本拾遗

2.1 go 1.21

2.1.1 函数新增

函数minmaxgo 1.21已经被实现,不过由于其建立在go1.18泛型实现的基础上,需要在后面提到泛型的时候讲到。

2.1.2 for循环的变化

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个44个44个5

go1.21时代中,你加入环境变量GOEXPERIMENT=loopvar,这些题目的答案就统一变为1,2,3,4

这里需要提醒面试官们更新自己的面试题了,对于各位面试者来说,这个知识点反杀面试官,想想是不是很帅呢?

等会,面试官也不知道这个知识把你刷了,那么这样的公司不去也罢(这家公司根本不关注语言的更新,你想这个公司能给你多少成长?)。

2.1.3 panic函数

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)

3 函数简介

后面为了简单起见,我指导AI完成这方面的写作,如果你觉得这些知识较为简单,可以略过不看。

3.1 函数声明

Go语言的函数结构声明简单明了,由关键字func、函数名、参数列表、返回类型以及函数体组成。下面是一个基本的Go语言函数的结构示例:

func functionName(parameter1 type1, parameter2 type2) returnType {  
    // 函数体  
    // 执行语句  
    return value  
}

让我们来详细了解一下每个部分的作用:

  1. func:这是Go语言中定义函数的关键字,它表示接下来要定义一个函数。
  2. functionName:这是函数的名称,你可以根据自己的需求为函数命名。函数名应该具有描述性,能够清晰地表达函数的功能。
  3. parameter1, parameter2:这些是函数的参数,你可以根据需要定义任意数量的参数。每个参数都有一个名称和一个类型。在函数体内,你可以通过这些参数名称来访问传递给函数的值。
  4. type1, type2:这些参数的类型,指定了传递给函数的值的类型。Go语言是一种静态类型语言,因此在定义函数时,你需要明确指定每个参数的类型。
  5. returnType:这是函数的返回类型,表示函数执行完成后返回的数据类型。如果函数不需要返回任何值,则返回类型可以为空。
  6. return value:这是函数的返回语句,用于返回函数的执行结果。返回值的类型必须与函数的返回类型相匹配。
  7. 函数体:这是包含函数执行语句的代码块。在这里,你可以实现函数的具体逻辑,包括处理参数、执行计算、调用其他函数等。

这是一个简单的Go语言函数示例,用于计算两个整数的和:

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

在这个示例中,add是函数的名称,它接受两个整数类型的参数ab,并返回一个整数类型的结果。函数体内将ab相加,并将结果赋值给变量sum,然后通过return语句返回sum的值。

3.1.1 多返回值

在Go语言中,函数可以返回多个值。这是Go语言的一项非常强大的功能,使得函数能够更灵活地处理多种情况并返回多个相关的信息。

要在函数中返回多个值,只需在函数签名中指定多个返回类型,并在函数体中使用逗号分隔的值列表来返回这些值。下面是一个简单的示例:

func calculate(a, b int) (int, int) {  
    sum := a + b  
    diff := a - b  
    return sum, diff  
}

在上面的示例中,calculate函数接受两个整数参数ab,并返回它们的和与差。函数签名中指定了两个返回类型int,分别对应和与差的结果。在函数体内,我们计算了和与差,并使用return语句返回这两个值。

要调用返回多个值的函数,可以使用多个变量来接收返回值。例如:

result1, result2 := calculate(10, 5)  
fmt.Println(result1) // 输出:15  
fmt.Println(result2) // 输出:5

在上面的代码中,我们调用了calculate函数,并使用两个变量result1result2来接收返回的和与差。然后,我们可以根据需要使用这些返回值。

多返回值功能使得函数能够更灵活地处理多种情况,并返回多个相关的信息。这在很多情况下都非常有用,比如同时获取某个操作的结果和状态码,或者同时获取多个计算结果等。

3.1.2 可变形参

在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语言中非常有用,特别是当你不确定要传递多少个参数给函数时。它们使得函数更加灵活和通用化。

3.2 匿名函数

在Go语言中,可以使用匿名函数(也称为闭包函数)来创建没有名称的函数。匿名函数可以直接赋值给变量,或者作为参数传递给其他函数,或者作为函数的返回值。

下面是一个简单的示例,演示了如何在Go语言中定义和使用匿名函数:

// 定义一个匿名函数,并将其赋值给变量add  
add := func(a, b int) int {  
    return a + b  
}  
  
// 调用匿名函数  
result := add(3, 4)  
fmt.Println(result) // 输出:7

在上面的示例中,我们定义了一个匿名函数,并将其赋值给变量add。匿名函数接受两个整数参数ab,并返回它们的和。然后,我们调用了匿名函数,并将结果赋值给变量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。最后,我们使用了加法器函数进行计算,并将结果打印出来。

3.2.1 闭包

在Go语言中,闭包(Closure)是指一个函数值(function value),它引用了自己函数体之外的变量。换句话说,闭包是由函数及其相关的引用环境组合而成的实体。

闭包在Go语言中有很多实际的应用场景,比如在并发编程中常用的goroutine和匿名函数等。闭包可以让函数访问并操作其词法环境中的变量,即使函数是在其定义的词法环境之外调用的。

下面是一个简单的Go语言闭包示例:

func main() {  
    // 外部函数  
    outer := func() {  
        // 内部函数引用了外部函数的变量  
        count := 0  
        inner := func() {  
            count++  
            fmt.Println(count)  
        }  
        // 调用内部函数  
        inner()  
    }  
    // 调用外部函数  
    outer() // 输出:1  
}

在上面的示例中,outer函数是一个闭包,它包含了一个内部函数innerinner函数引用了outer函数中的count变量。在outer函数被调用时,会创建一个新的count变量,并在inner函数中对其进行操作。每次调用outer函数时,都会创建一个新的闭包实例,并且每个闭包实例都有自己的count变量。在上述代码中,我们只调用了一次outer函数,所以只有一个闭包实例,并且输出为1。

闭包在Go语言中有很多用途,比如在并发编程中可以使用闭包来创建goroutine,以便在每个goroutine中执行不同的任务。闭包还可以用于实现函数工厂、回调函数、高阶函数等功能。由于闭包可以捕获其外部环境的变量,因此它们也是一种非常有用的工具,可以在不改变外部变量的情况下对其进行操作。

3.3 defer语句

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语句,可以更方便地处理错误和异常情况。

3.4 panic/recover函数

panicrecover是Go语言中的两个内置函数,用于处理异常情况。它们一起构成了Go语言的异常处理机制。

panic函数用于引发一个异常,它会中断当前的程序执行流程,并向上层调用栈传播panic,直到被捕获或程序终止。panic函数接受一个任意类型的参数,该参数会被传递给捕获异常的代码,通常用于传递错误信息。

recover函数用于捕获并处理异常。它只能在defer函数中调用,并且通常与panic函数配合使用。当一个异常被引发时,程序执行流程会被中断,但在中断之前,Go语言会执行所有尚未执行的defer函数。在defer函数中调用recover函数可以捕获异常,并返回传递给panic函数的值。如果没有异常发生,或者recover函数不是在defer函数中调用的,那么recover函数会返回nil。

下面是一个简单的示例,演示了panicrecover函数的用法:

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

可以看到,通过使用panicrecover函数,我们可以实现异常处理机制,以便在发生错误时优雅地处理异常情况。

4 写作拾遗

4.1 defer的性能问题

defer语句在Go语言中的性能问题是一个经常被讨论的话题。由于defer语句会将函数的执行推迟到外部函数返回之前,这意味着在外部函数执行期间,被defer的函数会一直保持在调用栈中,这可能会增加内存占用和执行时间。

问题其实早在go1.14中已经得到了完美解决。该版本能保证defer在绝大多数场景下的开销几乎为0,这就意味着无论什么情况下,我们都可以使用defer一些清理操作,比如关闭文件、释放锁等。

回顾其优化的历史,Go语言最早在go1.8defer进行了优化处理,另外在go1.13go1.14连续两个版本提升defer的性能,彻底解决了defer的性能问题。

4.2 多返回值/error/panic/recover

回到go语言不优雅的错误处理这边,其实我想说的是函数多返回值事实上是无奈之举,go语言没有像Java/C++那样的异常捕获机制,使得其错误处理显得很不优雅,这个可能是go语言本身支持多携程的一种妥协,利用多返回值就可以返回错误和函数结果来帮助进行错误处理。

至于panic/recover作为一种异常处理机制,PostgreSQL 数据库交互的第三方包github.com/lib/pq就利用了这点,但是需要注意的是并不是所有的错误都能通过recover恢复,也就是说recover并不是万能的。

4.3 recover不是万能的

在Go语言中,recover函数只能用于捕获并处理由panic函数引发的异常,它不能恢复由其他错误或异常情况导致的程序中断。

recover函数只能在defer函数中调用,并且通常与panic函数配合使用。当一个异常被引发时,程序执行流程会被中断,但在中断之前,Go语言会执行所有尚未执行的defer函数。在defer函数中调用recover函数可以捕获异常,并返回传递给panic函数的值。然后,程序可以继续执行,就好像没有发生异常一样。

然而,如果程序是由于其他原因而中断的,比如运行时错误、内存溢出、无效的指针引用等,那么recover函数就无法恢复程序的执行。在这些情况下,程序会立即终止,不会执行任何尚未执行的defer函数。

此外,即使在defer函数中调用了recover函数,它也只能捕获并处理当前goroutine中的异常。如果其他goroutine中发生了异常,那么该goroutine的执行会被中断,但不会影响当前goroutine的执行。

因此,在编写Go程序时,应该谨慎使用panicrecover函数,并确保它们只用于处理可预见的异常情况。对于不可预见的错误或异常情况,应该使用其他错误处理机制来处理,比如返回错误码、使用错误类型等。

5 下篇预告

使用go语言刷Leetcode题

posted @ 2023-10-01 23:27  Breeze0806  阅读(196)  评论(0编辑  收藏  举报