4.1 定义
函数是结构化编程的最小模块单元。它将复杂的算法过程分解成若干较小任务,隐藏相关细节,使程序结构更加清晰,易于维护。函数被设计成相对独立,通过接收输入参数完成一段算法指令,输出或存储相关结果。因此,函数还是代码复用和测试的基本单元。
关键字func用于定义函数。Go中的函数有些不太方便的限制,但也借鉴了动态语言的某些优点。
- 无需前置声明
- 不支持命名嵌套定义
- 不支持同名函数重载
- 不支持默认参数
- 支持不定长变参
- 支持多返回值
- 支持命名返回值
- 支持匿名函数和闭包
package main
func data() []int //missing function body
{ //syntax error: unexpected semicolon or newline before
return []int{1, 2, 3, 4, 5}
}
func data() []int { //data redeclared in this block
return []int{1, 2, 3, 4, 5}
}
func main() {
func test() []int { //syntax error: unexpected test, expecting (
return []int{1, 2, 3, 4, 5}
}
}
函数属于第一类对象,具备相同签名(参数及返回值列表)的视作同一类型。
package main
func hello() {
println("a")
}
func exec(f func()) {
f()
}
func main() {
f := hello
exec(f)
}
第一类对象指可在运行期间创建,可用作函数参数或返回值,可存入变量的实体。最常见的用法就是匿名函数。
从阅读和代码维护的角度来说,使用命名类型更加方便。
type FormatFunc func(string,...interface{})(string,error)
// 如果不使用命名类型,这个参数的签名就会长到没法看
func format(f FormatFunc,s string, a ...interface{})(string,error){
return f(s,a...)
}
函数只能判断其是否为nil,不支持其他比较操作
package main
func a() {}
func b() {}
func main() {
println(a == nil)
println(a == b) //invalid operation: a == b (func can only be compared to nil)
}
从函数返回局部变量指针是安全的,编译器会通过逃逸分析来决定是否在堆上分配内存
package main
func test()*int{
a:=0x100
return &a
}
func main() {
var b *int = test()
println(b,&b,*b)
}
4.2 参数
Go对参数的处理偏向保守,不支持有默认值的可选参数,不支持命名实参。调用时,必须按签名顺序传递指定类型参数,就算以“_”命名的参数也不能忽略
在参数列表中,相邻的同类型参数可合并
package main
func test(x, y int, s string, _ bool) *int {
return nil
}
func main() {
test(1, 2, "a") //not enough arguments in call to test
}
参数可视作函数局部变量,因此不能在相同层次定义同名变量
package main
func add(x, y int) int {
x := 100 //no new variables on left side of :=
var y int //y redeclared in this block
return x + y
}
func main() {
add(10, 20)
}
形参是指函数中定义的参数,实参则是函数调用时传递的参数。形参类似函数局部变量,而实参则是函数外部对象,可以是常量、变量、表达式或函数。
不管是指针、引用类型,还是其他类型参数,都是值拷贝传递。区别无非是拷贝目标对象还是拷贝指针而已。在函数调用前,会为形参和返回值分配内存空间,并将实参拷贝到形参内存。
package main
import "fmt"
func test(x *int) {
fmt.Printf("pointer:%p,target:%v\n", &x, x)
}
func main() {
a := 0x100
p := &a
fmt.Printf("pointer:%p,target:%v\n", &p, p)
test(p)
}
从表面上看,指针参数的性能更好一些,但实际上得具体分析。被复制的指针会延长目标对象声明周期,还可能导致它被分配到堆上,那么其性能消耗就得加上堆内存分配和垃圾回收的成本。
其实,在栈上复制小对象只需很少的指令即可完成,远比运行时进行堆内存分配要快得多,另外,并发编程也提倡尽可能使用不可变对象(只读或复制),这可消除数据同步等麻烦。当然,如果复制成本很高,或需要修改原对象状态
要实现传出参数,通常建议使用返回值,当然也可继续使用二级指针
package main
func test(p **int) {
x := 100
*p = &x
}
func main() {
var p *int
test(&p)
println(*p) //100
}
如果函数参数过多,建议将其重构为一个复合结构类型,也算是变相实现可选参数和命名实参功能。
package main
import (
"fmt"
"log"
"time"
)
type serverOption struct {
address string
port int
path string
timeout time.Duration
log *log.Logger
}
func newOption() *serverOption {
return &serverOption{
address: "0.0.0.0",
port: 8080,
path: "/var/teset",
timeout: time.Second * 5,
log: nil,
}
}
func server(option *serverOption) {
(*option).address = "127.0.0.1"
option.port = 8000
fmt.Printf("%v\n", option)
}
func main() {
opt := newOption()
server(opt)
}
结果
&{127.0.0.1 8000 /var/teset 5000000000 <nil>}
将过多的参数独立成option struct ,即方便扩展参数集,也方便通过newOption函数设置默认配置。这也是代码复用的一种方式,避免多处调用时烦琐的参数配置
变参
变参本质上就是一个切片。只能接收一个或多个同类型参数,且必须放在列表末尾。
package main
import (
"fmt"
)
func test(s string, a ...int) {
fmt.Printf("%T,%v\n", a, a)
}
func main() {
test("a", 1, 2, 3, 4)
}
[]int,[1 2 3 4]
将切片作为变参时,须进行展开操作。如果是数组,先将其转换为切片。
package main
import (
"fmt"
)
func test(s string, a ...int) {
fmt.Printf("%T,%v\n", a, a)
}
func main() {
s := [4]int{4, 5, 6, 7}
test("a", s[:]...)
}
既然变参是切片,那么复制的仅是切片自身,并不包含底层数组,也因此可以修改原数据,如果需要,可用内置函数copy复制底层数据
package main
import "fmt"
func test(a ...int) {
for i := range a {
a[i] += 100
}
}
func main() {
s := [4]int{4, 5, 6, 7}
test(s[:]...)
fmt.Println(s)
}
4.3 返回值
有返回值的函数,必须有明确的return语句
func test(x int) int {
if x > 0 {
return 1
} else if x < 0 {
return -1
}
} //missing return at end of function
除非有panic,或者无break的死循环,则无需return终止
func test(x int) int {
for {
break
}
} //missing return at end of function
借鉴自动态语言的多返回值模式,函数得以返回更多状态,尤其是error模式。
func div(x, y int) (int, error) {
if y == 0 {
return 0, errors.New("division by zero")
}
return x / y, nil
}
稍有不便的是没有元组类型,也不能用数组,切片接收,但可用“_”忽略掉不想要的返回值。多返回值可用作其他函数调用实参,或当做结果直接返回
命名返回值
对返回值命名和简短变量定义一样,优缺点共存
func paging(sql string, index int) (count int, pages int, err error) {
return
}
命名返回值让函数声明更加清晰,同时也会改善帮助文档和代码编辑器提示。
命名返回值和参数一样,可当做函数局部变量使用,最后由return 隐式返回
func div(x, y int) (z int, err error) {
if y == 0 {
err = errors.New("division by zero")
return
}
z = x / y
return
}
这些特殊的“局部变量”会被不同层级的同名变量遮蔽。好在编译器能检查到此类情况,只要改成显示return返回即可
func add(x, y int) (z int) {
{
z := x + y
return // z is shadowed during return
}
return
}
除遮蔽外,我们还必须对全部返回值命名,否则编译器会搞不清状况
func test() (int, s string, e error) {
return 0, "", nil //cannot use 0 (type int) as type string in return argument
}
显然编译器在处理return语句时,会跳过未命名返回值,无法准确匹配。
如果返回值类型能明确表明其含义,就尽量不要对其命名
4.4 匿名函数
匿名函数是指没有定义名字符号的函数。
除没有名字外,匿名函数和普通函数完全相同。最大区别是,我们可以在函数内定义匿名函数,形成类似嵌套效果。匿名函数可以直接调用,保存到变量,作为参数或返回值。
直接执行:
func(s string){
println(s)
}("hello")
赋值给变量
func main() {
add := func(x, y int) int {
return x + y
}
println(add(1, 2))
}
作为参数:
func test(f func()) {
f()
}
func main() {
test(func() { println("hello") })
}
作为返回值
func test() func(int, int) int {
return func(x, y int) int {
return x + y
}
}
func main() {
add := test()
println(add(1, 2))
}
将匿名函数赋值给变量,与为普通函数提供名字标识符有着根本的区别。当然,编译器会为匿名函数随机生成一个符号名。
普通函数和匿名函数都可作为结构体字段,或经通道传递。
func testStruct(){
type calc struct{
mul func(x,y int)int
}
x := calc{
mul:func(x,y int)int{
return x * y
},
}
println(x.mul(1,2))
}
func testChannel(){
c := make(chan func(int,int) int,2)
c <- func(x,y int)int{
return x + y
}
println((<-c)(1,2))
}
不曾使用的匿名函数会被编译器当做错误
func main() {
func(x int) int { //func literal evaluated but not used
return x
}
}
除闭包因素外,匿名函数也是一种常见重构方法。可将大函数分解成多个相对独立的匿名函数块,然后用相对简洁的调用完成逻辑流程,以实现框架和细节分离。
相比语句块,匿名函数的作用域被隔离开(不使用闭包),不会引发外部污染,更加灵活。没有定义顺序限制,必要时可抽离,便于实现干净、清晰的代码结构。
闭包
闭包是在其词法上下文中引入了自由变量的函数,或者说是函数和其引用的环境的组合体。
package main
func test(x int) func(){
return func(){
println(x)
}
}
func main() {
f := test(22)
f()
}
就这段代码而言,test返回的匿名函数会引用上下文变量x。当该函数在main中执行时,它依然可以正确读取x的值,这种现象就称为闭包。
func test(x int) func(){
println(&x)
return func(){
println(&x,x)
}
}
func main() {
f := test(22)
f()
}
结果
0xc000040000
0xc000040000 22
通过输出指针,我们注意到闭包直接引用了环境变量。分析汇编代码,你会看到返回的不仅仅是匿名函数,还包括所引用的环境变量指针。所以说,闭包是函数和引用环境的组合体更加确切
正因为闭包通过指针引用环境变量,那么可能会导致其生命周期延长,甚至被分配到堆内存。另外,还有所谓延迟求值的特性
package main
func test()[]func(){
var s []func()
for i:=0;i<2;i++{
s = append(s,func(){
println(&i,i)
})
}
return s
}
func main() {
for _,i := range test(){
i()
}
}
结果:
0xc000040000 2
0xc000040000 2
对于这个输出结果不必惊讶,很简单,for循环复用局部变量i,那么,每次添加的匿名函数引用的自然是同一变量。添加操作仅仅将匿名函数放入列表,并未执行。因此,当main执行这些函数时,它们读取的是环境变量最后一次循环时的值。
解决方法就是每次用不同的环境变量或传参复制,让各自闭包环境不同
package main
func test()[]func(){
var s []func()
for i:=0;i<2;i++{
x := i
s = append(s,func(){
println(&x,x)
})
}
return s
}
func main() {
for _,i := range test(){
i()
}
}
结果
0xc000040000 0
0xc000040008 1
多个匿名函数引用同一环境变量,也会让事情变得更加复杂。任何修改行为都会影响其他函数取值,在并发模式下可能要做同步处理
func test(x int)(func(),func()){
return func(){
println(x)
x += 100
},func(){
println(x)
}
}
func main() {
func1,func2:= test(22)
func1()
func2()
}
结果
22
122
闭包让我们不用传递参数就可以读取或修改环境状态,当然也要为此付出额外代价。对于性能要求比较高的场合,须慎重使用
4.5 延迟调用
语句defer向当前函数注册稍后执行的函数调用。这些调用被称为延迟调用,因为他们直到当前函数执行结束前才被执行,常用于资源释放、解除锁定,以及错误处理等操作
package main
import (
"fmt"
"log"
"os"
)
func main() {
f, err := os.Open("./5.go")
if err != nil {
log.Fatalln(err)
}
defer f.Close()
fmt.Printf("%T", f)
}
注意延迟调用注册的是调用,必须提供执行所需参数(哪怕为空)。参数值在注册时被复制并保存起来。如果状态敏感,可改用指针或闭包
package main
func main() {
x, y := 1, 2
defer func(a int) {
println("defer a,x,y", a, x, y)
}(x)
x += 100
y += 200
println(x, y )
}
result:
101 202
defer a,x,y 1 101 202
多个延迟注册按FILO次序执行
package main
func main() {
defer println("x")
defer println("y")
}
result:
y
x
编译器通过插入额外的指令来实现延迟调用执行,而return和panic语句都会终止当前函数流程,引发延迟调用。另外,return不是ret汇编语句,他会先更新返回值
package main
func test()(z int){
defer func(){
println("defer z:",z)
z += 100
}()
return 100
}
func main() {
println("test:",test())
}
result:
defer z: 100
test: 200
误用
延迟调用在调用函数结束时才会被执行。不合理的使用方式会浪费更多资源,甚至造成逻辑错误。
案例:循环处理多个日志文件,不恰当的defer导致文件关闭时间延长。
package main
import (
"fmt"
"log"
"os"
)
func main() {
for i := 0; i < 10000; i++ {
path := fmt.Sprintf("./log/%d.txt", i)
f, err := os.Open(path)
if err != nil {
log.Println(err)
continue
}
defer f.Close()
}
}
应该直接调用,或重构成为函数,将循环和处理算法分离
package main
import (
"fmt"
"log"
"os"
)
func main() {
do := func(x int){
path := fmt.Sprintf("./log/%d.txt",x)
f,err := os.Open(path)
if err != nil{
log.Println(err)
}
defer f.Close()
}
for i:=0;i<1000;i++{
do(i)
}
}
性能
相比直接调用CALL汇编指令调用函数,延迟调用则须花费更大的代价,这其中包括注册调用等操作,还有额外的缓存开销
4.6错误处理
反古的错误处理方式,是Go被谈及最多的内容之一,有人戏称“struck in
70‘s”,可见它与流行趋势背道而驰。
error
官方推荐的标准做法是返回error状态。
func Scanla(a ...interface{})(n int,err error)
标准库将error定义为接口类型,以便实现自定义错误类型。
type error interface {
Error() string
}
按惯例,error总是最后一个返回参数。标准库提供了相关创建函数,可方便地创建包含简单错误文本的error对象
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() {
z, err := div(5, 0)
if err == errDivByZero {
log.Fatalln(err)
}
println(z)
}
应通过错误变量,而文本内容来判定错误类型
错误变量通常以err作为前缀,且字符串内容全部小写,没有结束标点,以便嵌入到其他格式化字符串中输出。
全局错误变量并非没有问题因为他们可能被用户重新赋值,这就可能导致结果不匹配。
与errors.New类似的还有fmt.Errorf,它返回一个格式化内容的错误对象
某些时候,我们需要自定义错误类型,以容纳更多的上下文状态信息。这样的话,还可基于类型做出判断。
package main
import (
"fmt"
"log"
)
type DivError struct {
x, y int
}
func (DivError) Error() string {
return "division by zero"
}
func div(x, y int) (int, error) {
if y == 0 {
return 0, DivError{x, y}
}
return x / y, nil
}
func main() {
z, err := div(5, 0)
if err != nil {
fmt.Printf("%T,%d\n", err, err)
switch e := err.(type) {
case DivError:
fmt.Println(e, e.x, e.y)
default:
fmt.Println(e)
}
log.Fatalln(err)
}
println(z)
}
结果
main.DivError,{5 0}
division by zero 5 0
2019/03/04 21:03:55 division by zero
exit status 1
大量函数和方法返回error,使得调用代码变得很难看,一堆堆得检查语句充斥在代码行间。解决思路有:
- 使用专门的检查函数处理错误逻辑(比如记录日志),简化检查代码。
- 在不影响逻辑的情况下,使用defer延后处理错误状态(err退化赋值)。
- 在不中断逻辑的情况下,将错误作为内部状态保存,等最终“提交”时再处理。
panic,recover
与error相比,panic/recover在使用上更接近try/catch结构化异常
func panic(v interface{})
func recover() interface()
比较有趣的是,它是内置函数而非语句。panic会立即中断当前函数流程,执行延迟调用。而在延迟调用函数中,recover可捕获并返回panic提交的错误对象。
func main() {
defer func() {
if err := recover(); err != nil {
log.Fatalln(err)
}
}()
panic("i am dead")
println("ok")
}
因为panic参数是空接口类型,因此可使用任何对象作为错误状态。而recover返回结果同样要做转型才能获得具体信息。
无论是否执行recover,所有延迟调用都会被执行。但中断性错误会沿着调用栈堆向外传递,要么被外捕获,要么导致程序崩溃。
func test() {
defer println("test.1")
defer println("test.2")
panic("i am out")
}
func main() {
defer func() {
log.Println(recover())
}()
test()
}
结果:
test.2
test.1
2019/03/04 21:34:40 i am out
连续调用panic,仅最后一个会被recover捕获
package main
import "log"
func main() {
defer func() {
for {
if err := recover(); err != nil {
log.Println(err)
} else {
log.Fatalln("fatal")
}
}
}()
defer func() {
panic("defer panic")
}()
panic("main panic")
}
结果
2019/03/04 21:38:13 defer panic
2019/03/04 21:38:13 fatal
exit status 1
在延迟函数中panic,不会影响后续延迟调用执行。而recover之后panic,可被再次捕获。另外,recover必须在延迟调用函数中执行才能正常工作
func catch() {
log.Println("catch:", recover())
}
func main() {
defer catch() // 它捕获到了
defer log.Println(recover())
defer recover()
panic("out ...")
}
结果
2019/03/04 21:42:40 <nil>
2019/03/04 21:42:40 catch: out ...
考虑到recover特性,如果要保护代码片段,那么只能将其重构为函数调用
package main
func test(x, y int) {
z := 1
func() {
defer func() {
if recover() != nil {
println("????")
z = 0
}
}()
z = x / y
}()
println("x/y=", z)
}
func main() {
test(5, 0)
}