这是一个 API 设计的思想实验,它从典型的 Go 单元测试惯用形式开始:

func TestOpenFile(t *testing.T) {
f, err := os.Open("notfound")
if err != nil {
t.Fatal(err)
} // ...
}

这段代码有什么问题?断言 if err != nil { ... } 是重复的,并且需要检查多个条件的情况下,如果测试的作者使用 t.Error 而不是 t.Fatal 的话会容易出错,例如:

f, err := os.Open("notfound")
if err != nil {
t.Error(err)
}
f.Close() // boom!

有什么解决方案?当然,通过将重复的断言逻辑移到辅助函数中,来达到 DRY(Don't Repeat Yourself)。

func TestOpenFile(t *testing.T) {
f, err := os.Open("notfound")
check(t, err) // ...
} func check(t *testing.T, err error) {
if err != nil {
t.Helper()
t.Fatal(err)
}
}

使用 check 辅助函数使得这段代码更简洁一些,并且更加清晰地检查错误,同时有望解决 t.Errort.Fatal 的混淆使用。 将断言抽象为一个辅助函数的缺点是,现在你需要将一个 testing.T 传递到每一个调用上。更糟糕的是,为了以防万一,你需要传递 *testing.T 到每一个需要调用 check 的地方。

我猜,这并没有关系。但我会观察到只有在断言失败的时候才会用到变量 t —— 即使在测试场景下,大多数时候,大部分的测试是通过的,因此在相对罕见的测试失败的情况下,会产生对这些变量 t 的固定读写开销。

如果我们这样做怎么样?

func TestOpenFile(t *testing.T) {
f, err := os.Open("notfound")
check(err) // ...
} func check(err error) {
if err != nil {
panic(err.Error())
}
}

是的,可以,但是有一些问题。

% go test
--- FAIL: TestOpenFile (0.00s)
panic: open notfound: no such file or directory [recovered]
panic: open notfound: no such file or directory goroutine 22 [running]:
testing.tRunner.func1(0xc0000b4400)
/Users/dfc/go/src/testing/testing.go:874 +0x3a3
panic(0x111b040, 0xc0000866f0)
/Users/dfc/go/src/runtime/panic.go:679 +0x1b2
github.com/pkg/expect_test.check(...)
/Users/dfc/src/github.com/pkg/expect/expect_test.go:18
github.com/pkg/expect_test.TestOpenFile(0xc0000b4400)
/Users/dfc/src/github.com/pkg/expect/expect_test.go:10 +0xa1
testing.tRunner(0xc0000b4400, 0x115ac90)
/Users/dfc/go/src/testing/testing.go:909 +0xc9
created by testing.(*T).Run
/Users/dfc/go/src/testing/testing.go:960 +0x350
exit status 2

先从好的方面说起,我们不需要传递一个 testing.T 到每一个调用 check 函数的地方,且测试会立即失败。我们还从 panic 中获得了一条不错的信息 —— 尽管重复出现了两次。但是,哪里断言失败却不容易看到。它发生在 expect_test.go:11,你知道这一点是不可以原谅的。

所以 panic 不是一个好的解决办法,但是你能从堆栈跟踪信息里面看到什么有用的信息吗?这有一个提示:github.com/pkg/expect_test.TestOpenFile(0xc0000b4400)

TestOpenFile 有一个 t 的值,它由 tRunner 传递过来,所以 testing.T 在内存中位于地址 0xc0000b4400 上。如果我们可以在 check 函数内部获取 t 会怎样?那我们可以通过它来调用 t.Helper 来 t.Fatal。这可能吗?

动态作用域

我们想要的是能够访问一个变量,而该变量的申明既不是在全局范围,也不是在函数局部范围,而是在调用堆栈的更高的位置上。这被称之为动态作用域。Go 并不支持动态作用域,但事实证明,某些情况下,我们可以模拟它。回到正题:

// getT 返回由 testing.tRunner 传递过来的 testing.T 地址
// 而调用 getT 的函数由它(tRunner)所调用. 如果在堆栈中无法找到 testing.tRunner
// 说明 getT 在主测试 goroutine 没有被调用,
// 这时 getT 返回 nil.
func getT() *testing.T {
var buf [8192]byte
n := runtime.Stack(buf[:], false)
sc := bufio.NewScanner(bytes.NewReader(buf[:n]))
for sc.Scan() {
var p uintptr
n, _ := fmt.Sscanf(sc.Text(), "testing.tRunner(%v", &p)
if n != 1 {
continue
}
return (*testing.T)(unsafe.Pointer(p))
}
return nil
}

我们知道每个测试(Test)由 testing 包在自己的 goroutine 上调用(看上面的堆栈信息)。testing 包通过一个名为 tRunner 的函数来启动测试,该函数需要一个testing.T 和一个 func(testing.T)来调用。因此我们抓取当前 goroutine 的堆栈信息,从中扫描找到已 testing.tRunner 开头的行——由于 tRunner 是私有函数,只能是 testing 包——并解析第一个参数的地址,该地址是一个指向 testing.T 的指针。有点不安全,我们将这个原始指针转换为一个 *testing.T 我们就完成了。

如果搜索不到则可能是 getT 并不是被 Test 所调用。这实际上是行的通的,因为我们需要*testing.T 是为了调用 t.Fatal,而 testing 包要求 t.Fatal 被主测试 goroutine所调用。

import "github.com/pkg/expect"

func TestOpenFile(t *testing.T) {
f, err := os.Open("notfound")
expect.Nil(err) // ...
}

综上,在预期打开文件所产生的 err 为 nil 后,我们消除了断言样板,并且是测试看起来更加清晰易读。

这样好吗?

这时你应该会问,这样好吗?答案是,不,这不好。此时你应该会感到震惊,但是这些不好的感觉可能值得反思。除了在 goroutine 的调用堆栈乱窜的固有不足以外,同样存在一些严重的设计问题:

  1. expect.Nil 的行为依赖于谁调用它。同样的参数,由于调用堆栈位置的原因可能导致行为的不同——这是不可预期的。
  2. 采取极端的动态作用域,将传递给单个函数之前的所有函数的所有变量纳入单个函数的作用域中。这是一个在函数申明没有明确记录的情况下将数据传入和传出的辅助手段。

讽刺的是,这恰恰是我对context.Context的评价。我会将这个问题留给你自己判断是否合理。

最后的话

这是个坏主意,这点没有异议。这不是你可以在生产模式中使用的模式。但是,这也不是生产代码。这是在测试,也许有着不同的规则适用于测试代码。毕竟,我们使用模拟(mocks)、桩(stubs)、猴子补丁(monkey patching)、类型断言、反射、辅助函数、构建标志以及全局变量,所有这些使得我们更加有效率得测试代码。所有这些,奇技淫巧是不会让它们出现在生产代码里面的,所以这真的是世界末日吗?

如果你读完本文,你也许会同意我的观点,尽管不太符合常规,并无必要将*testing.T 传递到所有需要断言的函数中去,从而使测试代码更加清晰。

如果你感兴趣,我已分享了一个应用这个模式的小的断言库。小心使用。


via: https://studygolang.com/subject/1?p=1

作者:Dave Cheney 译者:dust347 校对:unknwon

本文由 GCTT 原创编译,[Go语言中文网

Go 中的动态作用域变量的更多相关文章

  1. data.table 中的动态作用域

    data.table 中最常用的语法就是 data[i, j, by],其中 i.j 和 by 都是在动态作用域中被计算的.换句话说,我们不仅可以直接使用列,也可以提前定义诸如 .N ..I 和 .S ...

  2. 词法作用域 vs 动态作用域

    词法作用域 vs 动态作用域 链接:https://www.jianshu.com/p/cdebb5965000 scheme是一门采用词法作用域(lexical scoping)的lisp方言,这个 ...

  3. php中在局部作用域内访问全局变量

    php中,由于作用域的限制,导致变量的访问限制: 1.局部作用域内不能访问全局变量 2.全局作用域内不能访问局部变量 对于第一种情况,如下代码将不能正常运行: <?php //局部作用域(函数内 ...

  4. Javascript中的词法作用域、动态作用域、函数作用域和块作用域(四)

    一.js中的词法作用域和动态作用域      词法作用域也就是在词法阶段定义的作用域,也就是说词法作用域在代码书写时就已经确定了.       js中其实只有词法作用域,并没有动态作用域,this的执 ...

  5. Spark中Lambda表达式的变量作用域

    通常,我们希望能够在lambda表达式的闭合方法或类中访问其他的变量,例如: package java8test; public class T1 { public static void main( ...

  6. C++中 auto自己主动变量,命名空间,using作用以及作用域

     1.autokeyword的用途 A:自己主动变量.能够自己主动获取类型,输出,类似泛型 B:自己主动变量,能够实现自己主动循环一维数组 C:自己主动循环的时候,相应的必须是常量 2.auto自 ...

  7. 《浏览器工作原理与实践》<10>作用域链和闭包 :代码中出现相同的变量,JavaScript引擎是如何选择的?

    在上一篇文章中我们讲到了什么是作用域,以及 ES6 是如何通过变量环境和词法环境来同时支持变量提升和块级作用域,在最后我们也提到了如何通过词法环境和变量环境来查找变量,这其中就涉及到作用域链的概念. ...

  8. 动态作用域与this +apply和call +bind

    词法作用域是一套关于引擎如何寻找变量以及会在何处找到变量的规则. (函数作用域和块作用域) JavaScript 中的作用域就是词法作用域,也就是静态作用域,由定义代码决定 动态作用域似乎暗示有很好的 ...

  9. 深入理解javascript作用域系列第二篇——词法作用域和动态作用域

    × 目录 [1]词法 [2]动态 前面的话 大多数时候,我们对作用域产生混乱的主要原因是分不清楚应该按照函数位置的嵌套顺序,还是按照函数的调用顺序进行变量查找.再加上this机制的干扰,使得变量查找极 ...

随机推荐

  1. L-BFGS算法详解(逻辑回归的默认优化算法)

    python信用评分卡建模(附代码,博主录制) https://study.163.com/course/introduction.htm?courseId=1005214003&utm_ca ...

  2. tensorflow对鸢尾花进行分类——人工智能入门篇

    tensorflow之对鸢尾花进行分类 任务目标 对鸢尾花数据集分析 建立鸢尾花的模型 利用模型预测鸢尾花的类别 环境搭建 pycharm编辑器搭建python3.* 第三方库 tensorflow1 ...

  3. JVM——内存区域:运行时数据区域详解

    关注微信公众号:CodingTechWork,一起学习进步. 引言   我们经常会被问到一个问题是Java和C++有何区别?我们除了能回答一个是面向对象.一个是面向过程编程以外,我们还会从底层内存管理 ...

  4. Web Security Academy ___XXE injection___Lab

    实验网站:https://portswigger.net/web-security/xxe XXE学习看一参考下面这篇文章,讲得很全: https://xz.aliyun.com/t/3357#toc ...

  5. 设计模式:Adapter模式

    目的:复用代码和兼容以前的代码 思想:提供一个中间层,做兼容 方法:“继承”的方式,“委托”的方式 继承关系图: 委托方式 继承方式 例子: //原来的打印 class Print { public: ...

  6. python-多任务编程04-生成器(generator)

    生成器是一类特殊的迭代器,创建方法比自定迭代器类更加简单 使用()创建生成器 把列表生成式的 [ ] 改成 ( ) In [15]: L = [ x*2 for x in range(5)] In [ ...

  7. asp.net core 3 使用nlog日志组件,使用$ {basedir}保存位置不对,记录下怎么解决

    $ {basedir}指向的是  AppDomain.CurrentDomain.BaseDirectory, Asp.Net.Core的解决方法可能如下(在Program.cs中添加两行): var ...

  8. jmeter接口测试 -- 设置跨线程组的全局变量

    一.操作步骤 1.先提取被设置的变量 2.再用 [线程组] - [后置处理] - [BeanShell PostProcessor]来设置跨线程的全局变量:${__setProperty(新变量名,$ ...

  9. TCP-三次握手和四次挥手简单理解

    TCP-三次握手和四次挥手简单理解 背景:TCP,即传输控制协议,是一种面向连接的可靠的,基于字节流的传输层协议.作用是在不可靠的互联网络上提供一个可靠的端到端的字节流服务,为了准确无误的将数据送达目 ...

  10. Python异常及异常处理

    Python异常及异常处理: 当程序运行时,发生的错误称为异常 例: 0 不能作为除数:ZeroDivisionError 变量未定义:NameError 不同类型进行相加:TypeError 异常处 ...