在包名和变量中,是否应该避免使用阴影效果?

huangapple go评论126阅读模式
英文:

Should shadowing be avoided in with package names and variables?

问题

测试代码中有很多示例,这些示例会遮蔽测试库(如testify)中的包名。

例如:

  1. import (
  2. "testing"
  3. "github.com/stretchr/testify/assert"
  4. "github.com/stretchr/testify/require"
  5. )
  6. func TestSomething(t *testing.T) {
  7. assert := assert.New(t)
  8. require := require.New(t)
  9. var a string = "Hello"
  10. var b string = "Hello"
  11. assert.Equal(a, b, "The two words should be the same.")
  12. }

覆盖包命名空间的局部变量应该是一个明确的决策和罕见的情况吗?

  1. import (
  2. "testing"
  3. "github.com/stretchr/testify/assert"
  4. "github.com/stretchr/testify/require"
  5. )
  6. func TestSomething(t *testing.T) {
  7. assertions := assert.New(t)
  8. req := require.New(t)
  9. var a string = "Hello"
  10. var b string = "Hello"
  11. assert.Equal(a, b, "The two words should be the same.")
  12. }

我期望正确的行为是给包起一个别名,比如pkgassert,但这并不符合惯用方式,或者确保变量的命名不与包名相同:assertions := assert.New(t)

其他示例:

  • 好的:buf := bytes.Buffer{}
  • 不好的:bytes := bytes.Buffer{}

注意:我并不关注变量命名模式,因为Go倾向于在缩小的作用域中使用单个字母或短变量名。假设由于函数长度的原因,较长的名称是可接受和期望的。

如果这是测试包中的期望行为,我想知道原因是什么?这似乎违反了Go的一个关键原则,即“没有魔法”,并且可读性差。

如果这样做会产生不希望的副作用,我也想知道。我假设这意味着在导入后,同一作用域中对包的任何调用都不起作用,因为变量遮蔽了它。

编辑:

Scope Shadowing in Go 是一篇很好的文章,提供了一些关于使用的重要观点。我从这篇文章和Stack的评论中得出的主要结论是:

  1. 它可以增强可读性。(我认为这可能是因为Go更喜欢对包使用小而简洁的命名,使得Go中的包名长度可变。)
  2. Parallel Test Issues 是我以前阅读过的一些内容。我不理解将循环变量“隐藏”起来的建议,方法是在循环内部进行赋值:
  1. for _, tc := range tests {
  2. tc := tc
  3. t.Run(tc.name, func(t *testing.T) {
  4. t.Parallel()
  5. // 在这里,你可以将tc.value与一个测试函数进行比较。
  6. // 让我们使用t.Log作为我们的测试函数 :-)
  7. t.Log(tc.value)
  8. })
  9. }

通过对遮蔽的理解更深入,我发现“隐藏”是指在循环内部赋值,从而遮蔽了调用者作用域中的tc变量。

这似乎是一个很好的用例,尽管如果没有注释,我仍然觉得它有点“聪明”,并且违反了Go的惯用目标,即清晰、可读且没有“魔法”。

英文:

There's a lot of examples in test code that are shadowing package names in test libraries like testify.

For example:

  1. import (
  2. "testing"
  3. "github.com/stretchr/testify/assert"
  4. "github.com/stretchr/testify/require"
  5. )
  6. func TestSomething(t *testing.T) {
  7. assert := assert.New(t)
  8. require := require.New(t)
  9. var a string = "Hello"
  10. var b string = "Hello"
  11. assert.Equal(a, b, "The two words should be the same.")
  12. }

Shouldn't it be an explicit decision and rare occurrence to override the value of a package namespace with a local variable?

  1. import (
  2. "testing"
  3. "github.com/stretchr/testify/assert"
  4. "github.com/stretchr/testify/require"
  5. )
  6. func TestSomething(t *testing.T) {
  7. assertions := assert.New(t)
  8. req := require.New(t)
  9. var a string = "Hello"
  10. var b string = "Hello"
  11. assert.Equal(a, b, "The two words should be the same.")
  12. }

I'd expect the proper behavior to be aliasing the package like pkgassert which I doesn't feel idiomatic, or ensuring that variables are not set to the same name as the package: assertions := assert.New(t).

Other examples:

  • Ok: buf := bytes.Buffer{}
  • Bad: bytes := bytes.Buffer{}

Note: I'm not focused on variable naming patterns, as Go tends to recommend single letter, or short variables in the narrowed scope you might use the buffer for. Assume the longer name is acceptable and desired due to the function length.

If this is a desired behavior in testing packages I'd like to know why?
It appears to violate a key principle of idiomatic Go in "no magic" and being readable.

If there are undesired side effects from this I'd like to know of that as well.
My assumption is that is means after import that any calls to the package in the same scope wouldn't work due to variable shadowing

Edit

Scope Shadowing in Go was a nice read and gave some great points on usage. The main takeaway I had from this and from Stack commenters:

  1. It can enhance readability. (I think this is probably due to Go preferring small concise naming for packages, making package names in Go variable length.)

  2. Parallel Test Issues was something I recall reading a while back. I didn't understand the recommendation to "hide" the loop variable by putting in:

    1. for _, tc := range tests {
    2. tc := tc
    3. t.Run(tc.name, func(t *testing.T) {
    4. t.Parallel()
    5. // Here you test tc.value against a test function.
    6. // Let's use t.Log as our test function :-)
    7. t.Log(tc.value)
    8. })
    9. }

    }

With a better understanding of shadowing, I'm seeing this "hiding" was talking about shadowing the caller scope variable of tc by assigning inside the loop, thereby shadowing.

This seems like a good use case, though I still feel it is being "clever" rather if not commented, and violates the idiomatic Go goals of being clear and readable with no "magic".

答案1

得分: 1

哇,那是糟糕的代码。虽然这可能不是编译器强制执行的规则,但如果我看到这样的代码:

  1. import "github.com/stretchr/testify/assert"

那么在该文件中,就我而言,任何拼写为assert的标识符都是来自该包的顶级导入,没有例外。任何违反这个模式的人,以“聪明”或其他理由为名,都只是在自找麻烦。人们的肌肉记忆已经训练成识别顶级导入,所以,请不要做这样的事情。如果你想避免思考一个新的名称,只需删除一些字母:

  1. asrt := assert.New(t)

或者添加一个字母:

  1. nAssert := assert.New(t)

或者给导入起一个别名:

  1. import tAssert "github.com/stretchr/testify/assert"

请不要按照那段糟糕代码中的不良示例去做。

英文:

Wow, that is awful code. While it might not be a compiler enforced rule, if I see this:

  1. import "github.com/stretchr/testify/assert"

Then in that file, as far as I'm concerned, any identifier spelled assert is the top level import from that package, period. Anyone who breaks that pattern, in the name of being "clever" or whatever, is just asking for trouble. People's muscle memory is trained to recognize top level imports, so please, do not do stuff like this. If you want to avoid thinking of a new name, just remove some letters:

  1. asrt := assert.New(t)

or add a letter:

  1. nAssert := assert.New(t)

or alias the import:

  1. import tAssert "github.com/stretchr/testify/assert"

Just please, do not follow the poor example given in that code.

答案2

得分: 1

在审查了所有的反馈和我看到的写作之后,我将在这里总结一下调查结果。

如果你在看到像Goland这样的工具的Go静态检查警告后调查这种行为,希望这个答案能节省你一些时间,并清楚地表明这是可以接受的、符合惯用法的,在某些情况下,只有在初始化后重用实际包调用时才会遇到问题。

使用局部变量遮蔽包名

  • 这是一个观点问题,没有官方声明表明这被认为是非惯用代码。

  • 总体上,人们似乎倾向于避免遮蔽,除非出于清晰和可读性的目的而有意这样做,但这又是一个观点,而不是要求。

  • 一旦被覆盖,该作用域中的包将不可用,因为局部变量已经遮蔽了包名。

  • 我观察到这种行为在Go中似乎比某些语言更常见,因为Go提示的包名很短,不是多个单词,容易输入。当发生这种情况时,似乎不可避免地会出现assert := assert.New(t)

  • 一如既往,在这种情况下,一致性是关键。如果一个团队决定永不遮蔽包名,那么可以使用assertions := assert.New(t),或者如上所述,可以给包起别名,例如import tassert "github.com/stretchr/testify/assert"。这是一个观点标准,无论哪种方式都是合法的,也被认为是惯用的。

  • 在测试的情况下,这通常是在初始变量设置之后进一步使用包的情况下才会这样做。下面是使用is is包的示例。包的作者在这里发表了评论(再次强调这是有观点的,但使用is := is.New(t)不会破坏任何东西)。

  1. package package_test
  2. import (
  3. "testing"
  4. "package"
  5. iz "github.com/matryer/is"
  6. )
  7. func TestFuncName(t *testing.T) {
  8. is := iz.New(t)
  9. got := FuncName()
  10. want := ""
  11. is.Equal(got, want) // AssertMessage
  12. }

遮蔽行为可以解决Goroutine变量问题

遮蔽是一种已知的行为,并在Effective Go中有明确的文档。

> 这个错误是在Go的for循环中,循环变量在每次迭代中被重用,所以req变量在所有goroutine之间是共享的。这不是我们想要的。

> 写req := req可能看起来很奇怪,但在Go中这是合法且惯用的做法。你会得到一个具有相同名称的新变量版本,有意地在本地遮蔽循环变量,但对每个goroutine来说是唯一的。

这可以通过将变量作为参数传递到嵌套函数中来解决,简化代码。根据指南,这两种方法都是合法的和惯用的。

用于测试的遮蔽

与goroutine行为相关,但在测试的上下文中,平行测试在goroutine中运行,因此也受益于这种模式。

> 在平行测试示例中,tc := tc语句是必需的,因为闭包在goroutine中运行。更一般地说,在闭包的上下文中使用tc := tc习语时,闭包可以在下一个循环迭代开始后执行-感谢评论中的@gopher。

Be Careful With Table Driven Tests中提到了这种行为,以及How To Solve This

检测

检查go vet可以检查遮蔽,但它被认为是稳定包的一部分,并且已经在这里被移除,与GitHub Issue 29260GitHub Issue 34053有关。这个改变似乎是出于兼容性的考虑,因为需要unsafeptr标志的要求。其他评论似乎指向在不作为实验性功能的情况下允许在go vet中使用之前需要更好的启发式算法。

  1. go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow
  2. go vet -vettool=$(which shadow)

对于那些想要对此进行lint的人,golangci-lint配置中有一个选项来配置遮蔽检查(在文本块中搜索check-shadowing,没有书签指向该文本块)。

进一步阅读

英文:

After reviewing all the feedback and writing I've seen I'm going to summarize the findings here.

If you are investigating this behavior after seeing Go linting alerts from a tool like Goland, hopefully this answer will save you same time and make clear that it's ok, idiomatic, and only in some cases of reusing the actual package calls after initializing would any issues be experienced.

Shadowing Package Names With Local Variables

  • This is a matter of opinion, there is no official statement indicating this is considered non-idiomatic code.

  • Opinions seem to overall lean towards avoiding shadowing unless intentionally done for clarity & readability, but this is again opinion, not a requirement.

  • Once overridden, the package would be unavailable in that scope as the package name has been shadowed by the local variable.

  • I've observed that this behavior seems more likely in Go than some languages, because Go prompts package names that are short, not multi-word, and easy to type. It seems inevitable that assert := assert.New(t) could result when this happens.

  • Consistency, as always in matters like this, is the key. If a team determines to never shadow the package name, then assertions := assert.New(t) could be used, or as mentioned the package could be aliased such as import tassert "github.com/stretchr/testify/assert". This is an opinion standard, and either way is legal and considered idiomatic.

  • In the case of testing, this is commonly done as further usage of the package after the initial variable set isn't used further. Example below using the is is package. Package author comment on this (again this is opinionated, but it doesn't break anything to use it with is := is.New(t).

  1. package package_test
  2. import (
  3. "testing"
  4. "package"
  5. iz "github.com/matryer/is"
  6. )
  7. func TestFuncName(t *testing.T) {
  8. is := iz.New(t)
  9. got := FuncName()
  10. want := ""
  11. is.Equal(got,want) // AssertMessage
  12. }

Shadowing Behavior Can Address Goroutine Variable Issues

Shadowing is a known behavior and documented clearly in Effective Go.

> The bug is that in a Go for loop, the loop variable is reused for each iteration, so the req variable is shared across all goroutines. That's not what we want.

> It may seem odd to write req := req but it's legal and idiomatic in Go to do this. You get a fresh version of the variable with the same name, deliberately shadowing the loop variable locally but unique to each goroutine.

This can be solved without shadowing by passing in the variable as a parameter into the embedded func as well, simplifying the code. Both are legal and considered idiomatic per the guide.

Shadowing for Tests

Related to this goroutine behavior, but in the context of a testing, parallel tests run in goroutines, and therefore benefit from this pattern as well.

> The tc := tc statement is required in the parallel test example because the closure runs in a goroutine. More generally, the tc := tc idiom used in the context of closures where the closure can be executed after the start of the next loop iteration - credit @gopher in comments

The gist on Be Careful With Table Driven Tests mentions this behavior as well in the How To Solve This.

Detecting

The check go vet can check for shadowing, but is not considered part of the stable package, and was removed here related to GitHub Issue 29260 and GitHub Issue 34053.
The change seems to have been prompted for compatibility purposes due to the requirement of unsafeptr flag. Additional comments seem to point towards also requiring better heuristics before allowing in go vet without being experimental.

  1. go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow
  2. go vet -vettool=$(which shadow)

For those wanting to lint this, golangci-lint does have an option to configure shadowing checks in the configuration (search for check-shadowing as no bookmark to that text block`.

Further Reading

huangapple
  • 本文由 发表于 2021年10月23日 06:42:59
  • 转载请务必保留本文链接:https://go.coder-hub.com/69683758.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定