欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

学习使用Go反射的用法示例

程序员文章站 2022-06-03 11:40:07
什么是反射 大多数时候,go中的变量,类型和函数非常简单直接。当需要一个类型、变量或者是函数时,可以直接定义它们: type foo struct { a int...

什么是反射

大多数时候,go中的变量,类型和函数非常简单直接。当需要一个类型、变量或者是函数时,可以直接定义它们:

type foo struct {
 a int
 b string
}

var x foo

func dosomething(f foo) {
 fmt.println(f.a, f.b)
}

但是有时你希望在运行时使用变量的在编写程序时还不存在的信息。比如你正在尝试将文件或网络请求中的数据映射到变量中。或者你想构建一个适用于不同类型的工具。在这种情况下,你需要使用反射。反射使您能够在运行时检查类型。它还允许您在运行时检查,修改和创建变量,函数和结构体。

go中的反射是基于三个概念构建的:类型,种类和值(types kinds values)。标准库中的reflect包提供了 go 反射的实现。

反射变量类型

首先让我们看一下类型。你可以使用反射来调用函数vartype := reflect.typeof(var)来获取变量var的类型。这将返回类型为reflect.type的变量,该变量具有获取定义时变量的类型的各种信息的方法集。下面我们来看一下常用的获取类型信息的方法。

我们要看的第一个方法是name()。这将返回变量类型的名称。某些类型(例如切片或指针)没有名称,此方法会返回空字符串。

下一个方法,也是我认为第一个真正非常有用的方法是kind()。type是由kind组成的---kind 是切片,映射,指针,结构,接口,字符串,数组,函数,int或其他某种原始类型的抽象表示。要理解type和kind之间的差异可能有些棘手,但是请你以这种方式来思考。如果定义一个名为foo的结构体,则kind为struct,类型为foo。

使用反射时要注意的一件事:反射包中的所有内容都假定你知道自己在做什么,并且如果使用不正确,许多函数和方法调用都会引起 panic。例如,如果你在reflect.type上调用与当前类型不同的类型关联的方法,您的代码将会panic。

如果变量是指针,映射,切片,通道或数组变量,则可以使用vartype.elem()找出指向或包含的值的类型。

如果变量是结构体,则可以使用反射来获取结构体中的字段数,并从每个字段上获取reflect.structfield结构体。 reflection.structfield为您提供了字段的名称,标号,类型和结构体标签。其中标签信息对应reflect.structtag类型的字符串,并且它提供了get方法用于解析和根据特定key提取标签信息中的子串。

下面是一个简单的示例,用于输出各种变量的类型信息:

type foo struct {
  a int `tag1:"first tag" tag2:"second tag"`
  b string
}

func main() {
  sl := []int{1, 2, 3}
  greeting := "hello"
  greetingptr := &greeting
  f := foo{a: 10, b: "salutations"}
  fp := &f

  sltype := reflect.typeof(sl)
  gtype := reflect.typeof(greeting)
  grptype := reflect.typeof(greetingptr)
  ftype := reflect.typeof(f)
  fptype := reflect.typeof(fp)

  examiner(sltype, 0)
  examiner(gtype, 0)
  examiner(grptype, 0)
  examiner(ftype, 0)
  examiner(fptype, 0)
}

func examiner(t reflect.type, depth int) {
  fmt.println(strings.repeat("\t", depth), "type is", t.name(), "and kind is", t.kind())
  switch t.kind() {
  case reflect.array, reflect.chan, reflect.map, reflect.ptr, reflect.slice:
    fmt.println(strings.repeat("\t", depth+1), "contained type:")
    examiner(t.elem(), depth+1)
  case reflect.struct:
    for i := 0; i < t.numfield(); i++ {
      f := t.field(i)
      fmt.println(strings.repeat("\t", depth+1), "field", i+1, "name is", f.name, "type is", f.type.name(), "and kind is", f.type.kind())
      if f.tag != "" {
        fmt.println(strings.repeat("\t", depth+2), "tag is", f.tag)
        fmt.println(strings.repeat("\t", depth+2), "tag1 is", f.tag.get("tag1"), "tag2 is", f.tag.get("tag2"))
      }
    }
  }
}

变量的类型输出如下:

type is and kind is slice
   contained type:
   type is int and kind is int
 type is string and kind is string
 type is and kind is ptr
   contained type:
   type is string and kind is string
 type is foo and kind is struct
   field 1 name is a type is int and kind is int
     tag is tag1:"first tag" tag2:"second tag"
     tag1 is first tag tag2 is second tag
   field 2 name is b type is string and kind is string
 type is and kind is ptr
   contained type:
   type is foo and kind is struct
     field 1 name is a type is int and kind is int
       tag is tag1:"first tag" tag2:"second tag"
       tag1 is first tag tag2 is second tag
     field 2 name is b type is string and kind is string

run in go playground: https://play.golang.org/p/lz97yauhxx

使用反射创建新实例

除了检查变量的类型外,还可以使用反射来读取,设置或创建值。首先,需要使用refval := reflect.valueof(var) 为变量创建一个reflect.value实例。如果希望能够使用反射来修改值,则必须使用refptrval := reflect.valueof(&var);获得指向变量的指针。如果不这样做,则可以使用反射来读取该值,但不能对其进行修改。

一旦有了reflect.value实例就可以使用type()方法获取变量的reflect.type。

如果要修改值,请记住它必须是一个指针,并且必须首先对其进行解引用。使用refptrval.elem().set(newrefval)来修改值,并且传递给set()的值也必须是reflect.value。

如果要创建一个新值,可以使用函数newptrval := reflect.new(vartype)来实现,并传入一个reflect.type。这将返回一个指针值,然后可以像上面那样使用elem().set()对其进行修改。

最后,你可以通过调用interface()方法从reflect.value回到普通变量值。由于go没有泛型,因此变量的原始类型会丢失;该方法返回类型为interface{}的值。如果创建了一个指针以便可以修改该值,则需要使用elem().interface()解引用反射的指针。在这两种情况下,都需要将空接口转换为实际类型才能使用它。

下面的代码来演示这些概念:

type foo struct {
  a int `tag1:"first tag" tag2:"second tag"`
  b string
}

func main() {
  greeting := "hello"
  f := foo{a: 10, b: "salutations"}

  gval := reflect.valueof(greeting)
  // not a pointer so all we can do is read it
  fmt.println(gval.interface())

  gpval := reflect.valueof(&greeting)
  // it's a pointer, so we can change it, and it changes the underlying variable
  gpval.elem().setstring("goodbye")
  fmt.println(greeting)

  ftype := reflect.typeof(f)
  fval := reflect.new(ftype)
  fval.elem().field(0).setint(20)
  fval.elem().field(1).setstring("greetings")
  f2 := fval.elem().interface().(foo)
  fmt.printf("%+v, %d, %s\n", f2, f2.a, f2.b)
}

他们的输出如下:

hello
goodbye
{a:20 b:greetings}, 20, greetings

run in go playground https://play.golang.org/p/pfceyfzqz8

反射创建引用类型的实例

除了生成内置类型和用户定义类型的实例之外,还可以使用反射来生成通常需要make函数的实例。可以使用reflect.makeslice,reflect.makemap和reflect.makechan函数制作切片,map或通道。在所有情况下,都提供一个reflect.type,然后获取一个reflect.value,可以使用反射对其进行操作,或者可以将其分配回一个标准变量。

func main() {
 // 定义变量
  intslice := make([]int, 0)
  mapstringint := make(map[string]int)

 // 获取变量的 reflect.type
  slicetype := reflect.typeof(intslice)
  maptype := reflect.typeof(mapstringint)

  // 使用反射创建类型的新实例
  intslicereflect := reflect.makeslice(slicetype, 0, 0)
  mapreflect := reflect.makemap(maptype)

  // 将创建的新实例分配回一个标准变量
  v := 10
  rv := reflect.valueof(v)
  intslicereflect = reflect.append(intslicereflect, rv)
  intslice2 := intslicereflect.interface().([]int)
  fmt.println(intslice2)

  k := "hello"
  rk := reflect.valueof(k)
  mapreflect.setmapindex(rk, rv)
  mapstringint2 := mapreflect.interface().(map[string]int)
  fmt.println(mapstringint2)
}

使用反射创建函数

反射不仅仅可以为存储数据创造新的地方。还可以使用reflect.makefunc函数使用reflect来创建新函数。该函数期望我们要创建的函数的reflect.type,以及一个闭包,其输入参数为[]reflect.value类型,其返回类型也为[] reflect.value类型。下面是一个简单的示例,它为传递给它的任何函数创建一个定时包装器:

func maketimedfunction(f interface{}) interface{} {
  rf := reflect.typeof(f)
  if rf.kind() != reflect.func {
    panic("expects a function")
  }
  vf := reflect.valueof(f)
  wrapperf := reflect.makefunc(rf, func(in []reflect.value) []reflect.value {
    start := time.now()
    out := vf.call(in)
    end := time.now()
    fmt.printf("calling %s took %v\n", runtime.funcforpc(vf.pointer()).name(), end.sub(start))
    return out
  })
  return wrapperf.interface()
}

func timeme() {
  fmt.println("starting")
  time.sleep(1 * time.second)
  fmt.println("ending")
}

func timemetoo(a int) int {
  fmt.println("starting")
  time.sleep(time.duration(a) * time.second)
  result := a * 2
  fmt.println("ending")
  return result
}

func main() {
  timed := maketimedfunction(timeme).(func())
  timed()
  timedtoo := maketimedfunction(timemetoo).(func(int) int)
  fmt.println(timedtoo(2))
}

你可以在goplayground运行代码https://play.golang.org/p/qz8ttfzzgx并看到输出如下:

starting
ending
calling main.timeme took 1s
starting
ending
calling main.timemetoo took 2s
4

反射是每个go开发人员都应了解并学会的强大工具。但是使用他们可以用来做什么呢?在下一篇博客文章中,我将探讨现有库中反射的一些用法,并使用反射来创建一些新的东西。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。