Golang 冻结时间
部分单元测试的时候,需要考虑时间的影响,有时需要我们冻结时间,或者整个测试过程中设定时间的返回数据。
因此需要考虑,如何在不修改原有逻辑的情况下冻结时间,或者替换时间函数。
update list
2022年05月12日12:57:50: add REPO
已经在这里实现了,注意 方法非常不安全。
https://github.com/MXuDong/test-utils-go
效果
大家可以看到已经可以直接停止掉时间。
原理
方案1 类似动态语言的操作,直接修改机器码
另外编译的时候需要禁用内联:-gcflags=-l
当前方案有一定问题,对不同平台的支持性有待考察。
根据 golang plan9 汇编的描述,我们可以得知,调用函数的实际过程中本质上是对函数执行了 call 的操作。
那么理论上来讲,在执行 0x108b9e0 的逻辑地址时,将 main.a(SB) 更换为 其它函数,操作是存在可行性的。我们可以直接 jmp 到我们的对应函数上。
具体实现方案,等确定平台兼容性后再做处理。
另外,本质上这个操作是直接控制内存,因此,不要再线上模式中使用!
甚至我认为直接覆盖原来的方法来达到某些奇怪的目的好像也是可以得了。
代码:
// stopTime will replace the time function, and always return yeas first time.
func stopTime(year int) {
timeFunc := time.Now
// custom function
value = year // value is a 问题。。。。。
f := func() time.Time {
return time.Date(value, 1, 1, 00, 00, 00, 00, time.UTC)
}
timeFuncLocation := **(**uintptr)(unsafe.Pointer(&timeFunc))
timeFuncWindow := (*(*[0xFF]byte)(unsafe.Pointer(timeFuncLocation)))[:]
// disable in-line and given permission to operator the memory
err := syscall.Mprotect(
(*(*[0xFFFFFF]byte)(unsafe.Pointer(timeFuncLocation & ^uintptr(syscall.Getpagesize()-1))))[:syscall.Getpagesize()],
syscall.PROT_READ|syscall.PROT_WRITE|syscall.PROT_EXEC)
if err != nil {
fmt.Println(err)
}
newTimeJumpAssemble := *(*uintptr)(unsafe.Pointer(&f))
funcJumpAssemble := []byte{
0x48, 0xC7, 0xC2,
byte(newTimeJumpAssemble >> 0),
byte(newTimeJumpAssemble >> 8),
byte(newTimeJumpAssemble >> 16),
byte(newTimeJumpAssemble >> 24), // MOV rdx, fn address byte (use to 32 bytes)
0xFF, 0x22, // JMP [rdx]
}
copy(timeFuncWindow, funcJumpAssemble)
}
// value: the default year value, and define it as global value.
var value = 200
func main() {
// 未冻结时间
println(time.Now().String())
time.Sleep(1 * time.Second)
println(time.Now().String())
//time.Sleep(10 * time.Second)
println(time.Now().String())
//冻结时间
stopTime(2030)
println(time.Now().String())
time.Sleep(1 * time.Second)
println(time.Now().String())
//time.Sleep(10 * time.Second)
println(time.Now().String())
}
方案2 使用原生的 time_fake.go
golang 的 runtime 包下的 time.go 提供了最基础的获取当前时间的函数,大部分获取时间的操作本质上都是调用了该文件的该方法执行的。
官方已经提供了一个用于 fake 时间的文件,在编译的时候加上参数即可 ‘faketime’。
但是这种方式使用场景有限。