面试题答案
一键面试函数调用开销
- 理论层面:
- 普通具名函数:在Go语言中,普通具名函数在编译时就确定了其调用地址,调用时通过该固定地址进行跳转执行,函数调用开销相对稳定。
- 匿名函数:匿名函数在使用时会创建一个新的函数对象,其调用地址是在运行时动态确定的。这意味着每次调用匿名函数可能会有额外的开销,比如获取动态生成的函数地址等操作。不过,现代编译器和处理器对于这种动态地址调用有一定的优化,在实际使用中,这种差异可能并不显著。
- 示例:
package main
import (
"fmt"
"time"
)
// 普通具名函数
func namedFunction() {
fmt.Println("This is a named function")
}
func main() {
start := time.Now()
for i := 0; i < 1000000; i++ {
namedFunction()
}
elapsed1 := time.Since(start)
start = time.Now()
anonymousFunction := func() {
fmt.Println("This is an anonymous function")
}
for i := 0; i < 1000000; i++ {
anonymousFunction()
}
elapsed2 := time.Since(start)
fmt.Printf("Time taken by named function: %s\n", elapsed1)
fmt.Printf("Time taken by anonymous function: %s\n", elapsed2)
}
在这个示例中,对普通具名函数和匿名函数分别进行100万次调用并计时。实际运行结果可能因机器性能和编译器优化等因素有所不同,但理论上匿名函数的调用开销可能稍高一点。
内存分配
- 理论层面:
- 普通具名函数:普通具名函数的代码存放在只读的数据段(text segment),在程序启动时加载到内存,其生命周期与程序相同。具名函数在调用时,通常在栈上分配局部变量等空间,函数调用结束后,栈空间会被回收。
- 匿名函数:匿名函数创建时,会在堆上分配内存来存储函数对象,包括函数体、闭包捕获的变量等。这是因为匿名函数可能会在其定义的作用域之外被引用(例如作为返回值返回,或者传递给其他函数并在其他地方调用),所以需要在堆上分配内存以保证其生命周期的灵活性。这就导致匿名函数相比普通具名函数,可能会有更多的内存分配和管理开销。
- 示例:
package main
import (
"fmt"
)
func returnAnonymousFunction() func() {
num := 10
return func() {
fmt.Println(num)
}
}
func main() {
anonFunc := returnAnonymousFunction()
anonFunc()
}
在这个例子中,returnAnonymousFunction
返回一个匿名函数,该匿名函数捕获了局部变量num
。由于匿名函数可能在returnAnonymousFunction
函数返回后仍然被使用,所以匿名函数及其捕获的变量num
会在堆上分配内存,以确保其生命周期的正确性。而普通具名函数通常不需要这种堆上的内存分配来处理捕获变量的情况(除非显式使用了new
等堆分配操作)。