这是我参与8月更文挑战的第 16 天,活动详情查看: 8月更文挑战
函数
在go语言中,函数是一等公民:
- 函数本身可以作为值进行传递
- 函数可以作为变量的值
- 函数可以作为参数和返回值
函数声明
每个函数声明都包含一个名字、一个形参列表、一个可选的返回值列表及函数体
func name(paramter-list) (result-list) {
body
}
形参列表指定了一组变量的参数名和参数类型。返回值列表指定了返回值的类型。当函数返回一个未命名的返回值或者没有返回值的时候,返回值列表的圆括号可以省略
func hypot(x,y float64) float64 {
return math.Sqrt(x*x + y*y)
}
fmt.Println(hypot(3, 4))// 5
返回值也可以像形参一样命名。这时候,每一个命名的返回值会声明为一个局部变量,并根据变量类型,初始化为对应的零值
func test(a,b int) (z int) {
z = a + b
return z//z可以省略
}
在函数的参数列表或返回值列表中,如果几个形参或返回值类型相同,那么类型只需要写一次(下边两个是等价的)
func f(i,j,k int, s,t string) {...}
func f(i int, j int, k int, s string, t string) {...}
下边定义了几种带有两个形参和一个返回值的函数
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"
多返回值
一个函数能够返回不止一个结果,一般是一个为期望得到的计算结果,一个为错误值,或者一个表示函数调用是否正确的布尔值
发起一个http的get请求,解析返回的html页面
func findLinks(url string) ([]string, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
return nil, fmt.Errorf("getting %s: %s", url, resp.Status)
}
doc, err := html.Parse(resp.Body)
resp.Body.Close()
if err != nil {
return nil, fmt.Errorf("parsing %s as HTML: %v", url, err)
}
return visit(nil, doc), nil
}
函数变量
函数变量也有类型,而且他们可以复制给变量或者当做函数参数,也可以作为一个函数的返回值。函数变量也可以像其他函数一样调用
func square(n int) int { return n*n }
func negative(m, n int) int { return -n }
func product(m, n int) int { return m*n }
f := square
fmt.Println(f(3))//9
f = negative
fmt.Println(f(3)) // -3
fmt.Printf("%T\n", f)// "func(int) int"
f = product //编译错误:不能把类型func(int, int) int 赋给 func(int) int
函数类型的零值是nil,调用一个空的函数变量,会导致宕机
var f func(int) int
f(3) // 宕机:调用空函数
变长函数
变长函数被调用的时候,可以有可变的参数个数。最熟悉的例子就是fmt.Printf和类似它的函数。Printf需要在开头提供一个固定的参数,后续便可以接受任意数目的参数
在参数列表最后的类型名称之前使用省略号"..." ,表示声明一个变长函数,调用这个函数的时候,可以传递该类型任意数目的参数
func sum(vals ...int) int {
total := 0
for _, val := range vals {
total += val
}
return total
}
fmt.Println(sum()) // 0
fmt.Println(sum(3)) // 3
fmt.Println(sum(1, 2, 3, 4)) // 10
在函数体内,vals是一个int型的slice,调用sum的时候,任何数量的参数,都将提供给vals参数
调用者显式地申请一个数组,将实参复制给这个数组,并把一个数组slice传递给函数
values := []int{1,2,3,4}
fmt.Println(sum(...values))
尽管...int参数就像函数体内的slice,但是变长函数的类型和一个带有普通slice参数的函数的类型是不同的
func f(...int) {...}
func g([]int) {...}
fmt.Printf("%T\n", f)
fmt.Printf("%T\n", g)
函数式编程
函数式编程不是go语言特有的,有许多非常正统的函数式编程的语言,比如Erlang。在go语言中,函数式编程的定位不一样,它作为一门通用的语言,和java、python差不多,它对函数式编程,主要是体现在闭包上边。比如下边一个函数,就提现了一个闭包的能力
func adder() func(int) int {//累加器
sum := 0
return func(v int) int {
sum += v
return sum
}
}
func main() {
a := adder()
for i:=0;i<10;i++{
fmt.Printf("0 + 1 + ... + %d = %d\n", i, a(i))
}
}
函数与闭包
在上边有提到,在go语言中,函数是一等公民,也就是说在go语言中,函数可以作为参数、变量、返回值
还是以上边的代码为例,它的运行结果是
0 + 1 + ... + 0 = 0
0 + 1 + ... + 1 = 1
0 + 1 + ... + 2 = 3
0 + 1 + ... + 3 = 6
0 + 1 + ... + 4 = 10
0 + 1 + ... + 5 = 15
0 + 1 + ... + 6 = 21
0 + 1 + ... + 7 = 28
0 + 1 + ... + 8 = 36
0 + 1 + ... + 9 = 45
可以发现,返回的闭包函数中,它存了sum,所以当有新的i传给它的时候,它会把前边累加的结果再加上新的i
对于闭包函数,首先函数体里边它有自己的局部变量(比如上边传进来的参数v),其中的sum并不是在闭包函数体里边的变量,是在闭包函数外边的变量,叫做自由变量。对于自由变量,编译器就会像连线一样,连到sum中去,如果sum是一个结构,它还可以继续连下去,最后组成的是一棵树,编译器会不断的找这种连接关系,最终把所有需要的变量都连接完,全部连接完成之后,这个整体就称作一个闭包
所以,上边的adder函数,返回的闭包,并不是返回了那一段代码,它返回了那个函数以及它对sum的引用,它会将这个sum变量的引用保存到这个函数里边去
闭包的应用
斐波那契数列
下边是为了掩饰闭包,所以通过这种方式来实现斐波那契数列
func fibonacci() func() int {
a, b := 0, 1
return func() int {
a, b = b, a+b
return a
}
}
func main() {
f := fibonacci()
fmt.Println(f()) // 1
fmt.Println(f()) // 1
fmt.Println(f()) // 2
fmt.Println(f()) // 3
fmt.Println(f()) // 5
fmt.Println(f()) // 8
}
二叉树遍历
常规的实现()
package tree
import "fmt"
type Node struct {
Value int
Left, Right *Node
}
func (n *Node) Print() {
fmt.Println(n.Value)
}
func (n *Node) Traverse() { // 中序遍历
if n == nil {
return
}
n.Left.Traverse()
n.Print()
n.Right.Traverse()
}
package main
import "google.go/part6/functional/closure/tree"
func main() {
var root tree.Node
root = tree.Node{Value: 1}
root.Left = &tree.Node{Value: 2}
root.Right = &tree.Node{Value: 3}
root.Left.Right = &tree.Node{Value: 4}
root.Right.Left = &tree.Node{Value: 5}
root.Traverse()//打印结果:2、4、1、5、3
}
上边这种实现不够健壮,比如我现在想知道这棵树有多少个节点,还需要单独的写个方法去实现。下边是通过传一个函数的方式来实现二叉树的遍历
package tree
import "fmt"
type Node struct {
Value int
Left, Right *Node
}
func (n *Node) Print() {
fmt.Println(n.Value)
}
func (n *Node) Traverse() {
n.TraverseFunc(func(node *Node) {
fmt.Println(node.Value)
})
fmt.Println()
}
//参数传递一个函数
func (n *Node) TraverseFunc(f func(node *Node)) {// 中序遍历
if n == nil {
return
}
n.Left.TraverseFunc(f)
f(n)
n.Right.TraverseFunc(f)
}
package main
import (
"fmt"
"google.go/part6/functional/closure/tree"
)
func main() {
var root tree.Node
root = tree.Node{Value: 1}
root.Left = &tree.Node{Value: 2}
root.Right = &tree.Node{Value: 3}
root.Left.Right = &tree.Node{Value: 4}
root.Right.Left = &tree.Node{Value: 5}
root.Traverse()
//如果想计算节点的数量,就可以这么做
nodeCount := 0
root.TraverseFunc(func(node *tree.Node) {
nodeCount++
})
fmt.Println("Node Count: ", nodeCount)
}
第二种实现方式的打印结果是
2
4
1
5
3
Node Count: 5
参考
《Go程序设计语言》—-艾伦 A. A. 多诺万
《Go语言学习笔记》—-雨痕