GO语言函数

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

函数定义:

func 函数名 (参数1 数据类型, 参数2 ...数据类型) (返回值1 数据类型, 返回值2 数据类型) {
    函数内部代码
    ...
    deder 延迟执行语句
    return 变量名或者数值
}

其中:

  • 函数名:由字母、数字、下划线组成。但函数名的第一个字母不能是数字。在同一个包内,函数名也称不能重名(包的概念详见后文)。
  • 参数:参数由参数变量和参数变量的类型组成,多个参数之间使用,分隔。
  • 返回值:返回值由返回值变量和其变量类型组成,也可以只写返回值的类型,多个返回值必须用()包裹,并用,分隔。
  • 函数体:实现指定功能的代码块。

函数的调用

使用函数名()调用函数,有返回值的函数可以不接收返回值。

函数参数

参数:参数支持简写func 函数名 (参数1,参数2 数据类型, 参数3 ...数据类型) {}。参数和数据类型中间隔一个空格。...数据类型叫可变参数,也有人叫解包。可变参数一定要放在固定参数的末尾,也就是说如果需要可变参数,那么它一定是在最后一个。一个函数仅支持一个可变参数。可变参数在函数内部是一个切片。看下列:

func add(a ...int) {
    fmt.Printf("可变参数类型:[%T] \n", a)
    fmt.Printf("%#v", a)
}
func main() {
    add(1, 2, 3, 4, 5)
}

调用函数add()后运行结果如下:

可变参数类型:[[]int]
[]int{1, 2, 3, 4, 5}

函数返回值

返回值:使用return关键字向外输出返回值,一个函数可以没有返回值。在声明函数时命名过的返回值在函数内部可以直接使用,不需要在函数内部再次声明。当一个返回一个切片时,nil可以看做是一个有效的切片,没必要返回一个长度为0的切片[]int{}

func someFunc(x string) []int {
    if x == "" {
        return nil // 没必要返回[]int{}
    }
    ...
}

变量左右域

变量作用域–全局变量是定义在函数(func main()也是一个函数)之外的变量,他在整个运行周期都有效。在函数中可以访问到全局变量。如果全局变量和函数内部的变量重名,将优先使用函数内部的变量。另外函数访问不到其他函数内部的变量。

//全局变量
var a = 8
var c = 99

//函数f2
func f2(f2 int) {
    //变量f2在这里
}

//函数f1
func f1(a, b int) {
    // fmt.Printf(" f2:%v \n", f2)
    fmt.Printf("a:%v b:%v c:%v \n", a, b, c)
}
func main() {

    //调用函数f1。其中参数a=1 b=2
    f1(1, 2)

}

运行效果如下:

a:1 b:2 c:99

同一级别并列的{}的变量不能互相识别,但是包含在{}内部的{}内的变量从内向外识别,入下例ifforswitch中的变量不能互相识别,但是可以识别到函数变量或者全局变量。

//全局变量
var quanju1 = 8
var quanju2 = 99

//函数f1
func f1(a int) {

    //例一:for内部可识别范围
    for b := 0; b < 3; b++ {
        for_a := 99
        fmt.Println(quanju1, a, b, for_a)
    }

    //if内部不能识别for内部变量'for_a'
    if a == 1 {
        if_a := 66
        fmt.Println(quanju1, a, if_a)
        // fmt.Println(for_a)
        // fmt.Println(b)
    }

    //if从内到外识别
    for i := 0; i < 3; i++ {
        for_b := 998
        if a == 1 {
            fmt.Println(quanju1, a, for_b)
            // fmt.Println(if_a)
            // fmt.Println(for_a)
        }
    }
}
func main() {

    //调用函数f1。其中参数a=1
    f1(1)

}

defer延迟执行

defer延迟时机,defer后面的语句会在函数执行完毕真正返回之前执行。一个函数可以有多个defer语句,多个defer语句按照先进后出执行。如下例:

func main() {
    fmt.Println("start")
    defer fmt.Println(1)
    defer fmt.Println(2)
    defer fmt.Println(3)
    fmt.Println("end")
}

运行结果如下:

start
end
3
2
1

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

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

上面代码先看函数的返回值,这是一个未命名返回值的函数,可以把他的返回值想象成z,然后看函数体x := 5,接下来就是一个defer,跳过defer看返回语句return x,这句可以理解成z = x; defer; return,现在返回值z变成5了。回过头再来看那一句defer语句里面有个x++,返回值z已经是5了,那个x++和返回值z没关系了。在下一句就是就是return了,所以这个函数返回的是5。在看下面的例子:

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

这个函数是命名返回值的函数,记住他的返回值是x。然后看函数体没其它语句,上来就是一个defer,先跳过看最后的return 5,一样先翻译一下x = 5; defer; return,这样比较容易理解,现在的返回值是x,再来看defer语句,x++也就是6了,现在的返回值x是6昂。在然后就是真的的return了,所以这个函数返回6。下面是全部代码,懒得打字了:

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

还要记住一点,函数传参数,穿的是副本。修改传到函数内部的参数,并不会影响之前的本体。

函数类型

函数类型:函数也是一种数据类型

func add(a, b int) {
    fmt.Println("hello world")
}
func main() {
    a := add
    fmt.Printf("%T \n", a)
}

运行结果显示,变量a的数据类型是func(int, int)

定义函数类型

定义函数类型:使用关键字type定义函数类型

type hanshu func(参数类型1,参数类型2) 返回值类型

来声明一个类型hanshu,他有两个int参数和一个int返回值。

type hanshu func(int, int) int

使用函数类型

使用函数类型:要使用hanshu类型首先要有符和它的函数

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

然后声明一个hanshu类型的变量c

var c hanshu

为变量c赋值一个函数,这个函数要符号hanshu类型的要求,两个int参数和一个int返回值

c = add

接下来就该使用这个hanshu类型的变量c了,使用它和使用函数的方法一样,看起来只是把函数的名字换成了变量名。额~~~饶了一大圈,并没什么卵用啊~~~

c(2,3)

这里查看一下变量c的数据类型显示为

main.hanshu

fmt.Printf("%T \n", c)

下面是全部代码:

//定义一个类型'hanshu'它有两个'int'参数和一个'int'返回值
type hanshu func(int, int) int

//add函数符合hanshu类型
func add(x, y int) int {
    return x + y
}

//sub函数符合hanshu类型
func sub(x, y int) int {
    return x - y
}

func main() {

    //声明一个类型为'hanshu'的变量'c'
    var c hanshu

    //为变量c赋值一个符合类型'hanshu'的函数add
    c = add

    //输出并使用变量变量c,c的类型是'hanshu'
    fmt.Println(c(2, 3)) //看起来好像只是把'add'函数改了个名字~~~

    fmt.Println("--------------------")

    //查看变量'c'的数据类型,显示为'main.hanshu'
    fmt.Printf("%T \n", c)

    //再把变量c赋值一个sub函数
    c = sub
    fmt.Println(c(2, 3))
}

如果配合switch倒是能少写点代码~~~~~~

函数类型可以作为函数参数

函数类型可以作为参数使用,在声明函数的时候写明要传进来的函数具体类型,入下例第三个参数运行传入一个函数,要求传入的这个函数有两个int参数和一个int返回值。

//第三个参数'z'允许传入一个函数。
func add(x, y int, z func(int, int) int) int {

    //使用当做参数传进来的函数。
    z(x, y)
    return x + y + z(x, y)
}

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

func main() {

    //直接把函数'sub'函数当做参数
    fmt.Println(add(9, 5, sub))
}

函数类型作为函数返回值

也可以把函数当做返回值来使用:

//定义该函数的返回值是函数类型
func add() func(string) {
    fmt.Println("我是add")

    //返回函数'dayin()'
    return dayin
}

func dayin(s string) {
    if s == "" {
        s = "打印"
    } else {
        s = "打印" + s
    }
    fmt.Println(s)
}

func main() {

    //调用函数'dayin()'
    dayin("")

    //调用函数'add()'后在调用它返回的函数
    add()("add()()")

}

匿名函数

匿名函数,匿名函数就是一个没有名字的函数,一般用在函数内部。一般来说在函数内部不能声明一个正常的函数,但是可以调用函数,既然不能声明正常函数,那就是说可以声明不正常的函数,也就是匿名函数。

func main() {

    //在函数内部声明函数的办法
    var ff = func(s string) { fmt.Printf("%s \n", s) }

    //调用在函数内部声明的函数
    ff("匿名函数")
    fmt.Printf("变量ff的类型:%T \n", ff)
}

上面代码就这main()函数内部声明了一个函数,并成功的调用了这个函数。这个函数可以多次调用,当然也可以定义一个只用一次的函数,用一次就不再使用了,就销毁了。

func main() {
    //在函数内部声明函数的办法,这个函数立即执行,只能使用一次
    func(s string) { fmt.Printf("%s \n", s) }("立即执行的匿名函数,只能使用一次。")
}

闭包

先看下面这个列子,这个函数返回一个没有参数没有返回值的函数。如果仅调用这个函数add()只会执行fmt.Println("add")这一行代码,也就是说只会输出add。因为他的返回值是一个函数,所以要想执行fmt.Println("aaa")的话,必须这样调用add()()。或者先用一个变量接收他的返回值add_fanhui,然后在用add_fanhui()的方式输出aaa

func add() func() {
    fmt.Println("add")
    return func() {
        fmt.Println("aaa")
    }
}
func main() {
    add_fanhui := add()
    fmt.Printf("add_fanhui数据类型:%T \n", add_fanhui)
    add_fanhui()
}

可能会觉得上面这种写法不好理解,如果把函数add()返回的那个函数重新写成一个函数,并起一个民那个字,return xxx会更直观更好理解。其实这样写是迫不得已,看下面的例子:

func add() func() {
    s := "add"
    fmt.Printf("%s \n", s)
    return func() {
        b := "add2" + s
        fmt.Printf("%s \n", b)
    }
}
func main() {
    add()()
}

如果分成两个函数写的话,变量b就拿不到变量s的数据了。想想函数变量的作用域。两个函数属于并列关系,都拿不到对方的变量数据,而现在这种不好理解的写法,属于包含关系,所以返回的那个函数可以拿到属于函数add()中变量s的数据。
这个函数返回一个没有参数没有返回值的函数
如果仅调用这个函数add()只会执行fmt.Println("add")这一行代码,也就是说只会输出add
因为他的返回值是一个函数,所以要想执行fmt.Println("aaa")的话,必须这样调用add()()。或者先用一个变量接收他的返回值add_fanhui,然后在用add_fanhui()的方式输出aaa

func add() func() {
    fmt.Println("add")
    return func() {
        fmt.Println("aaa")
    }
}
func main() {
    add_fanhui := add()
    fmt.Printf("add_fanhui数据类型:%T \n", add_fanhui)
    add_fanhui()
}

可能会觉得上面这种写法不好理解,如果把函数add()返回的那个函数重新写成一个函数,并起一个民那个字,return xxx会更直观更好理解。其实这样写是迫不得已,看下面的例子:

func add() func() {
    s := "add"
    fmt.Printf("%s \n", s)
    return func() {
        b := "add2" + s
        fmt.Printf("%s \n", b)
    }
}
func main() {
    add()()
}

如果分成两个函数写的话,变量b就拿不到变量s的数据了。想想函数变量的作用域。两个函数属于并列关系,都拿不到对方的变量数据,而现在这种不好理解的写法,属于包含关系,所以返回的那个函数可以拿到属于函数add()中变量s的数据。还是上面的代码稍微改一下,改成下面这种,原理一样只是改成带参数。

func add(a int) func(int) int {
    // a = 0
    fmt.Printf("add函数内变量a:[%d] a地址:[%v] \n", a, &a)
    return func(y int) int {
        fmt.Printf("返回的匿名函数内a:[%d] a的地址[%v]--1 \n", a, &a)
        a += y
        fmt.Printf("返回的匿名函数内a:[%d] a的地址[%v]--2 \n", a, &a)
        return a
    }
}
func main() {
    aa := add(1) //0
    // fmt.Printf("%T", aa)
    fmt.Println(aa(1)) //0----1-[1]
    add(9)             //0
    fmt.Println(aa(5)) //1----6-[6]
    add(1)             //0
    fmt.Println(aa(0)) //6----6-[6]
    fmt.Println(add(0)(9)) //这种方式调用匿名函数会重置变量'a'的数据
    fmt.Println(add(1)(3))
}

我们会发现,返回的这个匿名函数永远会包括这个变量a,仔细观察每次输出的变量a的内存地址,只有第一次匿名函数拿到的变量a的内存地址和add()函数内部变量a的内存地址一样,后面在调用add()的话,add内的变量a会换另一个内存地址。我也不知道怎么表达了,总计一下就是,匿名函数包括函数体和他第一次拿到的那个变量a,但是不包括下一次调用add()时的变量a。其主要原因在于aa := add(1)这一句,aa拿到的是add()返回的匿名函数的内存地址,这个返回的匿名函数的内存地址包括了add()内变量a,所以只要aa拿到的这个匿名函数的内存地址不变,那个a变量的数据也就不会变。或者可以使用add()()这种调用匿名函数的方式来重置变量a的数据。
下面在来一个高难度的,下面这个ff函数能输出钢蛋?基佬,中间的?号由参数y func()来控制。并且这个函数不允许修改。

func ff(y func()) {
    fmt.Print("钢蛋")
    y()
    fmt.Print("基佬 \n")
}

下面这个fd函数能输出或者不是,至于具体输出什么由参数s控制,这个函数也不允许修改。

func fd(s string) {
    fmt.Print(s)
}

现在的要求是要用函数ff()调用函数fd()来实现输出钢蛋是基佬或者钢蛋不是基佬,至于或者不是由函数fd()计算后得出。这里的两个函数ff()fd()都不允许修改。来看看怎么实现的。看下面这个函数

//自己写的封装函数
func yesORno(s string) func() {
    return func() {
        fd(s)
    }
}

这个封装函数接受一个字符串变量s,返回一个没有参数没有返回值的的你卖匿名函数。

//这个函数不允许修改
func ff(y func()) {
    fmt.Print("钢蛋")
    y()
    fmt.Print("基佬 \n")
}

//这个函数同样不允许修改
func fd(s string) {
    fmt.Print(s)
}

//自己写的封装函数
func yesORno(s string) func() {
    return func() {
        fd(s)
    }
}

func main() {
    ff(yesORno("==>"))
}

实例面试题

管于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
}

defer语句后面的语句,程序会先别确切的参数或者数据算出来(仅包括变量的值,函数的参数等。不包括不包括函数内部代码也就是不包括{}内的代码。),然后在圧栈,具体流程如下:

  1. x=1
  2. y=2
  3. defer calc(“AA”,1,calc(“A”,1,2))
    输出:”A” 1 2 3
    defer calc(“AA”,1,3)
  4. x=10
  5. defer calc(“BB”,10,calc(“B”,10,2))
    输出:”B” 10 2 12
    defer calc(“BB”,10,12)
  6. y=20
  7. 输出:”BB” 10 12 22
  8. 输出:”AA” 1 3 4

分金币练习

你有50枚金币,需要分配给以下几个人:Matthew,Sarah,Augustus,Heidi,Emilie,Peter,Giana,Adriano,Aaron,Elizabeth。
分配规则如下:

  1. 名字中每包含1个eE分1枚金币
  2. 名字中每包含1个iI分2枚金币
  3. 名字中每包含1个oO分3枚金币
  4. 名字中每包含1个uU分4枚金币

写一个程序,计算每个用户分到多少金币,以及最后剩余多少金币?
程序结构如下,请实现 dispatchCoin 函数

package main

import "fmt"

var (
    coins = 50
    users = []string{
        "Matthew", "Sarah", "Augustus", "Heidi", "Emilie", "Peter", "Giana", "Adriano", "Aaron", "Elizabeth",
    }
    distribution = make(map[string]int, len(users))
)

func dispatchCoin() int {

    //    字符串切片导入map
    for _, v := range users {

        //遍历users字符
        for _, x := range v {

            //具体计算获得金边数量
            if x == 'e' || x == 'E' {
                distribution[v]++
                coins--
            } else if x == 'i' || x == 'I' {
                distribution[v] += 2
                coins -= 2
            } else if x == 'o' || x == 'O' {
                distribution[v] += 3
                coins -= 3
            } else if x == 'u' || x == 'U' {
                distribution[v] += 4
                coins -= 4
            }
        }
    }

    //返回金边剩余数量
    return coins
}
func main() {
    left := dispatchCoin()
    fmt.Println("剩下:", left)

    for k, v := range distribution {
        fmt.Printf("%s分得%d枚金边 \n", k, v)
    }
}

递归函数

递归函数:递归函数就是自己调用自己的函数,递归函数一定要有一个明确的退出条件,比如阶乘:
比如5的阶乘是:
3*2*1
4*3*2*1
5*4*3*2*1
6*5*4*3*2*1
6的阶乘是6乘以5的阶乘
5的阶乘是5乘以4的阶乘
n的阶乘是n乘以n-1的阶乘,所以~~~

package main

import "fmt"

var s = "-"

//递归函数练习
func digui(a int) int {
    //无实际作用,仅作为辅助理解递归函数运行模式使用
    fmt.Printf("%sif之前a[%d] ", s, a)

    if a <= 1 {
        //无实际作用,仅作为辅助理解递归函数运行模式使用
        fmt.Printf("-if内,准备返回a[%d] \n", a)
        s += "-"

        return a
    }
    //无实际作用,仅作为辅助理解递归函数运行模式使用
    fmt.Printf("-if之后a[%d]这里返回的是%d * digui(%d-1) \n", a, a, a)
    s += "-"

    return a * digui(a-1)
}
func main() {
    fmt.Println(digui(3))
}

运行结果如下:

  1. -if之前a[3] -if之后a[3]这里返回的是3 * digui(3-1)
    这里程序digui(3)运行结束,准备返回3*digui(3-1)时,要先算出digui(3-1)返回的具体数字。

  2. –if之前a[2] -if之后a[2]这里返回的是2 * digui(2-1)
    和上一步一样,在运行3*digui(3-1)时,准备返回2*digui(2-1)时,需要先算出这个digui(2-1)的具体数字。

  3. —if之前a[1] -if内,准备返回a[1]

    • 这里是程序digui(2-1)运行结束时,程序明确的知道要返回1
    • 所以第二步的返回值2*digui(2-1)实际运行的就是2*1,返回值就是明确的数字2
    • 所以第一步3*digui(3-1)实际运行的就是3*2,返回值是6
  4. 6
    所以main函数中digui(3)的返回值就是6

乘法表

九九乘法表,支持正序或倒序输出。源代码如下,自行编译去吧。

package main

import "fmt"

var shuru = 0

/*
打印九九乘法表
作用:根据选项o正三角或倒三角输出九九乘法表。
参数:o bool类型。true以正三角输出九九乘法表。false倒三角输出九九乘法表。
返回值:无
实例:chengfabiao(true)
*/
func chengfabiao(o bool) {
    if o {
        //正三角输出九九乘法表
        for i := 1; i <= 9; i++ {
            for j := 1; j <= i; j++ {
                fmt.Printf("%d*%d=%d \t", i, j, i*j)
            }
            fmt.Println()
        }
    } else {
        //倒三角输出九九乘法表
        for i := 9; i >= 1; i-- {
            for j := 1; j <= i; j++ {
                fmt.Printf("%d*%d=%d \t", j, i, j*i)
            }
            fmt.Println()
        }
    }
}

//菜单函数
func menu(shuru int) {
    //菜单功能
    switch shuru {

    //用户选1,正序输出
    case 1:
        chengfabiao(true)

        // 用户选择2,倒序输出
    case 2:
        chengfabiao(false)

        //不要胡乱操作
    default:
        fmt.Println("如需要退出,请按[3]后回车,致谢~")
    }
}

func main() {

    for {
        //欢迎辞
        fmt.Println(`----------九九乘法表----------
1:正序输出     2:倒序输出     3:退出或者Ctrl + c
--------------------建议横屏获得最佳显示效果-------------------`)

        //获取用户输入
        fmt.Scanln(&shuru)

        //调用menu函数或者退出
        if shuru != 3 {
            menu(shuru)
        } else {
            break
        }
    }
}

  转载请注明: So Cold GO语言函数

 上一篇
GO语言包 GO语言包
定义包我们还可以根据自己的需要创建自己的包。一个包可以简单理解为一个存放.go文件的文件夹。 该文件夹下面的所有go文件都要在代码的第一行添加如下代码,声明该文件归属的包。 package 包名注意事项: 一个文件夹下面直接包含的文件只能
2020-05-17
下一篇 
Fmt包 Fmt包
fmt标准库学习测试fmt.Print系列输出到终端,fmt.Sprint系列输出到变量,实际就是拼接字符串fmt.Fprint系列输出到文件fmt.Scan系列从标准输入读取fmt.Fscan系列从文件中读取fmt.Sscan系列从字符串
2020-05-04
  目录