今天在对一个golang工程做性能分析时遇到一个需求:有一个函数列表,存放了各种函数,程序一个一个处理列表中的函数,现在需要统计每个函数执行时间,并且打印执行慢的函数的名字及所在文件名和行号。runtime.FuncForPC可以获取一个函数的这些信息,但是参数需要传入函数地址,如何正确获取函数地址呢?取决于一个函数变量和函数地址的关系,让我们看一个例子:

package main

func foo() {
}

func main() {
    var f [2]func()
    f[0] = foo 
    f[1] = foo 
    f[0]()
    f[1]()
}                 

编译成二进制文件,objdump -D main 反汇编结果main.main部分:

...
0000000000452340 <main.main>:
  452340:   64 48 8b 0c 25 f8 ff    mov    %fs:0xfffffffffffffff8,%rcx
  452347:   ff ff  
  452349:   48 3b 61 10             cmp    0x10(%rcx),%rsp
  45234d:   76 42                   jbe    452391 <main.main+0x51>
  45234f:   48 83 ec 18             sub    $0x18,%rsp
  452353:   48 89 6c 24 10          mov    %rbp,0x10(%rsp)
  452358:   48 8d 6c 24 10          lea    0x10(%rsp),%rbp
  45235d:   0f 57 c0                xorps  %xmm0,%xmm0
  452360:   0f 11 04 24             movups %xmm0,(%rsp)
  452364:   48 8d 05 dd 59 02 00    lea    0x259dd(%rip),%rax        # 477d48 <go.func.*+0x46>
  45236b:   48 89 04 24             mov    %rax,(%rsp)
  45236f:   48 89 44 24 08          mov    %rax,0x8(%rsp)
  452374:   48 8b 14 24             mov    (%rsp),%rdx
  452378:   48 8b 02                mov    (%rdx),%rax
  45237b:   ff d0                   callq  *%rax  
  45237d:   48 8b 54 24 08          mov    0x8(%rsp),%rdx
  452382:   48 8b 02                mov    (%rdx),%rax
  452385:   ff d0                   callq  *%rax  
  452387:   48 8b 6c 24 10          mov    0x10(%rsp),%rbp
  45238c:   48 83 c4 18             add    $0x18,%rsp
  452390:   c3                      retq   
  452391:   e8 0a 7b ff ff          callq  449ea0 <runtime.morestack_noctxt>
  452396:   eb a8                   jmp    452340 <main.main>
...

地址452364~45236b的指令可以认为匹配 f[0] = foo ,可以看出来函数变量实际上被赋予了地址0x259dd(%rip),亦即注释中的地址值477d48,查找这个地址:

    ...
  477d46:   00 00                   add    %al,(%rax)
  477d48:   30 23                   xor    %ah,(%rbx)
  477d4a:   45 00 00                add    %r8b,(%r8)
  477d4d:   00 00                   add    %al,(%rax)
  477d4f:   00 10                   add    %dl,(%rax)
  ...

后面的汇编指令无意义,只看477d48地址内存中8个字节的值是 452330 (little endian byte order),看起来也是一个地址值,查找这个地址:

...
0000000000452330 <main.foo>:
  452330:   c3                      retq
  452331:   cc                      int3
  452332:   cc                      int3
  452333:   cc                      int3
  452334:   cc                      int3
  452335:   cc                      int3
  452336:   cc                      int3
  452337:   cc                      int3
  452338:   cc                      int3
  452339:   cc                      int3
  45233a:   cc                      int3
  45233b:   cc                      int3
  45233c:   cc                      int3
  45233d:   cc                      int3
  45233e:   cc                      int3
  45233f:   cc                      int3

0000000000452340 <main.main>:
...

正好是foo函数地址。由此我们可以得出结论:函数变量存的其实是一个指针指向函数地址,因此我们可以这样获取函数地址:

package main

import (
    "log"
    "runtime"
    "unsafe"
)

func foo() {
}

func main() {
    var f func()
    f = foo 
    pc := **(**uintptr)(unsafe.Pointer(&f))
    fi := runtime.FuncForPC(pc)
    log.Printf("func name=%v", fi.Name())
}                            

go源码中没有函数变量内存布局的结构定义,但是slice,string类型都有:

type SliceHeader struct {
	Data uintptr
	Len  int
	Cap  int
}


type StringHeader struct {
	Data uintptr
	Len  int
}

特别的,对于interface{}类型变量,go源码中定义的内存布局是这样的:

type eface struct {
	_type *_type
	data  unsafe.Pointer
}

_type 表示类型,data 指向具体值,可以理解interface{}为什么保存了类型信息,这是为了保证type switch和type assert正常工作,同时虽然它很像c语言中的void*,但是他能相对安全的使用。

interface{}变量有一个很容易误用的case,变量i本身是nil和通过一个具体类型的nil值赋值的i是具有不同意义的。interface{}可以认为是一种特殊的interface type,所有类型都实现了这个特殊接口,因此这个容易误用的case存在在任何interface上,举一个例子:

package main

import "log"

type IFoo interface {
    foo()
}

type Foo struct {
    d int
}

func (f *Foo) foo() {
    log.Printf("d=%v", f.d)
}

func main() {
    var ifs []IFoo
    var f Foo
    var pf *Foo
    ifs = append(ifs, &f, pf, nil)
    for _, i := range ifs {
        if i != nil {
            i.foo()
        }
    }
}

这段代码最终会panic在第二个i.foo()调用上,我们容易记得判断接口是否为nil,但是常常忘记判定接口中的值是否为nil,一种解决方法是使用反射:

 for _, i := range ifs {
        if i != nil && !reflect.ValueOf(i).IsNil() {
            i.foo()
        }
   }