理解 Go 中的反射

Go 的反射特性增强了语言的表达能力,并已广泛用于许多 API 的实现。本文提供了关于反射的介绍性思想,并解释了在 Go 编程中如何处理反射。

Go 中的反射概述

我们可以在运行时更新变量并检查它们的值调用方法而不知道它们的类型的机制称为反射。反射暴露了实现的内部结构,使我们能够进行各种元编程。它提供了代码自省能力,我们不仅可以检查,还可以修改代码的内部工作。这种强大的能力应该负责任地使用,并且必须谨慎使用,因为它也很容易破坏语言提供的保护并损害其完整性。尽管如此,它还是有很多用途。

反射被标准 Go 库广泛使用,例如fmt提供的字符串格式,编码/JSONencoding/XML等包中的协议编码,实现text/templateHTML/template提供的模板机制。例如,fmt.Fprintf中的格式化逻辑是有效使用反射的常见示例。

该函数能够统一处理不支持通用接口的类型值,并且可以打印任何类型的任意值,甚至是用户定义的值。尽管反射和内省的概念几乎是同义词。许多编程语言声称支持反射,而实际上它们支持称为自省的东西。不同之处在于内省可以对程序中各种元素的属性进行低级检查。反思不仅可以做到这一点,而且还可以修改它们。因此,内省实际上是反思的一个子集。

在 Go 中反映类型和值

Go 中支持反射的包称为reflect。使用这个包中的 API,我们可以很容易地找出变量的类型。正如我们所知,任何值都与两个属性相关联:值和类型。值决定内容,类型决定内容类型。有两个重要的结构称为TypeValue,分别封装了变量的类型和值。有趣的是,我们可以从现有价值中创造任何一种。函数reflect.TypeOf()专门告诉我们值的类型并返回Type。例如:

a := 3.5
b := "hello"
c := 10
fmt.Printf("variable a : type=%v, value=%v\n", reflect.TypeOf(a), a)
fmt.Printf("variable b : type=%v, value=%v\n", reflect.TypeOf(b), b)
fmt.Printf("variable c : type=%v, value=%v\n", reflect.TypeOf(c), c)

这将产生以下输出:

输出
variable a : type=float64, value=3.5
variable b : type=string, value=hello
variable c : type=int, value=10

同样,有一个reflect.ValueOf()函数来调用 value 并返回reflect.Value

str := "greetings"
greet := reflect.ValueOf(str).String()
fmt.Println(greet)

这里的输出将是:

输出
greetings

请注意,reflect.Value是由reflect.ValueOf()函数返回的结构。因此,如果我们只想要变量的值,我们必须使用底层类型的特定提取方法,例如reflect.Value.Int()为整数,reflect.Value.Float()为浮点数,reflect.Value.Bool()为布尔值,reflect.Value.String()用于字符串,reflect.Value.Complex()用于复杂值等等。

Go 不支持在运行时创建新类型,但可以构造新值或修改现有值。函数reflect.TypeOf接受任何接口并将其动态类型返回为reflect.Type。有趣的是,当我们为接口类型分配一个具体的值时,它会隐式转换为它的类型,它最终有两个组成部分:动态类型和动态值。例如,如果我们分配一个具体的值(比如 3.56),那么对reflect.TypeOf()的调用实际上会将值分配给interface{}参数。

tt:=reflect.TypeOf(3.56)

有一个速记 %T 在内部使用reflect.TypeOf()并可用于打印类型。这对于调试和日志记录特别有用。

fmt.Printf("Type=%T\n", 3.56)

同样,reflect.ValueOf()也接受接口并返回reflect.Value作为其动态内容。与reflect.TypeOf() 不同,reflect.ValueOf()结果总是具体的,但reflect.Value可以包含接口值。一个简单的例子如下:

vv := reflect.ValueOf(3.56)
fmt.Printf("Value=%v", vv)

速记 %v 可用于打印调用reflect.ValueOf()返回的reflect.Value的内容。

使用 Go 进行集合类型的反射

reflect包以类似的方式与集合类型(如切片和映射)一起工作。它也适用于结构。此功能广泛用于JSONXML编码器和解码器。例如,对于结构,它的工作方式如下:

package main
import (
  "fmt"
  "reflect"
)
func main() {
  type Employee struct {
    id int "check:range(1,999)"
    name string "check:len(3,20)"
  }
  emp1 := Employee{111, "Jerry"}
  sType := reflect.TypeOf(emp1)
  if sField, ok := sType.FieldByName("name"); ok {
    fmt.Printf("Type=%q field name=%q tag=%q\n", sField.Type, sField.Name, sField.Tag)
  }
}

这给了我们以下输出:

输出
Type="string" field name="name" tag="check:len(3,20)"

使用切片,我们可以使用自省替换 []string 中的给定项目,如下所示。通常,我们可以通过在特定索引位置直接分配更改的值来做到这一点。但通过反省,它看起来像这样:

weekdays := []string{"Sunday", "Monday", "Tuesday", "HOLIDAY", "Thursday", "Friday", "Saturday"}
sv := reflect.ValueOf(weekdays)
v := sv.Index(3)
fmt.Println("Before change: ", weekdays)
v.SetString("Wednesday")
fmt.Println("After change: ", weekdays)

再一次,这里是输出:

输出
Before change: [Sunday Monday Tuesday HOLIDAY Thursday Friday Saturday]
After change: [Sunday Monday Tuesday Wednesday Thursday Friday Saturday]

请注意,Go 字符串是不可变的。因此,值本身无法更改,但只要我们有原始值的地址,我们就可以轻松地将不可变值与另一个值交换。

使用函数和方法进行反射

我们可以使用反射调用任意函数和方法。这是一个示例,其中有一个名为IsPalindrome的函数,该函数检查字符串值是否为回文并返回一个布尔值。此外,还有另一个名为ReverseString的函数,它只是简单地反转字符串。在这里,我们在IsPalindrome函数中使用了ReverseString函数,我们将原始字符串与其反向字符串进行了比较。如果它们相同,则该字符串是回文。

package main

import (
  "fmt"
  "reflect"
)

func ReverseString(s string) string {
  r := []rune(s)
  for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 {
    r[i], r[j] = r[j], r[i]
  }
  return string(r)
}

func IsPalindrome(s string) bool {
  flag := false
  if s == ReverseString(s) {
    flag = true
  }
  return flag
}

func main() {
  str := "madam"
  ret := IsPalindrome(str)
  fmt.Printf("Is '%s' Palindrome :%v\n", str, ret)

  fv := reflect.ValueOf(IsPalindrome)
  values := fv.Call([]reflect.Value{reflect.ValueOf(str)})
  ret = values[0].Bool()
  fmt.Printf("Is '%s' Palindrome :%v\n", str, ret)
}

这会产生以下输出:

输出
Is 'madam' Palindrome :true
Is 'madam' Palindrome :true

请注意,这里我们调用了两次IsPalindrome函数。第一次调用是普通的函数调用,但在第二次中,我们使用了反射来调用函数。尽管reflect.Value.Call返回了[]reflect.Value的一部分,但在这里我们传递了一个值,因此得到了一个结果。

关于 Go 中反射的最终想法

反射是一种出路的常见情况是我们希望统一处理没有公共接口的类型的值,或者没有办法统一表示它。fmt.Fprintf中的格式化逻辑尤其如此. 这个有用的函数可以打印任何类型的任意值。此功能使用反射使其能够处理这种情况。Go 中的反射 API 还有很多。在这里,我们只是划伤了表面,让我们一睹它的风采。反射无疑是一种强大的机制,但应谨慎使用。由于反射在运行时起作用,编译器不会捕捉到这里的错误,运行时错误可能是一场灾难。此外,反射降低了自动重构和分析工具的安全性和准确性。事实上,有足够的理由不将反射用作孩子手中的新玩具,而是由经验丰富的程序员负责任地玩。