- 函数是代码复用和测试的基本单元
- 函数属于第一类对象,具备相同签名(参数及返回值列表)的视作同一类型;第一类对象指可在运行期创建,可用作函数参数或返回值,可存入变量的实体。最常见的用法就是匿名函数
- 函数只能判断其是否为nil,不支持其他比较操作
- 从函数返回局部变量指针是安全的,编译器会通过逃逸分析来决定是否在堆上分配内存
- 定义函数类型
type FormatFunc func(string, ...interface{}) (string, error) // 如不使用命名类型,这个参数签名会长到没法看 func format(f FormatFunc, s string, a ...interface{}) (string, error) { return f(s, a...) }
- 不管是指针、引用类型,还是其他类型参数,都是值拷贝传递,区别无非是拷贝目标对象,还是拷贝指针而已。在函数调用前,会为形参和返回值分配内存空间,并将实参拷贝到形参内存
- 表面上看,指针参数的性能要更好一些,但实际上得具体分析。被复制的指针会延长目标对象生命周期,还可能会导致它被分配到堆上,那么其性能消耗就得加上堆内存分配和垃圾回收的成本。其实在栈上复制小对象只需很少的指令即可完成,远比运行时进行堆内存分配要快得多。另外,并发编程也提倡尽可能使用不可变对象(只读或复制),这可消除数据同步等麻烦。当然,如果复制成本很高,或需要修改原对象状态,自然使用指针更好
- 将过多的参数独立成option struct,既便于扩展参数集,也方便通过newOption函数设置默认配置。这也是代码复用的一种方式,避免多处调用时繁琐的参数配置
- 变参本质上就是一个切片。只能接收一到多个同类型参数,且必须放在列表尾部。不能有多个变参,如 i …int, s …string
func test(s string, a ...int) { fmt.Printf("%T, %v \n", a, a) } func main() { test("abc", 1, 2, 3, 4) // 输出: []int, [1 2 3 4] } }
- 将切片作为变参时,须进行展开操作。如果是数组,先将其转换为切片。既然变参是切片,如果参数复制的仅是切片本身,并不包括底层数组,也因此可修改原数据。如果需要,可用内置函数copy复制底层数据
func test(a ...int) { fmt.Println(a) } func main() { a := [3]int{10, 20, 30} test(a[:]...) // 先转换为slice后展开 } }
- 有返回值的函数,必须有明确的return终止语句;多返回值可用作其他函数调用实参,或当作结果直接返回;如果返回值类型能明确表明其含义,就尽量不要对其命名
- 匿名函数和普通函数的最大区别是我们可在函数内部定义匿名函数,形成类似嵌套效果。匿名函数可以直接调用,保存到变量,作为参数或返回值
// 直接执行 func main() { func(s string) { println(s) }("hello,world") } // 赋值给变量 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,world") }) } // 作为返回值 func test() func(int, int) int { return func(x, y int) int { return x + y } } func main() { add := test() println(add(1, 2)) }
- 普通函数和匿名函数都可作为结构体字段,或经通道传递
- 不曾使用的匿名函数会被编译器当作错误
- 除闭包外,匿名函数也是一种常见重构手段。可将大函数分解成多个相对独立的匿名函数块,然后用相对简洁的调用完成逻辑流程,以实现框架和细节分离
- 闭包
16.1 闭包是在其词法上下文中引用了自由变量的函数,或者说是函数和其引用的环境的组合体
16.2 闭包直接引用了原环境变量,返回的不仅是匿名函数,还包括所引用的环境变量指针。所以闭包是函数和引用环境的组合体,即闭包是返回的匿名函数和函数使用到的变量组合成的整体
16.3 test返回的匿名函数会引用上下文环境变量x
16.4func test(x int) func() { return func() { println(x) } } func main() { f := test(123) f() } // 输出:123
16.5 for循环复用局部变量i,那么每次添加的匿名函数引用的自然是同一变量。添加操作仅仅是将匿名函数放入列表,并未执行。因此,当main执行这些函数时,它们读取的是环境变量 i 最后一次循环时的值。func add() func(int) int { n := 10 return func(x int) int { n += x return n } } func main() { f := add() println(f(1)) println(f(2)) println(f(3)) } // 输出:11 13 16 // 原因是匿名函数和n形成一个整体,每次调用n都在改变
解决方法是每次用不同的环境变量或传参复制,让各自闭包环境各不相同func test() []func() { var s []func() for i := 0; i < 2; i++ { s = append(s,func() { // 将多个匿名函数添加到列表 println(&i, i) }) } return s // 返回匿名函数列表 } func main() { for _, f := range test() { // 迭代执行所有匿名函数 f() } } // 输出:0xc82000a0078 2 // 0xc82000a0078 2
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 _, f := range test() { // 迭代执行所有匿名函数 f() } } // 输出:0xc82006e000 0 // 0xc82006e008 1
- 多个延迟注册按FILO次序进行
- return 和 panic语句都会终止当前函数流程,引发延迟调用。另外,return语句不是ret汇编指令,它会先更新返回值
func test() (z int) { defer func() { println("defer:",z) z+=100 // 修改命名返回值 }() return 100 } func main() { println("test:",test()) } // 输出:defer:100 // test:200
- panic会立即中断当前函数流程,执行延迟调用,而在延迟调用函数中,recover可捕获并返回panic提交的错误对象
func main() { defer func() { if err := recover(); err != nil { log.Fatalln(err) } }() panic("i am dead") println("exit.") //永远不会执行 }
- 无论是否执行recover,所有延迟调用都会被执行。但中断性错误会沿调用堆栈向外传递,要么被外层捕获,要么导致进程崩溃
func test() (z int) { defer println("test.1") defer println("test.2") panic("i am dead") } func main() { defer func() { log.Println(recover()) }() test() } // 输出:test.2 // test.1 // i am dead
- 连续调用panic,仅最后一个会被recover捕获
func main() { defer func() { for { if err := recover(); err != nil { log.Println(err) } else { log.Fatalln("fatal") } } }() defer func() { panic("you are dead") }() panic("i am dead") } // 输出:you are dead // fatal
- 另外,recover必须在延迟调用函数中执行才能正常工作