Go语言——反射
文章目录
- 一、reflect-反射-浅析-重要
- Go语言中的类型
- 反射第一定律:反射可以将“接口类型变量”转换为“反射类型对象”
- 反射第二定律:反射可以将“反射类型对象”转换为“接口类型变量”
- 反射第三定律:如果要修改“反射类型对象”其值必须是“可写的”
- 结构体
- 总结
- 二、reflect-反射类型对象-获取类型信息
- 1. TypeOf()和reflect.Type类型
- 1.1 理解反射的类型(Type)与种类(Kind)
- 1.1.1 反射种类(Kind)的定义
- 1.1.2 从类型对象中获取类型名称和种类的例子
- 1.2. reflect.Elem()
- 1.3. 通过反射类型对象获取结构体的成员类型
- 1.3.1 结构体字段类型(StructField )
- 1.3.2 获取成员反射信息
- 1.4 通过反射类型对象创建实例
- reflect-反射值对象-获取值信息
- 2. reflect.ValueOf()和reflect.Value类型
- 2.1 使用反射值对象<包装>任意值
- 2.2 从反射值对象<获取>被包装的值
- 2.2.1 从反射值对象(reflect.Value)中获取值的方法
- 2.2.2 从反射值对象(reflect.Value)中获取值的例子
- 2.3 通过反射值对象访问结构体成员的值
- 2.4 判断反射值对象的空和有效性
- 2.4.1 reflect.IsNil()和reflect.IsValid()
- 2.5通过反射值对象修改变量的值
- 判定及获取元素的相关方法
- 反射值对象的值修改相关方法
- 值可修改条件之一:可被寻址
- 值可修改条件之一:被导出
- 反射调用函数
- inject库:依赖注入
- inject 实践
- inject 原理分析
- 概述
- injector,InterfaceOf(),New()
- Map(),MapTo(),Get(),SetParent()
- Invoke()
- Apply()
- 拓展:inject 官方文档
- 用法
- func InterfaceOf
- type Applicator
- type Injector
- func New
- type Invoker
- type TypeMapper
一、reflect-反射-浅析-重要
反射是众多编程语言中的一个非常实用的功能,它是一种能够自描述、自控制的应用,Go语言也对反射提供了友好的支持。
Go语言中使用反射可以在编译时不知道类型的情况下更新变量,在运行时查看值、调用方法以及直接对他们的布局进行操作。
由于反射是建立在类型系统(type system)上的,所以我们先来复习一下Go语言中的类型。
Go语言中的类型
Go语言是一门静态类型的语言,每个变量都有一个静态类型,类型在编译的时候确定下来。
type MyInt intvar i int
var j MyInt
变量 i 的类型是 int,变量 j 的类型是 MyInt,虽然它们有着相同的基本类型,但静态类型却不一样,在没有类型转换的情况下,它们之间无法互相赋值。
接口是一个重要的类型,它意味着一个确定的方法集合,一个接口变量可以存储任何实现了接口的方法的具体值(除了接口本身),例如 io.Reader 和 io.Writer:
// Reader是包装基本Read方法的接口。
type Reader interface {Read(p []byte) (n int, err error)
}// Writer是包装基本Write方法的接口。
type Writer interface {Write(p []byte) (n int, err error)
}
如果一个类型声明实现了 Reader(或 Writer)方法,那么它便实现了 io.Reader(或 io.Writer),这意味着一个 io.Reader 的变量可以持有任何一个实现了 Read 方法的的类型的值。
// 例一:
var r io.Reader
r = os.Stdin
fmt.Printf("%T\n",r) // *os.File
r = bufio.NewReader(r)
fmt.Printf("%T\n",r) // *bufio.Reader
r = new(bytes.Buffer)
fmt.Printf("%T\n",r) // *bytes.Buffer
// 三者都是实现了io.Reader接口的结构体
必须要弄清楚的一点是,不管变量 r 中的具体值是什么,r 的类型永远是 io.Reader,由于Go语言是静态类型的,r 的静态类型就是 io.Reader。
// 例二:
type person interface {speak() string
}
type student struct {name string
}
func (h student) speak() string {return h.name
}
func main() {var per personfmt.Printf("%T\n",per) // <nil>per = student{name: "胡宇洋"}fmt.Printf("%T\n",per) // main.student
}
可以看到student
结构体实现了person
接口,per的具体值是一个student结构体,但由于GO语言是静态类型的,per的静态类型就是person
。
如果不知道咋回事(我就不知道)就先略过,这个需要看interface深度解析,里面涉及汇编等东西,在以后有时间可以看一下。
在接口类型中有一个极为重要的例子——空接口:
interface{}
它表示了一个空的方法集,一切值都可以满足它,因为它们都有零值或方法。
有人说Go语言的接口是动态类型,这是错误的,它们都是静态类型,虽然在运行时中,接口变量存储的值也许会变,但接口变量的类型是不会变的。
我们必须精确地了解这些,因为反射与接口是密切相关的。
关于接口我们就介绍到这里,下面我们看看Go语言的反射三定律。
反射第一定律:反射可以将“接口类型变量”转换为“反射类型对象”
即:
TypeOf(i interface{})
和ValueOf(i interface{})
方法返回反射类型:reflect.Type类型和reflect.Value类型的变量
注:这里反射类型指
reflect.Type
和reflect.Value。
从使用方法上来讲,反射提供了一种机制,允许程序在运行时检查接口变量内部存储的 (value, type) 对。
在最开始,我们先了解下 reflect 包的两种类型 Type 和 Value,这两种类型使访问接口内的数据成为可能,它们对应两个简单的方法,分别是 reflect.TypeOf 和 reflect.ValueOf,分别用来读取接口变量的 reflect.Type 和 reflect.Value 部分。
当然,从 reflect.Value 也很容易获取到 reflect.Type,目前我们先将它们分开。
首先,我们下看 reflect.TypeOf:
func main() {var x float64 = 3.4 fmt.Println("type:", reflect.TypeOf(x))
}
运行结果如下
type: float64
大家可能会疑惑,为什么没看到接口?
这段代码看起来只是把一个 float64 类型的变量 x 传递给 reflect.TypeOf 并没有传递接口。
其实在 reflect.TypeOf 的函数签名里包含一个空接口:
// TypeOf returns the reflection Type of the value in the interface{}.
func TypeOf(i interface{}) Type
我们调用 reflect.TypeOf(x) 时,x 被存储在一个空接口变量中被传递过去,然后 reflect.TypeOf 对空接口变量进行拆解,恢复其类型信息。
函数 reflect.ValueOf 也会对底层的值进行恢复:
func main() {var x float64 = 3.4 fmt.Println("value:", reflect.ValueOf(x))
}
运行结果如下:
value: 3.4
类型reflect.Type
和reflect.Value
都有很多方法,我们可以检查和使用它们,这里我们举几个例子。
类型reflect.Value
有一个方法Type()
,它会返回一个reflect.Type
类型的对象。
Type 和 Value 都有一个名为Kind ()
的方法,它会返回一个常量,表示底层数据的类型,常见值有:Uint、Float64、Slice 等。
Value 类型也有一些类似于 Int、Float 的方法,用来提取底层的数据:
- Int 方法用来提取 int64
- Float 方法用来提取 float64,示例代码如下:
func main() {var x float64 = 3.4v := reflect.ValueOf(x) // 返回一个reflect.Value 类型的对象fmt.Println("type:", v.Type()) // v.Type返回一个reflect.Type类型的对象fmt.Println("kind is float64:", v.Kind() == reflect.Float64)// kind()返回一个常量,表示底层数据类型fmt.Println("value:", v.Float()) // 提取底层的数据
}
运行结果如下:
type: float64
kind is float64: true
value: 3.4
还有一些用来修改数据的方法,比如 SetInt、SetFloat。
在介绍它们之前,我们要先理解“可修性”(settability),这一特性会在下面进行详细说明。
反射库提供了很多值得列出来单独讨论的属性,下面就来介绍一下。
首先是介绍下 reflect.Value类型 的 getter 和 setter 方法,为了保证 API 的精简,这两个方法操作的是某一组类型范围最大的那个。比如,处理任何含符号整型数,都使用 int64,也就是说 Value 类型的 Int 方法返回值为 int64 类型,SetInt 方法接收的参数类型也是 int64 类型。实际使用时,可能需要转化为实际的类型:
func main() { 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()返回的是uint64类型.
}
运行结果如下:
type: uint8
kind is uint8: true
其次,反射对象的 Kind()
方法描述的是基础类型,而不是静态类型。如果一个反射对象包含了用户定义类型的值,如下所示:
func main() {type MyInt intvar x MyInt = 7v := reflect.ValueOf(x)fmt.Println(v.Kind()) // intfmt.Println(v.Type()) // main.MyInt
}
上面的代码中,虽然变量 v 的静态类型是 MyInt,而不是 int,但 Kind 方法仍然会返回 reflect.Int。
换句话说 Kind ()方法不会像 Type() 方法一样区分 MyInt 和 int。
反射第二定律:反射可以将“反射类型对象”转换为“接口类型变量”
和物理学中的反射类似,Go语言中的反射也能创造自己反面类型的对象。
根据一个reflect.Value
类型的变量,我们可以使用 Interface 方法恢复其接口类型的值。事实上,这个方法会把 type 和 value 信息打包并填充到一个接口变量中,然后返回。
其函数声明如下:
// Interface returns v's value as an interface{}.
// 相当于:var i interface{}=(v的底层值)。
func (v Value) Interface() interface{}
然后,我们可以通过断言,恢复底层的具体值:
func main() {var x float64 = 12.1v:=reflect.ValueOf(x)y,ok:= v.Interface().(float64) // y will have type float64.fmt.Println(ok,y)// true 12.1
}
上面这段代码会打印出一个 float64 类型的值,也就是反射类型变量 v 所代表的值。
事实上,我们可以更好地利用这一特性,标准库中的 fmt.Println 和 fmt.Printf 等函数都接收空接口变量作为参数,fmt 包内部会对接口变量进行拆包,因此 fmt 包的打印函数在打印 reflect.Value 类型变量的数据时,只需要把 Interface 方法的结果传给格式化打印程序:
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() 的结果进行类型断言,空接口值内部包含了具体值的类型信息,Printf 函数会恢复类型信息。
func main() {var x float64 = 12.1v:=reflect.ValueOf(x)fmt.Printf("%T\n",v) // reflect.Valuefmt.Printf("%T\n",v.Interface()) // float64
}
简单来说 Interface 方法和 ValueOf 函数作用恰好相反,唯一一点是,返回值的静态类型是 interface{}。
Go的反射机制可以将“接口类型的变量”转换为“反射类型的对象”,然后再将“反射类型对象”转换过去。
反射第三定律:如果要修改“反射类型对象”其值必须是“可写的”
这条定律很微妙,也很容易让人迷惑,但是如果从第一条定律开始看,应该比较容易理解。
下面这段代码虽然不能正常工作,但是非常值得研究:
var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1) // Error: will panic
如果运行这段代码,它会抛出一个奇怪的异常:
panic: reflect: reflect.flag.mustBeAssignable using unaddressable value
这里问题不在于值7.1
不能被寻址,而是因为变量 v 是“不可写的”,“可写性”是反射类型变量的一个属性,但不是所有的反射类型变量都拥有这个属性。
我们可以通过 CanSet()
方法检查一个 reflect.Value 类型变量的“可写性”,对于上面的例子,可以这样写:
func main() {var x float64 = 12.1v:=reflect.ValueOf(x)fmt.Println("settability of v:", v.CanSet())
}
运行结果如下:
settability of v: false
对于一个不具有“可写性”的 Value 类型变量,调用 Set 方法会报出错误。
首先我们要弄清楚什么是“可写性”,“可写性”有些类似于寻址能力,但是更严格,它是反射类型变量的一种属性,赋予该变量修改底层存储数据的能力。“可写性”最终是由一个反射对象是否存储了原始值而决定的。
示例代码如下:
var x float64 = 3.4
v := reflect.ValueOf(x)
这里我们传递给 reflect.ValueOf 函数的是变量 x 的一个拷贝,而非 x 本身,想象一下如果下面这行代码能够成功执行:
v.SetFloat(7.1)
如果这行代码能够成功执行,它不会更新 x,虽然看起来变量 v 是根据 x 创建的,相反它会更新 x 存在于反射对象 v 内部的一个拷贝,而变量 x 本身完全不受影响。这会造成迷惑,并且没有任何意义,所以是不合法的。“可写性”就是为了避免这个问题而设计的。
这看起来很诡异,事实上并非如此,而且类似的情况很常见。考虑下面这行代码:
f(x)
代码中,我们把变量 x 的一个拷贝传递给函数,因此不期望它会改变 x 的值。如果期望函数 f 能够修改变量 x,我们必须传递 x 的地址(即指向 x 的指针)给函数 f,如下所示:
f(&x)
反射的工作机制与此相同,如果想通过反射修改变量 x,就要把想要修改的变量的指针传递给反射库。
首先,像通常一样初始化变量 x,然后创建一个指向它的反射对象,命名为 p:
func main() {var x float64 = 3.4p := 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 指向的数据,可以调用 Value 类型的 Elem 方法。Elem 方法能够对指针进行“解引用”,然后将结果存储到反射 Value 类型对象 v 中:
func main() { var x float64 = 3.4 p := reflect.ValueOf(&x) // Note: take the address of x. v := p.Elem() fmt.Println("settability of v:", v.CanSet())
}
运行结果如下:
settability of v: true
由于变量 v 代表 x, 因此我们可以使用 v.SetFloat 修改 x 的值:
func main() {var x float64 = 3.4p := reflect.ValueOf(&x) // Note: take the address of x.v := p.Elem() // 获取指针&x指向的变量x,等同于*(&x),但是在反射中不可以那么写,有一个专门的方法Elem()获取指针指向的变量v.SetFloat(7.1)fmt.Println(v.Interface())fmt.Println(x)
}
运行结果如下:
7.1
7.1
Elem():专门用来获取指针指向的变量,返回仍然还是一个reflect.Vlaue类型
反射不太容易理解,reflect.Type 和 reflect.Value 会混淆正在执行的程序,但是它做的事情正是编程语言做的事情。只需要记住:只要反射对象要修改它们表示的对象,就必须获取它们表示的对象的地址。
结构体
我们一般使用反射修改结构体的字段,只要有结构体的指针,我们就可以修改它的字段。
下面是一个解析结构体变量 t 的例子,用结构体的地址创建反射变量,再修改它。然后我们对它的类型设置了 typeOfT,并用调用简单的方法迭代字段。
需要注意的是,我们从结构体的类型中提取了字段的名字,但每个字段本身是正常的 reflect.Value 对象。
func main() {type Student struct {Age intName string}t := Student{19, "hyy"}s := reflect.ValueOf(&t).Elem() // 用结构体的地址创建反射变量stypeOfT := s.Type()fmt.Println(typeOfT) // main.Student// 迭代字段// s.NumField() 返回结构体的字段个数for i := 0; i < s.NumField(); i++ {f := s.Field(i) // 取第i个字段fmt.Printf("字段名:%s 字段类型:%s = %v\n",typeOfT.Field(i).Name, // 获取字段名f.Type(), // 获取类型f.Interface(), // 获取值)}
}
运行结果如下:
main.Student
字段名:Age 字段类型:int = 19
字段名:Name 字段类型:string = hyy
Student 字段名之所以大写,是因为结构体中只有可导出的字段是“可设置”的。
// 如果设置成
type Student struct {Age intname string
}
会报错painc:
因为 s 包含了一个可设置的反射对象,我们可以修改结构体字段:
func main() {type Student struct {Age intName string}t := Student{19, "hyy"}s := reflect.ValueOf(&t).Elem() // 用结构体的地址创建反射变量ss.Field(0).SetInt(123) // t.Age=123s.Field(1).SetString("123") // t.Name="123"fmt.Println("修改后:",t)
}
运行结果如下:
修改后: {123 123}
如果我们修改了程序让 s 由 t(而不是 &t)创建,程序就会在调用 SetInt 和 SetString 的地方失败,因为 t 的字段是不可设置的。
总结
反射规则可以总结为如下几条:
- 反射可以将“接口类型变量”转换为“反射类型对象”;
- 反射可以将“反射类型对象”转换为“接口类型变量”;
- 如果要修改“反射类型对象”,其值必须是“可写的”。
二、reflect-反射类型对象-获取类型信息
1. TypeOf()和reflect.Type类型
通过反射获取类型信息
在 Go语言中通过调用 reflect.TypeOf()
函数,我们可以从一个任何非接口类型
的值创建一个 reflect.Type
值。
reflect.Type
值表示着此非接口值
的类型。通过此值,我们可以得到很多此非接口类型
的信息。
当然,我们也可以将一个接口值
传递给一个 reflect.TypeOf() 函数调用,但是此调用将返回一个表示着此接口值的动态类型的 reflect.Type
值。
实际上,reflect.TypeOf()
函数的唯一参数的类型为 interface{},reflect.TypeOf()
函数将总是返回一个表示着此唯一接口参数值的动态类型的 reflect.Type
值。
那如何得到一个表示着某个接口类型的 reflect.Type 值呢?
我们必须通过下面将要介绍的一些间接途径来达到这一目的。
类型 reflect.Type
为一个接口类型,它指定了若干方法(Type)。
通过这些方法,我们能够观察到一个 reflect.Type 值所表示的 Go类型的各种信息。这些方法中的有的适用于所有种类(Kind)的类型,有的只适用于一种或几种类型。
通过不合适的 reflect.Type 值调用某个方法将在运行时产生一个painc。
使用 reflect.TypeOf() 函数可以获得任意值的类型对象(reflect.Type),程序通过类型对象可以访问任意值的类型信息。下面通过例子来理解获取类型对象的过程:
func main() { var a int// 通过 reflect.TypeOf() 取得变量 a 的类型对象 typeOfA,类型为 reflect.Type()。typeOfA := reflect.TypeOf(a)// 通过 typeOfA 类型对象的成员函数,// 可以分别获取到 typeOfA 变量的类型名为 int,种类(Kind)为 int。fmt.Println(typeOfA.Name(), typeOfA.Kind())
}
代码输出如下:
int int
1.1 理解反射的类型(Type)与种类(Kind)
在使用反射时,需要首先理解类型(Type)和种类(Kind)的区别。
编程中,使用最多的是类型,但在反射中,当需要区分一个大品种的类型时,就会用到种类(Kind)。
例如,需要统一判断类型中的指针时,使用种类(Kind)信息就较为方便。
总结:
type Student struct{...}
type 类型是Student
kind 种类是struct
1.1.1 反射种类(Kind)的定义
Go 程序中的类型(Type)指的是系统原生数据类型,如 int、string、bool、float32 等类型,以及使用 type 关键字定义的类型,这些类型的名称就是其类型本身的名称。
例如使用 type A struct{}
定义结构体时,A 就是 struct{} 的类型。
种类(Kind)指的是对象归属的品种,在 reflect 包中有如下定义:
type Kind uint
const ( Invalid Kind = iota // 非法类型 Bool // 布尔型 Int // 有符号整型 Int8 // 有符号8位整型 Int16 // 有符号16位整型 Int32 // 有符号32位整型 Int64 // 有符号64位整型 Uint // 无符号整型 Uint8 // 无符号8位整型 Uint16 // 无符号16位整型 Uint32 // 无符号32位整型 Uint64 // 无符号64位整型 Uintptr // 指针 Float32 // 单精度浮点数 Float64 // 双精度浮点数 Complex64 // 64位复数类型 Complex128 // 128位复数类型 Array // 数组 Chan // 通道 Func // 函数 Interface // 接口 Map // 映射 Ptr // 指针 Slice // 切片 String // 字符串 Struct // 结构体 UnsafePointer // 底层指针
)
Map、Slice、Chan 属于引用类型,使用起来类似于指针,但是在种类常量定义中仍然属于独立的种类,不属于 Ptr。
type A struct{} 定义的结构体属于 Struct 种类,*A 属于 Ptr。
1.1.2 从类型对象中获取类型名称和种类的例子
Go语言中的类型名称对应的反射获取方法是 reflect.Type 中的 Name() 方法,返回表示类型名称的字符串。
类型归属的种类(Kind)使用的是 reflect.Type 中的 Kind() 方法,返回 reflect.Kind 类型的常量。
下面的代码中会对常量和结构体进行类型信息获取。
// Enum 定义一个Enum类型
type Enum int
const (Zero Enum = 0
)
func main() {// 声明一个空结构体type cat struct{}// 获取结构体实例的反射类型对象typeOfCat := reflect.TypeOf(cat{})// 显示反射类型对象的名称和种类fmt.Println(typeOfCat.Name(), typeOfCat.Kind())// 获取Zero常量的反射类型对象typeOfA := reflect.TypeOf(Zero)// 显示反射类型对象的名称和种类fmt.Println(typeOfA.Name(), typeOfA.Kind())
}
代码输出如下:
cat struct
Enum int
1.2. reflect.Elem()
通过反射获取指针指向的元素类型。
func main() {// 声明一个空结构体type cat struct {}// 创建cat的实例ins := cat{}// 获取结构体实例的反射类型对象typeOfCat := reflect.TypeOf(&ins)// 显示反射类型对象的名称和种类fmt.Printf("name:'%v' kind:'%v'\n",typeOfCat.Name(), typeOfCat.Kind())// 取类型的元素typeOfCat = typeOfCat.Elem()// 显示反射类型对象的名称和种类fmt.Printf("element name: '%v', element kind: '%v'\n", typeOfCat.Name(), typeOfCat.Kind())
}
对于指针一般都是直接
typeOfX := reflect.TypeOf(&x).Elem
取指针指向元素类型
1.3. 通过反射类型对象获取结构体的成员类型
任意值通过 reflect.TypeOf() 获得反射类型对象后,
如果它的类型是结构体,可以通过反射值对象(reflect.Type)的 NumField() 和 Field() 方法获得结构体成员的详细信息。
与成员获取相关的 reflect.Type 的方法如下表所示。
方法 | 说明 |
---|---|
Field(i int) StructField | 根据索引,返回索引对应的结构体字段的信息。 当值不是结构体或索引超界时发生panic |
NumField() int | 返回结构体成员字段数量。 当类型不是结构体或索引超界时发生panic |
FieldByName(name string) (StructField, bool) | 根据给定字符串返回字符串对应的结构体字段的信息。 没有找到时 bool 返回 false,当类型不是结构体或索引超界时发生panic |
FieldByIndex(index []int) StructField | 多层成员访问时,根据 []int 提供的每个结构体的字段索引,返回字段的信息。没有找到时返回零值。当类型不是结构体或索引超界时 发生panic |
FieldByNameFunc( match func(string) bool) (StructField,bool) | 根据匹配函数匹配需要的字段。 当值不是结构体或索引超界时发生panic |
1.3.1 结构体字段类型(StructField )
reflect.Type 的 Field() 方法返回 StructField 结构,这个结构描述结构体的成员信息,通过这个信息可以获取成员与结构体的关系,如偏移、索引、是否为匿名字段、结构体标签(Struct Tag)等,而且还可以通过 StructField 的 Type 字段进一步获取结构体成员的类型信息。
StructField 的结构如下:
type StructField struct { Name string // 字段名 PkgPath string // 字段路径 Type Type // 字段反射类型对象 Tag StructTag // 字段的结构体标签 Offset uintptr // 字段在结构体中的相对偏移 Index []int // Type.FieldByIndex中的返回的索引值 Anonymous bool // 是否为匿名字段
}
字段说明如下。
- Name:为字段名称。
- PkgPath:字段在结构体中的路径。
- Type:字段本身的反射类型对象,类型为 reflect.Type,可以进一步获取字段的类型信息。
- Tag:结构体标签,为结构体字段标签的额外信息,可以单独提取。
- Index:FieldByIndex 中的索引顺序。
- Anonymous:表示该字段是否为匿名字段。
func main() { // 声明一个结构体 type cat struct {// 带有结构体tag的字段Type int `json:"type" id:"100"` } // 创建cat的实例 ins := cat{Name: "mimi", Type: 1} // 获取结构体实例的反射类型对象 typeOfCat := reflect.TypeOf(&ins).Elem() // 2. 通过字段名, 找到字段类型信息 catType, ok := typeOfCat.FieldByName("Type") if ok {fmt.Println(catType.Name) //Typefmt.Println(catType.PkgPath) // ""fmt.Println(catType.Type) // intfmt.Println(catType.Tag) // json:"type" id:"100"fmt.Println(catType.Offset) // 16fmt.Println(catType.Index) // [1]fmt.Println(catType.Anonymous) // false } }
1.3.2 获取成员反射信息
下面代码中,实例化一个结构体并遍历其结构体成员,再通过 reflect.Type 的 FieldByName() 方法查找结构体中指定名称的字段,直接获取其类型信息。
反射访问结构体成员类型及信息:
func main() {// 声明一个空结构体type cat struct {Name string// 带有结构体tag的字段Type int `json:"type" id:"100"`}// 创建cat的实例ins := cat{Name: "mimi", Type: 1}// 获取结构体实例的反射类型对象typeOfCat := reflect.TypeOf(&ins).Elem()// 方法1. 遍历结构体所有成员字段for i := 0; i < typeOfCat.NumField(); i++ {// 获取每个成员的结构体字段类型fieldType := typeOfCat.Field(i)// 输出成员名,成员反射类型和tagfmt.Printf("name: %v,type: %v,tag: '%v'\n", fieldType.Name,fieldType.Type, fieldType.Tag)}// 方法2. 通过字段名, 找到字段类型信息catType, ok := typeOfCat.FieldByName("Type")if ok {// 从tag中取出需要的tagfmt.Println(catType.Tag.Get("json"), catType.Tag.Get("id"))}
}
代码输出如下:
name: Name,type: string,tag: ''
name: Type,type: int,tag: 'json:"type" id:"100"'
type 100
1.4 通过反射类型对象创建实例
当已知 reflect.Type 时,可以动态地创建这个类型的实例,实例的类型为指针。
例如 reflect.Type 的类型为 int 时,创建 int 的指针,即*int
,代码如下:
func main() {var a int// 取变量a的反射类型对象typeOfA := reflect.TypeOf(a)// 根据反射类型对象创建类型实例// 使用 reflect.New() 函数传入变量 a 的反射类型对象,创建这个类型的实例值,值以 reflect.Value 类型返回。这步操作等效于:new(int),因此返回的是 *int 类型的实例。aIns := reflect.New(typeOfA)// 输出Value的类型和种类fmt.Println(aIns.Type(), aIns.Kind())
}
运行结果:
*int ptr
reflect-反射值对象-获取值信息
2. reflect.ValueOf()和reflect.Value类型
通过反射获取值信息
当我们将一个接口值
传递给一个 reflect.ValueOf 函数调用时,此调用返回的是代表着此接口值的动态值
的一个 reflect.Value 值。我们必须
通过间接的途径获得一个代表一个接口值的 reflect.Value 值。
reflect.Value 类型有很多方法(https://golang.google.cn/pkg/reflect/)。我们可以调用这些方法来观察和操纵一个 reflect.Value 属主值表示的 值。这些方法中的有些适用于所有种类类型的值,有些只适用于一种或几种类型的值。
通过不合适的 reflect.Value 属主值调用某个方法将在运行时产生一个painc。
请阅读 reflect 代码库 中各个方法的文档来获取如何正确地使用这些方法。
一个 reflect.Value 值的 CanSet 方法将返回此 reflect.Value 值代表的 Go 值是否可以被修改(可以被赋值)。
如果一个 Go 值可以被修改,则我们可以调用对应的 reflect.Value 值的 Set 方法来修改此 Go 值。
注意:reflect.ValueOf(&v) 函数
直接
返回的 reflect.Value 值都是不可修改的。.Elem() 函数后返回的(虽然也是reflect.Value类型)才可以修改。
反射不仅可以获取值的类型信息,还可以动态地获取或者设置变量的值。
Go语言中使用 reflect.Value 获取和设置变量的值。
2.1 使用反射值对象<包装>任意值
Go语言中,使用 reflect.ValueOf()
函数获得反射值对象(reflect.Value)。
书写格式如下:
valueOfR := reflect.ValueOf(rawValue)
reflect.ValueOf 返回 reflect.Value 类型,包含有 rawValue 的值信息。
reflect.Value 与原值间可以通过值包装
和值获取
互相转化。reflect.Value 是一些反射操作的重要类型,如反射调用函数。
2.2 从反射值对象<获取>被包装的值
Go语言中可以通过 reflect.Value 重新获得原始值。
2.2.1 从反射值对象(reflect.Value)中获取值的方法
可以通过下面几种方法从反射值对象 reflect.Value 中获取原值,如下表所示。
方法名 | 说 明 |
---|---|
Interface() interface {} | 将值以 interface{} 类型返回,可以通过类型断言转换为指定类型val,ok := valueOfS.Interface().(Type) |
Int() int64 | 将值以 int 类型返回,所有有符号整型均可以此方式返回 |
Uint() uint64 | 将值以 uint 类型返回,所有无符号整型均可以此方式返回 |
Float() float64 | 将值以双精度(float64)类型返回,所有浮点数(float32、float64)均可以此方式返回 |
Bool() bool | 将值以 bool 类型返回 |
Bytes() []bytes | 将值以字节数组 []bytes 类型返回 |
String() string | 将值以字符串类型返回 |
2.2.2 从反射值对象(reflect.Value)中获取值的例子
下面代码中,将整型变量中的值使用 reflect.Value 获取反射值对象(reflect.Value)。再通过 reflect.Value 的 Interface() 方法获得 interface{} 类型的原值,通过 int 类型对应的 reflect.Value 的 Int() 方法获得整型值。
func main() {// 声明整型变量a并赋初值var a int = 1024// 获取指针变量&a的反射值对象,并取元素valueOfA := reflect.ValueOf(&a).Elem()// 方法1. 获取interface{}类型的值, 通过类型断言转换var getA int = valueOfA.Interface().(int)// 方法2. 获取64位的值, 强制类型转换为int类型var getA2 int = int(valueOfA.Int())fmt.Printf("%T\t%T\n",getA, getA2)
}
代码输出如下:
int int
2.3 通过反射值对象访问结构体成员的值
方法 | 说明 |
---|---|
Field(i int) StructField | 根据索引,返回索引对应的结构体字段的信息。 当值不是结构体或索引超界时发生panic |
NumField() int | 返回结构体成员字段数量。 当类型不是结构体或索引超界时发生panic |
FieldByName(name string) (StructField, bool) | 根据给定字符串返回字符串对应的结构体字段的信息。 没有找到时 bool 返回 false,当类型不是结构体或索引超界时发生panic |
FieldByIndex(index []int) StructField | 多层成员访问时,根据 []int 提供的每个结构体的字段索引,返回字段的信息。没有找到时返回零值。当类型不是结构体或索引超界时 发生panic |
FieldByNameFunc( match func(string) bool) (StructField,bool) | 根据匹配函数匹配需要的字段。 当值不是结构体或索引超界时发生panic |
简单使用:
// 定义结构体
type dummy struct {a intb stringfloat32boolnext *dummy // 嵌入字段
}func main() {dum := dummy{next: &dummy{},}// 值包装结构体valueOfDum := reflect.ValueOf(&dum).Elem()// 获取字段数量fmt.Println("NumField(): ", valueOfDum.NumField())// 获取索引为2的字段(float32字段 匿名字段)floatField := valueOfDum.Field(2)// 输出字段类型fmt.Println("Field(2): ", floatField.Type())// 根据名字查找字段fmt.Println("FieldByName(\"b\").Type(): ", valueOfDum.FieldByName("b").Type())// 根据索引查找值中, next字段的int字段的值fmt.Println("FieldByIndex([]int{4}).Type(): ", valueOfDum.FieldByIndex([]int{4}).Type())fmt.Println("FieldByIndex([]int{4, 1}).Type(): ", valueOfDum.FieldByIndex([]int{4,1}).Type())
}
运行结果:
NumField(): 5
Field(2): float32
FieldByName("b").Type(): string
FieldByIndex([]int{4}).Type(): *main.dummy
FieldByIndex([]int{4, 0}).Type(): string
例二:
type student struct {Name stringAge int
}
func main() {stu:=student{Name: "Hyy",Age: 19,}ValueOfStu := reflect.ValueOf(&stu).Elem()ValueOfStu.FieldByName("Name").SetString("胡宇洋")ValueOfStu.FieldByName("Age").SetInt(12)fmt.Println(stu)
}
运行结果:
{胡宇洋 12}
2.4 判断反射值对象的空和有效性
2.4.1 reflect.IsNil()和reflect.IsValid()
反射值对象(reflect.Value)提供一系列方法进行零值和空判定,如下表所示。
方 法 | 说 明 |
---|---|
IsNil() bool | 返回值是否为 nil。 如果值类型不是通道(channel)、函数、接口、map、指针或切片 时发生 panic ,类似于语言层的v == nil 操作 |
IsValid() bool | 判断值是否有效。 当值本身非法时,返回 false,例如 reflect Value不包含任何值,值为 nil 等。 |
下面的例子将会对各种方式的空指针进行 IsNil() 和 IsValid() 的返回值判定检测。
同时对结构体成员及方法查找 map 键值对的返回值进行 IsValid() 判定,参考下面的代码。
反射值对象的零值和有效性判断:
func main() {// *int的空指针var a *intfmt.Println("var a *int:", reflect.ValueOf(a).IsNil())// nil值fmt.Println("nil:", reflect.ValueOf(nil).IsValid())// *int类型的空指针fmt.Println("(*int)(nil):", reflect.ValueOf((*int)(nil)).Elem().IsValid())// 实例化一个结构体s := struct{}{}// 尝试从结构体中查找一个不存在的字段fmt.Println("不存在的结构体成员:", reflect.ValueOf(s).FieldByName("").IsValid())// 尝试从结构体中查找一个不存在的方法fmt.Println("不存在的结构体方法:", reflect.ValueOf(s).MethodByName("").IsValid())// 实例化一个mapm := map[int]int{}// 尝试从map中查找一个不存在的键fmt.Println("不存在的键:", reflect.ValueOf(m).MapIndex(reflect.ValueOf(3)).IsValid())
}
注意:
IsValid()
:对于任何值都调用,判断值的有效性。
IsNil()
:仅对于接口、map、指针、切片、通道、函数类型可用,其他类型调用 ,panic
2.5通过反射值对象修改变量的值
一个变量就是一个可寻址的内存空间,里面存储了一个值,并且存储的值可以通过内存地址来更新。
对于 reflect.Values 也有类似的区别。
有一些 reflect.Values 是可取地址的;其它一些则不可以。
考虑以下的声明语句:
x := 2 // value type variable?
a := reflect.ValueOf(2) // 2 int no
b := reflect.ValueOf(x) // 2 int no
c := reflect.ValueOf(&x) // &x *int no
d := reflect.ValueOf(&x).Elem() // 2 int yes (x)
其中 a 对应的变量则不可取地址,因为 a 中的值仅仅是整数 2 的拷贝副本。
b 中的值也同样不可取地址。
c 中的值还是不可取地址,它只是一个指针 &x 的拷贝。
实际上,所有通过 reflect.ValueOf(x) 直接返回的 reflect.Value 都是不可取地址的。
但是对于 d,它是 c 的解引用方式生成的,指向另一个变量,因此是可取地址的。
我们可以通过调用 reflect.ValueOf(&x).Elem()
,来获取任意变量x对应的可取地址的 Value。
我们可以通过调用 reflect.Value 的 CanAddr()
方法来判断其是否可以被取地址:
fmt.Println(a.CanAddr()) // "false"
fmt.Println(b.CanAddr()) // "false"
fmt.Println(c.CanAddr()) // "false"
fmt.Println(d.CanAddr()) // "true"
每当我们通过指针间接
地获取的 reflect.Value 都是可取地址
的,即使开始的是一个不可取地址的 Value。
在反射机制中,所有关于是否支持取地址的规则都是类似的。
func main() {y:=[]int{1,2,3}e:= reflect.ValueOf(y)fmt.Println(e.CanAddr()) //falsefmt.Println(e.Index(1).CanAddr()) // true
}
例如,slice 的索引表达式 e[i]将隐式地包含一个指针,它就是可取地址的,即使开始的e表达式不支持也没有关系。
以此类推,reflect.ValueOf(e).Index(i) 对于的值也是可取地址的,即使原始的 reflect.ValueOf(e) 不支持也没有关系。
使用 reflect.Value 对包装的值进行修改时,需要遵循一些规则。
如果没有按照规则进行代码设计和编写,轻则无法修改对象值,重则程序在运行时会发生宕机。
判定及获取元素的相关方法
使用 reflect.Value 取元素、取地址及修改值的属性方法请参考下表。
方法名 | 备 注 |
---|---|
Elem() Value | 取反射值对象指向的元素值,类似于语言层* 操作。当值类型不是指针或接口时发生宕 机,空指针时返回 nil 的 Value |
Addr() Value | 对可寻址的值返回其地址,类似于语言层& 操作。当值不可寻址时发生宕机 |
CanAddr() bool | 表示值是否可寻址 |
CanSet() bool | 返回值能否被修改。 要求值可寻址且是导出(即首字母大写)的字段 |
反射值对象的值修改相关方法
使用 reflect.Value 修改值的相关方法如下表所示。
Set(x Value) | 将值设置为传入的反射值对象的值 |
---|---|
Setlnt(x int64) | 使用 int64 设置值。 当值的类型不是 int、int8、int16、 int32、int64 时会发生宕机 |
SetUint(x uint64) | 使用 uint64 设置值。 当值的类型不是 uint、uint8、uint16、uint32、uint64 时会发生宕机 |
SetFloat(x float64) | 使用 float64 设置值。 当值的类型不是 float32、float64 时会发生宕机 |
SetBool(x bool) | 使用 bool 设置值。 当值的类型不是 bool 时会发生宕机 |
SetBytes(x []byte) | 设置字节数组 []bytes值。 当值的类型不是 []byte 时会发生宕机 |
SetString(x string) | 设置字符串值。 当值的类型不是 string 时会发生宕机 |
以上方法,在 reflect.Value 的 CanSet 返回 false 仍然修改值时会发生宕机。
在已知值的类型时,应尽量使用值对应类型的反射设置值。
值可修改条件之一:可被寻址
通过反射修改变量值的前提条件之一:这个值必须可以被寻址。
简单地说就是这个变量必须能被修改。
示例代码如下:
func main() {var a int = 1024valueOfA := reflect.ValueOf(a)// 尝试将a修改为1(此处会发生崩溃)valueOfA.SetInt(1)
}
程序运行崩溃,打印错误:
panic: reflect: reflect.Value.SetInt using unaddressable value
报错意思是:SetInt 正在使用一个不能被寻址的值。从 reflect.ValueOf 传入的是 a 的值,而不是 a 的地址,这个 reflect.Value 当然是不能被寻址的。将代码修改一下,重新运行:
func main() {var a int = 1024valueOfA := reflect.ValueOf(&a).Elem()valueOfA.SetInt(1)
}
注意:
当 reflect.Value 不可寻址时,使用
Addr()
方法也是无法取到值的地址的,同时会发生宕机。虽然说 reflect.Value 的
Addr()
方法类似于语言层的&
操作;Elem()
方法类似于语言层的*
操作,但并不代表这些方法与语言层操作等效。
值可修改条件之一:被导出
结构体成员中,如果字段没有被导出,即便不使用反射也可以被访问,但不能通过反射修改,代码如下:
type student struct {name stringAge int
}
func main() {stu := student{"hyy", 19}valueOfStu := reflect.ValueOf(&stu).Elem()valueOfStuAge:=valueOfStu.FieldByName("Age")valueOfStuAge.SetInt(20)fmt.Println(stu)valueOfStuName:=valueOfStu.FieldByName("name")valueOfStuName.SetString("hyy2")
}
运行结果:
{hyy 20}
panic: reflect: reflect.Value.SetString using value obtained using unexported field
报错的意思是:SetString() 使用的值来自于一个未导出的字段。
反射调用函数
如果反射值对象
(reflect.Value)中值的类型为函数时,可以通过 反射值对象 调用该函数。
使用反射调用函数时,需要将参数使用反射值对象的切片 []reflect.Value 构造后传入 Call() 方法中,调用完成时,函数的返回值通过 []reflect.Value 返回。
下面的代码声明一个加法函数,传入两个整型值,返回两个整型值的和。
将函数保存到反射值对象(reflect.Value)中,然后将两个整型值构造为反射值对象的切片([]reflect.Value),使用 Call() 方法进行调用。
func add(a int, b int) int {return a+b
}
func main() {funcValue:=reflect.ValueOf(add)realParameter:= []reflect.Value{reflect.ValueOf(10),reflect.ValueOf(12)}resList:=funcValue.Call(realParameter)fmt.Println(resList[0].Int())
}
提示
反射调用函数的过程需要构造大量的 reflect.Value 和中间变量,对函数参数值进行逐一检查,还需要将调用参数复制到调用函数的参数内存中。
调用完毕后,还需要将返回值转换为 reflect.Value,用户还需要从中取出调用值。
因此,反射调用函数的性能问题尤为突出,不建议大量使用反射函数调用。
inject库:依赖注入
在介绍 inject 之前我们先来简单介绍一下“依赖注入”和“控制反转”这两个概念。
正常情况下,对函数或方法的调用是我们的主动直接行为,在调用某个函数之前我们需要清楚地知道被调函数的名称是什么,参数有哪些类型等等。
所谓的控制反转就是将这种主动行为变成间接的行为,我们不用直接调用函数或对象,而是借助框架代码进行间接的调用和初始化,这种行为称作“控制反转”,库和框架能很好的解释控制反转的概念。
依赖注入是实现控制反转的一种方法,如果说控制反转是一种设计思想,那么依赖注入就是这种思想的一种实现,通过注入参数或实例的方式实现控制反转。如果没有特殊说明,我们可以认为依赖注入和控制反转是一个东西。
控制反转的价值在于解耦,有了控制反转就不需要将代码写死,可以让控制反转的的框架代码读取配置,动态的构建对象,这一点在 Java 的 Spring 框架中体现的尤为突出。
inject 实践
inject 是依赖注入的Go语言实现,它能在运行时注入参数,调用方法,是 Martini 框架(Go语言中著名的 Web 框架)的基础核心。
在介绍具体实现之前,先来想一个问题,如何通过一个字符串类型的函数名来调用函数?
Go语言没有 Java 中的 Class.forName 方法可以通过类名直接构造对象,所以这种方法是行不通的,能想到的方法就是使用 map 实现一个字符串到函数的映射,示例代码如下:
func fl() {println ("fl")
}
func f2 () {println ("f2")
}
funcs := make(map[string] func ())
funcs ["fl"] = fl
funcs ["f2"] = fl
funcs ["fl"]()
funcs ["f2"]()
但是这有个缺陷,就是 map 的 Value 类型被写成 func(),不同参数和返回值的类型的函数并不能通用。
将 map 的 Value 定义为 interface{} 空接口类型即可以解决该问题,但需要借助类型断言或反射来实现,通过类型断言实现等于又绕回去了,反射是一种可行的办法。
inject 包借助反射实现函数的注入调用,下面通过一个示例来看一下。
type S1 interface{}
type S2 interface{}func Format(name string, company S1, level S2, age int) {fmt.Printf("name = %s, company=%s, level=%s, age = %d!\n", name, company, level, age)
}func main() {// 控制实例的创建inj := inject.New()// 实参注入// 基于调用reflect.TypeOf得到的类型映射interface{}的值。inj.Map("tom")// 基于提供的接口的指针,映射interface{}的值。// 该函数仅用来将一个值映射为接口,因为接口无法不通过指针而直接引用到。inj.MapTo("tencent", (*S1)(nil))inj.MapTo("T4", (*S2)(nil))inj.Map(23)// 函数反转调用// Invoke尝试将interface{}作为一个函数来调用,并基于Type为函数提供参数。// 它将返回reflect.Value的切片,其中存放原函数的返回值。// 如果注入失败则返回error.inj.Invoke(Format)
}
运行结果:
name = tom, company=tencent, level=T4, age = 23!
可见 inject 提供了一种注入参数调用函数的通用功能,inject.New() 相当于创建了一个控制实例,由其来实现对函数的注入调用。
inject 包不但提供了对函数的注入,还实现了对 struct 类型的注入,示例代码如下所示:
type S1 interface{}
type S2 interface{}
type Staff struct {Name string `inject`Company S1 `inject`Level S2 `inject`Age int `inject`
}func main() {//创建被注入实例s := Staff{}//控制实例的创建inj := inject.New()//初始化注入值inj.Map("tom")inj.MapTo("tencent", (*S1)(nil))inj.MapTo("T4", (*S2)(nil))inj.Map(23)// 实现对 struct 注入// 在Type map中维持对结构体中每个域的引用,并用\'inject\'来标记// 如果注入失败将会返回一个error.inj.Apply(&s)//打印结果fmt.Printf("s = %v\n", s)
}
运行结果:
s = {tom tencent T4 23}
可以看到 inject 提供了一种对结构类型的通用注入方法。
至此,我们仅仅从宏观层面了解 iniect 能做什么,下面从源码实现角度来分析 inject。
inject 原理分析
概述
inject 包中只有 2 个文件,一个是 inject.go 文件和一个 inject_test.go 文件,这里我们只需要关注 inject.go 文件即可。
inject.go 短小精悍,包括注释和空行在内才 157 行代码,代码中定义了 4 个接口,包括一个父接口和三个子接口,如下所示:
type Injector interface {ApplicatorInvokerTypeMapperSetParent(Injector)
}
type Applicator interface {Apply(interface{}) error
}
type Invoker interface {Invoke(interface{}) ([]reflect.Value, error)
}
type TypeMapper interface {Map(interface{}) TypeMapperMapTo(interface{}, interface{}) TypeMapperSet(reflect.Type, reflect.Value) TypeMapperGet(reflect.Type) reflect.Value
}
-
Injector
接口是Applicator
、Invoker
、TypeMapper
接口的父接口,所以实现了 Injector 接口的类型,也必然实现了 Applicator、Invoker 和 TypeMapper 接口:-
Applicator
接口只规定了Apply
成员,它用于注入 struct。 -
Invoker
接口只规定了Invoke
成员,它用于执行被调用者。 -
TypeMapper
接口规定了三个成员,Map
和MapTo
都用于注入参数,但它们有不同的用法,Get 用于调用时获取被注入的参数。
-
另外 Injector
还规定了 SetParent
行为,它用于设置父 Injector
,其实它相当于查找继承。也即通过 Get 方法在获取被注入参数时会一直追溯到 parent,这是个递归过程,直到查找到参数或为 nil 终止。
injector,InterfaceOf(),New()
type injector struct {values map[reflect.Type]reflect.Valueparent Injector
}
func InterfaceOf(value interface{}) reflect.Type {t := reflect.TypeOf(value)for t.Kind() == reflect.Ptr {t = t.Elem()}if t.Kind() != reflect.Interface {panic("Called inject.InterfaceOf with a value that is not a pointer to an interface. (*MyInterface)(nil)")}return t
}
func New() Injector {return &injector{values: make(map[reflect.Type]reflect.Value),}
}
-
injector
是 inject 包中唯一定义的 struct,所有的操作都是基于 injector struct 来进行的,它有两个成员 values 和 parent。
values
用于保存注入的参数,是一个用 reflect.Type 当键、reflect.Value 为值的 map,理解这点将有助于理解Map
和MapTo
。 -
New
方法用于初始化 injector struct,并返回一个指向 injector struct 的指针,但是这个返回值被 Injector 接口包装了。 -
InterfaceOf
方法虽然只有几句实现代码,但它是 Injector 的核心。InterfaceOf
方法的参数必须是一个接口类型的指针,如果不是则引发 panic。InterfaceOf
方法的返回类型是 reflect.Type,大家应该还记得 injector 的成员 values 就是一个 reflect.Type 类型当键的 map。这个方法的作用其实只是获取参数的类型,而不关心它的值。
示例:
type SpecialString interface{} func main() {fmt.Println(inject.InterfaceOf((*interface{})(nil)))fmt.Println(inject.InterfaceOf((*SpecialString)(nil))) }
运行结果:
interface {} main.SpecialString
InterfaceOf 方法就是用来得到参数类型,而不关心它具体存储的是什么值。
Map(),MapTo(),Get(),SetParent()
func (i *injector) Map(val interface{}) TypeMapper {i.values[reflect.TypeOf(val)] = reflect.ValueOf(val)return i
}
func (i *injector) MapTo(val interface{}, ifacePtr interface{}) TypeMapper {i.values[InterfaceOf(ifacePtr)] = reflect.ValueOf(val)return i
}
func (i *injector) Get(t reflect.Type) reflect.Value {val := i.values[t]if !val.IsValid() && i.parent != nil {val = i.parent.Get(t)}return val
}
func (i *injector) SetParent(parent Injector) {i.parent = parent
}
-
Map
和MapTo
方法都用于注入参数,保存于 injector 的成员 values 中。这两个方法的功能完全相同,唯一的区别就是 Map 方法用参数值本身的类型当键,而 MapTo 方法有一个额外的参数可以指定特定的类型当键。但是 MapTo 方法的第二个参数
ifacePtr
必须是接口指针类型,因为最终 ifacePtr 会作为InterfaceOf
方法的参数。为什么需要有 MapTo 方法?
因为注入的参数是存储在一个以类型为键的 map 中,可想而知,当一个函数中有一个以上的参数的类型是一样时,后执行 Map 进行注入的参数将会覆盖前一个通过 Map 注入的参数。
-
SetParent
方法用于给某个 Injector 指定父 Injector。Get 方法通过 reflect.Type 从 injector 的 values 成员中取出对应的值,它可能会检查是否设置了 parent,直到找到或返回无效的值,最后 Get 方法的返回值会经过 IsValid 方法的校验。
示例代码如下所示:
type SpecialString interface{}
func main() {inj := inject.New()inj.Map("傻子") // map[string]="傻子"inj.MapTo("你呀", (*SpecialString)(nil)) // map[SpecialString]="你呀"inj.Map(20) // map[int]=20// 相当于查找键为string的map键值对fmt.Println("字符串是否有效?", inj.Get(reflect.TypeOf("hyy")).IsValid())// 查找map[SpecialString]fmt.Println("特殊字符串是否有效?", inj.Get(inject.InterfaceOf((*SpecialString)(nil))).IsValid())// map[int]fmt.Println("int 是否有效?", inj.Get(reflect.TypeOf(18)).IsValid())// map[[]byte]fmt.Println("[]byte 是否有效?", inj.Get(reflect.TypeOf([]byte("Golang"))).IsValid())inj2 := inject.New()// map2[[]byte]=[]byte("test")inj2.Map([]byte("test"))// 设置到父inject中,如果在子中查不到,自动去父中查,父中再找不到,errorinj.SetParent(inj2)fmt.Println("[]byte 是否有效?", inj.Get(reflect.TypeOf([]byte("Golang"))).IsValid())// 从map中取元素fmt.Println(string(inj.Get(reflect.TypeOf([]byte("12"))).Bytes())) // test
}
运行结果:
字符串是否有效? true
特殊字符串是否有效? true
int 是否有效? true
[]byte 是否有效? false
[]byte 是否有效? true
test
通过以上例子应该知道 SetParent 是什么样的行为,是不是很像面向对象中的查找链?
Invoke()
func (inj *injector) Invoke(f interface{}) ([]reflect.Value, error) {// 获取参数f的反射类型对象t := reflect.TypeOf(f)// 准备实参,根据参数 函数f 需要的参数数量创建切片var in = make([]reflect.Value, t.NumIn()) //Panic if t is not kind of Func// 遍历获取所需参数类型,根据类型去values中获取对应键值对,获取不到就报错,获取到就并入实参切片。for i := 0; i < t.NumIn(); i++ {argType := t.In(i)val := inj.Get(argType)if !val.IsValid() {return nil, fmt.Errorf("Value not found for type %v", argType)}in[i] = val}// 根据实参切片作为参数调用函数freturn reflect.ValueOf(f).Call(in), nil
}
Invoke 方法用于动态执行函数,当然执行前可以通过 Map 或 MapTo 来注入参数,因为通过 Invoke 执行的函数会取出已注入的参数,然后通过 reflect 包中的 Call 方法来调用。
NumIn()
NumIn 返回一个函数类型的输入参数计数。如果类型的 Kind 不是 Func,它会恐慌。In()
In 返回函数类型的第 i 个输入参数的类型。
如果类型的 Kind 不是 Func,它会恐慌。
如果 i 不在 [0, NumIn()) 范围内,它会恐慌。
Invoke 接收的参数 f 是一个接口类型,但是 f 的底层类型必须为 func,否则会 panic。
例子:
type SpecialString interface{}
func Say(name string, gender SpecialString, age int) {fmt.Printf("My name is %s, gender is %s, age is %d!\n", name, gender, age)
}
func main() {inj := inject.New()// map[string]="张三"inj.Map("张三")// map[SpecialString]="男"inj.MapTo("男", (*SpecialString)(nil))inj2 := inject.New()// map2[int]=25inj2.Map(25)inj.SetParent(inj2)inj.Invoke(Say)
}
运行结果:
My name is 张三, gender is 男, age is 25!
上面的例子如果没有定义 SpecialString 接口作为 gender 参数的类型,而把 name 和 gender 都定义为 string 类型,那么 gender 会覆盖 name 的值。
Apply()
func (inj *injector) Apply(val interface{}) error {// 获取参数的反射值对象v := reflect.ValueOf(val)// 如果你传入的参数的种类是指针类型,就取元素for v.Kind() == reflect.Ptr {v = v.Elem()}// 如果传入的参数的种类不是结构体,返回nilif v.Kind() != reflect.Struct {return nil}// 获取反射类型对象t := v.Type()// 遍历字段域for i := 0; i < v.NumField(); i++ {// 选择 反射值对象字段域f := v.Field(i)// 选择 反射类型对象字段域structField := t.Field(i)// 判断当前反射值对象是否可修改,且当前反射类型对象的标签是否为'inject'if f.CanSet() && structField.Tag == "inject" {// 获取当前反射值对象的反射类型对象ft := f.Type()// 从values中获取以ft为键的值v := inj.Get(ft)if !v.IsValid() {return fmt.Errorf("Value not found for type %v", ft)}f.Set(v)}}return nil
}
Apply 方法是用于对 struct 的字段进行注入,参数为指向底层类型为结构体的指针。
可注入的前提是:字段必须是导出的(也即字段名以大写字母开头),并且此字段的 tag 设置为inject
。
实例:
type SpecialString interface{}
type TestStruct struct {Name string `inject`Nick []byteGender SpecialString `inject`uid int `inject`Age int `inject`
}
func main() {s := TestStruct{}inj := inject.New()inj.Map("张三")inj.MapTo("男", (*SpecialString)(nil))inj2 := inject.New()inj2.Map(26)inj.SetParent(inj2)inj.Apply(&s)fmt.Println("s.Name =", s.Name)fmt.Println("s.Gender =", s.Gender)fmt.Println("s.Age =", s.Age)
}
运行结果:
s.Name = 张三
s.Gender = 男
s.Age = 26
拓展:inject 官方文档
– import “github.com/codegangsta/inject”
inject包提供了多种对实体的映射和依赖注入方式。
用法
func InterfaceOf
就是取类型,不过传入的参数要是接口类型的指针
例如
type SpecialString interface{} func main(){fmt.Println(inject.InterfaceOf((*SpecialString)(nil))) // main.SpecialString }
func InterfaceOf(value interface{}) reflect.Type
函数InterfaceOf返回指向接口类型的指针。如果传入的value值不是指向接口的指针,将抛出一个panic异常。
type Applicator
type Applicator interface {// 在Type map中维持对结构体中每个域的引用并用'inject'来标记// 如果注入失败将会返回一个error.Apply(interface{}) error
}
Applicator接口表示到结构体的依赖映射关系。
type Injector
type Injector interface {ApplicatorInvokerTypeMapper// SetParent用来设置父injector. 如果在当前injector的Type map中找不到依赖,// 将会继续从它的父injector中找,直到返回error.SetParent(Injector)
}
Injector接口表示对结构体、函数参数的映射和依赖注入。
func New
func New() Injector
New创建并返回一个Injector.
type Invoker
type Invoker interface {// Invoke尝试将interface{}作为一个函数来调用,并基于Type为函数提供参数。// 它将返回reflect.Value的切片,其中存放原函数的返回值。// 如果注入失败则返回error.Invoke(interface{}) ([]reflect.Value, error)
}
Invoker接口表示通过反射进行函数调用。
type TypeMapper
type TypeMapper interface {// 基于调用reflect.TypeOf得到的类型映射interface{}的值。Map(interface{}) TypeMapper// 基于提供的接口的指针映射interface{}的值。// 该函数仅用来将一个值映射为接口,因为接口无法不通过指针而直接引用到。MapTo(interface{}, interface{}) TypeMapper// 为直接插入基于类型和值的map提供一种可能性。// 它使得这一类直接映射成为可能:无法通过反射直接实例化的类型参数,如单向管道。Set(reflect.Type, reflect.Value) TypeMapper// 返回映射到当前类型的Value. 如果Type没被映射,将返回对应的零值。Get(reflect.Type) reflect.Value
}
TypeMapper接口用来表示基于类型到接口值的映射。
map()
传入一个参数,该参数的反射类型对象作为values的键,反射值对象作为values的值
mapTo()
传入两个参数,第一个参数的反射值对象作为values的值,第二个参数传入interfaceOf()获取其类型作为values的键
如若内容造成侵权/违法违规/事实不符,请联系编程学习网邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
相关文章
- 深度学习-线性回归+基础优化算法
线性模型:(可以看做是单层的神经网络) w表示权重,b表示偏差 线性回归是对n位输入的加权外加偏差 对预估质量进行衡量--平方损失(衡量预测值和真实值的差异) 训练数据:收集一些数据点决定参数值…...
2024/5/4 23:15:42 - 通过form上传图片后端request.files为空,但是通过js提交请求缺可以获得上传图片的问题
JS大概是这样提交的: var form_data new FormData(document.getElementById("MainForm"));$.ajax({ url: "{{url_for(main.dm_form, data_codedata_model.data_code)?record_idrecord.id|string }}" type: "post",data: form_data…...
2024/4/16 21:59:55 - 某云服务器性能简单测试
内存2GB,没测试。 CPU 1核心1线程 分数 C盘读写 D盘读写(BitLocker加密过)...
2024/4/20 6:06:36 - Cherno C++ P30 C++数组
YouTube视频链接 C数组 本文是ChernoP30视频的学习笔记。 C数组就是表示一堆相同类型变量组成的集合。假设我想要有5个整数的数组名为example。 int example[5];现在我们要设置和访问这些整数,先写数组的名字然后再[]内写上一种叫做索引(index)的东西。索引是…...
2024/4/24 5:29:59 - Java实现简单的银行管理系统(最基本的功能/易上手)
文章目录一、简介二、详细设计三、代码四、测试结果五、总结实现了一个最基本的银行管理系统(基于类与对象),无复杂功能,易于初学者上手操作。基本框架实现后,可继续丰富其功能,实现更强大的管理功能&#…...
2024/4/14 8:27:12 - spring cloud alibaba(看完即入门)——Sentinel篇
目录spring cloud alibaba全部组件地址六、Sentinel服务容错6.1、认识Sentinel6.2、Sentiner与hystrix的区别6.3、安装sentinel控制台6.4、整合微服务6.4.1、公共模块引入sentinel依赖6.4.2、配置控制台地址6.5、簇点链路6.6、限流规则1.1、添加限流规则1.2、限流高级配置1.2.1…...
2024/4/14 8:27:02 - goby内测版联合xray高级版详细说明加举例Apache Tomcat示例目录漏洞
一、Goby介绍 Goby是一款新的网络安全测试工具,由赵武Zwell(Pangolin、JSky、FOFA作者)打造,它能够针对一个目标企业梳理最全的攻击面信息,同时能进行高效、实战化漏洞扫描,并快速地从一个验证入口点&…...
2024/4/16 4:51:46 - JAVA 常用方法实例 封装
封装:把属性变成private,提供get,set方法 类方法Student.java public class Student {//属性private int age;private String name;//get方法构造器public int getAge() {return age;}//set方法构造器public void setAge(int age) {this.age…...
2024/4/14 8:27:07 - C语言简单练习题——No.10 , 关于数组与循环的结合,数组元素个数的计算
题目链接: ASCII码_牛客题霸_牛客网 (nowcoder.com)https://www.nowcoder.com/practice/4280d330fe314e8f98cd5c593e3e9190?tpId107&gioEntermenu 知识点: 1. 数组是通过下标来访问的,例如:arr 【0】表示arr数组里存放的第…...
2024/4/7 15:58:54 - 论文慢递4:CSC论文分享_PointerNetwork_ACL2019
Confusionset-guided Pointer Networks for Chinese Spelling Check背景主要方法encoderdecoder实验结果结论讨论背景 CSC很重要; 任务存在特殊性,与MT等生成式不同,输入与输出大部分相同; 因此生成策略如下: copy输…...
2024/4/17 15:47:54 - [渝粤教育] 中国矿业大学 数字逻辑与数字系统设计 参考 资料
教育 -数字逻辑与数字系统设计-章节资料考试资料-中国矿业大学【】 第 1 讲 绪论-单元作业 第 1 讲 绪论-单元测试 1、【单选题】现代电子技术的发展,目前集成电路器件处于()阶段。 A、分立元器件 B、集成电路 C、大规模集成电路 D、超大规模…...
2024/4/20 18:33:51 - Mock服务(4)---- 利用fiddler完成一次mock
一、我们可能有这样的场景 我有一个web页面,这个页面有一个接口是查询书籍列表,然后在web页面展示 我想查在各个情况下,web页面的展示情况 那我们可能有这样的用例 1,如果书籍服务列表返回是空 2、如果书籍列表正常返回 3、如果书…...
2024/4/13 8:27:10 - Map集合,hashMap的存储过程,Set集合
1. Map接口 Map接口的特点 map集合的结构是:键值对、KEY与VALUE、Map.Entry<K,V>的映射关系 map中key值不允许重复,如果重复,对应的value会被覆盖 map中的映射关系是无序的 map没有自己的迭代器,所以迭代时通常需要转成…...
2024/5/3 16:43:26 - Mysql的常用命令
MySQL的常用命令 1.查看MySQL版本 MySQl程序选项具有以下两种通用形式: 长选项,由单词之前加两个减号组成短选项,由单个字母之前加一个减号组成 C:\WINDOWS\system32>mysql --version mysql Ver 14.14 Distrib 5.7.19, for Win64 (x86_6…...
2024/5/3 21:03:11 - 5974. 分隔长廊的方案数
分隔长廊的方案数 在一个图书馆的长廊里,有一些座位和装饰植物排成一列。给你一个下标从 0 开始,长度为 n 的字符串 corridor ,它包含字母 ‘S’ 和 ‘P’ ,其中每个 ‘S’ 表示一个座位,每个 ‘P’ 表示一株植物。 在…...
2024/4/7 15:58:49 - 【Java 数据结构 算法】宁可累死自己, 也要卷死别人 19 记事法
【Java 数据结构 & 算法】⚠️宁可累死自己, 也要卷死别人 19⚠️ 记事法概述时间复杂度大 O 记事法大 Ω\OmegaΩ 记事法大 Θ\ThetaΘ 记事法概述 从今天开始, 小白我将带大家开启 Java 数据结构 & 算法的新篇章. 时间复杂度 时间复杂度 (Time Complexity) 用来描述…...
2024/4/14 8:27:53 - VMware 16 安装 Windows server 2012 超详细
文章目录一、安装环境二、安装步骤一、安装环境 1.VMware workstation 16 Pro 2.点击下载:Windows Server 2012 镜像文件 提取码:jsxu 二、安装步骤 1.新建虚拟机 2.选择自定义,点击下一步 3.默认下一步 4.选择稍后安装操作系统&…...
2024/4/18 7:42:40 - FetchFetch的二次封装
前言 客户端服务器端通信方式ajax (ajax JQ的类库 /axios类库) /jsonp / fetch fetch是Es6新提供的API:基于不同于XMLHttpRequest的方式,基于客户端和服务器端的数据通信,而且本身是基于promise管理请求结果的(发送请求返回promi实例) fetch的基础知识和管理 语法: Pr…...
2024/4/19 12:51:37 - 九万五千一的关于maven的初步笔记
认识maven目录结构 src/main/java 存放pom。xml和所有子目录 src/main/resource 项目的资源,比如propert/文件 src/test/java 项目的测试类 src/text/rescoures 测试使用的资源 Maven基本命令 -v:查询Maven版本 本命令用于检查maven是否安装成功。 Maven安装…...
2024/4/24 7:24:37 - windows系统go的环境搭建
windows系统go的环境搭建 1. 下载go的编译器 https://golang.google.cn/ 进入官网,点击下载 选择对应的版本,这里下载的是最新的windows版本 下载速度很快,已经下载好了,下面我们双击这个文件 点击next 点击next 选择自己想把它…...
2024/4/15 7:25:32
最新文章
- SPARC VScode EIDE GDB 使用配置
前言 搞了多年的SPARC 最近接触了VSCODE插件感觉好用。想想看不是能方便调试和编译SPARC,决定使用开源的SPARC仿真环境和编译器来试试。感觉的却不错,借此献给使用SPARC的朋友们。安装 1.找微软官方的下载VSCODE. 2.电机左边的方块形状的图标࿰…...
2024/5/5 2:24:49 - 梯度消失和梯度爆炸的一些处理方法
在这里是记录一下梯度消失或梯度爆炸的一些处理技巧。全当学习总结了如有错误还请留言,在此感激不尽。 权重和梯度的更新公式如下: w w − η ⋅ ∇ w w w - \eta \cdot \nabla w ww−η⋅∇w 个人通俗的理解梯度消失就是网络模型在反向求导的时候出…...
2024/3/20 10:50:27 - 【Locust分布式压力测试】
Locust分布式压力测试 https://docs.locust.io/en/stable/running-distributed.html Distributed load generation A single process running Locust can simulate a reasonably high throughput. For a simple test plan and small payloads it can make more than a thousan…...
2024/5/4 16:36:43 - Unity核心学习
目录 认识模型的制作流程模型的制作过程 2D相关图片导入设置图片导入概述纹理类型设置纹理形状设置纹理高级设置纹理平铺拉伸设置纹理平台打包相关设置 SpriteSprite Editor——Single图片编辑Sprite Editor——Multiple图片编辑Sprite Editor——Polygon图片编辑SpriteRendere…...
2024/5/1 13:06:24 - 【外汇早评】美通胀数据走低,美元调整
原标题:【外汇早评】美通胀数据走低,美元调整昨日美国方面公布了新一期的核心PCE物价指数数据,同比增长1.6%,低于前值和预期值的1.7%,距离美联储的通胀目标2%继续走低,通胀压力较低,且此前美国一季度GDP初值中的消费部分下滑明显,因此市场对美联储后续更可能降息的政策…...
2024/5/4 23:54:56 - 【原油贵金属周评】原油多头拥挤,价格调整
原标题:【原油贵金属周评】原油多头拥挤,价格调整本周国际劳动节,我们喜迎四天假期,但是整个金融市场确实流动性充沛,大事频发,各个商品波动剧烈。美国方面,在本周四凌晨公布5月份的利率决议和新闻发布会,维持联邦基金利率在2.25%-2.50%不变,符合市场预期。同时美联储…...
2024/5/4 23:54:56 - 【外汇周评】靓丽非农不及疲软通胀影响
原标题:【外汇周评】靓丽非农不及疲软通胀影响在刚结束的周五,美国方面公布了新一期的非农就业数据,大幅好于前值和预期,新增就业重新回到20万以上。具体数据: 美国4月非农就业人口变动 26.3万人,预期 19万人,前值 19.6万人。 美国4月失业率 3.6%,预期 3.8%,前值 3…...
2024/5/4 23:54:56 - 【原油贵金属早评】库存继续增加,油价收跌
原标题:【原油贵金属早评】库存继续增加,油价收跌周三清晨公布美国当周API原油库存数据,上周原油库存增加281万桶至4.692亿桶,增幅超过预期的74.4万桶。且有消息人士称,沙特阿美据悉将于6月向亚洲炼油厂额外出售更多原油,印度炼油商预计将每日获得至多20万桶的额外原油供…...
2024/5/4 23:55:17 - 【外汇早评】日本央行会议纪要不改日元强势
原标题:【外汇早评】日本央行会议纪要不改日元强势近两日日元大幅走强与近期市场风险情绪上升,避险资金回流日元有关,也与前一段时间的美日贸易谈判给日本缓冲期,日本方面对汇率问题也避免继续贬值有关。虽然今日早间日本央行公布的利率会议纪要仍然是支持宽松政策,但这符…...
2024/5/4 23:54:56 - 【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响
原标题:【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响近日伊朗局势升温,导致市场担忧影响原油供给,油价试图反弹。此时OPEC表态稳定市场。据消息人士透露,沙特6月石油出口料将低于700万桶/日,沙特已经收到石油消费国提出的6月份扩大出口的“适度要求”,沙特将满…...
2024/5/4 23:55:05 - 【外汇早评】美欲与伊朗重谈协议
原标题:【外汇早评】美欲与伊朗重谈协议美国对伊朗的制裁遭到伊朗的抗议,昨日伊朗方面提出将部分退出伊核协议。而此行为又遭到欧洲方面对伊朗的谴责和警告,伊朗外长昨日回应称,欧洲国家履行它们的义务,伊核协议就能保证存续。据传闻伊朗的导弹已经对准了以色列和美国的航…...
2024/5/4 23:54:56 - 【原油贵金属早评】波动率飙升,市场情绪动荡
原标题:【原油贵金属早评】波动率飙升,市场情绪动荡因中美贸易谈判不安情绪影响,金融市场各资产品种出现明显的波动。随着美国与中方开启第十一轮谈判之际,美国按照既定计划向中国2000亿商品征收25%的关税,市场情绪有所平复,已经开始接受这一事实。虽然波动率-恐慌指数VI…...
2024/5/4 23:55:16 - 【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试
原标题:【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试美国和伊朗的局势继续升温,市场风险情绪上升,避险黄金有向上突破阻力的迹象。原油方面稍显平稳,近期美国和OPEC加大供给及市场需求回落的影响,伊朗局势并未推升油价走强。近期中美贸易谈判摩擦再度升级,美国对中…...
2024/5/4 23:54:56 - 【原油贵金属早评】市场情绪继续恶化,黄金上破
原标题:【原油贵金属早评】市场情绪继续恶化,黄金上破周初中国针对于美国加征关税的进行的反制措施引发市场情绪的大幅波动,人民币汇率出现大幅的贬值动能,金融市场受到非常明显的冲击。尤其是波动率起来之后,对于股市的表现尤其不安。隔夜美国股市出现明显的下行走势,这…...
2024/5/4 18:20:48 - 【外汇早评】美伊僵持,风险情绪继续升温
原标题:【外汇早评】美伊僵持,风险情绪继续升温昨日沙特两艘油轮再次发生爆炸事件,导致波斯湾局势进一步恶化,市场担忧美伊可能会出现摩擦生火,避险品种获得支撑,黄金和日元大幅走强。美指受中美贸易问题影响而在低位震荡。继5月12日,四艘商船在阿联酋领海附近的阿曼湾、…...
2024/5/4 23:54:56 - 【原油贵金属早评】贸易冲突导致需求低迷,油价弱势
原标题:【原油贵金属早评】贸易冲突导致需求低迷,油价弱势近日虽然伊朗局势升温,中东地区几起油船被袭击事件影响,但油价并未走高,而是出于调整结构中。由于市场预期局势失控的可能性较低,而中美贸易问题导致的全球经济衰退风险更大,需求会持续低迷,因此油价调整压力较…...
2024/5/4 23:55:17 - 氧生福地 玩美北湖(上)——为时光守候两千年
原标题:氧生福地 玩美北湖(上)——为时光守候两千年一次说走就走的旅行,只有一张高铁票的距离~ 所以,湖南郴州,我来了~ 从广州南站出发,一个半小时就到达郴州西站了。在动车上,同时改票的南风兄和我居然被分到了一个车厢,所以一路非常愉快地聊了过来。 挺好,最起…...
2024/5/4 23:55:06 - 氧生福地 玩美北湖(中)——永春梯田里的美与鲜
原标题:氧生福地 玩美北湖(中)——永春梯田里的美与鲜一觉醒来,因为大家太爱“美”照,在柳毅山庄去寻找龙女而错过了早餐时间。近十点,向导坏坏还是带着饥肠辘辘的我们去吃郴州最富有盛名的“鱼头粉”。说这是“十二分推荐”,到郴州必吃的美食之一。 哇塞!那个味美香甜…...
2024/5/4 23:54:56 - 氧生福地 玩美北湖(下)——奔跑吧骚年!
原标题:氧生福地 玩美北湖(下)——奔跑吧骚年!让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 啊……啊……啊 两…...
2024/5/4 23:55:06 - 扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!
原标题:扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!扒开伪装医用面膜,翻六倍价格宰客!当行业里的某一品项火爆了,就会有很多商家蹭热度,装逼忽悠,最近火爆朋友圈的医用面膜,被沾上了污点,到底怎么回事呢? “比普通面膜安全、效果好!痘痘、痘印、敏感肌都能用…...
2024/5/4 2:59:34 - 「发现」铁皮石斛仙草之神奇功效用于医用面膜
原标题:「发现」铁皮石斛仙草之神奇功效用于医用面膜丽彦妆铁皮石斛医用面膜|石斛多糖无菌修护补水贴19大优势: 1、铁皮石斛:自唐宋以来,一直被列为皇室贡品,铁皮石斛生于海拔1600米的悬崖峭壁之上,繁殖力差,产量极低,所以古代仅供皇室、贵族享用 2、铁皮石斛自古民间…...
2024/5/4 23:55:16 - 丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者
原标题:丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者【公司简介】 广州华彬企业隶属香港华彬集团有限公司,专注美业21年,其旗下品牌: 「圣茵美」私密荷尔蒙抗衰,产后修复 「圣仪轩」私密荷尔蒙抗衰,产后修复 「花茵莳」私密荷尔蒙抗衰,产后修复 「丽彦妆」专注医学护…...
2024/5/4 23:54:58 - 广州械字号面膜生产厂家OEM/ODM4项须知!
原标题:广州械字号面膜生产厂家OEM/ODM4项须知!广州械字号面膜生产厂家OEM/ODM流程及注意事项解读: 械字号医用面膜,其实在我国并没有严格的定义,通常我们说的医美面膜指的应该是一种「医用敷料」,也就是说,医用面膜其实算作「医疗器械」的一种,又称「医用冷敷贴」。 …...
2024/5/4 23:55:01 - 械字号医用眼膜缓解用眼过度到底有无作用?
原标题:械字号医用眼膜缓解用眼过度到底有无作用?医用眼膜/械字号眼膜/医用冷敷眼贴 凝胶层为亲水高分子材料,含70%以上的水分。体表皮肤温度传导到本产品的凝胶层,热量被凝胶内水分子吸收,通过水分的蒸发带走大量的热量,可迅速地降低体表皮肤局部温度,减轻局部皮肤的灼…...
2024/5/4 23:54:56 - 配置失败还原请勿关闭计算机,电脑开机屏幕上面显示,配置失败还原更改 请勿关闭计算机 开不了机 这个问题怎么办...
解析如下:1、长按电脑电源键直至关机,然后再按一次电源健重启电脑,按F8健进入安全模式2、安全模式下进入Windows系统桌面后,按住“winR”打开运行窗口,输入“services.msc”打开服务设置3、在服务界面,选中…...
2022/11/19 21:17:18 - 错误使用 reshape要执行 RESHAPE,请勿更改元素数目。
%读入6幅图像(每一幅图像的大小是564*564) f1 imread(WashingtonDC_Band1_564.tif); subplot(3,2,1),imshow(f1); f2 imread(WashingtonDC_Band2_564.tif); subplot(3,2,2),imshow(f2); f3 imread(WashingtonDC_Band3_564.tif); subplot(3,2,3),imsho…...
2022/11/19 21:17:16 - 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机...
win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”问题的解决方法在win7系统关机时如果有升级系统的或者其他需要会直接进入一个 等待界面,在等待界面中我们需要等待操作结束才能关机,虽然这比较麻烦,但是对系统进行配置和升级…...
2022/11/19 21:17:15 - 台式电脑显示配置100%请勿关闭计算机,“准备配置windows 请勿关闭计算机”的解决方法...
有不少用户在重装Win7系统或更新系统后会遇到“准备配置windows,请勿关闭计算机”的提示,要过很久才能进入系统,有的用户甚至几个小时也无法进入,下面就教大家这个问题的解决方法。第一种方法:我们首先在左下角的“开始…...
2022/11/19 21:17:14 - win7 正在配置 请勿关闭计算机,怎么办Win7开机显示正在配置Windows Update请勿关机...
置信有很多用户都跟小编一样遇到过这样的问题,电脑时发现开机屏幕显现“正在配置Windows Update,请勿关机”(如下图所示),而且还需求等大约5分钟才干进入系统。这是怎样回事呢?一切都是正常操作的,为什么开时机呈现“正…...
2022/11/19 21:17:13 - 准备配置windows 请勿关闭计算机 蓝屏,Win7开机总是出现提示“配置Windows请勿关机”...
Win7系统开机启动时总是出现“配置Windows请勿关机”的提示,没过几秒后电脑自动重启,每次开机都这样无法进入系统,此时碰到这种现象的用户就可以使用以下5种方法解决问题。方法一:开机按下F8,在出现的Windows高级启动选…...
2022/11/19 21:17:12 - 准备windows请勿关闭计算机要多久,windows10系统提示正在准备windows请勿关闭计算机怎么办...
有不少windows10系统用户反映说碰到这样一个情况,就是电脑提示正在准备windows请勿关闭计算机,碰到这样的问题该怎么解决呢,现在小编就给大家分享一下windows10系统提示正在准备windows请勿关闭计算机的具体第一种方法:1、2、依次…...
2022/11/19 21:17:11 - 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”的解决方法...
今天和大家分享一下win7系统重装了Win7旗舰版系统后,每次关机的时候桌面上都会显示一个“配置Windows Update的界面,提示请勿关闭计算机”,每次停留好几分钟才能正常关机,导致什么情况引起的呢?出现配置Windows Update…...
2022/11/19 21:17:10 - 电脑桌面一直是清理请关闭计算机,windows7一直卡在清理 请勿关闭计算机-win7清理请勿关机,win7配置更新35%不动...
只能是等着,别无他法。说是卡着如果你看硬盘灯应该在读写。如果从 Win 10 无法正常回滚,只能是考虑备份数据后重装系统了。解决来方案一:管理员运行cmd:net stop WuAuServcd %windir%ren SoftwareDistribution SDoldnet start WuA…...
2022/11/19 21:17:09 - 计算机配置更新不起,电脑提示“配置Windows Update请勿关闭计算机”怎么办?
原标题:电脑提示“配置Windows Update请勿关闭计算机”怎么办?win7系统中在开机与关闭的时候总是显示“配置windows update请勿关闭计算机”相信有不少朋友都曾遇到过一次两次还能忍但经常遇到就叫人感到心烦了遇到这种问题怎么办呢?一般的方…...
2022/11/19 21:17:08 - 计算机正在配置无法关机,关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机...
关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容,让我们赶快一起来看一下吧!关机提示 windows7 正在配…...
2022/11/19 21:17:05 - 钉钉提示请勿通过开发者调试模式_钉钉请勿通过开发者调试模式是真的吗好不好用...
钉钉请勿通过开发者调试模式是真的吗好不好用 更新时间:2020-04-20 22:24:19 浏览次数:729次 区域: 南阳 > 卧龙 列举网提醒您:为保障您的权益,请不要提前支付任何费用! 虚拟位置外设器!!轨迹模拟&虚拟位置外设神器 专业用于:钉钉,外勤365,红圈通,企业微信和…...
2022/11/19 21:17:05 - 配置失败还原请勿关闭计算机怎么办,win7系统出现“配置windows update失败 还原更改 请勿关闭计算机”,长时间没反应,无法进入系统的解决方案...
前几天班里有位学生电脑(windows 7系统)出问题了,具体表现是开机时一直停留在“配置windows update失败 还原更改 请勿关闭计算机”这个界面,长时间没反应,无法进入系统。这个问题原来帮其他同学也解决过,网上搜了不少资料&#x…...
2022/11/19 21:17:04 - 一个电脑无法关闭计算机你应该怎么办,电脑显示“清理请勿关闭计算机”怎么办?...
本文为你提供了3个有效解决电脑显示“清理请勿关闭计算机”问题的方法,并在最后教给你1种保护系统安全的好方法,一起来看看!电脑出现“清理请勿关闭计算机”在Windows 7(SP1)和Windows Server 2008 R2 SP1中,添加了1个新功能在“磁…...
2022/11/19 21:17:03 - 请勿关闭计算机还原更改要多久,电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机怎么办...
许多用户在长期不使用电脑的时候,开启电脑发现电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机。。.这要怎么办呢?下面小编就带着大家一起看看吧!如果能够正常进入系统,建议您暂时移…...
2022/11/19 21:17:02 - 还原更改请勿关闭计算机 要多久,配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以...
配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容,让我们赶快一起来看一下吧!配置windows update失败 还原更改 请勿关闭计算机&#x…...
2022/11/19 21:17:01 - 电脑配置中请勿关闭计算机怎么办,准备配置windows请勿关闭计算机一直显示怎么办【图解】...
不知道大家有没有遇到过这样的一个问题,就是我们的win7系统在关机的时候,总是喜欢显示“准备配置windows,请勿关机”这样的一个页面,没有什么大碍,但是如果一直等着的话就要两个小时甚至更久都关不了机,非常…...
2022/11/19 21:17:00 - 正在准备配置请勿关闭计算机,正在准备配置windows请勿关闭计算机时间长了解决教程...
当电脑出现正在准备配置windows请勿关闭计算机时,一般是您正对windows进行升级,但是这个要是长时间没有反应,我们不能再傻等下去了。可能是电脑出了别的问题了,来看看教程的说法。正在准备配置windows请勿关闭计算机时间长了方法一…...
2022/11/19 21:16:59 - 配置失败还原请勿关闭计算机,配置Windows Update失败,还原更改请勿关闭计算机...
我们使用电脑的过程中有时会遇到这种情况,当我们打开电脑之后,发现一直停留在一个界面:“配置Windows Update失败,还原更改请勿关闭计算机”,等了许久还是无法进入系统。如果我们遇到此类问题应该如何解决呢࿰…...
2022/11/19 21:16:58 - 如何在iPhone上关闭“请勿打扰”
Apple’s “Do Not Disturb While Driving” is a potentially lifesaving iPhone feature, but it doesn’t always turn on automatically at the appropriate time. For example, you might be a passenger in a moving car, but your iPhone may think you’re the one dri…...
2022/11/19 21:16:57