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题

5.go语言函数提纲的更多相关文章

  1. 从linux0.11中起动部分代码看汇编调用c语言函数

    上一篇分析了c语言的函数调用栈情况,知道了c语言的函数调用机制后,我们来看一下,linux0.11中起动部分的代码是如何从汇编跳入c语言函数的.在LINUX 0.11中的head.s文件中会看到如下一 ...

  2. C语言(函数)学习之strstr strcasestr

    C语言(函数)学习之[strstr]&[strcasestr]一.strstr函数使用[1]函数原型char*strstr(constchar*haystack,constchar*needl ...

  3. C语言函数sscanf()的用法

    从文件读取数据是一件很麻烦的事,所幸有sscanf()函数. C语言函数sscanf()的用法 sscanf() - 从一个字符串中读进与指定格式相符的数据. 函数原型: int sscanf( st ...

  4. 不可或缺 Windows Native (6) - C 语言: 函数

    [源码下载] 不可或缺 Windows Native (6) - C 语言: 函数 作者:webabcd 介绍不可或缺 Windows Native 之 C 语言 函数 示例cFunction.h # ...

  5. C#委托与C语言函数指针及函数指针数组

    C#委托与C语言函数指针及函数指针数组 在使用C#时总会为委托而感到疑惑,但现在总新温习了一遍C语言后,才真正理解的委托. 其实委托就类似于C/C++里的函数指针,在函数传参时传递的是函数指针,在调用 ...

  6. swift1.2语言函数和闭包函数介绍

    swift1.2语言函数和闭包函数介绍 在编程中,随着处理问题的越来越复杂,代码量飞速增加.其中,大量的代码往往相互重复或者近似重复.如果不采有效方式加以解决,代码将很难维护. swift1.2语言函 ...

  7. Swift 1.1语言函数参数的特殊情况本地参数名外部参数名

    Swift 1.1语言函数参数的特殊情况本地参数名外部参数名 7.4  函数参数的特殊情况 声明定义有参函数时,为函数的每一个参数都定义了参数名称.根据参数名定义的形式不同,函数参数包括本地参数和外部 ...

  8. C语言函数指针基础

    本文写的非常详细,因为我想为初学者建立一个意识模型,来帮助他们理解函数指针的语法和基础.如果你不讨厌事无巨细,请尽情阅读吧. 函数指针虽然在语法上让人有些迷惑,但不失为一种有趣而强大的工具.本文将从C ...

  9. 动态修改 C 语言函数的实现

    Objective-C 作为基于 Runtime 的语言,它有非常强大的动态特性,可以在运行期间自省.进行方法调剂.为类增加属性.修改消息转发链路,在代码运行期间通过 Runtime 几乎可以修改 O ...

  10. keil or c51 汇编调用c语言函数 容易忽视的问题

    最近,在用keil 写一个小程序时,想实践一下从汇编调用 C语言函数,我们都知道C语言调用汇编函数讨论得较多,但反过来,从汇编中调用C语言的函数未见深入分析:在开始的时候,还是忽视了一个问题,就是对现 ...

随机推荐

  1. k8s kong部署

    docker部署postgres docker run -d \ --name kong-postgres \ -e POSTGRES_PASSWORD=kong \ -e PGDATA=/var/l ...

  2. Vue项目学习

    一.二维数组尝试 var vm = new Vue({ el: "#app", data: { huilv:[ [6.8540, 132.9787, 1298.7013, 1.32 ...

  3. 5 大数据实战-hive实战分析

    1 内部表 Show databses; Use hive_data; 1.1 创建内部表 CREATE TABLE SOGOUQ2(DT STRING,WEBSESSION STRING,WORD ...

  4. Educational Codeforces Round 150 (Rated for Div. 2) A-E

    比赛链接 A 代码 #include <bits/stdc++.h> using namespace std; using ll = long long; bool solve() { i ...

  5. 如何使用libavfilter库给输入文件input.yuv添加视频滤镜?

    一.视频滤镜初始化 本次代码实现的是给输入视频文件添加水平翻转滤镜,在视频滤镜初始化部分我们可以分为以下几步进行: 1.创建滤镜图结构 视频滤镜功能最核心的结构为滤镜图结构,即AVFilterGrap ...

  6. Windows 环境下Docker 安装伪分布式 Hadoop

    1.环境 Windows 11 Docker 20.0.2 2.拉取镜像 我选择 ubuntu20.04: docker pull ubuntu:20.04 然后我们用命令看一下本地镜像: docke ...

  7. LeetCode 周赛上分之旅 #33 摩尔投票派上用场

    ️ 本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 和 [BaguTree Pro] 知识星球提问. 学习数据结构与算法的关键在于掌握问题背后的算法思维框架,你的思 ...

  8. Django学习笔记:第二章django的安装和创建应用

    1.安装Django 终端运行 pip install django 查看django是否安装成功 python -m django --version 1.1 安装虚拟环境 在控制台运行 pip i ...

  9. 自用 .net C# List集合和DataTable互转,可自定义表头

    using System; using System.Collections.Generic; using System.Data; using System.Linq; using System.R ...

  10. 【Azure K8S | AKS】在AKS集群中创建 PVC(PersistentVolumeClaim)和 PV(PersistentVolume) 示例

    问题描述 在AKS集群中创建 PVC(PersistentVolumeClaim)和 PV(PersistentVolume) 示例 问题解答 在Azure Kubernetes Service(AK ...