Go语言学习 Day09(函数)

Go语言学习 Day09(函数)

Scroll Down

Go语言学习 Day09(函数)

原文地址

原文地址(书籍翻译)

学习地址(项目地址)


函数参数与返回值

相较于java来说,多返回值是Go的一大特性,为我们判断一个函数是否正常的执行提供了方便。

函数定义的时候,它的形参一般是由名字成的,不过我们也可以定义没有形参名的函数,只有相应的形参类型,例如:func f(int,int,string)。内亦有参数的函数通常被称为niladic函数(niladic function),例如:main.main()


Go默认使用 按值传递 来传递参数,也就是传递参数的副本。函数在接收到参数副本后,在使用的过程中对副本的值进行修改,不会影响到原来的变量值。例如:function (arg1)

值传递:

func main() {
	args := "xiaoFsu"
	fmt.Printf("The old string is :%s \n",args)
	testFunction2(args)
	fmt.Printf("The new string is :%s \n",args)
	// The old string is :xiaoFsu 
	// The new string is :xiaoFsu
}
func testFunction2(args string){
	args = "look"
}

如果需要函数可以直接修改参数的值,而不是对参数的副本进行操作,需要将参数的地址传递给函数,这就是按引用传递 。例如:function(&arg1)。此时传递给函数的是指针的值,但指针值所指向的地址的值不会被复制。我们可以通过这个指针的值来修改这个值所指向的地址上的值。

传递指针给函数不仅可以节省内存,而且可以在函数直接修改其值,所以被修改的变量不需要通过return返回。

传递一个指针容易引发一些不确定的事情,所以在操作时要谨慎操作更改外部变量的值。

引用传递:

func main() {
	args := "xiaoFsu"
	fmt.Printf("The old string is :%s \n",args)
	testFunction(&args)
	fmt.Printf("The new string is :%s \n",args)
	// The old string is :xiaoFsu 
	// The new string is :xiaofsu.com 
}
func testFunction(args *string) {
	*args = "xiaofsu.com"
}

在大多数情况下,传递指针的消耗比传递一个副本来得少。

在函数调用时,像切片(slice)、字典(map)、接口(interface)、通道(channel) 这样的引用类型都是默认使用引用传递,即使没有显式的指出指针。


非命名返回值与命名返回值

以下案例演示了如何使用非命名返回值命名返回值的特性。当需要返回多个非命名返回值时,需要使用 () 把它们括起来,比如 (int, int)

func main() {
	num := 10
	var num2,num3 int
	num2, num3 = testFunction3(num)
	fmt.Printf("The Num is :%d,The Num2 is :%d,The Num3 is :%d \n",num,num2,num3)
	num2, num3 = testFunction4(num)
	fmt.Printf("The Num is :%d,The Num2 is :%d,The Num3 is :%d \n",num,num2,num3)
	// The Num is :10,The Num2 is :20,The Num3 is :30 
	// The Num is :10,The Num2 is :20,The Num3 is :30 
}

// 非命名返回值
func testFunction3(num int)(int,int){
	return 2 * num ,3 * num
}

// 命名返回值
func testFunction4(num int)(x2 int,x3 int){
	x2 = 2 * num
	x3 = 3 * num
	return  // return 则会返回x2与x3
}

命名返回值 作为结果形参被初始化为相应类型的默认值,当需要返回值,只需要一个不带参数的return语句。需要注意的是,即使只有一个命名返回值,也需要使用 () 括起来。

即使函数使用了 命名返回值 你也可以无视他返回明确的值。

// 命名返回值
func testFunction4(num int)(x2 int,x3 int){
	x2 = 2 * num
	x3 = 3 * num
	return 1,2
}

空白符

空白符用来匹配一些不需要的值,然后丢弃掉。

func main() {
	i,_,j := testFunction6()
	fmt.Printf("The number i is :%d,The number j is :%d \n",i,j)
	//The number i is :1,The number j is :4 
}

func testFunction6()(i int,_ int,j int){
	return 1,3,4
}

变长参数

如果函数的最后一个参数是采用...type形式,那么这个函就可以处理一个变长的参数,这个长度可以为0,这样的函数被称为变参函数。

func main() {
	testFunction7(1,"xiaoFsu","OK","No")
}

func testFunction7(num int,strN ...string){
	// The Number is :1, The strN is:[xiaoFsu OK No]
	fmt.Printf("The Number is :%d, The strN is:%s",num,strN)
}

如果参数存储在一个slice类型的变量中,则可以通过slice...的形式来传递参数调用变参函数。

func main() {
	slice := []int{1,3,4,5,6,7}
	minNum := testFunction8(slice...)
	fmt.Printf("The MinNumber is :%d \n",minNum)
}

func testFunction8(s ...int)(int){
	if len(s) == 0{
		return 0
	}
	minNum := s[0]
	for _,v := range s{
		if v < minNum{
			minNum = v
		}
	}
	return minNum
}

如果变长参数的类型不都是相同的话,可以使用以下两种方案来进行解决。

详细的方案会在之后给出解释。

1:使用结构类型

定义一个结构类型,用来存储所有可能的参数。

type Options struct {
	par1 type1,
	par2 type2,
	...
}

2:使用空接口

如果一个变长参数的类型没有被指定,则可以使用默认的空接口interface{},这样就可以接受任何类型的参数。

该方案不仅可以用于长度未知的参数,还可以用于任何不确定类型的参数。一般而言我们会使用一个for-range循环以及switch结构对每个参数的类型进行判断。

func typecheck(..,..,values … interface{}) {
	for _, value := range values {
		switch v := value.(type) {
			case int: …
			case float: …
			case string: …
			case bool: …
			default: …
		}
	}
}

defer与追踪

关键字defer允许我们推迟到函数返回之前(函数的正常执行完毕或者任意位置执行return语句之后)执行某个语句或者函数。

func main() {
	fmt.Println("Hello, There is one")
	defer testFunction9()
	fmt.Println("Hello, There is three")
}

func testFunction9(){
	fmt.Println("Hello, There is two")
}

输出:

Hello, There is one
Hello, There is three
Hello, There is two

如果将defer语句去掉之后,输出的结果如下:

Hello, There is one
Hello, There is two
Hello, There is three

当有多个defer被注册时,它们会以逆序执行,类似于栈,后进先出。

func main() {
	fmt.Println("Hello, There is one")
	defer testFunction9()
	fmt.Println("Hello, There is three")
	i := 100
	defer fmt.Printf("The Number is :%d \n",i)
	for i:=0;i<5;i++{
		defer fmt.Printf("The Number in for is :%d \n",i)
	}
}

func testFunction9(){
	fmt.Println("Hello, There is two")
}

输出:

Hello, There is one
Hello, There is three
The Number in for is :4 
The Number in for is :3 
The Number in for is :2 
The Number in for is :1 
The Number in for is :0 
The Number is :100 
Hello, There is two

通常使用关键字defer来做一些函数执行完成后的收尾工作,例如:

  1. 关闭文件流
  2. 解锁一个枷锁的资源
  3. 打印报告
  4. 关闭数据库连接
  5. ...

内置函数

Go语言拥有一些不需要进行导入操作就可以直接使用的函数,被称为内置函数。它们有的可以针对不同的类型进行操作,例如:lencapappend,或者必须使用系统级别的操作,例如:panic。因此,他们需要直接获得编译器的支持。

这里是一个简单的列表,列出了一些内置函数:

内置函数名称简单说明
close用于管道之间的通信
len,caplen用于返回某个类型的长度或数量(字符串、数组、切片、map和管道);cap是容器的意思,用户返回某个类型的最大容量(只能是切片或者map)
new,makenewmake均是用来分配内存;new用于值类型和用户定义的类型,比如自定义结构,make用户内置引用类型(切片、map和管道)。它们的用法就像是函数,但是将类型作为参数:new(type)make(type)new(T)分配类型类型T的零值并返回其地址,也就是指向T的指针。也可以用于基本数据类型:v := new(int)nake(T)返回类型T是初始化之后的值,因此他会比new()进行更多的工作。new()是一个函数,不要忘记他的括号。
copy,append用户复制和连接切片
panic,recover两者均用于错误处理机制
paint,paintln底层打印函数
complex,real,imag用户创建和操作复数

函数回调

函数可以作为其它函数的参数进行传递,然后在其他函数内调用执行,一般称之为回调。

func main() {
	testFunction12(1,testFunction13)
}

func testFunction12(a int,f func(int,int)){
	f(a,3)
}

func testFunction13(a int,b int) {
	fmt.Printf("The Number a is:%d \n",a)
	fmt.Printf("The Number b is:%d \n",b)
}

输出:

The Number a is:1 
The Number b is:3 

匿名函数

当我们不希望给函数起名字的时候,我们可以考虑匿名函数,例如:func(x,y int) int {return x + y}

这样的一个函数不能够独立的存在,编译器会返回错误:non-declaration statement outside function body 但是可以被赋值给某个变量,即保存函数的地址到变量中:fplus := func(x,y int) int {return x + y} 然后通过变量名对于函数进行调用:fplus(1,2)

当然也可以直接对匿名函数进行调用:func(x,y int) int {return x + y}(3,4)

func main() {
	fplus := func(a int,b int) int{return a+b}
	fmt.Printf("The Number is :%d \n",fplus(1,2))
	number := func(s string,num int)int{
		v, e := strconv.Atoi(s)
		if e == nil{
			return v + num
		}
		return 0
	}("12",32)
	fmt.Printf("The Number is :%d \n",number)
}

输出:

The Number is :3 
The Number is :44 

defer的执行顺序

对于以下两个函数可以进行一个思考:

func f() (ret int) {
	defer func() {
		ret++
	}()
	return 1
}
func main() {
	fmt.Println(f())
	fmt.Println(t())
}

func t()string{
	str := "xiaoFsu"
	defer func() {
		str = "AAA"
	}()
	return str
}

输出的结果为:

2
xiaoFsu
1、返回值在函数体内相当于一个临时变量,在函数最终结束的时候会出栈值拷贝给临时的返回数据区
2、`defer`可以操作变量,包括临时变量,也就是能操作命名的返回值
3、`defer`的执行,在代码`return`语句之后,实际上编译器会把`return`语句拆分,先执行`return`后面的子语句(如果有),再执行`defer`,最后`return`
4、`defer`的顺序类似于压栈,`FIFO`先入后出

关于命名返回值与匿名返回值的区别


闭包函数

func Add2() (func(b int) int)
func Adder(a int) (func(b int) int)

可以看到函数Add2Adder都会返回func (b int) int的函数,函数Add2不接受任何参数,但是函数Adder接受一个int类型的整数作为参数。

func main() {
    func1 := test1()
    fmt.Printf("The Number is :%d \n",func1(1))
    func2 := test2(12)
    fmt.Printf("The Number is :%d \n",func2(12))
}

func test1() func(b int) int{
    return func(b int) int{
        return b+2
    }
}

func test2(a int) func(b int) int{
    return func(b int) int {
        return a+b
    }
}

输出:

The Number is :3 
The Number is :24 

下面一个不同的实现:

func main() {
	func3 := test3()
	fmt.Printf("The Number is :%d \n",func3(1))
	fmt.Printf("The Number is :%d \n",func3(10))
	fmt.Printf("The Number is :%d \n",func3(100))
}

func test3()func(int) int{
	var x int
	fmt.Printf("The x is :%d \n",x)
	return func(delta int) int{
		x += delta
		return x
	}
}

输出:

The x is :0 
The Number is :1 
The Number is :11 
The Number is :111 

多次调用函数过程中,x变量的值是被保存的。闭包函数保存并积累其中变量的值,不管外部函数是否退出,他都继续操作外部函数的局部变量。

func main() {
	fbnc := fibonacci()
	for i:=0;i<10;i++{
		fmt.Printf("The fibonacci is :%d \n",fbnc())
	}
}
func fibonacci() func() int {
	a := -1
	b := 1
	return func() int {
		a, b = b, a + b
		return b
	}
}

使用闭包来重写斐波那契数列(即前两个数为1,从第三个数开始每个数为前两个数的和。)


工厂函数

一个返回值为另一个函数 的函数可以被称为 工厂函数。

func MakeAddSuffix(suffix string) func(string) string {
	return func(name string) string {
		if !strings.HasSuffix(name, suffix) {
			return name + suffix
		}
		return name
	}
}

之后我们可以生成以下函数:

addBmp := MakeAddSuffix(".bmp")
addJpeg := MakeAddSuffix(".jpeg")

之后使用它们:

addBmp("file") // returns: file.bmp
addJpeg("file") // returns: file.jpeg

可以返回其它函数的函数 和 接受其它函数作为参数的函数 都被称之为高阶函数,是函数式语言的特点。闭包在Go语言中十分常见,常用于goroutine 和 管道操作。


输出日志

可以使用 runtimelog 包中的特殊函数来分析和调试复杂程序。包 runtime 中的函数 Caller() 提供了相应的信息,因此可以在需要的时候实现一个 where() 闭包函数来打印函数执行的位置:

where := func() {
	_, file, line, _ := runtime.Caller(1)
	log.Printf("%s:%d", file, line)
}
where()
// some code
where()
// some more code
where()

也可以设置 log 包中的 flag参数来实现:

log.setFlags(log.Llongfile)
log.Print("")

或者更简单的方式

where := log.Print
func func1(){
	//do something
	where()
	//do something
	where()
	// ...
}

函数执行时间

可以使用 time 包中的 Now()Sub() 函数来进行计算一个函数的执行时间,或者是个别流程的执行时间。

func main() {
	start := time.Now()
	where := func() {
		_,file,line,_ := runtime.Caller(1)
		log.Printf("%s:%d \n",file,line)
	}
	where()
	fmt.Println("do  something")
	fmt.Println("do  something")
	where()
	where()
	fmt.Println("do  something")
	log.SetFlags(log.Llongfile)
	log.Print("111111111")
	test := log.Print
	test()
	end := time.Now()
	diff := end.Sub(start)
	log.Printf("The Time is :%d",diff)
	time.Sleep(1000000000)
	fmt.Print("Over!")
}

Go 语言中的 时间都是 纳秒 ,换算为:1秒(s)=1000000000纳秒(ns),不用数了1后面带有90,在Go 语言中,常用的表达方式为: 1e9,即:time.Sleep(1e9)


内存缓存

如果需要进行大量的运算时,避免重复计算可以有效的提升性能。通过在内存中缓存和重复利用相同计算的结果,称之为内存缓存,以下就是斐波那契使用与不适用 内存缓存 的运行时间

递归(不使用内存缓存):

func main() {
	result := 0
	start := time.Now()
	for i:=0;i<40;i++{
		result = fbnc1(i)
		fmt.Printf("The Fbnc %d is :%d \n",i,result)
	}
	end := time.Now()
	diff := end.Sub(start)
	fmt.Printf("Run Time is :%d \n",diff)
	// 最终耗时:886683700 为 0.8s
}
func fbnc1(f int) (ret int){
	if f <= 1{
		ret = 1
	}else{
		ret = fbnc1(f-1) + fbnc1(f-2)
	}
	return
}

递归(使用内存缓存):

// 计算50个菲波那切数列
const count = 50
// 因为40个太快了,计算出来时间都是0,所以搞50个终于出来时间了
var fbnc [count] int

func main() {
	result := 0
	start := time.Now()
	for i:=0;i<count;i++{
		result = fbnc1(i)
		fmt.Printf("The Fbnc %d is :%d \n",i,result)
	}
	end := time.Now()
	diff := end.Sub(start)
	fmt.Printf("Run Time is :%d \n",diff)
	//  最终耗时:997800 为 0.0009978s
}
func fbnc1(f int) (ret int){
	if fbnc[f] != 0{
		ret = fbnc[f]
		return
	}
	if f <= 1{
		ret = 1
	}else{
		ret = fbnc1(f-1) + fbnc1(f-2)
	}
	fbnc[f] = ret
	return
}