1. go中函数有以下几个特点:
(1)可返回多个值,但需用括号将返回值类型括起来
(2)参数个数可变,可以理解为切片,使用...表示
(3)支持匿名函数和闭包
(4)函数本身就是一种类型,可以赋值给变量
(5)返回值可以被命名,当返回值都被命名时,return语句之后可以没有参数,比如:
func calc(a, b int) (sum int, avg int) {
sum = a + b
avg = (a + b) / 2
return
}
但当代码较长时,该写法会影响代码可读性,故只建议在上面这种代码比较简短的情况时使用
明明返回参数允许defer延迟调用通过闭包读取和修改:
package main
func add(x, y int) (z int) {
defer func() {
z += 100
}()
z = x + y
return
}
func main() {
println(add(1,2))
}
运行结果:
package main
func add(x, y int) (z int) {
defer func() {
println(z)
z += 100
}()
z = x + y
return z + 200
}
func main() {
println(add(1,2))
}
运行结果:
由输出结果可以看出,defer会将函数的return延迟,待defer内的函数执行完之后才会返回,所以才会输出两个不一样的数字,由此可以看出上例add函数中的执行顺序为 z=z+200 ——> call defer ——>return
2. 关于go中函数可变参数的一个实例如下:
package main
import (
"fmt"
)
func test(s string, n ...int) string {
var x int
for _, i := range n {
x += i
}
return fmt.Sprintf(s,x)
}
func main() {
println(test("sum: %d",1, 2, 3))
s := []int{1, 2, 3}
res := test("sum: %d", s...) //使用slice对象做变参时,必须要使用...展开
println(res)
}
3. 多返回值可直接作为其他函数的实参来调用
package main
func test() (int, int) {
return 1, 2
}
func add(x, y int) int {
return x + y
}
func sum(n ...int) int {
var x int
for _, i := range n {
x += i
}
return x
}
func main() {
println(add(test()))
println(sum(test()))
}
4. 匿名函数
匿名函数由一个不带函数名和函数声明和函数体构成,其优越性在于可以直接使用函数体内的变量,不必声明
几种用法的实例如下:
package main
import (
"fmt"
"math"
)
func main() {
//1.赋值给变量
getSqrt := func(a float64) float64 {
return math.Sqrt(a)
}
fmt.Println(getSqrt(4))
fn := func() {println("Hello World!")}
fn()
//2.函数集合
fns := [](func (x int) int) {
func(x int) int {return x + 1},
func(x int) int {return x + 2},
}
println(fns[0](100))
//3.可赋值给变量,作为结构字段
d := struct {
fn func() string
}{
fn: func() string {return "Hello, World!"},
}
println(d.fn())
//channel of function
fc := make(chan func() string, 2)
fc <- func() string {return "Hello, World!"}
println((<-fc)())
}
运行结果:
5. 延迟调用(defer)
defer特性:
1. 关键字defer用于注册延迟调用
2. 这些调用直到return前才被执行。因此,可以用来做资源清理
3. 多个defer语句,按先进后出的方式执行
4. defer语句中的变量,在defer声明时就决定了
defer用途:
1. 关闭文件句柄
2. 锁资源释放
3. 数据库连接释放
(1)defer先进后出,因为后面的语句会依赖前面的资源,因此如果前面的资源先释放了,后面的语句就没法执行了
一个实例如下:
package main
import (
"fmt"
)
func main() {
var whatever [5]struct{}
for i := range whatever {
defer fmt.Println(i)
}
}
运行结果:
一个很容易犯错的地方(看下面这两个例子):
package main
import "fmt"
type Test struct {
name string
}
func (t *Test) Close() {
fmt.Println(t.name, " closed")
}
func main() {
ts := []Test{{"a"}, {"b"}, {"c"}}
for _, t := range ts {
defer t.Close()
}
}
package main
import "fmt"
type Test struct {
name string
}
func (t *Test) Close() {
fmt.Println(t.name, " closed")
}
func main() {
ts := []Test{{"a"}, {"b"}, {"c"}}
for _, t := range ts {
t2 := t
defer t2.Close()
}
}
上面两个例子,只差了一个看似多此一举的声明t2,结果却完全不同,第一个例子是我们需要注意的,不要这样的错误
至于为什么会有这种差异,由官方文档的解释来看,有如下结论:
defer后面的语句在执行的时候,函数调用的参数会被保存起来,但是不执行。也就是复制了一份,但是并没有说struct这里的this指针如何处理,通过上述例子可以看出go语言并没有把这个明确写出来的this指针当作参数来看待。
defer用于http.Get失败时抛出异常
package main
import (
"net/http"
)
func do() error {
res, err := http.Get("http://www.google.com")
if res != nil { //注意这里一定要判别res是否为nil,这是http.Get的一个警告,否则会将错误返回而造成panic
defer res.Body.Close()
}
if err != nil {
return err
}
return nil
}
func main() {
do()
}
6. 异常处理
Go没有结构化一场,使用panic抛出错误,recover捕获错误。
异常的使用场景简单描述:Go中可以抛出一个panic的异常,然后在defer中通过recover捕获这个异常,然后正常处理
panic(引发中断性错误):
(1) 内置函数
(2) 若函数中写了panic语句,则会终止其后要执行的代码
(3) 直到整个goroutine退出,并报告错误
recover:
(1) 内置函数
(2) 用来控制一个goroutine的panicking行为,捕获panic,从而影响应用的行为
(3) 一般的调用建议
a)在defer函数中,通过recover来终止一个goroutine的panicking过程,从而恢复正常代码的执行
b) 可以获取通过panic传递的error
注意:
(1) 利用recover处理panic指令,defer必须放在panic之前定义,并且recover必须放在defer的函数中,否则不会影响panic的扩散
(2) recover处理异常后,函数会恢复到defer之后的那个点
error(表示函数调用状态):使用标准库errors.New和fmt.Errorf函数用于创建实现error接口的错误对象
一个实例如下:
package main
import (
"errors"
"fmt"
)
var ErrDivByZero = errors.New("division by zero")
func div(x, y int) (int, error) {
if y == 0 {
return 0, ErrDivByZero
}
return x / y, nil
}
func main() {
defer func() {
fmt.Println(recover())
}()
switch z, err := div(10, 0); err{
case nil:
println(z)
case ErrDivByZero:
panic(err)
}
}
运行结果:
如何区别使用panic和error两种方式?
一般来说,导致关键流程出现不可修复性错误的使用panic,其他使用error
7. 单元测试
Go语言中的测试依赖go test命令,在包目录内,所有以_test.go为后缀名的源代码文件都是go test测试的一部分,不会被go build编译到最终的可执行文件中。
go test命令会遍历所有的*_test.go文件中符合上述命名规则的函数,然后生成一个临时的main包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件
Golang单元测试对文件名和方法名,参数都有很严格的要求:
(1) 文件名必须以*_test.go命名
(2) 方法必须是Test[^a-z]开头
(3) 方法参数必须时 t *testing.T
(4) 使用go test进行单元测试