Go基础之函数~

一、函数定义

在Go中,函数是一段可以被重复使用的代码片段,用于执行特定的任务或完成特定的操作。函数可以接受参数,执行特定的操作,然后返回结果。

Go支持函数、匿名函数以及闭包,通过使用func关键字来对函数进行定义,具体的格式如下:

go 复制代码
func 函数名(参数列表) (返回值列表) {
    函数体
}

上述结构中:

  • 函数名:由字母、数组、下划线组成。其中函数名首字母不能为数组,在同一个包中不能有相同的函数名。函数名与参数列表一起构成了函数签名。
  • 参数列表:函数的参数列表,由参数变量及其数据类型组成,多个参数变量用逗号分隔开,并且有些函数也可以不包含参数列表。
  • 返回值列表:由返回值变量及其数据类型组成,返回值列表如果有多个返回值则需要括号,多个返回值用逗号分隔。同样,函数也可以不需要返回值。
  • 函数体:函数的代码逻辑块。

例如:

go 复制代码
func sum(x int, y int) int {
    return x + y
}

func hello() {
    fmt.Println("Hello World")
}

在Go中,函数属于"一等公民",体现在:

  • 函数自身可以作为值进行传递,包括函数赋值变量、函数作为参数传递、函数作为返回值;
  • 支持匿名函数与闭包;

在后续的内容中会介绍到。

二、函数调用

在定义了函数之后,可以通过函数名()的方式调用函数。

go 复制代码
func main() {
    hello()
    result := sum(10, 20)
    fmt.Println(result)
}

func sum(x int, y int) int {
    return x + y
}

func hello() {
    fmt.Println("Hello World")
}

三、函数特性

1、参数

参数传递

在定义函数时,参数列表中有参数,可以称参数列表中的变量为函数的形参,形参即定义在函数内的局部变量。在调用函数时,放入的变量为函数的实参,可以通过两种方式向函数传递参数。

  • 值传递

在默认情况下,Go函数中使用的是值传递,即将传入的实参进行拷贝,在调用过程中不会影响到实际参数。

go 复制代码
func modify(num int) {
    num = 20
}

func main() {
    num := 10
    fmt.Println("modify before: ", num) // modify before:  10
    modify(num)
    fmt.Println("modify after: ", num) // modify after:  10
}
  • 引用传递

引用传递指的是在调用函数时,将实际参数的地址传递到函数中,在函数中对参数进行修改,会影响到实际参数。

go 复制代码
func modify(num *int) {
    *num = 20
}

func main() {
    num := 10
    fmt.Println("modify before: ", num) // modify before:  10
    modify(&num)
    fmt.Println("modify after: ", num) // modify after:  20
}

上述代码中,modify接收一个指针变量*int,在main调用时,modify接收&num&num为指向 num 的指针,是num变量的地址, *num = 20则是修改num指针变量指向的值,因此在函数中对参数进行修改,会影响到实际参数。

在Go函数中,无论是值传递还是引用传递,实际上传递给函数的都是变量的拷贝副本,只不过对于值传递来说,拷贝的是变量值,而引用传递拷贝的是变量的地址。一般来说,地址拷贝更为高效,而值拷贝取决于拷贝的对象大小,对象越大则性能越低。在Go中map、slice、chan、指针、interface默认以引用的方式传递。

参数类型简写

在Go函数中,如果参数列表的相邻参数的数据类型相同,则可以省略类型,例如:

go 复制代码
func sum(x, y int) int {
    return x + y
}

上述代码中,sum函数有x, y两个参数,这两个参数的数据类型均为int,因此可以省略x的类型,在y的后面声明数据类型,则x也是该数据类型。

可变参数

函数中的可变参数是指函数的参数数量可能不确定,Go语言的函数可以通过在参数列表使 用...标识来表示可变参数。

go 复制代码
func main() {
    result := sum(10, 20, 30)
    fmt.Printf("Result:%d", result)
}

func sum(x ...int) int {
    fmt.Println(x) // x为一个切片
    sum := 0
    for _, v := range x {
       sum = sum + v
    }
    return sum
}

// 执行结果
[10 20 30]
Result:60

Go函数中,可变参数本质上是一个切片,并且只能有一个可变参数,需要注意的是可变参数需要作为函数参数的最后一个参数使用,在传递可变参数时,可以一个个元素传递,同时也可以直接传递一个数组或者切片。

go 复制代码
func sum(x int, args ...int) int {
}

2、返回值

Go函数中,通过使用return关键字返回函数的输出值。并且Go函数允许返回多个返回值。

go 复制代码
func cal(x, y int) (sum, sub int) {
    sum = x + y
    sum = x -y
}

同时Go支持对返回值命名,默认值为类型的零值。

go 复制代码
func fun() (names []string, m map[string]int, num int) {
    m = make(map[string]int)
    m["m"] = 10
    return
}

func main() {
    names, m, num := fun()
    fmt.Println(names, m, num) // [] map[m:10] 0
}

3、变量作用域

全局变量

全局变量时定义在函数外部的变量,它在程序整个运行周期内都有效,在函数中可以访问到全局变量。

go 复制代码
package main

import "fmt"

var num int = 10

func sum(x int) int {
    return x + num
}

func main() {
    result := sum(10)
    fmt.Println(result) // 20
}

局部变量

局部变量主要分为两种,一种是在函数内定义的变量,在函数内可以使用,在函数外无法使用,若局部变量与全局变量同名,则优先访问局部变量。

go 复制代码
package main

import "fmt"

// 定义全局变量num
var num int = 10

func sum(x int) int {
    num := 20
    return x + num // 函数中优先使用局部变量
}

func main() {
    result := sum(10)
    fmt.Println(result) // 30
}

另一种是例如在for语句if语句内定义的变量,这种类型的变量的作用域只有在其forif代码块内能够访问。

go 复制代码
func sum(x... int) int {
    sum := 0
    for i := 0; i < len(x); i++ {
       sum += x[i]
    }
    // fmt.Println(i) // 变量i只能在当前for语句中访问
    return sum
}

func main() {
    result := sum(10, 20, 30)
    fmt.Println(result) // 30
}

4、函数类型

在Go中,可以使用type关键字来定义函数类型,具体如下:

go 复制代码
type calculator func(int, int) int

上述语句定义了一个sum类型,他是一种函数类型,接收两个int参数并返回一个int类型的返回值。只要满足定义的函数类型的函数都属于sum类型的函数。例如:

go 复制代码
func add(x, y int) int {
    return x + y
}

func sub(x, y int) int {
    return x - y
}

此时可以声明并初始化函数类型的变量,并且像add()调用函数一样,使用定义的函数变量进行调用cal()

go 复制代码
package main

import "fmt"

type calculator func(int, int) int // 定义函数类型,类型为func(int, int) int

func add(x int, y int) int {
    return x + y
}

func sub(x int, y int) int {
    return x - y
}

func main() {
    var cal calculator // 声明函数类型变量
    cal = add // 初始化
    fmt.Printf("type of cal: %T\n", cal)
    fmt.Println(cal(20, 10))

    cal = sub
    fmt.Printf("type of cal: %T\n", cal)
    fmt.Println(cal(20, 10))
}

// 执行结果
type of cal: main.calculator
30
type of cal: main.calculator
10

在Go中,函数做为一等公民,可以作为函数参数或者返回值。

  • 函数作为参数:
go 复制代码
package main

import "fmt"

func add(x int, y int) int {
    return x + y
}

func sub(x int, y int) int {
    return x - y
}

// 将func(int, int) int类型参数传入函数中
func calculator(x, y int, op func(int, int) int) int {
    return op(x, y)
}

func main() {
    result := calculator(10, 20, add)
    fmt.Println(result) // 30

    result = calculator(100, 20, sub)
    fmt.Println(result) // 80
}
  • 函数作为返回值:
go 复制代码
package main

import (
    "errors"
    "fmt"
)

func add(x int, y int) int {
    return x + y
}

func sub(x int, y int) int {
    return x - y
}

func calculator(operation string) (func(int, int) int, error) {
    switch operation {
    case "+":
       return add, nil
    case "-":
       return sub, nil
    default:
       err := errors.New("operation error")
       return nil, err
    }
}

func main() {
    cal, _ := calculator("+")
    result := cal(10, 20)
    fmt.Println(result) // 30

    cal, _ = calculator("-")
    result = cal(200, 50)
    fmt.Println(result) // 150
}

三、匿名函数

在Go中,可以在函数内部定义匿名函数,匿名函数即没有函数名的函数,具体定义格式如下:

go 复制代码
func(参数) (返回值) {
    函数体
}

在使用匿名函数时,需要将声明好的匿名函数赋值到某个变量中,或者立即执行匿名函数,例如:

go 复制代码
func main() {
    // 1、定义匿名函数并赋值给add变量
    add := func(x int, y int) int {
       return x + y
    }
    result := add(10, 20)
    fmt.Println(result) // 30

    // 2、定义匿名函数并立刻调用
    result = func(x int, y int) int {
       return x - y
    }(100, 50)
    fmt.Println(result) // 50
}

四、闭包

在Go中,闭包指的是函数与其相关的引用环境组成的实体。具体来说,就是一个拥有许多变量和绑定使用了这些变量的环境表达式(函数),即闭包 = 函数 + 引用环境,例如:

go 复制代码
package main

import "fmt"

func add() func(int) int {
    var x int
    return func(y int) int {
       fmt.Printf("x = %d\n", x)
       x += y
       return x
    }
}

func main() {
    adder1 := add()
    fmt.Println(adder1(10)) // x = 0 print: 10
    fmt.Println(adder1(20)) // x = 10 print: 30
    fmt.Println(adder1(30)) // x = 30 print: 60
    adder2 := add()
    fmt.Println(adder2(40)) // x = 0 print: 40
    fmt.Println(adder2(50)) // x = 40 print: 90
    fmt.Println(adder2(60)) // x = 90 print: 150
}

从上述代码可知,在add()函数中,return返回值返回了一个匿名函数,且该匿名函数使用了外部变量 x。在main函数中,变量adder1adder2是一个函数并且它引用了其外部作用域中的 x 变量,此时则adder1adder2为一个闭包。并且在一个闭包的生命周期内,外部变量 x 的值一直有效。

五、defer

1、defer介绍

在Go中,defer关键字放在指定的代码语句前,会将其跟随的语句进行延迟处理。在defer语句所在的函数将要退出时,将延迟处理的语句按照defer声明的逆序以此进行执行,即先声明的defer语句最后执行,最后声明的defer语句最先被执行。

go 复制代码
func main() {
    fmt.Println("defer start")
    defer fmt.Println("one")
    defer fmt.Println("two")
    defer fmt.Println("three")
}

// 执行结果
defer start
three
two
one

2、defer执行时机

在Go的函数中,return语句在底层并不是原子操作,而是分为两个步骤,一是将返回值进行赋值,二是执行RET指令。defer语句的执行时间则是在这两者之间,在返回值赋值操作后,RET指令执行前。

3、defer特性

由于defer语句的特性,常用于处理资源释放问题。例如资源清理、文件清理、解锁以及记录时间等等。

defer注册要延迟执行的函数时,需要该函数确定好所有的参数才能够进行注册。例如:

go 复制代码
package main

import "fmt"

func main() {
    x := 1
    y := 2
    defer calculator("AA", x, calculator("A", x, y))
    x = 10
    defer calculator("BB", x, calculator("B", x, y))
    y = 20
}

func calculator(tag string, x, y int) int {
    result := x + y
    fmt.Println(tag, x, y, result)
    return result
}

// 执行结果
A 1 2 3
B 10 2 12
BB 10 12 22
AA 1 3 4

观察上述代码的执行结果可知,在第8行defer语句想要注册calculator("AA", x, calculator("A", x, y))时,由于calculator("A", x, y)值未确定从而导致defer无法注册,因此先执行calculator("A", x, y)获取结果后,才能将calculator("AA", x, calculator("A", x, y))进行注册,defer calculator("BB", x, calculator("B", x, y))同理,因此执行结果为上述结果。

六、异常处理panic与recover

在Go中,可以抛出一个panic异常,之后在defer中通过recover捕获这个异常并进行正常处理。

panic具体来说:

  • 当函数内使用了panic语句后,会终止后续需要执行的代码,之后panic所在的函数如果有需要执行的defer执行列表,则按照defer注册的顺序逆序执行。
  • 如果是调用函数的调用者了含有panic的函数,则调用者之后的代码也不会执行,假如调用者所在的函数也存在需要执行的defer执行列表,则按照defer注册的顺序逆序执行。
  • 直到整个goroutine整个退出,并报告错误。

recover的作用在于捕获panic。当一个函数在执行过程中出现了异常或者遇到panic(),正常语句会立即终止,然后执行defer语句,再报告异常信息,最后退出goroutine。如果在defer中使用了recover(),则会捕获错误信息,并让该错误信息终止报告。

例如:

go 复制代码
package main

func testPanic() {
    defer func() {
       if err := recover(); err != nil {
          println(err.(string)) // 将 interface{} 转型为具体类型。
       }
    }()
    panic("panic error!")
}

func main() {
    testPanic()
}
// 执行结果
panic error!

上述代码中,利用recover处理panic,需要注意的是,defer应该放在panic定义之前,且recover只有在 defer 调用的函数中才有效。否则当panic时,recover无法捕获到panic

defer延迟调用中引发的panic错误,可被后续的defer延迟调用使用recover捕获,但只能捕获到最后一个panic错误,之前的错误无法捕获到。

go 复制代码
package main

func testPanic() {
    defer func() {
       if err := recover(); err != nil {
          println(err.(string))
       }
    }()

    defer func() {
       panic("defer panic")
    }()

    panic("testPanic panic") // 将 interface{} 转型为具体类型。
}

func main() {
    testPanic()
}

// 执行结果
defer panic

如果需要确保后续的代码可以执行,可以使用匿名函数将代码块重构,保护代码段。

go 复制代码
package main

import "fmt"

func testPanic(x int, y int) {
    var z int
    func() { // 匿名函数
       defer func() {
          if recover() != nil {
             z = 0
          }
       }()
       panic("testPanic panic")
       z = x / y
       return
    }()

    fmt.Printf("x / y = %d\n", z) // 后续的代码可执行
}

func main() {
    testPanic(20, 10)
}

// 执行结果
x / y = 0
相关推荐
蓝倾4 分钟前
淘宝获取商品分类接口操作指南
前端·后端·fastapi
小希爸爸9 分钟前
curl 网络测试常用方法
后端
星星电灯猴41 分钟前
iOS WebView 调试实战 页面跳转失效与历史记录错乱的排查路径
后端
重楼七叶一枝花1 小时前
MySQL的在线模式学习笔记
后端·mysql
代码男孩1 小时前
python包管理工具uv的使用
后端
CodeWolf2 小时前
关于端口号配置优先级的问题
后端
C182981825752 小时前
Ribbon轮询实现原理
后端·spring cloud·ribbon
鹿鹿的布丁2 小时前
freeswitch通过编译方式安装
后端
JavaDog程序狗2 小时前
【软件环境】Windows安装JDK21
后端
舒一笑2 小时前
撕碎语法教科书!PandaCoder教大模型「暴力越狱」逐字翻译
后端·程序员·intellij idea