Golang_Reflect了解学习

GolangReflect

小编是一个Java程序员,近来自学了一下golang这门语言。(秉持一个理念,语言无国界,万事皆可盘)
本文介绍主要介绍golang的映射关系部分。该部分为基础go的映射关系入门讲解。我看很多资料或教程上都将这一部分省略掉了,今天就专门在官网上学习了一下。小编也是刚入门,有所不足的地方还望大佬多多指教。

一 简介

计算中的反射是指程序检查自身结构的能力,特别是通过类型;它是元编程的一种形式。

在本文中,我们试图通过解释反射是如何在Go中工作。每种语言的反射模型都是不同的(许多语言根本不支持它),所以本文中的反射,我们称之为 “GolangReflect”。

二 接口类型

1,类型简要

因为反射建立在类型系统上,所以让我们先回顾一下Go中的类型。
Go是静态类型的。每个变量都有一个静态类型,也就是说,只有一种类型是已知的,在编译时是固定的:int、float32、*MyType、[]byte,等等。
例如声明:

type MyInt int

var i int
var j MyInt

这里的i是int类型,j是MyInt类型。变量i和j具有不同的静态类型,而且,尽管它们具有相同的底层类型,但在不进行转换的情况下,它们不能相互赋值。

2,接口类别

类型的一个重要类别是接口类别,它表示固定的方法集。一旦被实现,接口变量可以存储任何具体的(非接口)值。例如我们熟悉的io.Reader和io.Writer.(io包中的Reader和Writer类型)

// Reader&Writer是封装基本读取方法的接口。
type Reader interface {
    
    
    Read(p []byte) (n int, err error)
}
type Writer interface {
    
    
    Write(p []byte) (n int, err error)
}

使用该署名方式实现reade或write的类型都被称为实现io,而这意味着io.Reader的变量类型可以保存任何具有读方法的值。

var r io.Reader
r = os.Stdin
r = bufio.NewReader(r)
r = new(bytes.Buffer)
// 诸如此类

如上,我们得出,无论r的具体值是什么,r的类型总是io.Reader。(GO是静态类型的,r的静态类型是io.Reader)

3,空接口

接口类型的一个非常重要的例子是空接口:

interface{
    
    }

它表示空的方法集,任何值都可以满足它,因为任何值都有零个或多个方法
有些人说Go的接口是动态类型的,但这是误导。它们是静态类型的:接口类型的变量总是具有相同的静态类型,即使在运行时存储在接口变量中的值可能改变类型,该值也总是满足接口的要求。
(之所以讲这些,是因为在go中,反射和接口是密切相关。)

三 接口的表现

1 ,表现1

接口类型的变量一般存储一对值,一是赋给变量的具体值,二是该值的类型描述。(值是实现接口的底层具体数据项,而类型描述该项的完整类型。)
例如:

var r io.Reader
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
    
    
    return nil, err
}
r = tty

此处r包含(值,类型)即(tty, *os.File)。

2 ,表现2

(类型*os.File实现了Read以外的方法;)尽管接口值只提供对Read方法的访问,但里面的值携带关于该值的所有类型信息,所以会有下面的调用:

var w io.Writer
w = r.(io.Writer)

此赋值中的表达式是类型断言,它断言的是r里面的项也实现了io。赋值后,w将包含这对文件(tty, *os.File)。这与r中保存的对相同。接口的静态类型决定了可以用接口变量调用哪些方法,即使其中的具体值可能有更大的方法集

3,表现3

依照上述,我们可以继续将w 放到接口中

var empty interface{
    
    }
empty = w

我们的空接口值empty将再次包含相同的对(tty, *os.File)。这很方便:空接口可以保存任何值,并包含我们可能需要的关于该值的所有信息。(个人感觉类似于Java中的继承关系,继承一个类,同样便获得了类中的可能需要的所有方法,值等信息)

(这里注意:接口内的对总是具有形式(值,具体类型),而不能具有形式(值,接口类型)。接口不包含接口值)

四 反射定律

1 ,反射从接口值到反射对象

在基本层次上,反射只是一种检查存储在接口变量中的类型和值对的机制。首先,在package reflect中我们需要知道两种类型:类型和值。这两种类型允许访问接口变量的内容,以及两个名为reflect.TypeOf &&reflect.ValueOf的简单函数。从接口值中可以通过这两个函数取到对应的值。(通过reflect.value可以很容易得到reflect.type)
例如:

TypeOf:
package main

import (
    "fmt"
    "reflect"
)

func main() {
    
    
    var x float64 = 3.4
    fmt.Println("type:", reflect.TypeOf(x))
}

结果为:

type: float64

你可能想知道这个接口在哪里,因为程序看起来像是在传递float64变量x,而不是接口值给reflect.TypeOf。但它的确存在。 TypeOf包含一个空接口:

// TypeOf返回接口{}中值的反射类型
func TypeOf(i interface{
    
    }) Type

当调用reflect.TypeOf(x)时,x首先存储在一个空接口中,然后作为实参传递; reflect.TypeOf解包该空接口以恢复类型信息。

ValueOf:
var x float64 = 3.4
fmt.Println("value:", reflect.ValueOf(x).String())

输出:

value: <float64 Value>

(我们显式地调用String方法,因为默认情况下fmt包会挖掘反射。价值是指内在的具体值。String方法没有)

在reflect.type和reflect.value中有很多我们可以使用的方法。例如:

① Value有关于返回reflect.value的type方法。
② Type和Value都有一个Kind方法,该方法返回一个常量,指示存储哪种类型的项(Uint, Float64, Slice等项)。
③ 名称为Int和Float的Value方法让我们获取存储在内部的值(如int64和float64):
var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())
fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
fmt.Println("value:", v.Float())

//输出:
type: float64
kind is float64: true
value: 3.4

(上述方法,也有像SetIntSetFloat这样的方法但要使用它们,我们需要理解可设置性。)
反射库有几个值得单独指出的属性。首先,为了保持API的简单性,Value的“getter”和“setter”方法操作的是能够保存值的最大类型。例如,对于所有有符号的整数,都是int64。也就是说,Value的Int方法返回一个int64,而SetInt值接受一个int64;可能需要将其转换为实际涉及的类型。
例如:

var x uint8 = 'x'
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())                            // uint8.
fmt.Println("kind is uint8: ", v.Kind() == reflect.Uint8) // true.
x = uint8(v.Uint())                                       // v.Uint returns  a uint64.

第二个属性是反射对象的类型描述了底层类型,而不是静态类型。如果反射对象包含用户定义的整数类型的值,

type MyInt int
var x MyInt = 7
v := reflect.ValueOf(x)

v的类型仍然会reflect.Int,尽管x的静态类型是MyInt,而不是Int。换句话说,尽管类型可以区分Int和MyInt,但类不能区分它们 "。

2, 反射从反射对象到接口值。

*类似于物理反射一样,Go反射也会生成自己的逆个函数。

给定一个reflect.value,我们可以通过Interface 方法恢复接口值。该方法将类型和值信息打包回接口表示中,并返回结果

// 将v的值作为接口{}返回。
func (v Value) Interface() interface{
    
    }

y := v.Interface().(float64) // y will have type float64.
fmt.Println(y)

输出反射对象v所代表的float64值。

Fmt.Println、fmt.Printf等的参数都作为空接口值传递,然后由fmt包在内部解包,就像我们在前面的示例中所做的那样。因此,要正确打印reflect.Value的内容,只需将接口方法的结果传递给格式化的打印例程

fmt.Println(v.Interface())

为什么不fmt.Println (v) ?因为v是一个reflect.value;我们想要它所持有的具体值。)由于我们的值是一个float64,如果我们想的话,我们甚至可以使用浮点格式

fmt.Printf("value is %7.1e\n", v.Interface())
//输出3.4e+00

同样,没有必要将v.Interface()的结果类型断言为float64;空接口值包含具体值的类型信息,Printf将恢复它。简单来说print已经为我们做好了自动解包的操作。
(*反射从接口值到反射对象,然后再返回)

3,若要修改反射对象,该值必须是可设置的。

例如

var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1) // Error: will panic.

该处会报

panic: reflect.Value.SetFloat using unaddressable value

问题不在于值7.1无法寻址;v是不可设定的。可设置性是反射值的一个属性,并不是所有的反射值都具有该属性。值的CanSet方法可以告诉我们一个值的可设置性;

var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("settability of v:", v.CanSet())

输出:

settability of v: false

所以我们可以得出,对不可set的反射值赋值会造成失败。
可设置性有点像寻址性,但比寻址更严格。反射对象可以修改用于创建反射对象的实际存储的属性。可设置性取决于反射对象是否持有原始项。(这里只能算作一个副本,不能算原有项,所以被定义为非法
例如:当我们说

var x float64 = 3.4
v := reflect.ValueOf(x)

我们传递一个x的副本来反射。因此创建的接口值作为要反映的参数。ValueOf是x的副本,而不是x本身。因此,if语句

v.SetFloat(7.1)

如果success,它将不会更新x,即使v看起来像是从x创建的。相反,它会更新存储在反射值中的x的副本,而x本身不会受到影响。这将是令人困惑和无用的,所以它是非法的,可设置性是用来避免这个问题的属性

也许上述看起来比较奇怪,其实这是一种特殊场景

f(x)

我们不期望f能够修改x,因为我们传递的是x值的副本,而不是x本身。如果想让f直接修改x,就必须向函数传递x的地址(即指向x的指针):

f(&x)

如果我们想通过反射来修改x,我们必须给反射库一个指向我们想要修改的值的指针。接下来让我们动手演示一下。首先,我们像往常一样初始化x,然后创建一个指向它的反射值,称为p。

var x float64 = 3.4
p := reflect.ValueOf(&x) // Note: take the address of x.
fmt.Println("type of p:", p.Type())
fmt.Println("settability of p:", p.CanSet())

输出:

type of p: *float64
settability of p: false

反射对象p是不可设置的,但它不是我们想要设置的p,它(实际上)是*p。为了得到p指向什么,我们调用值的Elem方法,该方法通过指针向内定向,并将结果保存为反射值v:

v := p.Elem()
fmt.Println("settability of v:", v.CanSet())

现在v是一个可设置的反射对象,正如输出所示,

settability of v: true

因为它代表x,所以我们最终可以使用v.SetFloat来修改x的值:

v.SetFloat(7.1)
fmt.Println(v.Interface())
fmt.Println(x)

输出:

7.1
7.1

(*反射可能很难理解,但它所做的正是语言所做的,尽管反射类型和值可以掩盖正在发生的事情。请记住,反射值要修改它们所表示的内容,就需要某个对象的地址即指针。)

之前例子中,v本身并不是一个指针,它只是从一个指针派生出来的。出现这种情况的常见方法是使用反射修改结构的字段。只要我们有结构的地址,我们就可以修改它的字段。

五 结构体

下面是一个分析struct值t的简单示例。我们使用该struct的地址创建反射对象。因为我们在正真项目中不可能一个一个去映射,而结构体可以比较方便便历字段。(可将typeOfT设置为它的类型,并使用简单的方法调用遍历字段。注意,我们从结构类型中提取了字段的名称,但字段本身是常规refelect.value对象)。

例如:

type T struct {
    
    
    A int
    B string
}
t := T{
    
    23, "skidoo"}
s := reflect.ValueOf(&t).Elem()
typeOfT := s.Type()
for i := 0; i < s.NumField(); i++ {
    
    
    f := s.Field(i)
    fmt.Printf("%d: %s %s = %v\n", i,
        typeOfT.Field(i).Name, f.Type(), f.Interface())
}

输出:

0: A int = 23
1: B string = skidoo

(这里传入了关于可设置性的另一点:T的字段名是大写的(导出),因为只有导出的struct字段是可设置的。)

因为s包含一个可设置的反射对象,所以我们可以修改结构的字段

s.Field(0).SetInt(77)
s.Field(1).SetString("Sunset Strip")
fmt.Println("t is now", t)

输出:

t is now {
    
    77 Sunset Strip}

(如果我们修改程序,使s是从t创建的,而不是从&t创建的,那么对SetInt和SetString的调用将失败,因为t的字段将无法设置)

上述是一个简单实例,具体可参考官方package reflect文档。(https://golang.org/pkg/reflect/#TypeOf)

六 总结

反射定律:

• 反射从界面值到反射对象。
• 反射从反射对象到接口值。
• 若要修改反射对象,该值必须是可设置的

一旦你理解了这些规律,Golang中的反射就会变得更容易使用,尽管它仍然很微妙。这是一个强大的工具,应该小心使用,除非严格必要,否则应避免使用。关于反射,还有很多我们没有涉及到的内容,比如通道上的发送和接收、内存分配、使用片和映射、调用方法和函数等等。

具体情况和用法因人而异,个人感觉反射入门还是比较好理解的。简单来说就是反射对象和反射值之间的关系,能够使我们在很多场合调用和修改起来更加方便,快捷,高效。

参考链接:https://blog.golang.org/laws-of-reflection

猜你喜欢

转载自blog.csdn.net/MatChen/article/details/112613983
今日推荐