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
相关推荐
zhuyasen26 分钟前
深度定制 protoc-gen-go:实现结构体字段命名风格控制
后端·go·protobuf
eternal__day32 分钟前
Spring Cloud 多机部署与负载均衡实战详解
java·spring boot·后端·spring cloud·负载均衡
Livingbody42 分钟前
whisper 命令行解析【2】
后端
何中应43 分钟前
【设计模式-5】设计模式的总结
java·后端·设计模式
小胖同学~1 小时前
JavaWeb笔记
后端·servlet
风象南1 小时前
SpringBoot的5种日志输出规范策略
java·spring boot·后端
cccc来财1 小时前
Go中的协程并发和并发panic处理
开发语言·后端·golang
邪恶的贝利亚2 小时前
从webrtc到janus简介
后端·asp.net·webrtc
Livingbody2 小时前
Whisper 使用简单实例教程【1】
后端
花月C3 小时前
Mysql-定时删除数据库中的验证码
数据库·后端·mysql·spring