目录
一个具体问题
最近遇到一个问题,在使用工具 gocsv 将一个 csv 文件反序列化为 struct 的时候,反序列出来的字符串都带有一个后缀 \t
,显然后缀是多余的,但是反序列出来的 struct 字段有上百个, 不可能一个一个检查并去掉 \t
,于是想用反射来解决这个问题:如果遇到的字段类型是 string 或者 *string 时,检查是否有后缀 \t
,如果有则去掉。
思路很明确,实现起来却有点摸不着头脑,原来 reflect 我一直没搞清楚。
反射相关类型
先通过文档了解下 go 中的基本类型。go 反射机制中最主要的两个方法 reflect.ValueOf 以及 reflect.TypeOf 其参数都是一个 interface{} 类型,在获取其类型、值信息时,都是分解 interface{} 来获取信息的,我们先复习下 go 中的 interface 类型。
interface: eface 与 iface
eface 是 go 中类型 interface{}
也就是 any
的底层数据类型,其不包含任何接口方法,任何类型都可以赋值给 interface{},因此 eface 是不描述实际类型的方法信息的,只包含两部分信息:1)实际类型的类型信息;2)指向实际类型的指针。
type eface struct {
_type *_type // 具体实例的类型信息。
data unsafe.Pointer // 指向接口具体实例的指针。
}
其中 _type(位于 src/runtime/types.go) 是 go 中所有类型的基本类型,所有类型都是由这些数据类型加上其他信息组成的,这个类型与 reflect.Type 接口实现者 rtype(位于 src/reflect/type.go) 是一致的,都是 abi.Type
类型。
type _type = abi.Type
// rtype is a wrapper that allows us to define additional methods.
type rtype struct {
*abi.Type // embedding is okay here (unlike reflect) because none of this is public
}
type slicetype struct { // 其他类型都是由 _type 字段加上其他信息组成的。
typ _type // slice 自身类型
ele *_type // 元素类型
}
其中 abi.Type 的定义为(位于 src/internal/abi/type.go)。这个类型现在不展开介绍了,其描述了一些类型的元信息。
type Type struct {
Size_ uintptr
PtrBytes uintptr // number of (prefix) bytes in the type that can contain pointers
Hash uint32 // hash of type; avoids computation in hash tables
TFlag TFlag // extra type information flags
Align_ uint8 // alignment of variable with this type
FieldAlign_ uint8 // alignment of struct field with this type
Kind_ uint8 // enumeration for C
Equal func(unsafe.Pointer, unsafe.Pointer) bool
// GCData stores the GC type data for the garbage collector.
// If the KindGCProg bit is set in kind, GCData is a GC program.
// Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
GCData *byte
Str NameOff // string form
PtrToThis TypeOff // type for pointer to this type, may be zero
}
iface 与 eface 基本类似,与 eface 不同,iface 带有一个接口集合,比如 io 包中的 Reader 与 Writer 接口。因此在 iface 的结构体中,需要有字段来描述这些方法信息,这个字段是 itab
,其中 interfacetype 中的 Methods 描述的接口的方法列表。
type iface struct {
tab *itab
data unsafe.Pointer
}
type itab struct {
inter *interfacetype
_type *_type
hash uint32 // copy of _type.hash. Used for type switches.
_ [4]byte
fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}
type InterfaceType struct {
Type
PkgPath Name // import path
Methods []Imethod // sorted by hash
}
reflect.Type 接口
在大概了解了 go 中的 interface 之后,我们来看下反射中的相关类型,首先我们需要注意,reflect.Type 是一个接口,我们通过 reflect.TypeOf(a any) Type
来得到一个这样的接口,这个接口只描述类型信息,没有值信息。
reflect.TypeOf 的实现也相对比较明确,从下面代码看,就是取了 eface 的 _type 字段,也就是最终是 abi.Type 类型,其实现了 reflect.Type 接口。
func TypeOf(i any) Type {
eface := *(*emptyInterface)(unsafe.Pointer(&i))
return toType((*abi.Type)(noescape(unsafe.Pointer(eface.typ))))
}
那接口都包含哪些方法呢?首先我们知道这个接口只描述类型信息,不包含值信息,完整的方法列表可以参考 https://pkg.go.dev/reflect#Type,这里挑选几个常用的分析一下。还有个问题需要注意一下,虽然 Type 接口定义了很多接口,但是这些接口是分类型的,也就是一部分接口只适用于特定的类型,其他类型调用那些接口时则会 panic,所以在使用接口时需要注意一下。
- Kind() Kind: 返回 go 预定义基本类型的字面表示(使用 iota 生成的数字),如 Bool、String、Pointer(Ptr)、Interface、Struct、Func等。基本上,我们可以通过这个方法来判断变量或者字段的类型。
- Elem() Type: 返回一个类型的底层类型,这个方法只适用于 Array/Chan/Map/Pointer/Slice。对于 Array 等表示的数组元素的类型,对于指针类型来说,该方法返回指向的类型。比如 *string 的 Elem() 方法返回的 Type,其 Kind 为 String。需要注意 reflect.Value 结构体(是结构体,而非 interface)也有
Elem() Value
方法,不过其返回值是 Value 结构体,不要混淆。 - Field(i int) StructField: 这个方法只适用于 Struct 类型,否则会 panic。这个方法返回的类型为
StructField
,当我们读到这里的时候感觉没什么,但是当我们知道 reflect.Value 接口也有Field(i int) Value
方法的时候,就会感觉有点迷惑(go reflect 令人难以理解的一个原因是其类型和接口很多相似的,很容易产生混淆)。需要注意的是,reflect.Value 接口其Field(i int) Value
方法返回的类型也是 Value,跟 Elem() 也返回 Type 一样,看上去类似于递归调用。根据 StructField 类型,当我们需要解析 struct tag 的时候,需要用到这个方法。type StructField struct { Name string // field 名字 PkgPath string Type Type // field type Tag StructTag // field tag 标签 Offset uintptr Index []int Anonymous bool }
- Len() int: 返回数组类型实例的长度,非数组则 panic。
- In(i int) Type: 返回 function 类型的第 i 个参数的类型,如果类型的 kind 不是 Func 则 panic。同样,如果 i 不在区间
[0, NumIn())
之间时,也 panic。
reflect.Value 结构体
我们通过 reflect.ValueOf(a any) Value
方法可以得到任意类型的 Value 信息。其定义如下(为 src/reflect/value.go)。从 Value 的定义 中,可以看到非常类似 eface 的定义,包含类型信息,以及指向数据的指针。其中 flag 字段表示 Value 的元数据,比如是否可以 Set。
type Value struct {
typ_ *abi.Type
ptr unsafe.Pointer
flag
}
基本上 Value 能拿到类似 reflect.Type 接口的所有信息,至少 Value 结构体调用 Type() Type 方法就能拿到类型信息,因此如果我们如果只关注类型本身的信息可以通过 reflect.TypeOf 方法拿到类型信息,其他情况下用 reflect.ValueOf 方法就够了。
Value 同样有很多方法,参考 https://pkg.go.dev/reflect#Value,同样,我们只选择几个介绍一下。
- CanSet() bool: 表示 Value 是否可以被修改(重写),value 可被修改必须是 addressable,并且不是结构体的 unexported 字段。一般来说,只有一个 Value 被指针指向时才会被改写。并且是指针的 Elem() Value 可被改写,指针不是可改写的。比如下面例子
a := tea.String("abc") va := reflect.ValueOf(a) fmt.Println(va.CanSet()) // false fmt.Println(va.Elem().CanSet()) //true
- Elem() Value: 返回指针指向的值的 Value 或者 interface 真实值的 Value。如果 Pointer 或者 interface 为 nil,则该方法返回 nil。(注意区别 Type 的 Elem 方法)
- IsNil() bool: 判断 Value 是不是 nil,Value 必须是 chan/func/interface/map/pointer/slice。
- Field(i int) Value: 返回结构体的第 i 个字段,该方法返回的仍然是一个 Value 类型,类似于递归。(注意区别 Type 的 Value 方法,后者因为关注类型本身所以返回的是 StructField 结构体,不包含值信息)。
- Interface() (i any): 将当前 Value 作为 interface{} 返回,类似于:
var i interface{} = (Value's underlying value)
- Kind() Kind: 返回类型,这个跟 Type 接口的 Kind 方法是一致的。
- Type() Type: 返回 Value 的 Type 类型,跟对变量调用 reflect.TypeOf 方法效果是一样的。
解决具体问题
解决具体的方法很简单,如下,注意改方法的参数必须是指针类型,不然我们是无法修改字段的值的也就是无法 CanSet()。
func TrimStringFields(instance interface{}) {
v := reflect.ValueOf(instance).Elem() // 返回指针指向的类型的 Value,不是 pointer 就 panic
for i := 0; i < v.NumField(); i++ {
field := v.Field(i) // 返回字段的 Value
fieldType := field.Type() // 返回字段的 Type
if fieldType.Kind() == reflect.String {
sv := strings.TrimSpace(field.String())
field.SetString(sv)
} else if fieldType.Kind() == reflect.Ptr && fieldType.Elem().Kind() == reflect.String {
if field.IsNil() { // panic: reflect: call of reflect.Value.SetString on zero Value
return
}
sv := strings.TrimSpace(field.Elem().String())
field.Elem().SetString(sv)
}
}
}
另外需要注意,我们再给 interface{} 赋值时,也是值传递,对于 var i interface{} = Value
。如果 Value 是 int/struct 等,则 interface{} 保存的是副本,如果是指针,则是一个指针的副本。