在Go里面,函数是一等公民,这个是啥意思呢。
关于一等公民看看维基百科的定义:
In programming language design, a first-class citizen (also type, object, entity, or value) in a given programming language is an entity which supports all the operations generally available to other entities. These operations typically include being passed as an argument, returned from a function, modified, and assigned to a variable.
大意是说,在编程语言中,所谓一等公民,是指支持所有操作的实体, 这些操作通常包括作为参数传递,从函数返回,修改并分配给变量等。所以在Go中函数作为一等公民,可以把函数赋值给变量,也可以把函数作为其它函数的参数或者返回值。下面开始介绍下Go中的函数。
接下来会从一下几个方面来介绍Go中的函数。
- 一个简单的函数定义
- 函数可变长的入参
- 函数可以有多个返回值
- 函数可以当做参数传入
- 函数可以当做返回值
- 匿名函数
- 闭包
一个简单的函数定义
package main
import "fmt"
func test(a int, b int) (c int) {
return a + b
}
func main() {
fmt.Println(test(1, 2))
}
一个非常的test函数的定义,两个int类型的参数,加上一个int类型的返回值,也别的语言没有太大的差别。
函数可变长的入参
package main
import "fmt"
func test(a int, b ...int) (c int) {
var sum int = a
for _, item := range b {
sum = sum + item
}
return sum
}
func main() {
fmt.Println(test(1, 2, 3))
}
上面test函数中,有一个可变长的参数b,可以接受0个或者多个的入参,可以通过类似遍历列表的方式读取每一个值。这里要注意,可变长度的入参,是要作为后一个参数的。
函数可以有多个返回值
上面的这些函数的特性,和别的编程语言差不多,那下面这个特性,就是在其他编程语言中比较少见了。Go函数支持有多个返回值。如下:
package main
import "fmt"
func test(a int, b int) (sum int, sub int) {
return a + b, a - b
}
func main() {
sum, sub := test(1, 2)
fmt.Printf("sum : %v , sub : %v\n", sum, sub)
}
上面的一个test函数,在返回值里面,返回了和 以及 差 两个返回值。这个特性还是有用的。Go函数中,异常的处理是通过返回相应的error,所以这个时候一个函数的返回值一般会包含正常的返回值以及error,如下:
package main
import (
"errors"
"fmt"
)
func div(a int, b int) (result float32, err error) {
if b == 0 {
return 0, errors.New("invalid param , b can't be zero")
}
return float32(a) / float32(b), nil
}
func test(a int, b int) {
result, err := div(a, b)
if err == nil {
fmt.Println(result)
} else {
fmt.Println("error")
}
}
func main() {
test(1, 2) // 0.5
test(1, 0) // error
}
一个返回值result,作为正常的函数返回值,另外一个是error,作为异常返回。
函数可以当做参数传入
函数,和其他类型的数据一样,都可以当做变量传入到函数中,如下:
package main
import "fmt"
func add(a int, b int) (resp int) {
return a + b
}
func sub(a int, b int) (resp int) {
return a - b
}
func cal(a int, b int, f func(int, int) int) (resp int) {
return f(a, b)
}
func main() {
fmt.Println(cal(1, 2, add))
fmt.Println(cal(1, 2, sub))
}
函数可以当做返回参数
函数,和其他类型的数据一样,被函数当做一种返回值,如下:
package main
import "fmt"
func add(a int, b int) (resp int) {
return a + b
}
func sub(a int, b int) (resp int) {
return a - b
}
func getFunc(funcType string) func(int, int) int {
if "add" == funcType {
return add
} else if "sub" == funcType {
return sub
} else {
return nil
}
}
func main() {
fmt.Println(getFunc("add")(1, 2)) // 3
fmt.Println(getFunc("sub")(1, 2)) // -1
fmt.Println(getFunc("other")(1, 2)) // panic: runtime error: invalid memory address or nil pointer dereference
}
函数作为返回值,我们在闭包的场景下,会看到更加有意义的实际用途。
匿名函数
匿名函数,顾名思义,就是一个没有名字的函数。我们把上面的例子,改为一个匿名函数的实现方式。
package main
import "fmt"
func getFunc(funcType string) func(int, int) int {
if "add" == funcType {
return func(a int, b int) (resp int) {
return a + b
}
} else if "sub" == funcType {
return func(a int, b int) (resp int) {
return a - b
}
} else {
return nil
}
}
func main() {
fmt.Println(getFunc("add")(1, 2)) // 3
fmt.Println(getFunc("sub")(1, 2)) // -1
fmt.Println(getFunc("other")(1, 2)) // panic: runtime error: invalid memory address or nil pointer dereference
}
使用匿名函数的好处,一方面是减少定义函数名字的过程,另外一个用处,会在闭包中体现。
闭包
什么是闭包?摘用Wikipedia上的一句定义:
a closure is a record storing a function together with an environment.
闭包是由函数和与其相关的引用环境组合而成的实体 。
因此闭包的核心就是:函数和环境。
再来看一个官方的解释:
Function literals are closures: they may refer to variables defined in a surrounding function. Those variables are then shared between the surrounding function and the function literal, and they survive as long as they are accessible.
翻译过来
函数字面量(匿名函数)是闭包:它们可以引用在周围函数中定义的变量。然后,这些变量在周围的函数和函数字面量之间共享,只要它们还可以访问,它们就会继续存在。
闭包是由函数及其相关引用环境组合而成的实体。闭包 = 函数 + 引用环境
简而言之,闭包,首先是一个函数,其次还有引用了一些上下文的信息。下面我们来看几个有意思的例子。
斐波那契生成器
package main
import "fmt"
func makeFibGen() func() int {
f1 := 1
f2 := 1
return func() int {
f2, f1 = (f1 + f2), f2
return f1
}
}
func main() {
gen1 := makeFibGen()
fmt.Println(gen1()) // 1
fmt.Println(gen1()) // 2
fmt.Println(gen1()) // 3
gen2 := makeFibGen()
fmt.Println(gen2()) // 1
fmt.Println(gen2()) // 2
fmt.Println(gen2()) // 3
}
在上面的这个例子中,我们在makeFibGen里定义了一个匿名函数,并且把这个匿名函数当做结果返回了出去。这里面比较特别的地方是,匿名函数引用了makeFibGen的两个局部的变量 f1,f2。但是当makeFibGen结束后,匿名函数还是可以正常访问到这两个局部的变量。
所以这里返回的就是一个闭包,对应的函数就是匿名的函数,上下文就是f1,f2这两个变量。而且每一次生成闭包的时候,对应的f1,f2会重新赋值,而且两个闭包的上下文的数据是隔离的。
包装函数
之前看到一个非常有意思的例子。平时我们想记录一个函数的调用时间。类似下面我们已经完成了的一个http请求的处理函数。
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/hello", hello)
http.ListenAndServe(":3000", nil)
}
func hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "<h1>Hello!</h1>")
}
这个时候,如果我们想记录这个处理函数的执行时间,我们可以使用闭包的方式进行包装。
package main
import (
"fmt"
"net/http"
"time"
)
func main() {
http.HandleFunc("/hello", timed(hello))
http.ListenAndServe(":3000", nil)
}
func timed(f func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
f(w, r)
end := time.Now()
fmt.Println("The request took", end.Sub(start))
}
}
func hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "<h1>Hello!</h1>")
}
我们使用times函数,对hello函数进行包装,返回一个相同函数签名闭包函数。在这个闭包函数中,执行对应的业务函数。在执行业务函数的前后,分别做了额外的处理进行时间的记录。这个还是一个非常有意思的例子。
结语
今天学习了函数,后面继续继续努力。
参考文章
https://www.calhoun.io/5-useful-ways-to-use-closures-in-go/
https://juejin.cn/post/7140664403996868615
https://zhuanlan.zhihu.com/p/92634505