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

25
据我所知(请参见这里这里),在reflect包中没有类型发现机制,该包期望您已经拥有要检查的类型或值的实例。

是否有其他方法可以发现正在运行的Go包中所有导出类型(特别是结构体)?

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

import "time"
import "fmt"

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

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

顺便提一句,在我的使用情况下,识别类型的注册函数不是有效的方法。


无论你认为这是个好主意还是不好,我想要这种能力的原因在于(因为我知道你会问):

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


我能想到的唯一方法就是生成调用init()中注册函数的代码。 - THUNDERGROOVE
是的 @THUNDERGROOVE,基本上我已经实现了这个功能。但我不想生成代码。我宁愿运行 go test 并让库发现所有类型,实例化它们,然后调用它们。所有这些都在一个进程执行中完成。这是一个非常棘手的问题。 - Michael Whatcott
8个回答

阿里云服务器只需要99元/年,新老用户同享,点击查看详情
33

在 Go 1.5 中,你可以使用新的包 typesimporter 来检查二进制和源代码包。例如:

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来提取所有已安装的包。或者你可以配置Lookup导入器以检查环境外的二进制文件。

在1.5之前,唯一不需要hacky的方法是使用ast包编译源代码。


哇,我不知道这个新功能存在。时间真是太好了!现在来一个重要的问题:在获取pkg.Scope().Names()之后,我能否以某种方式连接到reflect包,实例化代表类型的Names()的新实例? - Michael Whatcott
1
看起来你正在寻找插件架构。目前,它在Go 1.5中尚未实现。阅读https://docs.google.com/document/d/1nr-TQHw_er6GOQRsF6T43GGhFDelrAP0NqSS_00RgZQ/edit#获取更多信息。 - Alvivi
@vda8888 这是因为playground运行在沙盒环境中。 “playground可以使用大多数标准库,但有一些例外。playground程序与外部世界的唯一通信方式是通过写入标准输出和标准错误。” 此示例仍在使用go 1.9的标准环境中工作。 - Alvivi
@Alvivi 这是否意味着,如果我只在运行时拥有内置二进制文件的访问权限(即没有访问Golang源代码或我的源代码),那么这将无法工作? - vda8888
1
这适用于本地包吗? - Vivere
显示剩余4条评论

19

(见2019年更新底部)

警告:未经测试且不可靠。可能会在每次发布新版本的Go时失效。

通过对Go运行时进行一些小小的hack,可以获取所有类型的运行时信息。在您自己的包中包含一个小型汇编文件,其中包含以下内容:

TEXT yourpackage·typelinks(SB), NOSPLIT, $0-0
    JMP reflect·typelinks(SB)
在你的包中,声明函数原型(不包含主体):

yourpackage 中,声明函数原型 (不包含主体):

func typelinks() []*typeDefDummy

在类型定义旁边:

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

那么只需调用typelinks方法,迭代该切片并读取每个StrPtr的名称。查找以yourpackage开头的内容。请注意,如果存在两个名为yourpackage的包位于不同路径中,此方法将不能明确地工作。

我是否可以以某种方式连接reflect包以实例化那些名称的新实例?

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

t := reflect.TypeOf(*(*interface{})(unsafe.Pointer(&d)))
现在,t 是一个 reflect.Type 值,您可以使用它来实例化 reflect.Value

编辑:我已经测试并成功执行了此代码,并将其作为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), $0-0
    JMP reflect·typelinks(SB)

TEXT tl·add(SB), $0-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)
        }
    }
}

愉快的黑客行动!


1
那是个不错的东西;-) - Volker
1
@thwd,这个答案 可能 是我已经寻找了几周的一切!现在是2019年,还有其他更新吗?此外,出于兴趣,如果我正在MacOS上构建它,那么zasm_linux_amd64.h将不存在 - 我要找什么替代方法? - Jimbo
1
@Jimbo 我修改了答案 ;) - thwd
1
@Jimbo 抱歉,我没有注意到的另一个更改是现在它们只包括指针、通道、映射、切片和数组类型在 typelinks 中。这在 src/reflect/type.go 中有记录 :( - thwd
1
嘿!你也可以使用这个代替汇编语言://go:linkname reflect_typelinks reflect.typelinks func reflect_typelinks() ([]unsafe.Pointer, [][]int32) - Sergey Kamardin
显示剩余11条评论

3

使用Go 1.18更新

在Go 1.18中,原先被接受的答案不再适用,但我可以改用go:linkname来实现它。通过使用该指令和unsafe包,这些内部函数现在可以在没有任何额外汇编代码的情况下访问。

package main

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

//go:linkname typelinks reflect.typelinks
func typelinks() (sections []unsafe.Pointer, offset [][]int32)

//go:linkname add reflect.add
func add(p unsafe.Pointer, x uintptr, whySafe string) unsafe.Pointer

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

2
很遗憾,我认为这是不可能的。在Go语言中,包并非“可执行”的,您无法对其“调用函数”。您也无法对类型调用函数,但可以在类型实例上调用reflect.TypeOf并获取reflect.Type,它是一种类型的运行时抽象。对于包,就没有这样的机制,也没有reflect.Package。 话虽如此,您可以提交问题关于缺少(以及添加)reflect.PackageOf等的实用性。

1
是的,我仍然可以打开一个问题,因为拥有类似于reflect.PackageOf("path").Types()这样的东西真的很好。 - Michael Whatcott

2

感谢 @thwd 和 @icio 的帮助,在他们的指导下,今天在1.13.6上仍然有效。

按照你们的方法,tl.s 将会是:

TEXT ·typelinks(SB), $0-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)
}
那么现在一切都正常了。 :)

1

适用于go 1.16版本(已测试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(在线 REPL 中不起作用,但在本地起作用)


有趣的方法!最初的希望是能够通过单个方法调用(而不必诉诸任何汇编黑科技)为包中的每种类型获取实际的reflect.Type实例。我怀疑reflect包将永远不会提供这样的实用程序。 - Michael Whatcott

0
在Go1.11中,添加了Dwarf调试符号的运行时类型信息,您可以使用此地址获得运行时类型。DW_AT_go_runtime_typegort 您可以查看更多内容。
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
}

-2

没有这样的功能。

如果您想要“知道”类型,您需要进行注册。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,