Golang Go语言中如何操作结构体的非导出字段

发布于 1周前 作者 sinazl 来自 Go语言

Golang Go语言中如何操作结构体的非导出字段

Dig101: dig more, simplified more and know more

我们都知道Gostruct里,小写字段是非导出的,即不可从包外部访问。

但非导出字段在外部也并不是没有办法访问,也不是不可以修改。

今天看下reflect包如何在包外操作非导出字段。

取地址访问

先来看第一个函数NewAt

对于结构体,通过其底层地址(指针 p )和类型,返回指向该结构体的一个指针

该值是可寻址的(addressable),即可访问该结构体

// reflect/value.go
// NewAt returns a Value representing a pointer to a value of the
// specified type, using p as that pointer.
func NewAt(typ Type, p unsafe.Pointer) Value {
  fl := flag(Ptr)
  t := typ.(*rtype)
  return Value{t.ptrTo(), p, fl}
}

有个这个方法,就可以通过struct的反射获取非导出字段

比如访问,对于如下含有非导出字段的结构体Example

package testData
type Example struct {
  a string
}

便可以通过对结构体eg取地址的方式,获取其非导出字段a的内容

这里Elem是获取其底层数据对象的方式,

如果知道类型,也可显示指定调用,如reflect.value.Interface,reflect.value.Int...

var eg testData.Example
a:=GetStructPtrUnExportedField(&eg, "a").String()

func GetStructPtrUnExportedField(source interface{}, fieldName string) reflect.Value { // 获取非导出字段反射对象 v := reflect.ValueOf(source).Elem().FieldByName(fieldName) // 构建指向该字段的可寻址( addressable )反射对象 return reflect.NewAt(v.Type(), unsafe.Pointer(v.UnsafeAddr())).Elem() }

这里注意必须要对eg取地址, 否则会panic:

panic: reflect: call of reflect.Value.Elem on struct Value

因为reflect.Value.Elem需要reflect.Value类型必须是interface或者ptr,

这样获取其底层的值才有意义:要么返回interface底层的值或者ptr指向的值

其注释如下:

// Elem returns the value that the interface v contains
// or that the pointer v points to.
// It panics if v's Kind is not Interface or Ptr.
// It returns the zero Value if v is nil.
func (v Value) Elem() Value {

取地址修改

那可以访问了,如何修改呢?

利用reflect.value.Set就可以:

上边Elem获取到的反射值是可修改的(assignable),突破了非导出字段不能从外部修改的限制

var eg testData.Example
err := SetStructPtrUnExportedStrField(&eg, "a", "test")

func SetStructPtrUnExportedStrField(source interface{}, fieldName string, fieldVal interface{}) (err error) { v := GetStructPtrUnExportedField(source, fieldName) rv := reflect.ValueOf(fieldVal) if v.Kind() != rv.Kind() { return fmt.Errorf(“invalid kind: expected kind %v, got kind: %v”, v.Kind(), rv.Kind()) } // 修改非导出字段值 v.Set(rv) return nil }

这里是以反射值来修改非导出字段值,内部类型须一致。修改后内容会直接反应到eg

类似的还有指定类型的设置方法如SetString,SetBool...

非取地址访问

当然不取地址也是可以访问非导出字段的。

这里用到的第二个函数是New:

基于指定类型创建一个可以表示该类型的指针

// New returns a Value representing a pointer to a new zero value
// for the specified type. That is, the returned Value's Type is PtrTo(typ).
func New(typ Type) Value {
  if typ == nil {
    panic("reflect: New(nil)")
  }
  t := typ.(*rtype)
  ptr := unsafe_New(t)
  fl := flag(Ptr)
  return Value{t.ptrTo(), ptr, fl}
}

具体访问代码如下:

func GetStructUnExportedField(source interface{}, fieldName string) (accessableField, addressableSourceCopy reflect.Value) {
  v := reflect.ValueOf(source)
  // since source is not a ptr, get an addressable copy of source to modify it later
    addressableSourceCopy = reflect.New(v.Type()).Elem()
    // make a copy of source
  addressableSourceCopy.Set(v)
  accessableField = addressableSourceCopy.FieldByName(fieldName)
  accessableField = reflect.NewAt(accessableField.Type(), unsafe.Pointer(accessableField.UnsafeAddr())).Elem()
  return
}

这样其实是内部构造了一个对该结构其取地址的指针,以满足后续调用Elem时可寻址!

非取地址修改

非取地址的方式访问没有问题,要还想修改就不会反应到原始结构体上了

毕竟是内部重新拷贝了一个结构体进行的操作。

具体操作类似取地址修改的方式,这里不赘述了。

实际使用中,还是通过NewAt获取可读写的非导出字段更方便一些。

本文代码见 NewbMiao/Dig101-Go


文章首发公众号:newbmiao

推荐阅读:Dig101-Go 系列

欢迎关注,获取及时更新内容


更多关于Golang Go语言中如何操作结构体的非导出字段的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html

8 回复

应该把 Go 1.15 的变动放进来 23333

更多关于Golang Go语言中如何操作结构体的非导出字段的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


都特意小写了就是不让你访问修改。真要是有这个需求请直接 issue 或者 pr 利己利民。奇葩需求也可以直接 fork 。

走非常规路线注定有踩不完的坑 想起了当初对浮点型比较 出现的偶然性失灵…

有什么意义。。。

我是直接建立一个同样的结构,成员变量全导出。
直接强制转换类型。

反射操作这么常见的功能。。。

没办法,有时你用的 sdk 非导出字段需要短期 hack 一下。不是很多小众需求都可以 PR 进去的。fork 还要自己维护更新。。。

在Go语言中,非导出字段(即字段名以小写字母开头)是包私有的,这意味着它们只能在定义它们的包内部被访问和修改。这是Go语言封装特性的一部分,旨在保护数据不被外部随意修改。

如果你需要在包外部操作结构体的非导出字段,有几种常见的解决方案:

  1. 提供getter和setter方法:这是最常见和推荐的方式。通过为结构体定义公共的getter和setter方法,你可以控制对私有字段的访问,同时保持封装性。

  2. 使用接口:如果结构体需要在多个包之间共享,并且需要访问某些非导出字段,可以考虑定义一个接口,并在接口中声明需要的方法。这样,其他包可以通过接口来操作这些字段,而不需要直接访问结构体。

  3. 反射(Reflection):虽然Go的反射包(reflect)允许你在运行时检查和修改结构体的字段,包括非导出字段,但这通常是不推荐的。反射会破坏封装性,使代码难以理解和维护,且可能引入运行时错误。

总之,直接操作非导出字段通常是不被鼓励的,因为这违反了Go语言的封装原则。相反,你应该通过提供公共的getter和setter方法,或者使用接口来间接地访问和修改这些字段。这样不仅可以保持代码的清晰和可维护性,还可以避免潜在的运行时错误。

回到顶部