函数是基本的代码块,用于执行一个任务,是构成代码执行的逻辑结构。
在Go语言中,函数的基本组成为:关键字func、函数名、参数列表、返回值、函数体和返回语句。
函数定义
函数其实在之前已经见过了,第一次执行hello world程序的main()其实就是一个函数,而且是一个比较特殊的函数。每个go程序都是从名为main的package包的main()函数开始执行包的概念不是这里的重点,以后做单独说明。同时main()函数是无参数,无返回值的。
Go函数的完成定义如下:
func function_name( [parameter list] ) [return_types] {
函数体
}
定义解析:
从Go的函数定义可以看出,Go的返回值是放在函数名和参数后面的,这点和C及Java的差别还是很多大的。
- func:Go的函数声明关键字,声明一个函数。
- function_name:函数名称,函数名和参数列表一起构成了函数签名。
- parameter list:参数列表,指定的是参数类型、顺序、及参数个数。参数是可选的,即函数可以不包含参数。参数就像一个占位符,这是参数被称为形参,当函数被调用时,将具体的值传递给参数,这个值被称为实际参数。
- return_types:返回类型,函数返回一列值。return_types 是该列值的数据类型。这里需要注意的是Go函数支持多返回值。有些功能不需要返回值,这种情况下 return_types 不是必须的。
- 函数体:函数定义的代码集合,表示函数完成的动作。
函数调用
Go的函数调用只要通过函数名然后向函数传递参数,函数就会执行并返回值回来。就像之前调用Println()输出信息一样。
这里提一点,如果函数和调用不在同一个包(package)内,需要先通过import关键字将包引入–import “fmt”。函数Println()就属于包fmt。
这里可能注意到Println()函数命名是首字母是大写的。在Go语言中函数名字的大小写不仅仅是风格,更直接体现了该函数的可见性。这和其他语言对于函数或方法的命名规定可能有很大不同,像Java就推荐是驼峰的写法,C也不建议函数名首字母是大写。但是在Go中,如果首字母不大写,你可能会遇到莫名其妙的编译错误, 比如你明明导入了对应的包,Go编译器还是会告诉你无法找到这个函数。
因此在Go中,需要记住一个规则:
小写字母开头的函数只在本包内可见,大写字母开头的函数才能被其他包使用。
同时这个规则也适用于变量的可见性,即首字母大写的变量才是全局的。
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)
}
上面定义了一个函数max(),用于比较两个数,并返回其中较大的一个。最终通过main() 函数中调用 max()函数执行。
这里关于函数的参数列表有一个简便写法,当连续两个或多个函数的已命名形参类型相同时,除最后一个类型以外,其它都可以省略。
就像上面的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)
一个简单的例子如下:
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)
}
上面实现了简单的字符串交换功能,代码实现上十分的简洁,因为支持多返回值,所以不需要想Java需要构建一个可以保存多个值得数据结构。
而且可以发现,对于返回值如果是同一类型,可以不定义变量名称,虽然代码看上去是简洁了很多,但是命名后的返回值可以让代码更清晰,可读性更强。
如果调用方调用了一个具有多返回值的方法,但是却不想关心其中的某个返回值,可以简单 地用一个下划线“_”来跳过这个返回值。就像上面的例子,如果我们只关注第一个返回值则可以写成:
a, _ := swap("hello", "world")
若值关注第二返回值则可以写成:
_, b := swap("hello", "world")
函数参数
函数定义时指出,函数定义时有参数,该变量可称为函数的形参。形参就像定义在函数体内的局部变量。
但当调用函数,传递过来的变量就是函数的实参,函数可以通过两种方式来传递参数:
- 值传递:指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
- 引用传递:是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
在默认情况下,Go 语言使用的是值传递,即在调用过程中不会影响到实际参数。
值传递
Go中int类型保存的的是一个数字类型,下面定义一个交换函数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()函数也是参数不定的。
不定参数类型
先看一个函数的定义:
func myfunc(args ...int) {
}
可以看出,上面的定义和之前的函数定义最大的不同就是,他的参数是以“…type”的方式定义的,这和Java的语法有些类似,也是用”…”实现。需要说明的是“…type”在Go中只能作为参数的形式出现,而且只能作为函数的最后一个参数。
从内部实现机理上来说,类型“…type“本质上是一个数组切片,也就是[]type,这点可以从下面的一个小程序验证一下。
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“本质上是一个数组切片,也就是[]type,所以参数args可以用for循环来获得每个传入的参数。
这是Go的一 个语法糖(syntactic sugar),即这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说,使用语法糖能够增加程序的可读性,从而减少程序出错的机会。
不定参数的传递
同样是上面的myfunc(args …int)函数为例,在参数赋值时可以不用用一个一个的赋值,可以直接传递一个数组或者切片,特别注意的是在参数后加上“…”即可。
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)
}
}
任意类型的不定参数
上面的例子在定义不定参数时,都有一个要求,参数的类型是一致的。那么如果函数的参数类型不一致,如何使用不定参数方式来定义。在Go中,要实现这个需求需要引入一个新的类型–interface{}。看名字可以看出,这种类型实际上就是接口。关于Go的接口这里只做引出。
看一下之前常用的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("=======")
}
}
匿名函数
匿名函数是指不需要定义函数名的一种函数实现方式。1958年LISP首先采用匿名函数。
在Go里面,函数可以像普通变量一样被传递或使用,Go语言支持随时在代码里定义匿名函数。
匿名函数由一个不带函数名的函数声明和函数体组成。匿名函数的优越性在于可以直接使用函数内的变量,不必申明。
直接看一个例子:
package main
import (
"fmt"
"math"
)
func main() {
getSqrt := func(a float64) float64 {
return math.Sqrt(a)
}
fmt.Println(getSqrt(4))
}
上面先定义了一个名为getSqrt 的变量,初始化该变量时和之前的变量初始化有些不同,使用了func,func是定义函数的,可是这个函数和上面说的函数最大不同就是没有函数名,也就是匿名函数。这里将一个函数当做一个变量一样的操作。
闭包
理解闭包
闭包的应该都听过,但到底什么是闭包呢?
闭包是由函数及其相关引用环境组合而成的实体(即:闭包=函数+引用环境)。
“官方”的解释是:所谓“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。
维基百科讲,闭包(Closure),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。
看着上面的描述,会发现闭包和匿名函数似乎有些像。可是可能还是有些云里雾里的。因为跳过闭包的创建过程直接理解闭包的定义是非常困难的。目前在JavaScript、Go、PHP、Scala、Scheme、Common Lisp、Smalltalk、Groovy、Ruby、 Python、Lua、objective c、swift 以及Java8以上等语言中都能找到对闭包不同程度的支持。通过支持闭包的语法可以发现一个特点,他们都有垃圾回收(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>
这段代码有两个特点:
- 函数b嵌套在函数a内部
- 函数a返回函数b
这样在执行完var c=a()后,变量c实际上是指向了函数b(),再执行函数c()后就会显示i的值,第一次为1,第二次为2,第三次为3,以此类推。
其实,这段代码就创建了一个闭包。因为函数a()外的变量c引用了函数a()内的函数b(),就是说:
当函数a()的内部函数b()被函数a()外的一个变量引用的时候,就创建了一个闭包。
在上面的例子中,由于闭包的存在使得函数a()返回后,a中的i始终存在,这样每次执行c(),i都是自加1后的值。
从上面可以看出闭包的作用就是在a()执行完并返回后,闭包使得Javascript的垃圾回收机制GC不会收回a()所占用的资源,因为a()的内部函数b()的执行需要依赖a()中的变量i。
在给定函数被多次调用的过程中,这些私有变量能够保持其持久性。变量的作用域仅限于包含它们的函数,因此无法从其它程序代码部分进行访问。不过,变量的生存期是可以很长,在一次函数调用期间所创建所生成的值在下次函数调用时仍然存在。正因为这一特点,闭包可以用来完成信息隐藏,并进而应用于需要状态表达的某些编程范型中。
下面来想象另一种情况,如果a()返回的不是函数b(),情况就完全不同了。因为a()执行完后,b()没有被返回给a()的外界,只是被a()所引用,而此时a()也只会被b()引 用,因此函数a()和b()互相引用但又不被外界打扰(被外界引用),函数a和b就会被GC回收。所以直接调用a();是页面并没有信息输出。
下面来说闭包的另一要素引用环境。c()跟c2()引用的是不同的环境,在调用i++时修改的不是同一个i,因此两次的输出都是1。函数a()每进入一次,就形成了一个新的环境,对应的闭包中,函数都是同一个函数,环境却是引用不同的环境。这和c()和c()的调用顺序都是无关的。
以上就是对闭包作用的非常直白的描述,不专业也不严谨,但大概意思就是这样,理解闭包需要循序渐进的过程。
Go的闭包
Go语言是支持闭包的,这里只是简单地讲一下在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的代码是一致的。具体的原因和上面的也是一样的,这里就不在赘述了。
这页说明Go语言是支持闭包的,至于具体是如何支持的目前就先做讨论了。
关于闭包这里也只讲到了基本的概念,至于更深入的东西我目前能力有限只能靠以后慢慢摸索。就像上面讲到的,理解闭包需要循序渐进的过程。