Go 语言系列教程(九) : 函数深入分析

前言

熬过了七夕,谢不杀之恩,赶紧写篇博客压压惊。

函数声明

函数声明包括函数名、形式参数列表、返回值列表(可省略)以及函数体。

func name(parameter-list) (result-list) {
    
    
	body
}

当然函数名前面还可以加上结构体绑定,参考上一节 Go 语言系列教程(八) : 结构体深入解析

Tip: 如果函数返回一个无名变量或者 没有返回值,返回值列表的括号是可以省略的

如果一组形参或返回值有相同的类型,我们不必为每个形参都写出参数类型。下面2个声明是等价的

func f(i , j ,k int, s, t string)
func f(i int, j int,k int, s string, t string)

Go语言的值传递

  • 思考Go语言中有没有引用传递??
    答:没有

实参通过值传递的方式传递,因此函数的形参是实参的拷贝。对形参进行修改不会影响实参。但是,如果实参包括引用类型,如指针,slice(切片)、map、function、channel等类型,实参可能会由于函数的间接引用被修改

Tip:在Go语言中 不存在引用传递,要么传递值的副本要么从传递指针的副本

下面我们写个小案例来说明

i := 10
p := &i
i的内存地址为: 0xc042060080,i的指针的内存地址为 0xc042080018

比如 我们创建一个整型变量 i,该变量的值为10,有一个指向整型变量 i 的指针ip,该ip包含了 i 的内存地址 0xc042060080 。但是p也有自己的内存地址 0xc042080018。

  • 值类型的值传递
package main

import "fmt"

func main() {
    
    
	i := 10 //整形变量 i
	p := &i //指向整型变量 i 的指针ip,包含了 i 的内存地址
	fmt.Printf("main中i的值为:%v,i 的内存地址为:%v,i的指针的内存地址为:%v\n",i,p,&p)
	search(i)
	fmt.Printf("main中i的值为:%v,i 的内存地址为:%v,i的指针的内存地址为:%v\n",i,p,&p)


}

func search(i int) {
    
    
	fmt.Printf("search i 为:%v,i的指针的内存地址为:%v\n",i,&i)
	i = 11
}

结果

main中i的值为:10,i 的内存地址为:0xc00000a0c8,i的指针的内存地址为:0xc000006028
search i 为:10,i的指针的内存地址为:0xc00000a0e8
main中i的值为:10,i 的内存地址为:0xc00000a0c8,i的指针的内存地址为:0xc000006028

内存示意图
image.png

  • 普通类型的指针传递

先整个简单版本,便于理解

package main

import "fmt"

func main() {
    
    
	i := 10 //整形变量 i
	//p := &i //指向整型变量 i 的指针ip,包含了 i 的内存地址
	fmt.Printf("main中i的值为:%v,i 的内存地址为:%v\n",i,&i)
	searchByP(&i)
	fmt.Printf("main中i的值为:%v,i 的内存地址为:%v\n",i,&i)
}

func searchByP(i *int) {
    
    
	fmt.Printf("指向i的指针的内存地址为:%v\n",&i)
	*i = 11
}


结果

main中i的值为:10,i 的内存地址为:0xc00000a0c8
指向i的指针的内存地址为:0xc000006030
main中i的值为:11,i 的内存地址为:0xc00000a0c8

大家可以发现 i在main的内存地址 和传递在searchByP的指针的内存地址不一样

package main

import "fmt"

func main() {
    
    
	i := 10 //整形变量 i
	p := &i //指向整型变量 i 的指针ip,包含了 i 的内存地址

	fmt.Printf("main中i的值为:%v,i 的内存地址为:%v,i的指针的内存地址为:%v\n",i,p,&p)
	searchByP(p)
	fmt.Printf("main中i的值为:%v,i 的内存地址为:%v,i的指针的内存地址为:%v\n",i,p,&p)
}



func searchByP(i *int) {
    
    
	fmt.Printf("searchByP i 为:%v,i的指针的内存地址为:%v\n",i,&i)
	*i = 11
}

结果

main中i的值为:10,i 的内存地址为:0xc00000a0c8,i的指针的内存地址为:0xc000006028
searchByP i 为:0xc00000a0c8,i的指针的内存地址为:0xc000006038
main中i的值为:11,i 的内存地址为:0xc00000a0c8,i的指针的内存地址为:0xc000006028

内存示意图
image.png

非引用类型(值类型)和指针的参数传递都是传递副本,那么对于引用类型的参数传递又是如何的呢?

  • 引用类型的值传递

参考 [简书–小杰的快乐时光] https://www.jianshu.com/p/f201d6da488a
感谢

引用类型(映射(map),数组切片(slice),通道(channel),方法与函数)

总结:在Go语言中只存在值传递(要么是该值的副本,要么是指针的副本),不存在引用传递。之所以对于引用类型的传递可以修改原内容数据,是因为在底层默认使用该引用类型的指针进行传递,但是也是使用指针的副本,依旧是值传递。

函数的多返回值

许多标准库中的函数返回2 个值,一个是期望得到的返回值,另一个是函数出错时的错误信

package main

import (
	"errors"
	"fmt"
)

func Add(a, b int) (sub int, err error) {
    
    
	if a < 0 || b < 0 {
    
    
		err = errors.New("a,b不同为负数")
		return 0, err
	}
	return a + b, nil
}

func main() {
    
    
	fmt.Println(Add(-1, 2))
}

结果

0 a,b不同为负数

Tip: errors.New() 方法返回的是什么? 一下是它的源码

package errors

// New returns an error that formats as the given text.

// New 返回一个给定文本格式的错误。
func New(text string) error {
    
    
    return &errorString{
    
    text}
}

// errorString is a trivial implementation of error.

// errorString 是 error 的一个琐碎的实现。
type errorString struct {
    
    
    s string
}

func (e *errorString) Error() string {
    
    
    return e.s
}

函数的递归

递归想必大家都比较熟悉,下面我们来用Go的递归来计算n!

func Cacule(n int) (sum int) {
    
    
	if n > 0 {
    
    
		return Cacule(n-1) * n
	}
	return 1
}
func main() {
    
    
	fmt.Println(Cacule(10))  // 3628800
}

匿名函数和闭包

匿名函数就是没有名字的函数
闭包就是一个函数“捕获”和它在同一作用域的其他常量或变量形成的环境,一个函数和与其相关的引用环境组合的一个整体(实体)

package main

import "fmt"

func add(x , y , i , k int) func() (int,int,int,int) {
    
    
	k++
	p := 0
	fmt.Println("p  k  初始化执行")
	return func() (int,int,int,int) {
    
    
		p++
		i++
		fmt.Println("i,k,p,x+y")
		return i ,k ,p , x + y
	}
}


func main() {
    
    
	fmt.Println("--------result--------")
	result := add(1,2,0,0)
	fmt.Println(result())
	fmt.Println(result())
	fmt.Println(result())
	fmt.Println("--------result2--------")
	result2 := add(3,4,0,0)
	fmt.Println(result2())



}

结果

--------result--------
p  k  初始化执行
i,k,p,x+y
1 1 1 3
i,k,p,x+y
2 1 2 3
i,k,p,x+y
3 1 3 3
--------result2--------
p  k  初始化执行
i,k,p,x+y
1 1 1 7

解释:

return func() (int,int,int,int) {
    
    
		p++
		i++
		fmt.Println("i,k,p,x+y")
		return i ,k ,p , x + y
	}

这里的匿名函数很明显形成了一个闭包,它的引用环境有 i ,p ,k, x,y
但是外部的

	k++
	p := 0

只在 result := add(1,2,0,0) 发生一次初始化的作用,之后匿名函数形成闭包环境,k 在匿名函数内部并没有发生其他操作。所以值还是保持传进来初始化后的值,这点我们从打印的 “p k 初始化执行” 也可以看出来只打印了一次,而i,p 不停的在自增,所以每一次调用一次相同的闭包环境他们就不停的新增。result2 显然 和 result 属于两个闭包环境,所以互不影响。

可变参数

在声明可变参数函数时,需要在参数列表的最后一个参数类型之前加上省略符号“…”,这表示 该函数会接收任意数量的该类型参数。

package main

import "fmt"

func sumNumber(paras ...int) int {
    
    
	var sum int
	for _,v := range paras{
    
    
		sum += v
	}
	return sum

}

func main() {
    
    
	fmt.Println(sumNumber(1))  // 1
	fmt.Println(sumNumber(1,2,3,4,5)) // 15

}

在上面的代码中 , 调用者隐式的创建一个数组,并将原始参数复制到数组中,再把数组的一 个切片作为参数传给被调函数
如果原始参数已经是切片类型,我们该如何传递给sum?只需在最后一个参数后加上省略符

	values := []int{
    
    1,2,3,4,5}
	fmt.Println(sumNumber(values...))  // 15

-----不管遇上什么问题,一颗炸弹总能解决你的困扰——如果不行就两颗。解决不了问题那是你当量不够,这是普世真理。

猜你喜欢

转载自blog.csdn.net/qq_37806753/article/details/108755759
今日推荐