Golang学习笔记(四) —— 函数

发布时间 2024-01-01 22:25:17作者: 昨晚没做梦

函数


 

函数定义

Go语言中定义函数使用 func 关键字,具体格式如下:

func (接收者)函数名(参数)(返回值){
    函数体
}

其中:

  • 接收者:只有在定义方法时,才需要设置接收者。(可选项)
  • 函数名:由字母、数字、下划线组成。但函数名的第一个字母不能是数字。在同一个包内,函数名也称不能重名。
  • 参数:参数由参数变量和参数变量的类型组成,多个参数之间使用,分隔。(可选项)
  • 返回值:返回值由返回值变量和其变量类型组成,也可以只写返回值的类型,多个返回值必须用()包裹,并用,分隔。(可选项)
  • 函数体:实现指定功能的代码块。

有返回值的函数,必须有明确的终止语句,否则会引发编译错误。

当两个或多个连续的参数是同一类型,则除了最后一个类型之外,其他都可以省略。例如:func add(x,y int) int

参数

值传递

Go 语言中,函数的参数传递只有值传递,没有引用传递。

  • 值传递:指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。

  • 引用传递:指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

 

为什么没有引用传递?

  • Go 语言中不存在引用变量,自然没有引用传递。

什么是引用变量?

  • 在 C++ 中,引用变量相当于原变量的一个别名
#include <stdio.h>

int main() {
    int a = 10;
    int &b = a;
    int &c = b;

    printf("&a = %p\n &b = %p\n &c = %p\n", &a, &b, &c); 
    // &a = 0x7ffe114f0b14
    // &b = 0x7ffe114f0b14 
    // &c = 0x7ffe114f0b14
    return 0;
}
  • 从上面的 c++ 程序可以看到,a、b、c 这三个变量的地址相同,共享一个内存地址(值自然也是相同的)。而在 Go 中,不同变量可以有相同的值,但地址是不会相同的,因此Go 语言中不存在引用变量。

没有引用传递,那我们想在函数内修改函数外的变量,该怎么做?

  • 传递指针,通过指针修改实际参数。

引用和指针的区别?

  • 指针变量是指向目的内存的变量,而引用变量是目的内存上的变量的一个别名

 

可变参数

可变参数是指函数的参数数量不固定。Go语言中的可变参数通过在参数名后加 ...type 来标识。(type是可变参数的类型)

注意:

  • 可变参数通常要作为函数的最后一个参数。
  • 可变参数底层是由切片实现的

举个例子:

func intAdd(args ...int) int {
    sum := 0
    for _, arg := range args {
        sum = sum + arg
    }
    return sum
}

若想传递任意类型的可变参数,可以将可变参数的类型设置为 interface{}

 

函数返回值

Go语言中通过 return 关键字向外输出返回值。

  • 支持多返回值,函数如果有多个返回值时必须用 ( ) 将所有返回值包裹起来。
  • 支持返回值命名

 

返回值命名

注意:

  • 当命名时,它们在函数开始时被初始化为其类型的零值
  • 命名后,如果 return 没有指定变量,则会默认返回声明的返回变量
  • 返回值命名,不能只命名一个,必须为该函数的所有返回值命名(可以用占位符 _ 命名,相当于没有命名)

举个例子:

func intAdd(args ...int) (sum int, str string, _ bool, _ error) {
    sum = 0
    str = "good"
    for _, arg := range args {
        sum = sum + arg
    }
    return
}

func main() {
    sum, str, right, err := intAdd(100, 150, 200, 1, 20)
    fmt.Println(sum, str, right, err)   //471 good false <nil>
}

  命名后,即便 return 其他变量(x),也会将 其他变量的值(x) 复制给 返回值命名的返回变量(y),再返回 返回值命名的返回变量(y)

package main

import "fmt"

func re_test() (y int) {
    x := 5
    defer func() {    //使用 defer 在调试时可以看的更明显
        y++    // y = y + 1 = 6
    }()
    return x    // y = x = 5
}

func main(){
    fmt.Println(re_test())    //6
}

  具体原理并不清楚,姑且先这么认为(也许有误),待之后再详细分析源码。

 

函数变量

函数类型

在 Go 中,函数类型也是一种数据类型,我们可以使用 type 关键字来定义一个函数类型,具体格式如下:

type int_calculation func(int, int) int

  上面语句定义了一个 int_calculation 类型,它是一种函数类型,这种函数接收两个 int 类型的参数并且返回一个 int 类型的返回值,简单来说,凡是满足这个条件的函数都是 int_calculation 类型的函数。

举个例子:

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

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

  上面两个函数都是 int_calculation 类型

 

函数类型变量

定义了一个函数类型后,我们可以声明该函数类型的变量,并赋值。

func main() {
    var c int_calculation           // 声明一个int_calculation类型的变量c
    c = add                         // 把add赋值给c
    fmt.Printf("type of c:%T\n", c) // type of c:main.int_calculation
    fmt.Println(c(1, 2))            // 像调用add一样调用c

    f := add                        // 将函数add赋值给变量f
    fmt.Printf("type of f:%T\n", f) // type of f:func(int, int) int
    fmt.Println(f(10, 20))          // 像调用add一样调用f

    var a struct {
    fn int_calculation    //结构体内封装函数类型的变量
    }
    a.fn = add    //为函数变量赋值
    fmt.Println(a.fn(1, 2))
}

 

匿名函数

匿名函数是指不需要定义函数名的一种函数实现方式。其定义格式如下:

func(参数)(返回值){
    函数体
}

  匿名函数因为没有函数名,所以需要赋值给某个变量或者作为立即执行函数

func main() {
    // 将匿名函数保存到变量
    add := func(x, y int) {
        fmt.Println(x + y)
    }
    add(10, 20) // 通过变量调用匿名函数

    //自执行函数:匿名函数定义完加()直接执行
    func(x, y int) {
        fmt.Println(x + y)
    }(10, 20)
}

  匿名函数能够直接作为函数类型,来声明变量

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

func main() {
    var a func(int, int) int= add
    fmt.Println(a(1, 2))

    d := struct {
        fn func(int, int) int
    }{
        fn: func(x int, y int) int { return x + y },
      //fn: add 也行
    }
    fmt.Println(d.fn(1, 2))
}

 

高阶函数

函数作为参数

函数既然能作为变量,自然也能够作为参数

func calc1(x, y int, op func(int, int) int) int {    //用匿名函数作为参数类型
    return op(x, y)
}

func calc2(x, y int, op int_calculation) int {    //用定义的函数类型作为参数类型
    return op(x, y)
}

func main() {
    ret2 := calc1(10, 20, add)
    fmt.Println(ret2) //30
}

 

函数作为返回值

func operator(s string) (int_calculation, error) {    //此处的 int_calculation  可以用 func(int, int) int 代替
    switch s {
    case "+":
        return add, nil
    case "-":
        return sub, nil
    default:
        err := errors.New("无法识别的运算符")
        return nil, err
    }
}

 

闭包

  闭包指的是一个函数和与其相关的引用环境组合而成的实体。简单来说,当匿名函数引用了外部作用域的变量(自由变量)时,就成了闭包函数。有了闭包后函数就成为了一个变量的值,只要变量没被释放,函数就会一直处于存活并独享的状态

举个例子:

func adder2(x int) func(int) int {
    return func(y int) int {
        x += y
        return x
    }
}
func main() {
    var f = adder2(10)
    fmt.Println(f(10)) //20
    fmt.Println(f(20)) //40
    fmt.Println(f(30)) //70

    f1 := adder2(20)
    fmt.Println(f1(40)) //60
    fmt.Println(f1(50)) //110
}

  这里只是简单了解闭包的概念,更具体的内容留到下一篇(函数进阶),再进一步了解。

 

内置函数

Go 语言提供了15个内置函数
内置函数 介绍
close 主要用来关闭channel
len 用来求长度,比如string、array、slice、map、channel
cap 返回值的容量,值的类型不同,值的容量含义也不同
new 用来分配内存,主要用来分配值类型,比如int、struct。返回的是指针
make 用来分配内存,主要用来分配引用类型,比如chan、map、slice
append 用来追加元素到数组、slice中
panic和recover 用来做错误处理
copy 可以将源切片中的元素拷贝到目标切片
delete

通过指定键 m[key] 删除 map 中的元素,如果 map 是 nil 或没有元素,delete 不做任何操作

print和printIn 输入到标准错误流中并打印,官方推荐使用 fmt 包
complex 将两个浮点型的值构造为一个复合类型的值,需要注意的是,实部和虚部必须是相同类型,即都是 float32 或 float64
real 返回复合类型的值的实部,返回值是对应的浮点数类型
imag 返回复合类型的值的虚部,返回值是对应的浮点数类型

 

defer

defer特性

1. 关键字 defer 用于注册延迟调用。

2. 这些调用直到 return 前才被执行。因此可以用来做资源清理。

3. 多个defer语句,按先进后出的方式执行。

4. defer语句中的变量,在defer声明时就决定,只是延后执行而已。

 

defer用途

1. 关闭文件句柄
2. 锁资源释放
3. 数据库连接释放

 

defer执行时机

  Go 语言的函数中 return 语句在底层并不是原子操作,它分为给 返回值赋值 和 RET指令 两步。而 defer 语句执行的时机就在返回值赋值操作后,RET指令执行前。具体如下图所示:

 

defer例子

func calc(index string, a, b int) int {
    ret := a + b
    fmt.Println(index, a, b, ret)
    return ret
}

func main() {
    x := 1
    y := 2
    defer calc("AA", x, calc("A", x, y))
    x = 10
    defer calc("BB", x, calc("B", x, y))
    y = 20
}
A 1 2 3
B 10 2 12
BB 10 12 22
AA 1 3 4

//前两个是因为:defer 函数在 运行到时 就确定了参数的值,于是就直接执行了参数里的函数

//后两个是因为:defer 延迟执行,在 return 后按 后进先出 的顺序执行
输出结果

 

函数的例子

本篇的最后以一个例子结尾,依此深化对函数的理解:

package main

import "fmt"

func f1() int {
    x := 5
    defer func() {
        x++
    }()
    return x
}

func f2() (x int) {
    defer func() {
        x++
    }()
    return 5
}

func f3() (y int) {
    x := 5
    defer func() {
        x++
    }()
    return x
}
func f4() (x int) {
    defer func(x int) {
        x++
    }(x)
    return 5
}

func main() {
    fmt.Println(f1())
    fmt.Println(f2())
    fmt.Println(f3())
    fmt.Println(f4())
}
5
6
5
5
输出结果
func f1() int {    //用户没有为返回值命名,但内部还是有对返回值的命名,这里是 ~r0
    x := 5    //① x =5
    defer func() {
        x++    //③ x = x + 1 = 6
    }()
    return x    //② ~r0 = x = 5
}// 返回 ~r0 = 5
f1()的执行过程
func f2() (x int) {    //返回值命名为 x,最后返回变量 x
    defer func() {    
        x++    //② x = x + 1 = 6
    }()
    return 5    //① x = 5
}// 返回 x = 6
f2()的执行过程
func f3() (y int) {    //返回变量 y
    x := 5    //① x = 5
    defer func() {
        x++    //③ x = x + 1 = 6
    }()
    return x    //② y = x = 5
}//返回 y = 5
f3()的执行过程
func f4() (x int) {    //返回变量 x
    defer func(x int) {    //① 运行到 defer,发现有传参,先传参
        x++    //④ x = x + 1 = 1,这里的 x 是局部变量,不会改变外面的返回变量 x
    }(x)    //② 传参,x = 0
    return 5    //③ x = 5
}// 返回 x = 5
f4()的执行过程