GO学习之 函数的进阶

一.函数的调用机制

1.函数的调用过程

函数的局部变量与全局变量

举例:

package main

import "fmt"

// 一个test函数
func test(n1 int){
    n1 += 1
    fmt.Println("n1=",n1)
}

// 一个get函数
func getSum(n1 int, n2 int) int {
    //
    sum := n1 + n2
    return sum
}

func main(){
    // 调用 函数
    n1 := 10
    test(n1)
    get_sum := getSum(1, 2)
    fmt.Println("n1=",n1)
    fmt.Println("get_sum",get_sum)

}

程序在运行时,在调用函数时,会在栈空间中分配给函数一个空间。当调用完毕后,空间被进行回收。

return 语句:

注意go 语言return支持多个返回值

基本语法:

func 函数名(形参列表) (返回值类型列表){

语句....

return 返回值列表

}

1)如果返回多个值时,在接收时,希望忽略某个返回值,则使用’_’符号表示占位忽略

2)如果返回值只有一个 (返回值类型列表)可以不写 ()

多个返回值与 _ 占位举例:

package main

import "fmt"
// 返回两个值的和和查
func getSumAndSub(a int, b int) (int, int){
    sum := a + b
    sub := a - b
    return sum,sub
}

func main(){
    // 调用 函数
    n1 := 10
    test(n1)
    get_sum := getSum(1, 2)
    fmt.Println("n1=",n1)
    fmt.Println("get_sum",get_sum)

    // 返回多个值
    re1,re2 := getSumAndSub(3,2)
    fmt.Println(re1)
    fmt.Println(re2)

    // 希望忽略某个返回值,用 _ 符号表示占位符
    _,re3 := getSumAndSub(5,4)
    fmt.Println(re3)
}

 

2.函数的递归调用

基本介绍

一个函数在函体内又调用了本身,我们称为递归调用。

举例:

package main

import (
    "fmt"
)

func test(n int) {
    if n > 2 {
        n--
        test(n) // 把test()函数全部带入
    }
    fmt.Println("n=", n)
}

func main() {
    test(4)
}

举例2

package main

import (
    "fmt"
)
func test2(n int) {
    if n > 2 { // 注意执行过if 不在执行else
        n--
        test(n) // 把test()函数全部带入
    } else {
        fmt.Println("n=", n)  //  n = 2
    }   
}

func main() {
    test2(4)   
}

函数递归需要遵守的重要原则:

1_执行一个函数时,就创建一个新的受保护的独立空间(新函数栈)

2_函数的局部变量时独立的,不受相互影响

3_递归必须向退出递归的条件逼近,有穷性

4_当一个函数执行完毕,或遇到return,就会返回,遵守谁调用,就将结果返回给谁。

同时当函数执行完毕或者返回时,该函数本身也会被销毁。

递归经典练习题:

练习01:斐波那契数

package main

import "fmt"

func fb(n int ) int {
    if (n == 2 || n == 1) {
        return 1
    } else {
        return fb(n-1) + fb(n-2)
    }
}

func main(){
    //
    num := fb(5)
    fmt.Println("num:",num)  // 输出第几个的斐波那契数
}

练习02:求函数值

已知f(1) = 3;f(n) = 2*f(n-1) + 1;

请使用递归的思想编程,求出f(n)的值

package main

import "fmt"

func test(n int) int {
    if n == 1 {
        return 3
    } else {
        return 2 * test(n-1) + 1
    }
}

func main(){
    fmt.Println("f(1)=",test(1))  // 3
    fmt.Println("f(5)=",test(5))  // 63

}

练习03:猴子吃桃吃一半多一个,10天剩一个,原来桃子多少

package main

import "fmt"
func test(n int) int {
    if n == 10 {
        return 1
    } else {
        return 2 * (test(n+1) + 1)
    }
}


func main(){
    fmt.Println(test(5))
}

3.函数的主要事项和细节

1_函数的形参可以是多个,返回列表也可以是多个

2_形参列表和返回列表的数据类型可以是值类型和引用类型

3_函数的命名遵循标识符命名规范,首字母不能是数字。首字母大写包能被本包问号和其他问号使用,类似pubilc,首字母小写只能在本包使用,其他文件不能使用,类似private

4_函数中的变量是局部的,函数外不生效

5_基本数据类型和数组默认都是值传递的,即进行值拷贝。在函数内修改不会影响到原来的值。

6_如果希望函数内的变量能修改函数外的变量,可以传入变的地址&,函数内以指针的方式操作变量。从效果看类似引用。

7_GO函数不能重载,函数名相同不支持

8_GO中,函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数调用。

package main

import "fmt"

func getSum(n1 int,n2 int) int {
    return n1 + n2
}

func main(){
    // 在GO中,函数也是一种数据类型,
    // 可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数调用。
    a := getSum
    fmt.Printf("a的类型%T,getSum类似是%T
",a,getSum)
    res := getSum(10,40)
    fmt.Println("res=",res)

}

9_函数既然是一种数据类型,因此在GO中,函数可以作为形参,并且调用。

package main

import "fmt"

func getSum(n1 int,n2 int) int {
    return n1 + n2
}
// 函数既然是一种数据类型,因此在GO中,函数可以作为形参,并且调用。
func myFun(funvar func(int, int) int, num1 int , num2 int) int {
    return funvar(num1, num2)
}

func main(){
    // 在GO中,函数也是一种数据类型,
    // 可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数调用。
    a := getSum
    fmt.Printf("a的类型%T,getSum类似是%T
",a,getSum)
    res := getSum(10,40)
    fmt.Println("res=",res)

    res2 := myFun(getSum, 50, 60)
    fmt.Println("res2=",res2)

}

11_为了简化数据类型定义,GO支持自定义数据类型。

基本语法:type自定义数据类型名 数据类型  // 理解:相当于一个别名

举例:type myint int // 这时myint就等价于int来使用

package main

import "fmt"

func main(){

    type myInt int   // 给int取别名 ,在go中myInt和int虽然都是int类型,但是go认为myInt和int还是不同的类型
    var num1 myInt
    var num2 int

    fmt.Printf("num1类型%T,num2类型%T",num1,num2) // 打印:num1类型main.myInt,num2类似int

}

举例02type mySum func(int, int) int // 这时mySum就等价一个函数类型 func(int,int) int

package main

import "fmt"

// 案例2
type myFunType func(int, int) int // 这时myFun就是func(int,int) int 类型

func getSum(n1 int, n2 int) int {
    return n1 + n2
}

func myFun(funvar myFunType, num1 int, num2 int) int {
    return funvar(num1, num2)
}

func main() {

    type myInt int // 给int取别名 ,在go中myInt和int虽然都是int类型,但是go认为myInt和int还是不同的类型
    var num1 myInt
    var num2 int

    fmt.Printf("num1类型%T,num2类型%T
", num1, num2) // 打印:num1类型main.myInt,num2类似int

    res := myFun(getSum, 500, 600)
    fmt.Println(res)  \ 1100

}

 

12_支持对返回值命名

package main

import "fmt"

func getSumAndSub(n1 int, n2 int) (sum int, sub int) {
    sum = n1 + n2

    sub = n1 - n2

    return
}

func main() {
    //
    a, b := getSumAndSub(3, 2)
    fmt.Println(a, b)

}

13.使用_标识符,忽略返回值

14.GO中支持可变参数

语法格式:

// 支持0到多个参数

func sum(args...int) sum int {

}

// 支持1到多个参数

func sum(n1 int,args...int) sum int {

}

说明:

1args slice 切片(类似数组),通过args[index] 可以访问到各个值。

举例:编写一个函数sum ,可以求出1到多个int的和

package main

import "fmt"

func sum(n1 int,args...int) int {
    sum := n1
    for i := 0; i < len(args); i ++ {
        sum += args[i]  // args[0] 表示第一个元素的值 其他以此类推
    } 

    return sum
}

func main(){
    // 编写一个函数sum ,可以求出1到多个int的和
    res := sum(5,6,7,8,1,2,3)
    fmt.Println(res)

}

练习:使用函数对两个值进行交换

package main

import "fmt"

func swap(n1 *int, n2 *int) { // 指针接收地址进行值的相应交换
    // 定义一个临时变量
    t := *n1
    *n1 = *n2
    *n2 = t

}

func main() {
    a := 10
    b := 20
    swap(&a, &b) // 传入的是地址
    fmt.Printf("a=%v,b=%v", a, b)

}

.常用函数的介绍

1.init函数

基本介绍:

每一个源文件都可以包含一个init函数,该函数在main函数执行前,被GO运行框架调用,也就是说init会在main函数前被调用。

package main

import "fmt"

func init() {
    fmt.Println("run init")
}

func main(){
    fmt.Println("run main")
}
// 打印结果:
// run init
// run main

init函数的注意事项和细节

(1)如果一个文件同时包含全局变量定义,init函数和main函数,则执行的流程全局变量定义--init函数-->main函数

package main

import "fmt"

var age = test()

func test() int {
    fmt.Println("run test")
    return 90
}

func init() {
    fmt.Println("run init")
}

func main(){
    fmt.Println("run main")
    fmt.Println("age=",age)
}

// 得出结论:先执行变量定义,再执行test-->init -- > main
// 打印结果:
// run test
// run init
// run main
// age= 90

(2)init 函数最主要的作用就是完成一些初始化的工作。(就是定义一个初始化的变量)

(3)当一个main函数从另外文件引入包的时候,优先执行包中的init函数

2.匿名函数

GO支持匿名函数,如果我们某个函数只希望使用一次,可以考虑使用匿名函数,匿名函数也可以实现多次调用。

匿名函数使用方式1

在定义匿名函数时就直接调用(这种方式只能调用一次)

举例01:

package main

import "fmt"

func main() {
    // 在定义匿名函数时就直接调用
    // 使用匿名函数求两个数和
    res := func (n1 int,n2 int) int {
        return n1 + n2
    }(10,20)

    fmt.Println(res)

}

匿名函数使用方式2

将匿名函数赋值给一个变量(函数变量),再通过该变量来调用匿名函数

举例02

package main

import "fmt"

func main() {

    // 将匿名函数func (n1 int n2 int) int 赋给 a 变量
    // 则 a 的数据类型就是函数类型 此时 我们可以通过a完成调用
    a := func (n1 int,n2 int) int {
        return n1 - n2
    }

    // 可以多次调用  多态???
    res2 := a(30,10)
    fmt.Println(res2)
    res3 := a(40,10)
    fmt.Println(res3)

}

全局匿名函数

如果将匿名函数赋值给一个全局变量,那么这个匿名函数,就成为一个全局匿名函数,可以在整个程序中有效。

举例:

package main

import "fmt"

var (
    // Func1 为全局匿名函数
    Func1 = func(n1 int,n2 int) int {
        return n1 * n2
    }
)

func main() {
    // 全局匿名函数的使用
    res4 := Func1(4,5)
    fmt.Println(res4)

}

3.闭包

基本介绍:

闭包就是一个函数和与其相关的引用环境组合的一个整体(实体)。

举例:

package main

import "fmt"

// 累加器
func Addupper() func(int) int {
    // 匿名函数与外面的n构成一个闭包
    var n int = 10
    return func(x int) int {  // 返回是一个函数   匿名函数引用到外面变量n
        n = n + x
        return n
    }

}

func main() {
    //
    f := Addupper()
    fmt.Println(f(1))  // 11
    fmt.Println(f(2))  // 13

}

返回的是一个匿名函数,但是这个匿名函数引用到函数外的n,因此这个匿名函数就和n形成一个整体,构成闭包。

可以理解为:闭包是一个类 ,函数是操作,n是字段。

当我们反复的调用f函数时,因为n只初始化一次

搞清楚闭包的关机,就是要分析出返回的函数它使用(引用)到哪些变量,因为函数和引用的变量共同构成闭包。

一个闭包的例子:

package main

import "fmt"
import "strings"

func makeSuffix(suffix string) func(string) string {
    // (1)编写一个函数 makeSuffix (suffix string) 可以接收一个文件后缀名比如后缀名(.jpg)
    // 并返回一个闭包
    // (2)调用闭包,可以传入一个文件名,如果该文件名没有指定的后缀比如后缀名(.jpg),则返回
    // 文件名.jpg,如果已经有.jpg后缀,则返回原文件名

    // (3) 要求使用闭包的方式完成
    // (4)strings.HasSuffix  这个函数可以判断是否存有指定的后缀
    return func(name string) string {
        if !strings.HasSuffix(name, suffix) {
            return name + suffix
        }
        return name
    }
}

func main() {
    // .jpg的后缀
    f := makeSuffix(".jpg")
    // 实例引用
    fmt.Println("new_name:", f("today"))

    fmt.Println("new_name:", f("today.jpg"))
    

}

// 以上代码说明:返回的匿名函数和maksSuffix(suffix string)的suffix 变量组成一个闭包
// 返回的函数引用到suffix变量

4.函数中defer

为什么需要defer

在函数中,需要创建资源(比如:数据库连接,文件句柄,锁等),为了在函数执行完毕后,及时的释放资源,GO的设计提供defer(延时机制)

defer的基本介绍

1)当go执行到一个defer时,不会立即执行defer后的语句,而是将defer后的语句压入到一个defer栈中(一个独立的栈),然后继续执行函数下一个语句。

2)当函数执行完毕后,在defer栈中,依次从栈取出语句执行(注:遵循栈,先入后出的机制),所以同学们看到前面案例输出的顺序。

3defer将语句放入到栈时,也会将相关的值拷贝同时入栈

举例:

package main

import "fmt"

func sum(n1 int, n2 int) int {
    // defer 的作用会让其本来要执行的语句暂时不执行,将要执行的语句压入到一个defer栈(独立栈)
    // 暂时不执行
    // 当函数执行完毕后,再执行defer栈中语句:先入后出执行
    defer fmt.Println("n1=", n1) // defer
    defer fmt.Println("n2=", n2) // defer
    res := n1 + n2
    fmt.Println("in_res=", res)
    return res
}

func main() {
    res := sum(5, 7)
    fmt.Println("out_res=", res)
    // 结果:
    // in_res= 12
    // n2= 7
    // n1= 5
    // out_res= 12
}

defer的最佳实践在于,当函数执行完毕后,可以及时的释放创建的资源(比如:打开文件,立即defer就不用考虑什么时候去close,数据库连接也是如此)。在defer以后可以继续使用创建的资源,当函数执行完毕后可以自动从defer中取出并进行关闭资源。

比如:

例子01:

func test() {

// 关闭文件资源

file = openfile(文件名)

def file.close()

// 使用文件句柄相关操作

}

例子02

func test() {

// 释放数据库资源

connect = openDatabse()

defer connect.close()

// 操作数据库

}

三.函数参数的传递方式

基本介绍:

值类型参数默认就是值传递,而引用类型参数默认就是引用传递。

1.两种传递方式

1)值传递

基本数据类型(int float,bool,string,数组和结构体struct

2)引用传递(指针传递)

指针,slice切片,map,管道chan,interface等都是引用类型

不管是值传递还是引用传递,传递函数的都是变量的副本,不同的是,值传递的是值的拷贝,引用传递的是地址的拷贝,一般来说,地址拷贝效率高,因为数据量小,而值拷贝决定的数据大小,数据越大效率越低。

2.值传递和引用传递使用特点

1)值类型默认是值传递:变量直接存储值,内存通常在栈中分配。

2)引用类型默认是引用传递:变量存储的是一个地址,这个地址对应的空间才真正存储数据(值),内存通常在堆上分配,当没有任何引用这个地址时,该地址对应的数据空间就成为一个垃圾,有GC来回收。

3)如果希望函数内的变量能修改函数外的变量,可以传入变量的地址&,函数内以指针的方式操作变量。从效果上看类似引用。

四.变量的作用域

说明:

1) 函数内声明/定义的变量叫局部变量,作用域仅限于函数内部。

2)函数外部声明/定义的变量叫全局变量,作用域在整个包都有效,如果其首字母为大写,则作用域在整个程序有效。

3)如果变量是在一个代码块,比如for /if 中,那么这个变量的作用域就在该代码块中。

例子:局部变量与全局变量

package main

import "fmt"

var age int = 50
var Name string = "zero"

// 函数
func test() {
    // age 和 Name 的作用域只在test函数内
    age := 10
    Name := "hsz"
    fmt.Println("age=", age)
    fmt.Println("Name=", Name)
}

func main() {
    test()
    fmt.Println("age=", age)   // 打印是test() 函数外的age变量
    fmt.Println("Name=", Name) // 打印是test()函数外的Name变量
    // 最后输出结果为:
    // age= 10
    // Name= hsz
    // age= 50
    // Name= zero
}

GO语言中:赋值语句不能在函数体外,如:Name := “hsz” 是错误的。

四.函数的练习

1.金字塔

  // 打印金字塔案例   使用函数的方式

 // 1   2   3   4    5

package main

import "fmt"

func Jinzita(totalLevel int){

    // 表示层数
    for i :=1;i <= totalLevel; i++ {
        // 在打印 *前先打印空格
        for k:=1; k <= totalLevel - i; k++ {
            fmt.Print(" ")
        }

        // j 表示每层打印多少
        for j :=1; j <= 2 * i - 1; j++ {
            fmt.Print("*")
        }
        fmt.Println()
    }
}

func main() {
    var n int
    fmt.Println("请输入金字塔层数:")
    fmt.Scanln(&n)
    Jinzita(n)
}

2.九九乘法表

用函数的方式打印九九乘法表

package main

import "fmt"

func NightNight(){
    // 打印九九乘法表
    for i:=1;i<=9;i++{
        for j := 1; j <= i; j++{
            fmt.Printf("%v*%v=%v",i,j,i*j)
        }
        fmt.Println()
    }
}

func main(){
    NightNight()
}
原文地址:https://www.cnblogs.com/hszstudypy/p/12827086.html