mirror of
https://github.com/protocolbuffers/protobuf-go.git
synced 2025-02-06 09:40:07 +00:00
23ccb359e1
Implement support in the protobuf runtime to better understand message types that are not generated by the official generator. In particular: * Add a best-effort implementation of protobuf reflection for "non-nullable" fields which are supposed to be represented by *T, but are instead represented by a T. "Non-nullable" message fields report presence based on whether the message is the zero Go value. * We do NOT implement support for "non-nullable" fields in the table-driven implementation since we assume that the aberrant messages that we care about have a Marshal and Unmarshal method. * We better handle custom messages that implement Marshal and Unmarshal, but do NOT implement Merge. In that case, we implement merge in terms of a back-to-back marshal and unmarshal. * We better tolerate the situations where a protobuf message field cannot be mapped to a Go struct field since the latter is missing. In such cases, reflection treats the field as if it were unpopulated. Setting such fields will panic. This change allows the runtime to handle all message types declared in the "go.etcd.io/etcd" and "k8s.io" modules where protobuf reflection, Marshal, Unmarshal, Reset, Merge, and Equal all work. The only types that still do not fully work are: * "k8s.io/api/authentication/v1".ExtraValue * "k8s.io/api/authentication/v1beta1".ExtraValue * "k8s.io/api/authorization/v1".ExtraValue * "k8s.io/api/authorization/v1beta1".ExtraValue * "k8s.io/api/certificates/v1".ExtraValue * "k8s.io/api/certificates/v1beta1".ExtraValue * "k8s.io/apimachinery/pkg/apis/meta/v1".MicroTime * "k8s.io/apimachinery/pkg/apis/meta/v1".Time * "k8s.io/apimachinery/pkg/apis/meta/v1".Verbs While Marshal, Unmarshal, Reset, and Merge continue to work, protobuf reflection and any functionality that depends on it (e.g., prototext, protojson, Equal, etc.) will not work. Change-Id: I67a9d2f1bec35248045ad0c16220d02fc2e0e172 Reviewed-on: https://go-review.googlesource.com/c/protobuf/+/300869 Trust: Joe Tsai <joetsai@digital-static.net> Trust: Joe Tsai <thebrokentoaster@gmail.com> Reviewed-by: Damien Neil <dneil@google.com>
544 lines
15 KiB
Go
544 lines
15 KiB
Go
// Copyright 2018 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package impl
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"reflect"
|
|
"sync"
|
|
|
|
"google.golang.org/protobuf/internal/flags"
|
|
pref "google.golang.org/protobuf/reflect/protoreflect"
|
|
preg "google.golang.org/protobuf/reflect/protoregistry"
|
|
)
|
|
|
|
type fieldInfo struct {
|
|
fieldDesc pref.FieldDescriptor
|
|
|
|
// These fields are used for protobuf reflection support.
|
|
has func(pointer) bool
|
|
clear func(pointer)
|
|
get func(pointer) pref.Value
|
|
set func(pointer, pref.Value)
|
|
mutable func(pointer) pref.Value
|
|
newMessage func() pref.Message
|
|
newField func() pref.Value
|
|
}
|
|
|
|
func fieldInfoForMissing(fd pref.FieldDescriptor) fieldInfo {
|
|
// This never occurs for generated message types.
|
|
// It implies that a hand-crafted type has missing Go fields
|
|
// for specific protobuf message fields.
|
|
return fieldInfo{
|
|
fieldDesc: fd,
|
|
has: func(p pointer) bool {
|
|
return false
|
|
},
|
|
clear: func(p pointer) {
|
|
panic("missing Go struct field for " + string(fd.FullName()))
|
|
},
|
|
get: func(p pointer) pref.Value {
|
|
return fd.Default()
|
|
},
|
|
set: func(p pointer, v pref.Value) {
|
|
panic("missing Go struct field for " + string(fd.FullName()))
|
|
},
|
|
mutable: func(p pointer) pref.Value {
|
|
panic("missing Go struct field for " + string(fd.FullName()))
|
|
},
|
|
newMessage: func() pref.Message {
|
|
panic("missing Go struct field for " + string(fd.FullName()))
|
|
},
|
|
newField: func() pref.Value {
|
|
if v := fd.Default(); v.IsValid() {
|
|
return v
|
|
}
|
|
panic("missing Go struct field for " + string(fd.FullName()))
|
|
},
|
|
}
|
|
}
|
|
|
|
func fieldInfoForOneof(fd pref.FieldDescriptor, fs reflect.StructField, x exporter, ot reflect.Type) fieldInfo {
|
|
ft := fs.Type
|
|
if ft.Kind() != reflect.Interface {
|
|
panic(fmt.Sprintf("field %v has invalid type: got %v, want interface kind", fd.FullName(), ft))
|
|
}
|
|
if ot.Kind() != reflect.Struct {
|
|
panic(fmt.Sprintf("field %v has invalid type: got %v, want struct kind", fd.FullName(), ot))
|
|
}
|
|
if !reflect.PtrTo(ot).Implements(ft) {
|
|
panic(fmt.Sprintf("field %v has invalid type: %v does not implement %v", fd.FullName(), ot, ft))
|
|
}
|
|
conv := NewConverter(ot.Field(0).Type, fd)
|
|
isMessage := fd.Message() != nil
|
|
|
|
// TODO: Implement unsafe fast path?
|
|
fieldOffset := offsetOf(fs, x)
|
|
return fieldInfo{
|
|
// NOTE: The logic below intentionally assumes that oneof fields are
|
|
// well-formatted. That is, the oneof interface never contains a
|
|
// typed nil pointer to one of the wrapper structs.
|
|
|
|
fieldDesc: fd,
|
|
has: func(p pointer) bool {
|
|
if p.IsNil() {
|
|
return false
|
|
}
|
|
rv := p.Apply(fieldOffset).AsValueOf(fs.Type).Elem()
|
|
if rv.IsNil() || rv.Elem().Type().Elem() != ot || rv.Elem().IsNil() {
|
|
return false
|
|
}
|
|
return true
|
|
},
|
|
clear: func(p pointer) {
|
|
rv := p.Apply(fieldOffset).AsValueOf(fs.Type).Elem()
|
|
if rv.IsNil() || rv.Elem().Type().Elem() != ot {
|
|
// NOTE: We intentionally don't check for rv.Elem().IsNil()
|
|
// so that (*OneofWrapperType)(nil) gets cleared to nil.
|
|
return
|
|
}
|
|
rv.Set(reflect.Zero(rv.Type()))
|
|
},
|
|
get: func(p pointer) pref.Value {
|
|
if p.IsNil() {
|
|
return conv.Zero()
|
|
}
|
|
rv := p.Apply(fieldOffset).AsValueOf(fs.Type).Elem()
|
|
if rv.IsNil() || rv.Elem().Type().Elem() != ot || rv.Elem().IsNil() {
|
|
return conv.Zero()
|
|
}
|
|
rv = rv.Elem().Elem().Field(0)
|
|
return conv.PBValueOf(rv)
|
|
},
|
|
set: func(p pointer, v pref.Value) {
|
|
rv := p.Apply(fieldOffset).AsValueOf(fs.Type).Elem()
|
|
if rv.IsNil() || rv.Elem().Type().Elem() != ot || rv.Elem().IsNil() {
|
|
rv.Set(reflect.New(ot))
|
|
}
|
|
rv = rv.Elem().Elem().Field(0)
|
|
rv.Set(conv.GoValueOf(v))
|
|
},
|
|
mutable: func(p pointer) pref.Value {
|
|
if !isMessage {
|
|
panic(fmt.Sprintf("field %v with invalid Mutable call on field with non-composite type", fd.FullName()))
|
|
}
|
|
rv := p.Apply(fieldOffset).AsValueOf(fs.Type).Elem()
|
|
if rv.IsNil() || rv.Elem().Type().Elem() != ot || rv.Elem().IsNil() {
|
|
rv.Set(reflect.New(ot))
|
|
}
|
|
rv = rv.Elem().Elem().Field(0)
|
|
if rv.Kind() == reflect.Ptr && rv.IsNil() {
|
|
rv.Set(conv.GoValueOf(pref.ValueOfMessage(conv.New().Message())))
|
|
}
|
|
return conv.PBValueOf(rv)
|
|
},
|
|
newMessage: func() pref.Message {
|
|
return conv.New().Message()
|
|
},
|
|
newField: func() pref.Value {
|
|
return conv.New()
|
|
},
|
|
}
|
|
}
|
|
|
|
func fieldInfoForMap(fd pref.FieldDescriptor, fs reflect.StructField, x exporter) fieldInfo {
|
|
ft := fs.Type
|
|
if ft.Kind() != reflect.Map {
|
|
panic(fmt.Sprintf("field %v has invalid type: got %v, want map kind", fd.FullName(), ft))
|
|
}
|
|
conv := NewConverter(ft, fd)
|
|
|
|
// TODO: Implement unsafe fast path?
|
|
fieldOffset := offsetOf(fs, x)
|
|
return fieldInfo{
|
|
fieldDesc: fd,
|
|
has: func(p pointer) bool {
|
|
if p.IsNil() {
|
|
return false
|
|
}
|
|
rv := p.Apply(fieldOffset).AsValueOf(fs.Type).Elem()
|
|
return rv.Len() > 0
|
|
},
|
|
clear: func(p pointer) {
|
|
rv := p.Apply(fieldOffset).AsValueOf(fs.Type).Elem()
|
|
rv.Set(reflect.Zero(rv.Type()))
|
|
},
|
|
get: func(p pointer) pref.Value {
|
|
if p.IsNil() {
|
|
return conv.Zero()
|
|
}
|
|
rv := p.Apply(fieldOffset).AsValueOf(fs.Type).Elem()
|
|
if rv.Len() == 0 {
|
|
return conv.Zero()
|
|
}
|
|
return conv.PBValueOf(rv)
|
|
},
|
|
set: func(p pointer, v pref.Value) {
|
|
rv := p.Apply(fieldOffset).AsValueOf(fs.Type).Elem()
|
|
pv := conv.GoValueOf(v)
|
|
if pv.IsNil() {
|
|
panic(fmt.Sprintf("map field %v cannot be set with read-only value", fd.FullName()))
|
|
}
|
|
rv.Set(pv)
|
|
},
|
|
mutable: func(p pointer) pref.Value {
|
|
v := p.Apply(fieldOffset).AsValueOf(fs.Type).Elem()
|
|
if v.IsNil() {
|
|
v.Set(reflect.MakeMap(fs.Type))
|
|
}
|
|
return conv.PBValueOf(v)
|
|
},
|
|
newField: func() pref.Value {
|
|
return conv.New()
|
|
},
|
|
}
|
|
}
|
|
|
|
func fieldInfoForList(fd pref.FieldDescriptor, fs reflect.StructField, x exporter) fieldInfo {
|
|
ft := fs.Type
|
|
if ft.Kind() != reflect.Slice {
|
|
panic(fmt.Sprintf("field %v has invalid type: got %v, want slice kind", fd.FullName(), ft))
|
|
}
|
|
conv := NewConverter(reflect.PtrTo(ft), fd)
|
|
|
|
// TODO: Implement unsafe fast path?
|
|
fieldOffset := offsetOf(fs, x)
|
|
return fieldInfo{
|
|
fieldDesc: fd,
|
|
has: func(p pointer) bool {
|
|
if p.IsNil() {
|
|
return false
|
|
}
|
|
rv := p.Apply(fieldOffset).AsValueOf(fs.Type).Elem()
|
|
return rv.Len() > 0
|
|
},
|
|
clear: func(p pointer) {
|
|
rv := p.Apply(fieldOffset).AsValueOf(fs.Type).Elem()
|
|
rv.Set(reflect.Zero(rv.Type()))
|
|
},
|
|
get: func(p pointer) pref.Value {
|
|
if p.IsNil() {
|
|
return conv.Zero()
|
|
}
|
|
rv := p.Apply(fieldOffset).AsValueOf(fs.Type)
|
|
if rv.Elem().Len() == 0 {
|
|
return conv.Zero()
|
|
}
|
|
return conv.PBValueOf(rv)
|
|
},
|
|
set: func(p pointer, v pref.Value) {
|
|
rv := p.Apply(fieldOffset).AsValueOf(fs.Type).Elem()
|
|
pv := conv.GoValueOf(v)
|
|
if pv.IsNil() {
|
|
panic(fmt.Sprintf("list field %v cannot be set with read-only value", fd.FullName()))
|
|
}
|
|
rv.Set(pv.Elem())
|
|
},
|
|
mutable: func(p pointer) pref.Value {
|
|
v := p.Apply(fieldOffset).AsValueOf(fs.Type)
|
|
return conv.PBValueOf(v)
|
|
},
|
|
newField: func() pref.Value {
|
|
return conv.New()
|
|
},
|
|
}
|
|
}
|
|
|
|
var (
|
|
nilBytes = reflect.ValueOf([]byte(nil))
|
|
emptyBytes = reflect.ValueOf([]byte{})
|
|
)
|
|
|
|
func fieldInfoForScalar(fd pref.FieldDescriptor, fs reflect.StructField, x exporter) fieldInfo {
|
|
ft := fs.Type
|
|
nullable := fd.HasPresence()
|
|
isBytes := ft.Kind() == reflect.Slice && ft.Elem().Kind() == reflect.Uint8
|
|
if nullable {
|
|
if ft.Kind() != reflect.Ptr && ft.Kind() != reflect.Slice {
|
|
// This never occurs for generated message types.
|
|
// Despite the protobuf type system specifying presence,
|
|
// the Go field type cannot represent it.
|
|
nullable = false
|
|
}
|
|
if ft.Kind() == reflect.Ptr {
|
|
ft = ft.Elem()
|
|
}
|
|
}
|
|
conv := NewConverter(ft, fd)
|
|
|
|
// TODO: Implement unsafe fast path?
|
|
fieldOffset := offsetOf(fs, x)
|
|
return fieldInfo{
|
|
fieldDesc: fd,
|
|
has: func(p pointer) bool {
|
|
if p.IsNil() {
|
|
return false
|
|
}
|
|
rv := p.Apply(fieldOffset).AsValueOf(fs.Type).Elem()
|
|
if nullable {
|
|
return !rv.IsNil()
|
|
}
|
|
switch rv.Kind() {
|
|
case reflect.Bool:
|
|
return rv.Bool()
|
|
case reflect.Int32, reflect.Int64:
|
|
return rv.Int() != 0
|
|
case reflect.Uint32, reflect.Uint64:
|
|
return rv.Uint() != 0
|
|
case reflect.Float32, reflect.Float64:
|
|
return rv.Float() != 0 || math.Signbit(rv.Float())
|
|
case reflect.String, reflect.Slice:
|
|
return rv.Len() > 0
|
|
default:
|
|
panic(fmt.Sprintf("field %v has invalid type: %v", fd.FullName(), rv.Type())) // should never happen
|
|
}
|
|
},
|
|
clear: func(p pointer) {
|
|
rv := p.Apply(fieldOffset).AsValueOf(fs.Type).Elem()
|
|
rv.Set(reflect.Zero(rv.Type()))
|
|
},
|
|
get: func(p pointer) pref.Value {
|
|
if p.IsNil() {
|
|
return conv.Zero()
|
|
}
|
|
rv := p.Apply(fieldOffset).AsValueOf(fs.Type).Elem()
|
|
if nullable {
|
|
if rv.IsNil() {
|
|
return conv.Zero()
|
|
}
|
|
if rv.Kind() == reflect.Ptr {
|
|
rv = rv.Elem()
|
|
}
|
|
}
|
|
return conv.PBValueOf(rv)
|
|
},
|
|
set: func(p pointer, v pref.Value) {
|
|
rv := p.Apply(fieldOffset).AsValueOf(fs.Type).Elem()
|
|
if nullable && rv.Kind() == reflect.Ptr {
|
|
if rv.IsNil() {
|
|
rv.Set(reflect.New(ft))
|
|
}
|
|
rv = rv.Elem()
|
|
}
|
|
rv.Set(conv.GoValueOf(v))
|
|
if isBytes && rv.Len() == 0 {
|
|
if nullable {
|
|
rv.Set(emptyBytes) // preserve presence
|
|
} else {
|
|
rv.Set(nilBytes) // do not preserve presence
|
|
}
|
|
}
|
|
},
|
|
newField: func() pref.Value {
|
|
return conv.New()
|
|
},
|
|
}
|
|
}
|
|
|
|
func fieldInfoForWeakMessage(fd pref.FieldDescriptor, weakOffset offset) fieldInfo {
|
|
if !flags.ProtoLegacy {
|
|
panic("no support for proto1 weak fields")
|
|
}
|
|
|
|
var once sync.Once
|
|
var messageType pref.MessageType
|
|
lazyInit := func() {
|
|
once.Do(func() {
|
|
messageName := fd.Message().FullName()
|
|
messageType, _ = preg.GlobalTypes.FindMessageByName(messageName)
|
|
if messageType == nil {
|
|
panic(fmt.Sprintf("weak message %v for field %v is not linked in", messageName, fd.FullName()))
|
|
}
|
|
})
|
|
}
|
|
|
|
num := fd.Number()
|
|
return fieldInfo{
|
|
fieldDesc: fd,
|
|
has: func(p pointer) bool {
|
|
if p.IsNil() {
|
|
return false
|
|
}
|
|
_, ok := p.Apply(weakOffset).WeakFields().get(num)
|
|
return ok
|
|
},
|
|
clear: func(p pointer) {
|
|
p.Apply(weakOffset).WeakFields().clear(num)
|
|
},
|
|
get: func(p pointer) pref.Value {
|
|
lazyInit()
|
|
if p.IsNil() {
|
|
return pref.ValueOfMessage(messageType.Zero())
|
|
}
|
|
m, ok := p.Apply(weakOffset).WeakFields().get(num)
|
|
if !ok {
|
|
return pref.ValueOfMessage(messageType.Zero())
|
|
}
|
|
return pref.ValueOfMessage(m.ProtoReflect())
|
|
},
|
|
set: func(p pointer, v pref.Value) {
|
|
lazyInit()
|
|
m := v.Message()
|
|
if m.Descriptor() != messageType.Descriptor() {
|
|
if got, want := m.Descriptor().FullName(), messageType.Descriptor().FullName(); got != want {
|
|
panic(fmt.Sprintf("field %v has mismatching message descriptor: got %v, want %v", fd.FullName(), got, want))
|
|
}
|
|
panic(fmt.Sprintf("field %v has mismatching message descriptor: %v", fd.FullName(), m.Descriptor().FullName()))
|
|
}
|
|
p.Apply(weakOffset).WeakFields().set(num, m.Interface())
|
|
},
|
|
mutable: func(p pointer) pref.Value {
|
|
lazyInit()
|
|
fs := p.Apply(weakOffset).WeakFields()
|
|
m, ok := fs.get(num)
|
|
if !ok {
|
|
m = messageType.New().Interface()
|
|
fs.set(num, m)
|
|
}
|
|
return pref.ValueOfMessage(m.ProtoReflect())
|
|
},
|
|
newMessage: func() pref.Message {
|
|
lazyInit()
|
|
return messageType.New()
|
|
},
|
|
newField: func() pref.Value {
|
|
lazyInit()
|
|
return pref.ValueOfMessage(messageType.New())
|
|
},
|
|
}
|
|
}
|
|
|
|
func fieldInfoForMessage(fd pref.FieldDescriptor, fs reflect.StructField, x exporter) fieldInfo {
|
|
ft := fs.Type
|
|
conv := NewConverter(ft, fd)
|
|
|
|
// TODO: Implement unsafe fast path?
|
|
fieldOffset := offsetOf(fs, x)
|
|
return fieldInfo{
|
|
fieldDesc: fd,
|
|
has: func(p pointer) bool {
|
|
if p.IsNil() {
|
|
return false
|
|
}
|
|
rv := p.Apply(fieldOffset).AsValueOf(fs.Type).Elem()
|
|
if fs.Type.Kind() != reflect.Ptr {
|
|
return !isZero(rv)
|
|
}
|
|
return !rv.IsNil()
|
|
},
|
|
clear: func(p pointer) {
|
|
rv := p.Apply(fieldOffset).AsValueOf(fs.Type).Elem()
|
|
rv.Set(reflect.Zero(rv.Type()))
|
|
},
|
|
get: func(p pointer) pref.Value {
|
|
if p.IsNil() {
|
|
return conv.Zero()
|
|
}
|
|
rv := p.Apply(fieldOffset).AsValueOf(fs.Type).Elem()
|
|
return conv.PBValueOf(rv)
|
|
},
|
|
set: func(p pointer, v pref.Value) {
|
|
rv := p.Apply(fieldOffset).AsValueOf(fs.Type).Elem()
|
|
rv.Set(conv.GoValueOf(v))
|
|
if fs.Type.Kind() == reflect.Ptr && rv.IsNil() {
|
|
panic(fmt.Sprintf("field %v has invalid nil pointer", fd.FullName()))
|
|
}
|
|
},
|
|
mutable: func(p pointer) pref.Value {
|
|
rv := p.Apply(fieldOffset).AsValueOf(fs.Type).Elem()
|
|
if fs.Type.Kind() == reflect.Ptr && rv.IsNil() {
|
|
rv.Set(conv.GoValueOf(conv.New()))
|
|
}
|
|
return conv.PBValueOf(rv)
|
|
},
|
|
newMessage: func() pref.Message {
|
|
return conv.New().Message()
|
|
},
|
|
newField: func() pref.Value {
|
|
return conv.New()
|
|
},
|
|
}
|
|
}
|
|
|
|
type oneofInfo struct {
|
|
oneofDesc pref.OneofDescriptor
|
|
which func(pointer) pref.FieldNumber
|
|
}
|
|
|
|
func makeOneofInfo(od pref.OneofDescriptor, si structInfo, x exporter) *oneofInfo {
|
|
oi := &oneofInfo{oneofDesc: od}
|
|
if od.IsSynthetic() {
|
|
fs := si.fieldsByNumber[od.Fields().Get(0).Number()]
|
|
fieldOffset := offsetOf(fs, x)
|
|
oi.which = func(p pointer) pref.FieldNumber {
|
|
if p.IsNil() {
|
|
return 0
|
|
}
|
|
rv := p.Apply(fieldOffset).AsValueOf(fs.Type).Elem()
|
|
if rv.IsNil() { // valid on either *T or []byte
|
|
return 0
|
|
}
|
|
return od.Fields().Get(0).Number()
|
|
}
|
|
} else {
|
|
fs := si.oneofsByName[od.Name()]
|
|
fieldOffset := offsetOf(fs, x)
|
|
oi.which = func(p pointer) pref.FieldNumber {
|
|
if p.IsNil() {
|
|
return 0
|
|
}
|
|
rv := p.Apply(fieldOffset).AsValueOf(fs.Type).Elem()
|
|
if rv.IsNil() {
|
|
return 0
|
|
}
|
|
rv = rv.Elem()
|
|
if rv.IsNil() {
|
|
return 0
|
|
}
|
|
return si.oneofWrappersByType[rv.Type().Elem()]
|
|
}
|
|
}
|
|
return oi
|
|
}
|
|
|
|
// isZero is identical to reflect.Value.IsZero.
|
|
// TODO: Remove this when Go1.13 is the minimally supported Go version.
|
|
func isZero(v reflect.Value) bool {
|
|
switch v.Kind() {
|
|
case reflect.Bool:
|
|
return !v.Bool()
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
return v.Int() == 0
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
|
return v.Uint() == 0
|
|
case reflect.Float32, reflect.Float64:
|
|
return math.Float64bits(v.Float()) == 0
|
|
case reflect.Complex64, reflect.Complex128:
|
|
c := v.Complex()
|
|
return math.Float64bits(real(c)) == 0 && math.Float64bits(imag(c)) == 0
|
|
case reflect.Array:
|
|
for i := 0; i < v.Len(); i++ {
|
|
if !isZero(v.Index(i)) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice, reflect.UnsafePointer:
|
|
return v.IsNil()
|
|
case reflect.String:
|
|
return v.Len() == 0
|
|
case reflect.Struct:
|
|
for i := 0; i < v.NumField(); i++ {
|
|
if !isZero(v.Field(i)) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
default:
|
|
panic(&reflect.ValueError{"reflect.Value.IsZero", v.Kind()})
|
|
}
|
|
}
|