【go语言学习】函数function

函数是组织好的、可重复使用的、用于执行指定任务的代码块。
Go语言中支持函数、匿名函数和闭包,并且函数在Go语言中属于“一等公民”。

一、函数的声明和调用

1、函数的声明

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

func funcName(parametername type) (output type) {
    //这里是处理逻辑代码
    //返回多个值
    return valu
}
  • func 函数由func开始声明
  • funcName 函数名
  • () 函数的标志
  • parametername type 函数的参数列表,参数列表指定参数的类型、顺序以及个数,参数是可选,可有可无;这里的参数相当于一个占位符,所以也叫形式参数,当函数被调用时,传入的值传递给参数,这个值被称为实际参数
  • ouput type 返回值列表,返回值可以由名称和类型组成,也可以只写类型不写名称;返回值可以有一个或多个,当有一个返回值时可以不加(),多个返回值时必须加()
  • 函数体:实现指定功能的代码块{}里面的内容。
2、函数的调用

可以通过funcName(parameter)的方式调用函数。调用有返回值的函数时,可以不接收其返回值。

package main

import "fmt"

func main() {
    a := 10
    b := 20
    res := sum(a, b)
    fmt.Printf("%v + %v = %v", a, b, res)
}

func sum(a, b int) int {
    return a + b
}

运行结果

10 + 20 = 30

二、函数的参数

1、参数的使用:

形式参数:定义函数时,用于接收外部传入的数据,叫做形式参数,简称形参。
实际参数:调用函数时,传给形参的实际的数据,叫做实际参数,简称实参。
函数调用:

  • A:函数名称必须匹配
  • B:实参与形参必须一一对应:顺序,个数,类型
    函数的参数中如果相邻变量的类型相同,则可以省略类型,如:
package main

import "fmt"

func main() {
    a := 10
    b := 20
    res := add(a, b)
    fmt.Printf("%v + %v = %v", a, b, res)
}

func add(a, b int) sum int {
    sum = a + b
    return
}
2、可变参数

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

注意:可变参数通常要作为函数的最后一个参数。

func funcName(arg ...int) {}

arg ...int告诉Go这个函数接受不定数量的参数。注意,这些参数的类型全部是int。在函数体中,变量arg是一个int类型的slice

3、参数的传递

go语言函数的参数也是存在值传递引用传递

数据类型:

  • 按照数据类型来分:

    • 基本数据类型
      int,float,string,bool
    • 复合数据类型
      arry,slice,map,struct,interface,chan...
  • 按照数据的存储特点来分:

    • 值类型的数据,操作的是数据本身。
      int,float,string,bool,arry,struct
    • 引用类型的数据,操作的数据的地址。
      slice,map,chan
  • 值传递:值类型的数据传递为值传递,传递的是数据的副本。修改数据,对原始数据没有影响。

package main

import "fmt"

func main() {
    arr1 := [3]int{1, 2, 3}
    fmt.Println("函数调用前,数组的数据:", arr1)
    fun(arr1)
    fmt.Println("函数调用后,数组的数据:", arr1)
}

func fun(arr2 [3]int) {
    fmt.Println("函数中,数组的数据:", arr2)
    arr2[0] = 100
    fmt.Println("函数中,修改后数据的数据:", arr2)
}

运行结果

函数调用前,数组的数据: [1 2 3]
函数中,数组的数据: [1 2 3]
函数中,修改后数据的数据: [100 2 3]
函数调用后,数组的数据: [1 2 3]
  • 引用传递:引用类型的数据传递为引用传递,传递的数据的地址,导致多个变量指向同一块内存。
package main

import "fmt"

func main() {
    slice1 := []int{1, 2, 3}
    fmt.Println("函数调用前,切片的数据:", slice1)
    fun(slice1)
    fmt.Println("函数调用后,切片的数据:", slice1)
}

func fun(slice2 []int) {
    fmt.Println("函数中,切片的数据:", slice2)
    slice2[0] = 100
    fmt.Println("函数中,修改后切片的数据:", slice2)
}

运行结果

函数调用前,切片的数据: [1 2 3]
函数中,切片的数据: [1 2 3]
函数中,修改后切片的数据: [100 2 3]
函数调用后,切片的数据: [100 2 3]
  • 指针传递:传递的就是数据的内存地址。

三、函数的返回值

函数的返回值:
一个函数的执行结果,返回给函数调用处,执行结果就叫函数的返回值。
return语句:
一个函数的定义上有返回值,那么函数中必须有return语句,将执行结果返回给函数的调用处。
函数的返回结果必须和函数定义的一致,类型、数量、顺序。

  • 返回值的命名
    函数定义时可以给返回值命名,并在函数体中直接使用这些变量,最后通过return关键字返回。
func add(x, y int) (sum int) {
    sum = x + y
    return
}
  • 多返回值
    一个函数可以没有返回值,也可以有一个返回值,也可以有返回多个值。
func calc(x, y int) (sum, sub int) {
    sum = x + y
    sub = x - y
    return
}
  • 空白标识符-可用来舍弃某些返回值。
func calc(x, y int) (sum, sub int) {
    sum = x + y
    sub = x - y
    return
}
_, sub := calc(10, 20) //舍弃sum

四、函数的作用域

作用域:变量可以使用的范围。

1、全局变量

全局变量是定义在函数外部的变量,它在程序整个运行周期内都有效。 所有的函数都可以使用,而且共享这一份数据。

package main

import "fmt"

var a = 10

func main() {
    fmt.Println("test调用前,main中访问a:", a)
    test()
    fmt.Println("test调用后,main中访问a:", a)
}

func test() {
    fmt.Println("操作前,test中访问a: ", a)
    a = 20
    fmt.Println("操作后,test中访问a: ", a)
}

运行结果

test调用前,main中访问a: 10
操作前,test中访问a:  10
操作后,test中访问a:  20
test调用后,main中访问a: 20
2、局部变量

一个函数内部定义的变量,就叫做局部变量
局部变量只能在定义的范围内访问操作

package main

import "fmt"

func main() {
    test()
    fmt.Println("main中访问a:", a) //undefined: a
}

func test() {
    a := 20
    fmt.Println("test中访问a: ", a)
}

运行结果

# command-line-arguments
.\main.go:7:35: undefined: a

局部变量和全局变量重名,优先访问局部变量。

package main

import "fmt"

var a = 100

func main() {
    test()
}

func test() {
    a := 20
    fmt.Println("test中访问a: ", a)
}

运行结果

test中访问a:  20

另外,ifswitchfor语句中声明的变量也属于局部变量,在代码块外无法访问。

五、函数的本质

函数也是Go语言中的一种数据类型,可以作为另一个函数的参数,也可以作为另一个函数的返回值。

1、函数是一种数据类型
package main

import "fmt"

func main() {
    fmt.Printf("%T\n", fun1) //fun1的类型是func(int, int)
    fmt.Printf("%T\n", fun2) //fun2的类型是func(int, int) int
}

func fun1(a, b int) {
    fmt.Println(a, b)
}

func fun2(c, d int) int {
    fmt.Println(c, d)
    return 0
}

运行结果

func(int, int)
func(int, int) int
2、定义函数类型的变量
var f fun(int, int) int

上面语句定义了一个变量f,它是一个函数类型,这种函数接收两个int类型的参数并且返回一个int类型的返回值。
所有参数和返回值符合条件的函数可以赋值给f变量

package main

import "fmt"

func main() {
    var f func(int, int) int
    f = sum
    res := f(20, 10)
    fmt.Println("20 + 10 = ", res)
    f = sub
    res = f(20, 10)
    fmt.Println("20 - 10 = ", res)
}

func sum(a, b int) int {
    return a + b
}

func sub(a, b int) int {
    return a - b
}

运行结果

20 + 10 =  30
20 - 10 =  10

六、匿名函数

匿名函数就是没有函数名的函数

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

匿名函数因为没有函数名,所以没办法像普通函数那样调用,所以匿名函数需要保存到某个变量或者作为立即执行函数。

package main

import "fmt"

func main() {
    // 将匿名函数保存到变量中
    sum := func(a, b int) int {
        return a + b
    }
    // 通过变量调用匿名函数
    res := sum(10, 20)
    fmt.Println("10 + 20 =", res)
    // 自执行函数,匿名函数定义完直接加()执行
    func(c, d int) {
        fmt.Printf("%v + %v = %v\n", c, d, c+d)
    }(10, 20)
}

运行结果

10 + 20 = 30
10 + 20 = 30

七、高阶函数

go语言支持函数式编程:

  • 将一个函数作为另一个函数的参数
  • 将一个函数作为另一个函数的返回值

1、回调函数

一个函数被作为参数传递给另一个函数,那么这个函数就叫做回调函数。
回调函数并不会马上被调用执行,它会在包含它的函数内的某个特定的时间点被“回调”(就像它的名字一样)。

package main

import "fmt"

func main() {
    res := calc(10, 20, add)
    fmt.Println(res)
}
// add是一个func(int, int)int类型的函数,可以作为参数传递给calc函数
func add(a, b int) int {
    return a + b
}

// calc 高阶函数,它有两个int类型的参数和一个func(int, int)int函数类型的参数
// oper 回调函数,它被作为参数传递给calc函数
func calc(a, b int, oper func(int, int) int) int {
    res := oper(a, b)
    return res
}

运行结果

30
2、函数作为返回值
package main

import "fmt"

func main() {
    fun := calc("+")
    res := fun(10, 20)
    fmt.Println("10 + 20 =", res)
}

func sum(a, b int) int {
    return a + b
}

func sub(a, b int) int {
    return a - b
}

func calc(s string) func(int, int) int {
    switch s {
    case "+":
        return sum
    case "-":
        return sub
    default:
        fmt.Println("你传的是个啥玩意!")
        return nil
    }
}

运行结果

10 + 20 = 30
3、闭包

一个外层函数,有内层函数,该内层函数会操作外层函数的局部变量(外层函数的参数,或外层函数定义的变量),并且该内层函数作为外层函数的返回值。
这个内层函数和外层函数的局部变量,统称为闭包结构

局部变量的生命周期会发生改变。正常的局部变量随着函数的调用而创建,随着函数的结束而销毁。
但是闭包结构的外层函数的局部变量并不会随着外层函数的结束而销毁,因为内层函数还要继续使用。

package main

import "fmt"

func main() {
    fun := add()
    res := fun()
    fmt.Println("第一次调用,res=", res)
    res = fun()
    fmt.Println("第二次调用,res=", res)
    res = fun()
    fmt.Println("第二次调用,res=", res)
}

func add() func() int {
    i := 0
    return func() int {
        i++
        return i
    }
}

运行结果

第一次调用,res= 1
第二次调用,res= 2
第二次调用,res= 3

七、defer语句

defer是Go语言中的延迟执行语句,用来添加函数结束时执行的代码,常用于释放某些已分配的资源、关闭数据库连接、断开socket连接、解锁一个加锁的资源。
Go语言机制担保一定会执行defer语句中的代码。

1、延迟函数
  • 被延迟的函数,在离开所在的函数或方法时,执行(报错的时候也会执行
  • 如果有很多defer语句,遵从“先进后出”的模式
package main

import "fmt"

func main() {
    a := 1
    b := 2
    c := 3
    d := 4
    //defer a++ //a++ 是一个语句,并非函数或方法,程序报错
    defer fmt.Println("defer", a)
    defer fmt.Println("defer", b)
    defer fmt.Println("defer", c)
    defer fmt.Println("defer", d)
    fmt.Println(a)
    fmt.Println(b)
    fmt.Println(c)
    fmt.Println(d)
}

运行结果

1
2
3
4
defer 4
defer 3
defer 2
defer 1
2、延迟方法

延迟并不仅仅局限于函数。延迟一个方法调用也是完全合法的。

package main

import "fmt"

// Student 学生结构体
type Student struct {
    name string
    city string
}

func (s Student) hello() {
    fmt.Printf("我叫%v, 我来自%v。\n", s.name, s.city)
}

func main() {
    s := Student{
        name: "jack",
        city: "北京市",
    }
    defer s.hello()
    fmt.Print("大家好,")
}

运行结果

大家好,我叫jack, 我来自北京市
3、延迟参数

defer声明时会先计算确定参数的值,defer推迟执行的仅是其函数体。

package main

import "fmt"

func main() {
    a := 1
    defer fun(a)
    a++
    fmt.Println("main中的a =", a)
}

func fun(a int) {
    fmt.Println("fun中的a =", a)
}

运行结果

main中的a = 2
fun中的a = 1
4、defer与return
  • 所有函数在执行 RET 返回指令之前,都会先检查是否存在 defer 语句,若存在则先逆序调用 defer 语句进行收尾工作再退出返回;
  • 匿名返回值是在 return 执行时被声明,有名返回值则是在函数声明的同时被声明,因此在 defer 语句中只能访问有名返回值,而不能直接访问匿名返回值;
  • return 其实应该包含前后两个步骤:第一步是给返回值赋值(若为有名返回值则直接赋值,若为匿名返回值则先声明再赋值);第二步是调用 RET 返回指令并传入返回值,而 RET 则会检查 defer 是否存在,若存在就先逆序插播 defer 语句,最后 RET 携带返回值退出函数;
  • 因此,‍‍defer、return、返回值三者的执行顺序应该是:return最先给返回值赋值;接着 defer 开始执行一些收尾工作;最后 RET 指令携带返回值退出函数。
(1)匿名返回值的情况
package main

import "fmt"

func main() {
    fmt.Println(fun1())
}

func fun1() int {
    var i int
    defer func() {
        i++
    }()
    return i
}

运行结果

0
(2)有名返回值的情况
package main

import "fmt"

func main() {
    fmt.Println(fun2())
}

func fun2() (i int) {
    defer func() {
        i++
    }()
    return i
}

运行结果

1

分析

  • fun1()int函数的返回值没有被提前声名,其值来自于其他变量的赋值,而 defer 中修改的也是其他变量(其实该 defer 根本无法直接访问到返回值),因此函数退出时返回值并没有被修改。
  • fun2()(i int) 函数的返回值被提前声名,这使得 defer 可以访问该返回值,因此在 return 赋值返回值 i 之后,defer 调用返回值 i 并进行了修改,最后致使 return 调用 RET 退出函数后的返回值才会是 defer 修改过的值。

经典案例

package main

import "fmt"

func main() {
    fmt.Println(f1())
    fmt.Println(f2())
    fmt.Println(f3())
    fmt.Println(f4())
}

func f1() int {
    x := 5
    defer func() {
        x++ // defer 访问的是变量x,访问不到返回值
        // fmt.Println("f1函数defer中的x =", x) //6
    }()
    return x // 返回值 = 5     //返回5
}

func f2() (x int) {
    defer func() {
        x++ //defer 访问x, 可以访问返回值,在RET之前,将返回值修改为6
        // fmt.Println("f2函数defer中的x =", x) //6
    }()
    return 5 // 返回值(x) = 5   //返回6
}

func f3() (y int) {
    x := 5
    defer func() {
        x++ // defer 访问变量x,将变量x修改为6
        // fmt.Println("f3函数defer中的x =", x) //6
    }()
    return x // 返回值(y) = 5   //返回5
}
func f4() (x int) {
    defer func(x int) {
        x++ // 这里修改的defer时传入的x(0),将其修改为1
        // fmt.Println("f4函数defer中的x =", x) //1
    }(x) // defer 语句调用时传入x的值为int类型的默认值0
    return 5 // 返回值(x) = 5   //返回5
}

运行结果

5
6
5
5
5、defer的作用域
  • defer 只对当前协程有效(main 可以看作是主协程);
  • 当任意一条(主)协程发生 panic 时,会执行当前协程中 panic 之前已声明的 defer;
  • 在发生 panic 的(主)协程中,如果没有一个 defer 调用 recover()进行恢复,则会在执行完最后一个已声明的 defer 后,引发整个进程崩溃;
  • 主动调用 os.Exit(int) 退出进程时,defer 将不再被执行。
package main

import (
    "fmt"
    // "os"
)

func main() {
    fmt.Println("start")
    // panic("崩溃了")              // defer和之后的语句都不再执行
    // os.Exit(1)                  // defer和之后的语句都不再执行
    defer fmt.Println("defer")
    // go func() {
    //  panic("崩溃了")
    // }()                         // defer不被执行
    // panic("崩溃了")             // defer会执行,但后面的语句不再执行
    fmt.Println("over")
    // os.Exit(1)                  // defer不被执行
}

八、内置函数

内置函数 介绍
close 主要用来关闭channel
len 用来求长度,比如string、array、slice、map、channel
new 用来分配内存,主要用来分配值类型,比如int、struct。返回的是指针
make 用来分配内存,主要用来分配引用类型,比如chan、map、slice
append 用来追加元素到数组、slice中
panic和recover 用来做错误处理

panic和recover

Go语言中目前是没有异常机制,但是使用panic/recover模式来处理错误。 panic可以在任何地方引发,但recover只有在defer调用的函数中有效。

package main

import (
    "fmt"
)

func main() {
    fmt.Println("start")
    defer func() {
        err := recover()
        if err != nil {
            fmt.Println("recover")
            fmt.Println("活了")
        }
    }()

    panic("panic")
    fmt.Println("over")
}

运行结果

start
recover
活了
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345