1. go函数
1.1 如何定义一个函数
go 定义一个函数比较简单,由关键字、函数名、参数类型、返回类型 组成,语法如下:
// optionalParameters 是 (param1 type1, param2 type2 ...) 这种形式
func functionName(optionalParameters) optionalReturnType {
body
}
来看一个非常简单的函数,计算两个数字之和:
func sum0(a int, b int) int {
return a + b
}
// 当多个参数类型相同时,可以只写一个类型声明
func sum1(a, b int) int {
return a + b
}
// 可以给返回值命名,并且通过赋值当方式更新结果,而且return可以不带返回值
func sum2(a, b int) (res int) {
res = a + b
return
}
go 还支持可变参数,在 python 里我们知道使用的是 *args,在 go 里边使用三个省略号来实现, 比如想要计算 n 个 int 数字之和,可以这么写:(注意可变参数其实被包装成了一个 slice)
func sum3(init int, vals ...int) int {
sum := init
for _, val := range vals { // vals is []int
sum += val
}
return sum
}
// fmt.Println(sum3(0, 1, 2, 3))
// fmt.Println(sum3(0, []int{1,2,3}...)) // 还可以解包一个 slice 来作为参数传入,给一个 slice 加上三个省略号
再进一步,函数还可以返回多个值,这个相比 c 来说非常方便,比如除了 sum 之外我们再返回一个可变参数的个数: (其实 go 最后一个参数经常用来返回错误类型,这个之后讨论错误处理的时候再涉及)
func sum4(init int, vals ...int) (int, int) {
sum := init
fmt.Println(vals, len(vals))
for _, val := range vals {
sum += val
}
return sum, len(vals)
}
1.2 函数的参数
Go针对不同的数据类型,在作为函数参数的时候,会有不同的效果
slicemap
实际上,这里其实 map/slice 等也是传递的副本,为啥它们就可以修改呢?我们以 slice 举例,它的内部实现其实是这样的,底层实现包含一个指向数组的指针(ptr), 一个长度 len 和容量 cap ,传参的时候实际上是 slice 这个结构体的拷贝(只有三个元素而不是copy所有的底层数组里的值),所以复制很轻量,而且通过底层的指针也可以实现修改。
// https://golang.org/src/runtime/slice.go
type slice struct {
array unsafe.Pointer
len int
cap int
}
所以我们看到go 里边所有的函数参数都是值拷贝,只不过对于一些复合结构因为复制的结构体里包含指针,所以可以修改它的底层结构。
1.3 匿名函数与闭包
所谓闭包就是一个函数“捕获”了和它在同一作用域的其他常量和变量。 当闭包被调用的时候,不管在程序什么地方调用,闭包能够使用这些常量或者变量,并且只要闭包还在使用它,这些变量就不会销毁,一直存在。 事实上,闭包能够捕获变量的原因,是把变量移动到了堆区,这样就能够保证程序结束之前,能够一直调用。
因此,go程序中,变量的生命周期不由它的作用域决定。
package main
import (
"fmt"
"strconv"
)
func tt() func() int {
var x int
fmt.Println("--------")
fmt.Println(x)
return func() int {
x++
return x * x
}
}
func main() {
i := 0
str := "mike"
f1 := func() { // 匿名函数,无参无返回值
// 可以引用到函数外的变量,此时就是闭包. 并且函数内部可以改变函数引用变量的值
i++
str += strconv.Itoa(i) + " "
fmt.Printf("方式1: i = %d, str = %s\n", i, str)
} // 只定义了,还未调用
f1() // 显示调用
fmt.Printf("打印: i = %d, str = %s\n", i, str)
// 方式1的另一种方式
type FuncType func()
var f2 FuncType = f1
fmt.Println("f2()")
f2() // 调用函数
fmt.Printf("打印: i = %d, str = %s\n", i, str)
//方式2
func() { // 匿名函数 无参无返回
i++
str += strconv.Itoa(i)
fmt.Printf("方式2: i = %d, str = %s\n", i, str)
}() // ()的作用是,此处直接调用匿名函数
// 方式3,有参有返回
v := func(a, b int) (res int) {
res = a + b
return
}
v1 := v(1, 2) // 此处是调用函数
fmt.Println("v1 =", v1)
v2 := v // 此处是函数定义赋值
fmt.Println("v2(3, 4) :", v2(3, 4))
// 此处构成闭包,为什么?
// 因为f 是一个 函数, 该函数由tt的返回值定义,且是一个匿名函数,因此就构成了闭包
// 捕获了 tt 中的 x值, 但是捕获的 x 值, 捕获的是第一次调用tt()时创建的值,将其放入堆中
f := tt()
fmt.Println(f())
fmt.Println(f())
fmt.Println(f())
fmt.Println(f())
// 以下就不会构成闭包,因为调用的是tt函数
fmt.Println(tt()())
fmt.Println(tt()())
fmt.Println(tt()())
}
运行:
zhou@zhoudeMacBook-Air helloworld % go run 11_匿名函数.go
方式1: i = 1, str = mike1
打印: i = 1, str = mike1
f2()
方式1: i = 2, str = mike1 2
打印: i = 2, str = mike1 2
方式2: i = 3, str = mike1 2 3
v1 = 3
v2(3, 4) : 7
--------
0
1
4
9
16
--------
0
1
--------
0
1
--------
0
1
zhou@zhoudeMacBook-Air helloworld %
1.4 函数类型
通过上一个例子可以知道,函数能够类似于“变量”一样的去赋值。实际上,go 里边函数其实也是『一等公民』,函数本身也是一种类型,所以我们可以定义一个函数然后赋值给一个变量,比如:
func testFuncType() {
myPrint := func(s string) { fmt.Println(s) }
myPrint("hello go")
}
这样的话,就可以用数据结构存储函数,比如用map值做函数的映射:
func testMapFunc() {
funcMap := map[string]func(int, int) int{
"add": func(a, b int) int { return a + b },
"sub": func(a, b int) int { return a - b },
}
fmt.Println(funcMap["add"](3, 2))
fmt.Println(funcMap["sub"](3, 2))
}
所以,也可以用作函数的参数传递进入,该方法一般可以用作回调函数:
func Double(n int) int {
return n * 2
}
func Apply(n int, f func(int) int) int {
return f(n) // f 的类型是 "func(int) int"
}
func funcAsParam() {
fmt.Println(Apply(10, Double))
}
2. 错误处理
2.1 defer语句
go 中提供了一个 defer 语句用来延迟一个函数(匿名函数)或者方法的执行,它会在函数执行完成(return)之前调用。一般为了防止代码里有资源泄露(文件、数据库连接、锁), 对于打开的资源比如文件等我们需要显式关闭,这种场合就是 defer 发挥作用最好的场景,也是 go 代码中使用 defer 最常用的场景。
// go
f, err := os.Open(file)
if err != nil {
// handle err
return err
}
defer f.Close() // 保证文件会在函数返回之后关闭,防止资源泄露
// 也常用在使用锁的地方,防止忘记释放锁
mu.Lock()
defer mu.UnLock()
另外函数里可以使用多个 defer 语句,如果有多个 defer 它们会按照后进先出(Last In First Out)的顺序执行。
package main
import (
"fmt"
)
func testDefer() string {
defer fmt.Println("defer 1")
defer fmt.Println("defer 2")
fmt.Println("函数体")
return "hello"
}
func main() {
fmt.Println(testDefer())
}
// 输出;
/*
zhou@zhoudeMacBook-Air helloworld % go run 12_defer.go
函数体
defer 2
defer 1
hello
zhou@zhoudeMacBook-Air helloworld %
*/
2.2 go 的 error 类型
在 go 中通过返回一个 error 来表示错误或者异常状态,这是 go 代码中最常见的方式。那 error 究竟是什么呢? 其实 error 是 go 的一个内置的接口类型,比如你可以使用开发工具跳进去看下 error 的定义。
// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
Error() string
}
error 的定义很简单,只要我们自己实现了一个类型的 Error() 方法返回一个字符串,就可以当做错误类型了。举一个简单小例子, 比如计算两个整数相除,我们知道除数是不能为 0 的,这个时候我们就可以写个函数:
import (
"errors" // 使用内置的 errors
"fmt"
)
// Divide compute int a/b
func Divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("divide by zero")
}
return a / b, nil
}
func main() {
// fmt.Println(testDefer())
a, b := 1, 0
res, err := Divide(a, b)
if err != nil {
fmt.Println(err) // error 类型实现了 Error() 方法可以打印出来
}
fmt.Println(res)
}
2.3 错误处理
error
// from https://8thlight.com/blog/kyle-krull/2018/08/13/exploring-error-handling-patterns-in-go.html
func (router HttpRouter) parse(reader *bufio.Reader) (Request, Response) {
requestText, err := readCRLFLine(reader) //string, err Response
if err != nil {
//No input, or it doesn't end in CRLF
return nil, err
}
requestLine, err := parseRequestLine(requestText) //RequestLine, err Response
if err != nil {
//Not a well-formed HTTP request line with {method, target, version}
return nil, err
}
if request := router.routeRequest(requestLine); request != nil {
//Well-formed, executable Request to a known route
return request, nil
}
//Valid request, but no route to handle it
return nil, requestLine.NotImplemented()
}
在 go 的惯例中,一般函数多个返回值的最后一个值用来返回错误,返回 nil 表示没有错误,调用者通过检查返回的错误是否是 nil 就知道是否需要处理错误了。
2.4 go 的异常处理 panic/recover
errorpanic
func MustDivide(a, b int) int {
if b == 0 {
panic("divide by zero")
}
return a / b
}
如果我们不幸传入了除数为0,但是又不想让进程退出呢?go 还提供了一个 recover 函数用来从异常中恢复,比如使用 recover 可以把一个 panic 包装成为 error 再返回,而不是让进程退出:
func Divide2(a, b int) (res int, e error) {
defer func() {
if err := recover(); err != nil {
e = fmt.Errorf("%v", err)
}
}()
res = MustDivide(a, b)
return // 命名返回值不用加上返回的参数
}
errorpanic