Go语言函数详解
摘要: 本文讲解了Go语言函数的定义、参数简写、可变参数、命名返回值等基础用法,以及函数作为变量、参数与返回值的高级应用。同时介绍匿名函数、自执行函数、闭包的概念及案例。最后详细说明了defer语句的执行顺序和错误处理机制,包括panic/recover的使用。内容配以代码示例,清晰展示了Go函数的核心特性和灵活性(由ChatGPT生成)
函数是组织好的,可重复使用的,用于执行指定任务的代码块
函数的定义
func 函数名(参数1 数据类型, ...) 返回值类型 {
函数执行流程
}
例如,定义一个进行两数相加的函数,可以这样定义:
package main
import "fmt"
// 执行两个int类型整数相加,返回值也是整数类型
func add(a int, b int) int {
return a + b
}
func main() {
c := add(5, 6)
fmt.Printf("%v - %T", c, c)
}
2.1 函数参数之类型简写
如果
相邻的两个或多个参数的类型一致,可以在最后一个参数中注明类型,前面相同数据类型的参数后面可以省略。
package main
import "fmt"
// 这里a和b类型都是int, c、d、e类型都是float
func add(a, b int, c, d, e float64) int {
fmt.Printf("a:(%v, %T)\n", a, a)
fmt.Printf("b:(%v, %T)\n", b, b)
fmt.Printf("c:(%v, %T)\n", c, c)
fmt.Printf("d:(%v, %T)\n", d, d)
fmt.Printf("e:(%v, %T)\n", e, e)
return a + b
}
func main() {
c := add(10, 15, 22, 33, 42)
fmt.Printf("%v - %T", c, c)
}
2.2 函数参数之可变参数
有些时候,我们可能并不确定参数个数(但是确定参数类型),这个时候我们就可以使用可变参数,用...来标识。注意:可变参数一定在最后
package main
import "fmt"
// d 是一个int类型的切片
func printD(a, b int, c string, d ...int) {
fmt.Printf("a(%v, %T)\n", a, a)
fmt.Printf("b(%v, %T)\n", b, b)
fmt.Printf("c(%v, %T)\n", c, c)
fmt.Printf("d(%v, %T)\n", d, d)
}
func main() {
printD(10, 15, "你好", 1, 2, 3)
fmt.Println("------------------------")
printD(10, 15, "hello")
}
2.3 函数之返回值命名
在我们定义函数时,可以直接定义出相关的返回值,这样做的好处时:无需在代码块中进行声明,和可以直接通过return返回,如果没有给该变量重新赋值,则使用默认值
import "fmt"
func add(a, b int) (c, d int) {
// 一旦在外面定义了返回值,则不能在创建其他值
//d: = a + b
//return d
a += b
// c = a + b // 此时返回值为 6 0
return
}
func main() {
e, f := add(1, 5)
fmt.Println(e, f) // 此时返回值为 0 0
}
函数类型和变量
package main
import "fmt"
func sumA(a, b int) int {
return a + b
}
func sumB(a int, b string) {
return
}
func sumC(a, b int) string {
return "hello"
}
func sumD(c, d int) int {
return c + d + 10
}
func sumE(c, d, e int) int {
return c + d + e
}
// 定义了一个calc函数类型,它接收两个int类型的参数,返回一个int类型的值
type calc func(int, int) int
func main() {
var c calc
//c = sumA // 可以
c = sumD // 可以
fmt.Println(c(1, 3))
//c = sumB // 不可以 参数类型 返回值类型不一致
//c = sumC // 不可以 返回值类型不一致
//c = sumE // 不可以 参数数量不一致
}
这样做的好处是,可以在定义某个变量时,通过自定义类型,来确定那些函数可以赋值给变量,有利于我们通过某个变量操作不同的函数,同时,在后面将函数作为参数中,也会用到这个方法。
高阶函数
4.1 函数可以作为参数
package main
import "fmt"
// 定义了一个calc函数类型,它接收两个int类型的参数,返回一个int类型的值
type calc func(int, int) int
func ca(a, b int, op calc) int {
return op(a, b)
}
func sum(a, b int) int {
return a + b
}
func sub(a, b int) int {
return a - b
}
func main() {
fmt.Println(ca(1, 2, sub))
fmt.Println(ca(10, 3, sum))
}
4.2 函数可以作为返回值
package main
import "fmt"
type calc func(int, int) int
func sum(a, b int) int {
return a + b
}
func sub(a, b int) int {
return a - b
}
// 也可以这样定义 func do(s string) calc {}
func do(s string) func(int, int) int {
switch s {
case "+":
return sum
case "-":
return sub
default:
return nil
}
}
func main() {
a1 := do("+")
fmt.Println(a1(10, 5))
a2 := do("-")
fmt.Println(a2(10, 5))
}
匿名函数
故名思意,就是没有名称的函数
在Go语言中,我们无法在一个函数中在定义另外一个函数,例如这样:
package main
import "fmt"
func do(a, b int)(int, int){
func sum(a, b int) int{
return a+b
}
func sub(a, b int) int{
return a - b
}
return sum(a, b), sub(a, b)
}
func main() {
fmt.Println(do(10, 5))
}
这样的代码逻辑在Go中时不被支持的,所以才有了匿名函数这一实现。
func(参数1 参数类型, ...)(返回值1类型, 返回值2类型){}
// 如果只有一个返回值,后面不用加括号
通过匿名函数,我们可以改良上面的代码,使其可以正常运行:
package main
import "fmt"
func do(a, b int) (int, int) {
sum := func(a, b int) int {
return a + b
}
sub := func(a, b int) int {
return a - b
}
return sum(a, b), sub(a, b)
}
func main() {
fmt.Println(do(10, 5))
}
匿名函数有两种使用方式,上面这种属于将匿名函数与某个变量绑定,通过变量来使用匿名函数。
5.1 匿名自执行函数
除了将匿名函数交给某个变量外,我们还可以让其自执行,方法如下:
package main
import "fmt"
func do(a, b int) (c int, d int) {
func(a, b int) int {
c = a + b
return c
}(a, b)
func(a, b int) int {
d = a - b
return d
}(a, b) // 该函数将在执行到这个位置时,自动创建并运行
return
}
func main() {
fmt.Println(do(10, 5))
}
闭包
简单理解就是: 一个函数中嵌套了另外一个函数
举一个常见的例子,当用户点赞时,我们运行一个up函数,将原有的点赞量加1,这种情况用闭包来实现就再好不过了。
package main
import "fmt"
func up() func() int {
num := 0
return func() int {
num += 1
return num
}
}
func main() {
v1 := up()
fmt.Println(v1())
fmt.Println(v1())
v2 := up()
fmt.Println(v2())
fmt.Println(v2())
fmt.Println(v2())
}
每次运行一次v1,num则自动增加1,且该变量能在内存中长期保存。而且相对于全局变量而言,每次将up绑定到一个新的变量时,该变量对应的num都是初始值0,不会受到其他函数的影响。
defer 语句
关于defer要了解的是:
- defer标记的函数执行流程要在正常函数之后
- defer标记了多个函数,则根据defer的标记顺序从后往前执行
- defer在函数注册时,其参数就已经确定了
关于第一和第二点,可以参考以下代码。
package main
import "fmt"
func stop() {
fmt.Println("程序运行完毕...")
}
func clean() {
fmt.Println("程序执行完毕,正在清除占用内存...")
}
func main() {
stop()
clean()
fmt.Println("程序开始运行")
fmt.Println(5 + 10)
}
这段代码正常运行的结果是这样的:
程序运行完毕...
程序执行完毕,正在清除占用内存...
程序开始运行
15
但是当我们通过defer标记stop和clean函数时,将会有不一样的效果
package main
import "fmt"
func stop() {
fmt.Println("程序运行完毕...")
}
func clean() {
fmt.Println("程序执行完毕,正在清除占用内存...")
}
func main() {
defer stop()
defer clean()
fmt.Println("程序开始运行")
fmt.Println(5 + 10)
}
此时运行效果如下:
程序开始运行
15
程序执行完毕,正在清除占用内存...
程序运行完毕...
关于第三条,可以分析该代码得到答案:
package main
import "fmt"
func add(a, b int) int {
return a + b
}
func main() {
a, b := 10, 15
defer fmt.Println("defer:", a, b)
a = 20
b = 30
fmt.Println(add(a, b))
}
这段代码的运行结果是:
50
defer: 10 15
之所以会出现这样的结果,是因为当代码运行到
defer fmt.Println("defer:", a, b)
注册Println这个函数时,此时的参数a、参数b是多少,传参就是多少,后面在改变也不会影响到Println函数的参数。
Go语言的错误处理
在Go中,目前还没有try .. catch,这对项目的稳定性是有一定影响的,但是不用担心,Go语言中也是提供了panic/recover来实现对错误的捕获,注意recover只能在defer调用的函数中才有效果。
package main
import "fmt"
func div(a, b int) int {
defer func() {
err := recover()
if err != nil {
fmt.Println(err)
return
}
}()
return a / b
}
func main() {
fmt.Println(div(10, 5))
fmt.Println(div(10, 0))
}
上面这样的方式属于被动捕获函数运行过程中的异常,我们也可以主动设置异常。
package main
import "fmt"
func div(a, b int) int {
defer func() {
err := recover()
if err != nil {
fmt.Println(err)
return
}
}()
if b == 0 {
panic("分母不能为0")
}
return a / b
}
func main() {
fmt.Println(div(10, 5))
fmt.Println(div(10, 0))
}