Skip to content

函数

1.函数定义

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

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

其中:

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

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

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

2.参数

值传递

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

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

为什么没有引用传递?

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

什么是引用变量?

  • 在 C++ 中,引用变量相当于原变量的一个别名
go
#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 语言中不存在引用变量。

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

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

引用和指针的区别?

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

img

可变参数

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

注意:

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

举个例子:

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

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

3.函数返回值

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

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

返回值命名

注意:

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

举个例子:

go
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)

go
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
}

具体原理 并不清楚,姑且先这么认为(也许有误),待之后有机会再详细分析。 在下一篇的函数进阶中会详细阐述。

4.函数变量

函数类型

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

type int_calculation func(int, int) int

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

举个例子:

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

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

上面两个函数都是 int_calculation 类型

函数类型变量

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

go
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))
}

5.匿名函数

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

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

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

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

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

go
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))
}

6.高阶函数

函数作为参数

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

go
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
}

函数作为返回值

go
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
    }
}

7.闭包

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

举个例子:

go
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
}

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

8.内置函数

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返回复合类型的值的虚部,返回值是对应的浮点数类型

10.defer

defer特性

  1. 关键字 defer 用于注册延迟调用。
  2. 这些调用直到 return 前才被执行。因此可以用来做资源清理。
  3. 多个defer语句,按先进后出的方式执行。
  4. defer语句中的变量,在defer声明时就决定,只是延后执行而已。

defer用途

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

defer执行时机

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

defer例子

go
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
}
go
A 1 2 3
B 10 2 12
BB 10 12 22
AA 1 3 4

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

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

输出结果

11.函数的例子

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

go
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())
}

输出结果

go
func f1() int {    //用户没有为返回值命名,但内部还是有对返回值的命名,这里是 ~r0
    x := 5    //① x =5
    defer func() {
        x++    //③ x = x + 1 = 6
    }()
    return x    //② ~r0 = x = 5
}// 返回 ~r0 = 5

f1()的执行过程

go
func f2() (x int) {    //返回值命名为 x,最后返回变量 x
    defer func() {    
        x++    //② x = x + 1 = 6
    }()
    return 5    //① x = 5
}// 返回 x = 6

f2()的执行过程

go
func f3() (y int) {    //返回变量 y
    x := 5    //① x = 5
    defer func() {
        x++    //③ x = x + 1 = 6
    }()
    return x    //② y = x = 5
}//返回 y = 5

f3()的执行过程

go
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()的执行过程

如有转载或 CV 的请标注本站原文地址