golang小寄巧-如何从模块外部修改结构体中的私有成员

目录

  1. 1. 为啥不用反射
  2. 2. 使用 unsafe 解决赋值问题
    1. 2.1. 获取偏移量
    2. 2.2. 测试

这个属于奇技淫巧,不建议项目中使用。如果要用一定要带上注释。

最近在搞 coredns 的开发,代码嘎嘎敲完之后开始啃单元测试。

在写测试的时候,需要测试的某个函数,我们这里假设是 func abc(r request.Request) ,它接收 coredns 的 request.Request 类型,在内部调用的 r.Name() r.IP() 获取请求域名和请求方 ip,但是对应单测文件的传入参数在我接手的时候写的还是字符串类型,估计好久没改过了。

这是 request.Request 的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Request contains some connection state and is useful in plugin.
type Request struct {
Req *dns.Msg
W dns.ResponseWriter

// Optional lowercased zone of this query.
Zone string

// Cache size after first call to Size or Do. If size is zero nothing has been cached yet.
// Both Size and Do set these values (and cache them).
size uint16 // UDP buffer size, or 64K in case of TCP.
do bool // DNSSEC OK value

// Caches
family int8 // transport's family.
name string // lowercase qname.
ip string // client's ip.
port string // client's port.
localPort string // server's port.
localIP string // server's ip.
}

想要测试这个函数,就需要向其中传入有效的 Request 结构体,但是问题是 name 和 ip 在里面都是小写字母开头,属于私有变量,也没有一个方便的方法可以在新建 Request 时初始化 name 和 ip。

为啥不用反射

写过 java,看到这个问题,第一个想法自然是使用反射来获取对应的成员赋值。但是理想和现实总有点差距,查了才知道,golang 中的反射限制有点多。

java 中反射的作用很强大,又可以取值又可以赋值。golang 中的反射只可以获取已知名称字段的值,如果想要给私有成员赋值就会抛出 panic 炸掉程序。(公有成员没事)

使用 unsafe 解决赋值问题

好在 golang 中还可以获取变量的指针,这样我们可以通过获取 Request 的 unsafe 指针来通过偏移量修改其成员的值。(奇技淫巧,如无必要,不宜使用)

获取偏移量

观察数据结构发现,name 和 ip 和结构体的尾部仅相差几个字符串的偏移量。因此采用修改 基址+sizeof(Request)-n*sizeof(string) 的格式来定位对应地址。

测试

先写个 demo 测试一下,鉴于 name 和 ip 分别是结构体倒数第五和第四个成员,因此下面 (unsafe.Sizeof(*new(string))) 需要分别乘 5 和乘 4

1
2
3
4
5
6
7
8
9
10
11
func GenTestRequest(name string) request.Request {
req_name := name
req_name_bytes := []byte(req_name)
req_ip := "127.0.0.1"
req_ip_bytes := []byte(req_ip)
test_req := request.Request{}
p := unsafe.Pointer(&test_req)
*(*[]byte)(unsafe.Pointer(uintptr(p) + uintptr(unsafe.Sizeof(request.Request{})) - uintptr(unsafe.Sizeof(*new(string)))*5)) = req_name_bytes
*(*[]byte)(unsafe.Pointer(uintptr(p) + uintptr(unsafe.Sizeof(request.Request{})) - uintptr(unsafe.Sizeof(*new(string)))*4)) = req_ip_bytes
return test_req
}

打上断点,直接开跑

可以看到,对应的私有变量已经被修改成我们想要的值