函数、闭包与指针(go语言)

版权声明:未经本人允许请勿转载 https://blog.csdn.net/m0_37606346/article/details/82153093

函数、闭包与指针

函数

语法格式:

func 函数名(参数列表)(返回参数列表){
    //函数体
}
  • (1) go语言中的函数支持可变参数,此函数的参数数量是不定的

arg …int告诉GO这个函数接收不定数量的参数,这些参数类型全是int,变量arg是一个int的slice

func myfunc(arg ...int){    }  

+ (2) GO语言的函数可以返回多个值
+ (3) 返回参数列表可以是数据的数据类型,或者是:变量名+变量类型的组合

func Test() (a int,b uint){}
func Test() (a,b uint){}
  • (4) 若只有一个返回值且不声明返回值变量,那么可以省略()

    func Test() int{}

函数变量(函数作为值)

  • 在GO语言中函数也是一种类型,可以保存在变量中
  • 可以通过type来定义一个自定义类型。函数的参数完全相同(包括:参数类型,个数,顺序),函数返回值相同

1. 定义一个函数类型
2. 创建这个类型的函数
3. 作为参数调用

下例: 函数被当成参数传递到另一个函数中

func getString(str string) string {
    return str + "666"
}
//第二参数为函数(要标明返回值类型和参数)
func Test02(str string, myfunc func(string) string) string {  
    return myfunc(str)
}
func main() {
    str := "zzx"
    fmt.Println(Test02(str, getString))
}

下例:使用type定义一个函数的类型,被传参时函数类型就可以写出自己定义的类型

type getFunc func(string) string

func getString(str string) string {
    return str + "666"
}

func Test02(str string, myfunc getFunc) string {
    return myfunc(str)
}

func main() {
    str := "zzx"
    fmt.Println(Test02(str, getString))
}

匿名函数

  • 匿名函数没有函数名,只有函数体,函数可以作为一种类型被赋值给变量,匿名函数往往以变量方式被传递
  • 匿名函数经常被用于实现回调函数、闭包等
    1. 下例中:在定义时调用匿名函数(没有函数名最后的(100)是实参)
func main() {
    func(i int) {
        fmt.Println(i)
    }(100)
}

2. 下例:将匿名函数赋值给变量(这个变量是函数类型)

func main() {
    f := func(str string) {
        fmt.Println(str)
    }
    f("世界你好!")
}

3. 作为回调函数

type genFunc func(float64) string

func main() {
    arr := []float64{1, 4, 9, 16, 25, 30}
    //进行求平方根操作
    result := getgenSlice(arr, func(val float64) string {
        val = math.Sqrt(val)
        return fmt.Sprintf("%.2f", val)
    })
    fmt.Print(result)

}

//遍历切片,对其中每个值进行运算处理
func getgenSlice(arr []float64, f genFunc) []string {
    var result []string
    for _, value := range arr {
        result = append(result, f(value))
    }
    return result
}

闭包

  • 闭包是由函数和与其相关的引用环境组合而成的实体
  • 闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例
    一个编程语言需要有一下特性来支持闭包:
  • 函数可以作为另一个函数的返回值或参数,还可以作为一个变量的值
  • 在一个函数内部可以定义另一个函数
  • 允许定义匿名函数
  • 可以捕获引用环境,并把引用环境和函数代码组成一个可调用的实体
  • 闭包函数的返回值一定是一个函数类型的
  • 闭包函数可以不写函数名,闭包函数里面要有匿名函数

下例使用闭包实现计数器: (adder函数内的sum不清零,main函数中变量res是一直存在的adder中的sum也是一直存在的,所以没有被当做垃圾回收)

例1func main() {
    res := adder()
    fmt.Printf("%T \n", res)
    for i := 0; i < 5; i++ {
        fmt.Printf("i=%d \t", i)
        fmt.Println(res(i))
    }
}

//实现计数器的闭包函数
func adder() func(int) int {
    sum := 0
    result := func(num int) int {
        sum += num
        return sum
    }
    return result
}

输出结果:

func(int) int 
i=0     0
i=1     1
i=2     3
i=3     6
i=4     10
示例2:
func main() {
    res := Counter()
    fmt.Printf("%T\n", res)
    fmt.Println("res:", res)
    fmt.Println("res():", res())
    fmt.Println("res():", res())
    fmt.Println("res():", res())

    res2 := Counter()
    fmt.Printf("%T\n", res2)
    fmt.Println("res2:", res2)
    fmt.Println("res2():", res2())
    fmt.Println("res2():", res2())
    fmt.Println("res2():", res2())

}

//闭包函数,实现计数器功能
func Counter() func() int {
    i := 0
    res := func() int {
        i++
        return i
    }
    fmt.Println("Countern内部:", res)
    return res
}

输出结果为:

Countern内部: 0x48d7c0
func() int
res: 0x48d7c0
res(): 1
res(): 2
res(): 3
Countern内部: 0x48d7c0
func() int
res2: 0x48d7c0
res2(): 1
res2(): 2
res2(): 3
例3func main() {
    res := func() func() int {
        i := 10
        return func() int {
            i++
            return i
        }
    }()
    fmt.Println(res)       //结果:0x48b640
    fmt.Println(res())     //结果:11
}

可变参数

  1. 如果一个函数的参数,类型一致,但个数不定,可以使用函数的可变参数
  2. 语法格式
    • (在一个变量之后加上三个点…表示从该处开始接受不定参数)
    • (当要传递若干参数时,可以手动书写每个参数,也可以将一个slice传递给该函数注意要在后面加“…”
    • 一个函数中只能有一个可变参数
    • 参数列表中若有其他参数,则可变参数写在所有参数的最后
    func 函数名(参数名 ...类型)[(返回值列表)]{
        函数体
    }
func main() {
    //传n个成绩

    user, sum, avg, count := getScore("张子祥", 90.0, 45.5, 56.5, 77.0, 12.5)

    fmt.Printf("学员姓名:%s共有%d门成绩,总成绩为%.2f,平均成绩为%.2f", user, count, sum, avg)
    scores := []float64{90.0, 45.5, 56.5, 77.0, 12.5}

    fmt.Println()
    user1, sum1, avg1, count1 := getScore("张子祥", scores...)
    fmt.Printf("学员姓名:%s共有%d门成绩,总成绩为%.2f,平均成绩为%.2f", user1, count1, sum1, avg1)
}
func getScore(name string, scores ...float64) (user string, sum, avg float64, count int) {
    for _, value := range scores {
        sum += value
        count++
    }
    user = name
    avg = sum / float64(count)
    return
}

递归函数

  • 使用递归函数要注意栈溢出,在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会增加一层栈,每当函数返回,栈就会减少一层,由于栈的大小不是无限的,所以递归调用的次数过多,会导致栈溢出
func main() {
    fmt.Println(Factory(5))
}
func Factory(n int) int {
    if n == 0 {
        return 1
    }
    return n * Factory(n-1)
}

指针

  1. 指针是存储另一个变量的内存地址的变量
  2. 指针不参与运算
  3. 获取变量地址:
    • ==取地址符&==,一个变量前使用&,会返回变量的内存地址
func main() {
    a := 10
    fmt.Println(&a)     //0xc042056080
}

(一):声明指针

(1) 声明指针

  • *用于指定变量是一个指针
  • var ip *int //指向整型的指针
  • var fp *float32 //指向浮点型的指针
  • 获取原始数据*fp

(2):使用指针 (指针变量前加*表示原始数据,指针前加&还是指针)

func main() {
    //实际变量
    a := 120
    //声明指针变量
    var ip *int
    //给指针变量赋值,将变量a的地址赋值给ip
    ip = &a
    fmt.Printf("&a的类型%T,值是%v\n", &a, &a)   //&a的类型*int,值是0xc042056080

    fmt.Printf("&a的类型%T,值是%v", ip, ip)    //ip的类型*int,值是0xc042056080  

    fmt.Printf("*ip的类型%T,值是%v\n", *ip, *ip)  //*ip的类型int,值是120
    fmt.Printf("*&a的类型%T,值是%v\n", *&a, *&a)   //*&a的类型int,值是120  
    fmt.Println(ip, *ip, &ip, *(&ip), &(*ip))
    //0xc042056080 120 0xc042076018 0xc042056080 0xc042056080
}

(二):复合数据类型指针

func main() {
    s1 := Student{"zzx", 23, 1, false}
    var a = &s1

    fmt.Printf("s1的类型为%T,值为%v\n", s1, s1)
    //s1的类型为main.Student,值为{zzx 23 1 false}

    fmt.Printf("*a的类型为%T,值为%v\n", *a, *a)
    //*a的类型为main.Student,值为{zzx 23 1 false}

    fmt.Printf("a的类型为%T,值为%v\n", a, a)
    //a的类型为*main.Student,值为&{zzx 23 1 false}

}

(三):空指针
+ 当一个指针被定义后没有分配到任何变量时,它的值为nul
+ nil指针也称为空指针
+ 一个指针变量通常缩写为ptr
(1)空指针的判断:
+ if(ptr != nil) //ptr不是空指针
+ if(ptr == nil) //ptr是空指针

(四):指针作为函数参数

例:基本类型数据交换(不用这种写法)

func main() {
    x, y := 10, 22
    swap(&x, &y)
    fmt.Println(x, y)
}
func swap(x, y *int) {
    *x, *y = *y, *x
}

例:改变切片的值

func main() {
    arr := []int{10, 20, 33, 44}
    change(&arr)
    fmt.Println(arr)
}
func change(arr *[]int) {
    (*arr)[0] = 55
}

(五):指针数组

const COUNT int = 4

func main() {
    a := [COUNT]string{"abc", "123", "ABC", "一二三"}
    //数组指针(数组的指针)
    fmt.Printf("数组指针类型:%T,值:%v\n", &a, &a)    //数组指针类型:*[4]string,值:&[abc 123 ABC 一二三]
    //定义指针数组ptr(指针类型的数组)
    var ptr [COUNT]*string
    fmt.Printf("指针数组类型:%T,值:%v\n", ptr, ptr)   //指针数组类型:[4]*string,值:[<nil> <nil> <nil> <nil>]
    for i := 0; i < COUNT; i++ {
        //将数组中每个元素的地址赋值给指针数组的每个元素
        ptr[i] = &a[i]
    }
    fmt.Printf("%T,%v\n", ptr, ptr)   //[4]*string,[0xc04203e0c0 0xc04203e0d0 0xc04203e0e0 0xc04203e0f0]
    //根据指针数组元素的每个地址获取该地址所指向的元素的真实数值
    for i := 0; i < COUNT; i++ {
        fmt.Println(*ptr[i])         //  abc  123   ABC   一二三
    }
    for _, value := range ptr {
        fmt.Println(*value)            //  abc  123   ABC   一二三
    }

}

(六):指针的指针:
+ 如果一个指针变量存放的又是另一个指针变量的地址,则称这个指针变量为指针的指针变量
+ 指向指针的指针变量值需要使用两个*号
1. var ptr **int
2. 获取原始值**ptr

案例代码:

func main() {
    a := 10
    var ptr *int
    var pptr **int
    //为指针赋值
    ptr = &a
    fmt.Println(ptr)            //0xc042056080
    //为pptr赋值
    pptr = &ptr
    fmt.Println(pptr)           //0xc042076018
    //获取指针指向的值
    fmt.Printf("指向到指针的变量 **pptr:%d\n", **pptr)         //指向到指针的变量 **pptr:10
}

函数的参数传递(值传递,引用传递)

(一):值传递(java里只有值传递)
+ 默认情况下,Go语言使用的是值传递,
+ 每次调用函数,都将实参拷贝一份在传递到函数中
(二):引用传递(go语言中传指针
+ 概念:在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到原内容数据
+ GO语言中严格意义是没有引用传递的,但可以借助传指针到达引用传递的效果,函数参数使用指针参数,传参就是拷贝一份指针,也就是拷贝一份变量地址

示例代码:(从始至终a的内存地址没变)

func main() {
    a := "abcd"
    fmt.Printf("变量a的内存地址:%p,值为%v\n", &a, a)
    changeStringVal(a)
    fmt.Printf("调用changeStringVal后变量a的内存地址:%p,值为%v\n", &a, a)
    changeStringPtr(&a)
    fmt.Printf("调用changeStringPtr后变量a的内存地址:%p,值为%v\n", &a, a)
}

//值传递
func changeStringVal(a string) {
    fmt.Printf("------changeStringVal函数内:值参数a的内存地址:%p,值为:%v \n", &a, a)
    a = strings.ToUpper(a)
}

//传引用
func changeStringPtr(a *string) {
    fmt.Printf("-----changeStringPtr函数内:指针参数a的内存地址:%p,值:%v \n", &a, a)
    *a = strings.ToUpper(*a)
}

结果:

变量a的内存地址:0xc04204a1c0,值为abcd
------changeStringVal函数内:值参数a的内存地址:0xc04204a1e0,值为:abcd 
调用changeStringVal后变量a的内存地址:0xc04204a1c0,值为abcd
-----changeStringPtr函数内:指针参数a的内存地址:0xc042076020,值:0xc04204a1c0 
调用changeStringPtr后变量a的内存地址:0xc04204a1c0,值为ABCD

注意:

  • go语言中所有的传参都是值传递,

(1):拷贝的内容有时候是非引用类型int,string,bool,数组,struct属于非引用类型),这样就在函数中就无法修改原内容数据

(2):有的是引用类型指针,slice,map,chan属于引用类型(传值和传指针效果一样可以修改原内容数据)),这样就可以修改原内容数据
+ 是否可以修改原内容数据,和传值、传引用没有必然关系,在go语言中虽然只有传值,但也可以修改原内容数据,因为参数是引用类型

代码实例:

func main() {
    a := []int{4, 6, 7, 9}
    fmt.Printf("变量a的内存地址:%p,值为%v\n", &a, a)
    changeStringVal(a)
    fmt.Printf("调用changeStringVal后变量a的内存地址:%p,值为%v\n", &a, a)
    changeStringPtr(&a)
    fmt.Printf("调用changeStringPtr后变量a的内存地址:%p,值为%v\n", &a, a)
}

//值传递
func changeStringVal(a []int) {
    fmt.Printf("------changeStringVal函数内:值参数a的内存地址:%p,值为:%v \n", &a, a)
    a[0] = 0
}

//传引用
func changeStringPtr(a *[]int) {
    fmt.Printf("-----changeStringPtr函数内:指针参数a的内存地址:%p,值:%v \n", &a, a)
    (*a)[1] = 0
}

输出结果:

变量a的内存地址:0xc0420503e0,值为[4 6 7 9]
------changeStringVal函数内:值参数a的内存地址:0xc042050440,值为:[4 6 7 9] 
调用changeStringVal后变量a的内存地址:0xc0420503e0,值为[0 6 7 9]
-----changeStringPtr函数内:指针参数a的内存地址:0xc042076020,值:&[0 6 7 9] 
调用changeStringPtr后变量a的内存地址:0xc0420503e0,值为[0 0 7 9]

猜你喜欢

转载自blog.csdn.net/m0_37606346/article/details/82153093