函数声明
函数声明包括函数名、形式参数列表、返回值列表(可省略)以及函数体。
func name(parameter-list) (result-list) { body}
形式参数列表描述了函数的参数名以及参数类型。这些参数作为局部变量,其值由参数调用者提供。返回值列表描述了函数返回值的变量名以及类型。
如果函数返回一个无名变量或者没有返回值,返回值列表的括号是可以省略的。如果一个函数声明不包括返回值列表,那么函数体执行完毕后,不会返回任何值。
在hypot函数中,
func hypot(x, y float64) float64 { return math.Sqrt(x*x + y*y)}fmt.Println(hypot(3,4)) // "5"
x和y是形参名,3和4是调用时的传入的实数,函数返回了一个float64类型的值。 返回值也可以像形式参数一样被命名。在这种情况下,每个返回值被声明成一个局部变量,并根据该返回值的类型,将其初始化为0。 如果一个函数在声明时,包含返回值列表,该函数必须以 return语句结尾,除非函数明显无法运行到结尾处。例如函数在结尾时调用了panic异常或函数中存在无限循环。
正如hypot一样,如果一组形参或返回值有相同的类型,我们不必为每个形参都写出参数类型。下面2个声明是等价的:
func f(i, j, k int, s, t string) { /* ... */ }func f(i int, j int, k int, s string, t string) { /* ... */ }
下面,我们给出4种方法声明拥有2个int型参数和1个int型返回值的函数。
func add(x int, y int) int {return x + y}func sub(x, y int) (z int) { z = x - y; return}func first(x int, _ int) int { return x }func zero(int, int) int { return 0 }fmt.Printf("%T\n", add) // "func(int, int) int"fmt.Printf("%T\n", sub) // "func(int, int) int"fmt.Printf("%T\n", first) // "func(int, int) int"fmt.Printf("%T\n", zero) // "func(int, int) int"
%T --- the type of the value
函数的类型被称为函数的标识符。如果两个函数形式参数列表和返回值列表中的变量类型一一对应,那么这两个函数被认为有相同的类型和标识符。形参和返回值的变量名不影响函数标识符也不影响它们是否可以以省略参数类型的形式表示。
每一次函数调用都必须按照声明顺序为所有参数提供实参(参数值)。在函数调用时,Go语言没有默认参数值,也没有任何方法可以通过参数名指定形参,因此形参和返回值的变量名对于函数调用者而言没有意义。
在函数体中,函数的形参作为局部变量,被初始化为调用者提供的值。函数的形参和有名返回值作为函数最外层的局部变量,被存储在相同的词法块中。
实参通过值的方式传递,因此函数的形参是实参的拷贝。对形参进行修改不会影响实参。但是,如果实参包括引用类型,如指针,slice(切片)、map、function、channel等类型,实参可能会由于函数的简介引用被修改。
你可能会偶尔遇到没有函数体的函数声明,这表示该函数不是以Go实现的。这样的声明定义了函数标识符。
package mathfunc Sin(x float64) float //implemented in assembly language
函数 - 按值传递 & 按引用传递
- 按值传递(call by value)
- 按引用传递(call by reference)
Go 默认使用按值传递来传递参数,也就是传递参数的副本。函数接收参数副本之后,在使用变量的过程中可能对副本的值进行更改,但不会影响到原来的变量,比如 Function(arg1)
。
如果你希望函数可以直接修改参数的值,而不是对参数的副本进行操作,你需要将参数的地址(变量名前面添加&符号,比如 &variable)传递给函数,这就是按引用传递,比如 Function(&arg1)
,此时传递给函数的是一个指针。如果传递给函数的是一个指针,指针的值(一个地址)会被复制,但指针的值所指向的地址上的值不会被复制;我们可以通过这个指针的值来修改这个值所指向的地址上的值。(译者注:指针也是变量类型,有自己的地址和值,通常指针的值指向一个变量的地址。所以,按引用传递也是按值传递。)
几乎在任何情况下,传递指针(一个32位或者64位的值)的消耗都比传递副本来得少。
在函数调用时,像切片(slice)、字典(map)、接口(interface)、通道(channel)这样的引用类型都是默认使用引用传递(即使没有显式的指出指针)。
函数 - 匿名函数
当我们不希望给函数起名字的时候,可以使用匿名函数,例如:func(x, y int) int { return x + y }
。
这样的一个函数不能够独立存在(编译器会返回错误:non-declaration statement outside function body
),但可以被赋值于某个变量,即保存函数的地址到变量中:fplus := func(x, y int) int { return x + y }
,然后通过变量名对函数进行调用:fplus(3,4)
。
当然,您也可以直接对匿名函数进行调用:func(x, y int) int { return x + y } (3, 4)
。
当我们不希望给函数起名字的时候,可以使用匿名函数,例如:func(x, y int) int { return x + y }
。
这样的一个函数不能够独立存在(编译器会返回错误:non-declaration statement outside function body
),但可以被赋值于某个变量,即保存函数的地址到变量中:fplus := func(x, y int) int { return x + y }
,然后通过变量名对函数进行调用:fplus(3,4)
。
当然,您也可以直接对匿名函数进行调用:func(x, y int) int { return x + y } (3, 4)
。
下面是一个计算从 1 到 1 百万整数的总和的匿名函数:
func() { sum := 0 for i := 1; i <= 1e6; i++ { sum += i }}()
表示参数列表的第一对括号必须紧挨着关键字 func
,因为匿名函数没有名称。花括号 {}
涵盖着函数体,最后的一对括号表示对该匿名函数的调用。
下面的例子展示了如何将匿名函数赋值给变量并对其进行调用(function_literal.go):
package mainimport "fmt"func main() { f()}func f() { for i := 0; i < 4; i++ { g := func(i int) { fmt.Printf("%d ", i) } //此例子中只是为了演示匿名函数可分配不同的内存地址,在现实开发中,不应该把该部分信息放置到循环中。 g(i) fmt.Printf(" - g is of type %T and has value %v\n", g, g) }}
输出:
0 - g is of type func(int) and has value 0x681a801 - g is of type func(int) and has value 0x681b002 - g is of type func(int) and has value 0x681ac03 - g is of type func(int) and has value 0x681400
我们可以看到变量 g
代表的是 func(int)
,变量的值是一个内存地址。
所以我们实际上拥有的是一个函数值:匿名函数可以被赋值给变量并作为值使用。
闭包的应用:将函数作为返回值
在程序 function_return.go
中我们将会看到函数 Add2 和 Adder 均会返回签名为 func(b int) int
的函数:
func Add2() (func(b int) int)func Adder(a int) (func(b int) int)
函数 Add2 不接受任何参数,但函数 Adder 接受一个 int 类型的整数作为参数。
我们也可以将 Adder 返回的函数存到变量中(function_return.go)。
package mainimport "fmt"func main() { // make an Add2 function, give it a name p2, and call it: p2 := Add2() fmt.Printf("Call Add2 for 3 gives: %v\n", p2(3)) // make a special Adder function, a gets value 2: TwoAdder := Adder(2) fmt.Printf("The result is: %v\n", TwoAdder(3))}func Add2() func(b int) int { return func(b int) int { return b + 2 }}func Adder(a int) func(b int) int { return func(b int) int { return a + b }}
输出:
Call Add2 for 3 gives: 5The result is: 5
下例为一个略微不同的实现(function_closure.go):
package mainimport "fmt"func main() { var f = Adder() fmt.Print(f(1), " - ") fmt.Print(f(20), " - ") fmt.Print(f(300))}func Adder() func(int) int { var x int return func(delta int) int { x += delta return x }}
函数 Adder() 现在被赋值到变量 f 中(类型为 func(int) int
)。
输出:
1 - 21 - 321
三次调用函数 f 的过程中函数 Adder() 中变量 delta 的值分别为:1、20 和 300。
我们可以看到,在多次调用中,变量 x 的值是被保留的,即 0 + 1 = 1
,然后 1 + 20 = 21
,最后 21 + 300 = 321
:闭包函数保存并积累其中的变量的值,不管外部函数退出与否,它都能够继续操作外部函数中的局部变量。
这些局部变量同样可以是参数,例如之前例子中的 Adder(as int)
。
这些例子清楚地展示了如何在 Go 语言中使用闭包。
在闭包中使用到的变量可以是在闭包函数体内声明的,也可以是在外部函数声明的:
var g intgo func(i int) { s := 0 for j := 0; j < i; j++ { s += j } g = s}(1000) // Passes argument 1000 to the function literal.
这样闭包函数就能够被应用到整个集合的元素上,并修改它们的值。然后这些变量就可以用于表示或计算全局或平均值。
函数 - 可变参数列表
参数数量可变的函数称为为可变参数函数。典型的例子就是fmt.Printf和类似函数。Printf首先接收一个的必备参数,之后接收任意个数的后续参数。
在声明可变参数函数时,需要在参数列表的最后一个参数类型之前加上省略符号“...”,这表示该函数会接收任意数量的该类型参数。
package mainimport ( "fmt")func main() { var res = sum(1, 2, 3, 4, 5, 6) fmt.Println(res) numbers := []int{1, 2, 3, 4, 5, 6} // res = sum(numbers) //如果原始参数已经是切片类型,我们该如何传递给sum?只需在最后一个参数后加上省略符 res = sum(numbers[1:2]...) fmt.Println(res) fmt.Printf("%T\n", sum) //func(...int) int fmt.Printf("%T\n", summary) // func([]int) int}func sum(vals ...int) int { total := 0 for _, val := range vals { total += val } return total}func summary(vals []int) int { total := 0 for _, val := range vals { total += val } return total}// go 不支持函数的重载// func sum(vals []int) int {// total := 0// for _, val := range vals {// total += val// }// return total// }
在上面的代码中,调用者隐式的创建一个数组,并将原始参数复制到数组中,再把数组的一个切片作为参数传给被调函数。如果原始参数已经是切片类型,我们该如何传递给sum?只需在最后一个参数后加上省略符。
虽然在可变参数函数内部,...int 型参数的行为看起来很像切片类型,但实际上,可变参数函数和以切片作为参数的函数是不同的。
函数 - 使用defer&panic&recover处理异常
func main() { defer println("in main") go func() { defer println("in goroutine") panic("panic test") }() time.Sleep(1 * time.Second)}
运行上述代码,
in goroutine
panic: panic testgoroutine 4 [running]:
main.main.func1() /Users/flyme/workspace-go/src/gitee.com/xxggy/go-learning/test/testpanic/main.go:9 +0x6a created by main.main /Users/flyme/workspace-go/src/gitee.com/xxggy/go-learning/test/testpanic/main.go:7 +0x6a
会发现在只有goroutine 中的defer 执行了,这是因为Go 语言在发生panic时只会执行当前协程中的defer函数。
再来看一下 recover 是如何在defer 函数中捕获异常的,
func main() { defer fmt.Println("in main") defer func() { if err := recover(); err != nil { fmt.Println(err) } }() panic("unknown err")}
执行程序如下,
unknown err
in main
从这个例子中我们可以看到,recover函数其实只是阻止了当前程序的崩溃,但是当前控制流中的其他defer函数还会正常执行。
这段程序中定义了两个defer ,defer的特点就是LIFO,即后进先出,所以如果在同一个函数下多个defer的话,会逆序执行。
函数 - 递归
递归一:为一组整数列求和
递归思想:如何为一组整数数列求和?按照通常命令式编程的思维,我们会采用循环,依次遍历列表中的每个元素进行累加,最终给出求和结果。这样的程序不难写,稍微具备一点编程经验的人在一分钟之内就能写出来。这次我们换个思维,如何用递归的方式求和?为此,我们不妨把问题简化一点,假设数列包含 N 个数,如果我们已经知道了后续 N – 1 个数的和,那么整个数列的和即为第一个数加上后续 N – 1 个数的和,依此类推,我们可以以同样的方式为 N – 1 个数继续求和,直到数列为空,显然,空数列的和为零。听起来复杂,事实上我们可以用一句话来总结:一个数列的和即为数列中的第一个数加上由后续数字组成的数列的和。 go代码表示如下,
package mainimport ( "fmt")func main() { /* 创建切片,不指定元素个数*/ numbers := []int{0, 1, 2, 3, 4, 5, 6, 7, 8} var result = sum(numbers) fmt.Println(result)}func sum(slice []int) int { if len(slice) == 0 { return 0 } else { // slice[1:] 默认上限为 len(s) // 使用下标读取切片的元素 return slice[0] + sum(slice[1:]) }}
递归二:求数列的最大值
递归思想:这个数列求和的例子并不是特别的,它代表了递归对于列表的一种普遍的处理方式,即对一个列表的操作,可转化为对第一个元素,及剩余列表的相同操作。比如我们可以用同样的方式求一个数列中的最大值。我们假设已经知道了除第一个元素外剩余数列的最大值,那么整个数列的最大值即为第一个元素和剩余数列最大值中的大者。这里需要注意的是对于一个空数列求最大值是没有意义的,所以我们需要向外抛出一个异常。当数列只包含一个元素时,最大值就为这个元素本身,这种情况是我们这个递归的边界条件。一个递归算法,必须要有这样一个边界条件,否则会一直递归下去,形成死循环。 go代码表示如下,
package mainimport ( "fmt")func main() { defer func() { // 必须要先声明defer,否则不能捕获到panic异常 if err := recover(); err != nil { fmt.Println(err) // 这里的err其实就是panic传入的内容 } }() /* 创建切片,不指定元素个数*/ numbers := []int{0, 1, 2, 3, 4, 5, 6, 7, 8} var res = max(numbers) fmt.Println(res) empty_slice := make([]int, 0, 0) var res2 = max(empty_slice) fmt.Println(res2) fmt.Println("end")}func max(slice []int) int { if len(slice) == 0 || slice == nil { panic("数列为空") } if len(slice) == 1 { return slice[0] } else if slice[0] > max(slice[1:]) { return slice[0] } else { return max(slice[1:]) }}
递归三:反转字符串
递归思想:让我们再看一个例子:如何反转一个字符串?比如给定一个字符串"abcd",经过反转之后变为 "dcba"。同样的,我们可以做一个大胆的假设,假设后续字符串已经反转过来,那么接上第一个字符,整个字符串就反转过来了。对于一个只有一个字符的字符串,不需要反转,这是我们这个递归算法的边界条件。 go代码表示如下,
package mainimport ( "fmt")func main() { str := "hello" var res = reverse(str) fmt.Println(res)}func reverse(str string) string { if len(str) == 1 { return str } else { rs := []rune(str) return reverse(string(rs[1:])) + string(rs[0:1]) }}
=========END=========