如何在运行时发现所有包类型?

How to discover all package types at runtime?

据我所知(参见 here, and here) there is no type discovery mechanism in the reflect package,它期望您已经拥有要检查的类型或值的实例。

是否有任何 other 方法来发现 运行ning go 包中的所有导出类型(尤其是结构)?

这是我希望拥有的(但它不存在):

import "time"
import "fmt"

func main() {
    var types []reflect.Type
    types = reflect.DiscoverTypes(time)
    fmt.Println(types)
}

最终目标是能够发现满足特定条件的包的所有结构,然后能够实例化这些结构的新实例。

顺便说一句,识别类型的注册函数不是我的用例的有效方法。


无论您认为这是个好主意,这就是我想要此功能的原因(因为我知道您会问):

我写了一个 code generation utility 加载源文件并构建 AST 来扫描嵌入指定类型的类型。该实用程序的输出是一组基于发现的类型的 go 测试函数。我使用 go generate 调用此实用程序来创建测试函数,然后 运行 go test 来执行生成的测试函数。每次测试更改(或添加新类型)时,我都必须重新运行 go generate before re-运行ning go test。这就是注册功能不是有效选项的原因。我想避免 go generate 步骤,但这需要我的实用程序成为由 运行ning 包导入的库。库代码需要在 init() 期间以某种方式扫描 运行ning 命名空间以查找嵌入预期库类型的类型。

没有。

如果您想 'know' 您的类型,您必须注册它们。

不幸的是,我认为这是不可能的。包在 Go 中不是 "actionable",你不能在它上面 "call a function"。您也不能在类型上调用函数,但可以在类型的实例上调用 reflect.TypeOf 并获得 reflect.Type,这是类型的运行时抽象。只是没有这样的包机制,没有 reflect.Package.

话虽如此,您可以 file an issue 关于缺少(以及添加的实用性)reflect.PackageOf

(2019 年更新见底部)

警告:未经测试且不可靠。每当发布新版本的 Go 时都会中断。

通过稍微修改一下 Go 的运行时,可以获得运行时知道的所有类型。在自己的包中包含一个小的汇编文件,包含:

TEXT yourpackage·typelinks(SB), NOSPLIT, [=10=]-0
    JMP reflect·typelinks(SB)

yourpackage中声明函数原型(无函数体):

func typelinks() []*typeDefDummy

除了类型定义:

type typeDefDummy struct {
    _      uintptr           // padding
    _      uint64            // padding
    _      [3]uintptr        // padding
    StrPtr *string           
}

然后只需调用类型链接,遍历切片并读取每个 StrPtr 的名称。寻找以 yourpackage 开头的那些。请注意,如果在不同路径中有两个名为 yourpackage 的包,则此方法将无法明确工作。

can I somehow hook into the reflect package to instantiate new instances of those names?

是的,假设 d 是类型 *typeDefDummy 的值(注意星号,非常重要):

t := reflect.TypeOf(*(*interface{})(unsafe.Pointer(&d)))

现在 t 是一个 reflect.Type 值,您可以使用它来实例化 reflect.Values。


编辑:我成功测试并执行了这段代码,uploaded it as a gist.

调整包名称并根据需要包含路径。

2019 年更新

自从我最初发布此答案以来发生了很多变化。下面是关于如何在 2019 年使用 Go 1.11 完成相同操作的简短描述。

$GOPATH/src/tl/tl.go

package tl

import (
    "unsafe"
)

func Typelinks() (sections []unsafe.Pointer, offset [][]int32) {
    return typelinks()
}

func typelinks() (sections []unsafe.Pointer, offset [][]int32)

func Add(p unsafe.Pointer, x uintptr, whySafe string) unsafe.Pointer {
    return add(p, x, whySafe)
}

func add(p unsafe.Pointer, x uintptr, whySafe string) unsafe.Pointer

$GOPATH/src/tl/tl.s

TEXT tl·typelinks(SB), [=15=]-0
    JMP reflect·typelinks(SB)

TEXT tl·add(SB), [=15=]-0
    JMP reflect·add(SB)

main.go

package main

import (
    "fmt"
    "reflect"
    "tl"
    "unsafe"
)

func main() {
    sections, offsets := tl.Typelinks()
    for i, base := range sections {
        for _, offset := range offsets[i] {
            typeAddr := tl.Add(base, uintptr(offset), "")
            typ := reflect.TypeOf(*(*interface{})(unsafe.Pointer(&typeAddr)))
            fmt.Println(typ)
        }
    }
}

祝您黑客愉快!

在 Go 1.5 中,您可以使用新包 types and importer 检查二进制包和源包。例如:

package main

import (
    "fmt"
    "go/importer"
)

func main() {
    pkg, err := importer.Default().Import("time")
    if err != nil {
        fmt.Printf("error: %s\n", err.Error())
        return
    }
    for _, declName := range pkg.Scope().Names() {
        fmt.Println(declName)
    }
}

您可以使用包 go/build to extract all the packages installed. Or you can configure the Lookup 导入器检查环境外的二进制文件。

在 1.5 之前,唯一简单的方法是使用包 ast 来编译源代码。

感谢@thwd 和@icio,按照您的指示,它今天在 1.13.6 上仍然有效。

按照您的方式进行 tl.s 将是:

TEXT ·typelinks(SB), [=10=]-0
    JMP reflect·typelinks(SB)

是的,没有包名,里面也没有 "add" 函数。

然后按照@icio 的方式将 "add" 函数更改为:

func add(p unsafe.Pointer, x uintptr, whySafe string) unsafe.Pointer {
    return unsafe.Pointer(uintptr(p) + x)
}

那么现在一切正常了。 :)

go 1.16 版本(已测试 go 版本 go1.16.7 linux/amd64)

  • 这只能生成代码和字符串。您必须将生成的代码粘贴到某处然后再次编译它
  • 只有资源可用时才有效。
import (
  "fmt"
  "go/ast"
  "golang.org/x/tools/go/packages"
  "reflect"
  "time"
  "unicode"
)

func printTypes(){
  config := &packages.Config{
    Mode:  packages.NeedSyntax,
  }
  pkgs, _ := packages.Load(config, "package_name")
  pkg := pkgs[0]

  for _, s := range pkg.Syntax {
    for n, o := range s.Scope.Objects {
      if o.Kind == ast.Typ {
        // check if type is exported(only need for non-local types)
        if unicode.IsUpper([]rune(n)[0]) {
          // note that reflect.ValueOf(*new(%s)) won't work with interfaces
          fmt.Printf("ProcessType(new(package_name.%s)),\n", n)
        }
      }
    }
  }
}

可能用例的完整示例:https://pastebin.com/ut0zNEAc(在线回复无效,但在本地有效)

  • go 1.11 dwarf debugging symbols增加运行时类型信息后,可通过此地址获取运行时类型
  • DW_AT_go_runtime_type
  • gort可以看到更多内容
package main

import (
    "debug/dwarf"
    "fmt"
    "log"
    "os"
    "reflect"
    "runtime"
    "unsafe"

    "github.com/go-delve/delve/pkg/dwarf/godwarf"
    "github.com/go-delve/delve/pkg/proc"
)

func main() {
    path, err := os.Executable()
    if err != nil {
        log.Fatalln(err)
    }

    bi := proc.NewBinaryInfo(runtime.GOOS, runtime.GOARCH)
    err = bi.LoadBinaryInfo(path, 0, nil)
    if err != nil {
        log.Fatalln(err)
    }
    mds, err := loadModuleData(bi, new(localMemory))
    if err != nil {
        log.Fatalln(err)
    }

    types, err := bi.Types()
    if err != nil {
        log.Fatalln(err)
    }

    for _, name := range types {
        dwarfType, err := findType(bi, name)
        if err != nil {
            continue
        }

        typeAddr, err := dwarfToRuntimeType(bi, mds, dwarfType, name)
        if err != nil {
            continue
        }

        typ := reflect.TypeOf(*(*interface{})(unsafe.Pointer(&typeAddr)))
        log.Printf("load type name:%s type:%s\n", name, typ)
    }
}

// delve counterpart to runtime.moduledata
type moduleData struct {
    text, etext   uint64
    types, etypes uint64
    typemapVar    *proc.Variable
}

//go:linkname findType github.com/go-delve/delve/pkg/proc.(*BinaryInfo).findType
func findType(bi *proc.BinaryInfo, name string) (godwarf.Type, error)

//go:linkname loadModuleData github.com/go-delve/delve/pkg/proc.loadModuleData
func loadModuleData(bi *proc.BinaryInfo, mem proc.MemoryReadWriter) ([]moduleData, error)

//go:linkname imageToModuleData github.com/go-delve/delve/pkg/proc.(*BinaryInfo).imageToModuleData
func imageToModuleData(bi *proc.BinaryInfo, image *proc.Image, mds []moduleData) *moduleData

type localMemory int

func (mem *localMemory) ReadMemory(data []byte, addr uint64) (int, error) {
    buf := *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{Data: uintptr(addr), Len: len(data), Cap: len(data)}))
    copy(data, buf)
    return len(data), nil
}

func (mem *localMemory) WriteMemory(addr uint64, data []byte) (int, error) {
    return 0, fmt.Errorf("not support")
}

func dwarfToRuntimeType(bi *proc.BinaryInfo, mds []moduleData, typ godwarf.Type, name string) (typeAddr uint64, err error) {
    if typ.Common().Index >= len(bi.Images) {
        return 0, fmt.Errorf("could not find image for type %s", name)
    }
    img := bi.Images[typ.Common().Index]
    rdr := img.DwarfReader()
    rdr.Seek(typ.Common().Offset)
    e, err := rdr.Next()
    if err != nil {
        return 0, fmt.Errorf("could not find dwarf entry for type:%s err:%s", name, err)
    }
    entryName, ok := e.Val(dwarf.AttrName).(string)
    if !ok || entryName != name {
        return 0, fmt.Errorf("could not find name for type:%s entry:%s", name, entryName)
    }
    off, ok := e.Val(godwarf.AttrGoRuntimeType).(uint64)
    if !ok || off == 0 {
        return 0, fmt.Errorf("could not find runtime type for type:%s", name)
    }

    md := imageToModuleData(bi, img, mds)
    if md == nil {
        return 0, fmt.Errorf("could not find module data for type %s", name)
    }

    typeAddr = md.types + off
    if typeAddr < md.types || typeAddr >= md.etypes {
        return off, nil
    }
    return typeAddr, nil
}