国内最全IT社区平台 联系我们 | 收藏本站
华晨云阿里云优惠2
您当前位置:首页 > php开源 > php教程 > Go起步:8、Go的函数

Go起步:8、Go的函数

来源:程序员人生   发布时间:2016-12-08 17:14:10 阅读次数:2809次

函数是基本的代码块,用于履行1个任务,是构成代码履行的逻辑结构。
在Go语言中,函数的基本组成为:关键字func、函数名、参数列表、返回值、函数体和返回语句。

函数定义

函数其实在之前已见过了,第1次履行hello world程序的main()其实就是1个函数,而且是1个比较特殊的函数。每一个go程序都是从名为main的package包的main()函数开始履行包的概念不是这里的重点,以后做单独说明。同时main()函数是无参数,无返回值的。
Go函数的完成定义以下:

func function_name( [parameter list] ) [return_types] { 函数体 }

定义解析:
从Go的函数定义可以看出,Go的返回值是放在函数名和参数后面的,这点和C及Java的差别还是很多大的。

  • func:Go的函数声明关键字,声明1个函数。
  • function_name:函数名称,函数名和参数列表1起构成了函数签名。
  • parameter list:参数列表,指定的是参数类型、顺序、及参数个数。参数是可选的,即函数可以不包括参数。参数就像1个占位符,这是参数被称为形参,当函数被调用时,将具体的值传递给参数,这个值被称为实际参数。
  • return_types:返回类型,函数返回1列值。return_types 是该列值的数据类型。这里需要注意的是Go函数支持多返回值。有些功能不需要返回值,这类情况下 return_types 不是必须的。
  • 函数体:函数定义的代码集合,表示函数完成的动作。

函数调用

Go的函数调用只要通过函数名然后向函数传递参数,函数就会履行并返回值回来。就像之前调用Println()输出信息1样。
这里提1点,如果函数和调用不在同1个包(package)内,需要先通过import关键字将包引入–import “fmt”。函数Println()就属于包fmt。
这里可能注意到Println()函数命名是首字母是大写的。在Go语言中函数名字的大小写不单单是风格,更直接体现了该函数的可见性。这和其他语言对函数或方法的命名规定可能有很大不同,像Java就推荐是驼峰的写法,C也不建议函数名首字母是大写。但是在Go中,如果首字母不大写,你可能会遇到稀里糊涂的编译毛病, 比如你明明导入了对应的包,Go编译器还是会告知你没法找到这个函数。
因此在Go中,需要记住1个规则:

小写字母开头的函数只在本包内可见,大写字母开头的函数才能被其他包使用。

同时这个规则也适用于变量的可见性,即首字母大写的变量才是全局的。

package main import "fmt" /* 函数返回两个数的较大值 */ func max(num1 int, num2 int) int { /* 定义局部变量 */ var result int if num1 > num2 { result = num1 } else { result = num2 } return result } func main() { var a int = 100 var b int = 200 var ret int /* 调用函数并返回较大值 */ ret = max(a, b) fmt.Printf("最大值是 : %d\n", ret) }

这里写图片描述
上面定义了1个函数max(),用于比较两个数,并返回其中较大的1个。终究通过main() 函数中调用 max()函数履行。
这里关于函数的参数列表有1个简便写法,当连续两个或多个函数的已命名形参类型相同时,除最后1个类型之外,其它都可以省略。
就像上面的func max(num1 int, num2 int) int {}定义,可以简写成

func max(num1 , num2 int) int {}

多返回值

前面定义函数时说过,Go的函数支持多返回值,这与C、C++和Java等开发语言极大不同。这个特性能够使我们写出比其他语言更优雅、更简洁的代码,比如File.Read()函 数就能够同时返回读取的字节数和毛病信息。如果读取文件成功,则返回值中的n为读取的字节 数,err为nil,否则err为具体的出错信息:

func (file *File) Read(b []byte) (n int, err Error)

1个简单的例子以下:

package main import "fmt" func swap(x, y string) (string, string) { return y, x } func main() { a, b := swap("hello", "world") fmt.Println(a, b) }

这里写图片描述
上面实现了简单的字符串交换功能,代码实现上10分的简洁,由于支持多返回值,所以不需要想Java需要构建1个可以保存多个值得数据结构。
而且可以发现,对返回值如果是同1类型,可以不定义变量名称,虽然代码看上去是简洁了很多,但是命名后的返回值可让代码更清晰,可读性更强。

如果调用方调用了1个具有多返回值的方法,但是却不想关心其中的某个返回值,可以简单 地用1个下划线“_”来跳过这个返回值。就像上面的例子,如果我们只关注第1个返回值则可以写成:

a, _ := swap("hello", "world")

若值关注第2返回值则可以写成:

_, b := swap("hello", "world")

函数参数

函数定义时指出,函数定义时有参数,该变量可称为函数的形参。形参就像定义在函数体内的局部变量。
但当调用函数,传递过来的变量就是函数的实参,函数可以通过两种方式来传递参数:

  1. 值传递:指在调用函数时将实际参数复制1份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
  2. 援用传递:是指在调用函数时将实际参数的地址传递到函数中,那末在函数中对参数所进行的修改,将影响到实际参数。

在默许情况下,Go 语言使用的是值传递,即在调用进程中不会影响到实际参数。

值传递

Go中int类型保存的的是1个数字类型,下面定义1个交换函数swap(),用于交换两个参数的值。

package main import "fmt" func main() { var a int = 100 var b int = 200 fmt.Printf("交换前 a 的值为 : %d\n", a) fmt.Printf("交换前 b 的值为 : %d\n", b) /* 通过调用函数来交换值 */ swap(a, b) fmt.Printf("交换后 a 的值 : %d\n", a) fmt.Printf("交换后 b 的值 : %d\n", b) } /* 定义相互交换值的函数 */ func swap(x, y int) int { var temp int temp = x /* 保存 x 的值 */ x = y /* 将 y 值赋给 x */ y = temp /* 将 temp 值赋给 y*/ return temp }

这里写图片描述

援用参数

通过前面的介绍可以知道,Go的指针类型是对变量地址的援用。
将上面的swap()做些修改,参数接受两个指针类型,然后做交换。

package main import "fmt" func main() { var a int = 100 var b int = 200 fmt.Printf("交换前 a 的值为 : %d\n", a) fmt.Printf("交换前 b 的值为 : %d\n", b) /* 调用 swap() 函数 * &a 指向 a 指针,a 变量的地址 * &b 指向 b 指针,b 变量的地址 */ swap(&a, &b) fmt.Printf("交换后 a 的值 : %d\n", a) fmt.Printf("交换后 b 的值 : %d\n", b) } /* 定义相互交换值的函数 */ func swap(x, y *int) { var temp int temp = *x /* 保存 x 的值 */ *x = *y /* 将 y 值赋给 x */ *y = temp /* 将 temp 值赋给 y*/ }

这里写图片描述
可以发现,终究传进来的参数指在履行交换函数swap()后也被修改了,这是由于参数终究指向的都是地址的援用,所有援用被修改了,值也就相应的变了。

不定参数

顾名思义,不定参数就是函数的参数不是固定的。这个在C和Java里都有。在之前的代码中,包fmt下面的 fmt.Println()函数也是参数不定的。

不定参数类型

先看1个函数的定义:

func myfunc(args ...int) { }

可以看出,上面的定义和之前的函数定义最大的不同就是,他的参数是以“…type”的方式定义的,这和Java的语法有些类似,也是用”…”实现。需要说明的是“…type”在Go中只能作为参数的情势出现,而且只能作为函数的最后1个参数。
从内部实现机理上来讲,类型“…type“本质上是1个数组切片,也就是[]type,这点可以从下面的1个小程序验证1下。

package main import "fmt" func main() { var a int = 100 var b int = 200 myfunc(a, b) } func myfunc(args ...int) { fmt.Println(args) for _, arg := range args { fmt.Println(arg) } }

这里写图片描述
从上面的结果可以看出,类型“…type“本质上是1个数组切片,也就是[]type,所以参数args可以用for循环来取得每一个传入的参数。
这是Go的1 个语法糖(syntactic sugar),即这类语法对语言的功能并没有影响,但是更方便程序员使用。通常来讲,使用语法糖能够增加程序的可读性,从而减少程序出错的机会。

不定参数的传递

一样是上面的myfunc(args …int)函数为例,在参数赋值时可以不用用1个1个的赋值,可以直接传递1个数组或切片,特别注意的是在参数后加上“…”便可。

package main import "fmt" func main() { arr := []int{100, 200, 300} myfunc(arr...) myfunc(arr[:2]...) } func myfunc(args ...int) { fmt.Println(args) for _, arg := range args { fmt.Println(arg) } }

这里写图片描述

任意类型的不定参数

上面的例子在定义不定参数时,都有1个要求,参数的类型是1致的。那末如果函数的参数类型不1致,如何使用不定参数方式来定义。在Go中,要实现这个需求需要引入1个新的类型–interface{}。看名字可以看出,这类类型实际上就是接口。关于Go的接口这里只做引出。
看1下之前经常使用的Printf函数的定义,位置在Go的src目录下的print.go文件中。

func Println(a ...interface{}) (n int, err error) { return Fprintln(os.Stdout, a...) }

其实用interface{}传递任意类型数据是Go语言的惯例用法,而且interface{}是类型安全的。
看下面的例子:

package main import ( "fmt" "reflect" ) func main() { arr := []int{100, 200, 300} myfunc(100, "abc", arr) } func myfunc(args ...interface{}) { fmt.Println(args) for _, arg := range args { fmt.Println(arg) fmt.Println(reflect.TypeOf(arg)) fmt.Println("=======") } }

这里写图片描述

匿名函数

匿名函数是指不需要定义函数名的1种函数实现方式。1958年LISP首先采取匿名函数。
在Go里面,函数可以像普通变量1样被传递或使用,Go语言支持随时在代码里定义匿名函数。
匿名函数由1个不带函数名的函数声明和函数体组成。匿名函数的优越性在于可以直接使用函数内的变量,没必要申明。
直接看1个例子:

package main import ( "fmt" "math" ) func main() { getSqrt := func(a float64) float64 { return math.Sqrt(a) } fmt.Println(getSqrt(4)) }

这里写图片描述
上面先定义了1个名为getSqrt 的变量,初始化该变量时和之前的变量初始化有些不同,使用了func,func是定义函数的,可是这个函数和上面说的函数最大不同就是没有函数名,也就是匿名函数。这里将1个函数当作1个变量1样的操作。

闭包

理解闭包

闭包的应当都听过,但到底甚么是闭包呢?
闭包是由函数及其相干援用环境组合而成的实体(即:闭包=函数+援用环境)。
“官方”的解释是:所谓“闭包”,指的是1个具有许多变量和绑定了这些变量的环境的表达式(通常是1个函数),因此这些变量也是该表达式的1部份。
维基百科讲,闭包(Closure),是援用了自由变量的函数。这个被援用的自由变量将和这个函数1同存在,即便已离开了创造它的环境也不例外。所以,有另外一种说法认为闭包是由函数和与其相干的援用环境组合而成的实体。闭包在运行时可以有多个实例,不同的援用环境和相同的函数组合可以产生不同的实例。
看着上面的描写,会发现闭包和匿名函数仿佛有些像。可是可能还是有些云里雾里的。由于跳过闭包的创建进程直接理解闭包的定义是非常困难的。目前在JavaScript、Go、PHP、Scala、Scheme、Common Lisp、Smalltalk、Groovy、Ruby、 Python、Lua、objective c、swift 和Java8以上等语言中都能找到对闭包不同程度的支持。通过支持闭包的语法可以发现1个特点,他们都有垃圾回收(GC)机制。
JavaScript应当是普及度比较高的编程语言了,通过这个来举例应当好理解写。看下面的代码,只要关注script里方法的定义和调用就能够了。

<!DOCTYPE html> <html lang="zh"> <head> <title></title> </head> <body> </body> </html> <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.2.6/jquery.min.js" type="text/javascript"></script> <script> function a(){ var i=0; function b(){ console.log(++i); document.write("<h1>"+i+"</h1>"); } return b; } $(function(){ var c=a(); c(); c(); c(); //a(); //不会有信息输出 document.write("<h1>=============</h1>"); var c2=a(); c2(); c2(); }); </script>

这里写图片描述
这段代码有两个特点:

  1. 函数b嵌套在函数a内部
  2. 函数a返回函数b

这样在履行完var c=a()后,变量c实际上是指向了函数b(),再履行函数c()后就会显示i的值,第1次为1,第2次为2,第3次为3,以此类推。
其实,这段代码就创建了1个闭包。由于函数a()外的变量c援用了函数a()内的函数b(),就是说:

当函数a()的内部函数b()被函数a()外的1个变量援用的时候,就创建了1个闭包

在上面的例子中,由于闭包的存在使得函数a()返回后,a中的i始终存在,这样每次履行c(),i都是自加1后的值。
从上面可以看出闭包的作用就是在a()履行完并返回后,闭包使得Javascript的垃圾回收机制GC不会收回a()所占用的资源,由于a()的内部函数b()的履行需要依赖a()中的变量i。

在给定函数被屡次调用的进程中,这些私有变量能够保持其持久性。变量的作用域仅限于包括它们的函数,因此没法从其它程序代码部份进行访问。不过,变量的生存期是可以很长,在1次函数调用期间所创建所生成的值在下次函数调用时依然存在。正由于这1特点,闭包可以用来完成信息隐藏,并进而利用于需要状态表达的某些编程范型中。

下面来想象另外一种情况,如果a()返回的不是函数b(),情况就完全不同了。由于a()履行完后,b()没有被返回给a()的外界,只是被a()所援用,而此时a()也只会被b()引 用,因此函数a()和b()相互援用但又不被外界打扰(被外界援用),函数a和b就会被GC回收。所以直接调用a();是页面并没有信息输出。

下面来讲闭包的另外一要素援用环境。c()跟c2()援用的是不同的环境,在调用i++时修改的不是同1个i,因此两次的输出都是1。函数a()每进入1次,就构成了1个新的环境,对应的闭包中,函数都是同1个函数,环境却是援用不同的环境。这和c()和c()的调用顺序都是无关的。

以上就是对闭包作用的非常直白的描写,不专业也不严谨,但大概意思就是这样,理解闭包需要按部就班的进程。

Go的闭包

Go语言是支持闭包的,这里只是简单地讲1下在Go语言中闭包是如何实现的。
下面我来将之前的JavaScript的闭包例子用Go来实现。

package main import ( "fmt" ) func a() func() int { i := 0 b := func() int { i++ fmt.Println(i) return i } return b } func main() { c := a() c() c() c() //a() //不会输出i }

这里写图片描述
可以发现,输出和之前的JavaScript的代码是1致的。具体的缘由和上面的也是1样的,这里就不在赘述了。
这页说明Go语言是支持闭包的,至于具体是如何支持的目前就先做讨论了。

关于闭包这里也只讲到了基本的概念,至于更深入的东西我目前能力有限只能靠以后渐渐摸索。就像上面讲到的,理解闭包需要按部就班的进程。

生活不易,码农辛苦
如果您觉得本网站对您的学习有所帮助,可以手机扫描二维码进行捐赠
程序员人生
------分隔线----------------------------
分享到:
------分隔线----------------------------
关闭
程序员人生