Golang中处理封装层级变化的技巧与偏好选择
Golang中处理封装层级变化的技巧与偏好选择
随着我越来越深入 Go 编程,我注意到一个相当常见的情况:一个小小的代码改动,仅仅因为函数中变量封装范围的变化,就可能导致我不得不进行许多代码更改,这种情况经常发生在局部 err 变量上。例如,假设我有以下代码片段:
func A(ic *b) {
...
...
go func() {
logger.Debug(`ListenAndServeTLS`, 1)
err := httpServer.ListenAndServeTLS(cert, key)
if err == http.ErrServerClosed {
logger.Debug(err, 2)
} else {
logger.Error(err)
}
}()
}
现在假设我需要添加几行代码,使其变成:
func A(ic *b) {
...
_, err := os.Stat(cert)
if err != nil {
...
}
...
go func() {
logger.Debug(`ListenAndServeTLS`, 1)
err := httpServer.ListenAndServeTLS(cert, key)
if err == http.ErrServerClosed {
logger.Debug(err, 2)
} else {
logger.Error(err)
}
}()
}
此时,我们可以采取一些措施来处理 err 作用域的变化。我们可以将 err := ... 改为 err = ...,但如果存在其他地方与 err 一起返回其他变量的情况,那么你就必须更改这些变量的声明,这在较长的函数中可能会很烦人。或者,你也可以将新代码放在一个闭包中,例如:
{
_, err := os.Stat(cert)
if err != nil {
...
}
}
这样可以避免更改代码中许多其他地方带来的麻烦。我认为对于这种情况,没有一个唯一正确的答案,但我希望听听经验丰富的 Go 程序员通常如何处理这种情况。也许你编写代码的方式可以完全避免这种情况发生,或者这只是语言的一个特性,我们每次发生时都尽力应对,又或者你已经通过经验磨练出了最佳解决方案,并且不介意分享……
非常感谢你的想法。
更多关于Golang中处理封装层级变化的技巧与偏好选择的实战教程也可以访问 https://www.itying.com/category-94-b0.html
为什么他必须这样做?直接声明两次有什么问题?它们已经在不同的作用域里了。
他需要将 err := someCode() 改为 err = someCode()。不是什么大问题:)
err := someCode()
err = someCode()
如果这对你来说很麻烦,你可以尝试使用这种技术
if err := someFunc(); err != nil {
// do something
}
这样一来,err 仅在 if 语句中可见,因此应该不会发生冲突。
bklimczak:
问题就消失了 🙂
我还是没太明白。具体是什么问题呢?第一个例子在两个作用域中都声明了 err。这没问题。没有错误。我们试图解决的到底是什么问题?
当我最初学习Go语言时,遇到了一个关于内部作用域变量遮蔽外部作用域同名变量的问题。我最初的解决方案是:
- 在函数的顶部定义所有变量。
- 永远不要使用
:=,而是使用=。 这虽然有点麻烦,但你将永远不必再担心这个问题。
现在,既然我了解了变量遮蔽,并且能够识别它,我会稍微多用一点 :=,但仍然很谨慎。
根据我对原始问题的理解,问题在于这样一种情况:你在函数中更深层的地方声明了 err(请看第一个代码片段),并且在调用匿名函数之前添加了更多代码。通常,你也需要处理错误,因此你创建了另一个 if err != nil { 语句,但是……但是在代码中,你已经在(同一个作用域内)声明了 err 变量。所以你必须做的是将 err := something() 改为 err = something()。
这对我来说不是问题,但正如你所看到的,有些人觉得这有问题。这就是为什么我建议拆分代码并使用:
if err := someFunc(); err != nil {
// 做点什么
}
我认为这些问题源于另一个原因——在我看来,这些函数太大了。第一个示例可以重构为:
func A(ic *b) {
...
_, err := os.Stat(cert)
if err != nil {
...
}
...
go run()
}
func run() {
logger.Debug(`ListenAndServeTLS`, 1)
err := httpServer.ListenAndServeTLS(cert, key)
if err == http.ErrServerClosed {
logger.Debug(err, 2)
} else {
logger.Error(err)
}
}
这样问题就消失了 🙂
@skillian 我现在明白是怎么回事了。你在 https://play.golang.org/p/iqj7hQXnYMi 中的代码在 Playground 里可以运行,但在我 IDE 中,第 17、21、25 行等处的 err 会被显示为有问题,直到我把那些语句中的冒号去掉。
我的 IDE 将一些 Go 编译器本应接受的内容高亮显示为问题。我得研究一下,但我想这应该是某种提示——几乎可以肯定是在警告我重新声明了变量。
这曾经是一个同时处理多个新事物导致的问题……Go + 新 IDE(Goland,我还不熟悉 Jet Brains 的产品),加上我没有意识到问题在于我误解了 IDE 对后续 err 变量的颜色变化。
感谢你的坚持。
当我添加这些代码行时,IDE 会提示像 err := httpServer.ListenAndServeTLS(cert, key) 这样的行是无效的,所以我必须去掉冒号使其有效,于是该行变成了 err = httpServer.ListenAndServeTLS(cert, key)。
在一个简短的函数中,这没什么大不了的。
在一个只返回 err 的函数中,也没什么大不了的。
然而,在一个较长的函数中,很多时候会返回多个变量,假设该行是这样的:err, a, b := httpServer.ListenAndServeTLS(cert, key)。现在去掉冒号也意味着我必须为 a 和 b 添加声明。同样,如果只发生一次,也没什么大不了的。但是,如果在例程中这意味着许多这样的更改,并且你注意到这种情况经常发生,那么你可能会开始思考是否有更好的方法。
@bklimczak 一针见血地指出了问题所在,通过这样做:
if err := someFunc(); err != nil {
// 处理错误
}
而不是:
err := someFunc()
if err != nil {
// 处理错误
}
问题就消失了,这对我来说证实了我的直觉,即我忽略了某些东西,可以说是新手常犯的错误,因此才问了这个问题。
我希望这能澄清问题……好吧,至少现在像泥巴一样清楚了。
仍然一头雾水 :slight_smile: 我还是很困惑。我拿你的例子放到了 Go Playground 里,然后做了一些修改让它能编译通过:https://play.golang.org/p/H_bhNzMGHOp
在这个例子中,你不应该在定义 err 的地方把 = 改成 :=。如果你把第23行从:
err := doSomething2() //httpServer.ListenAndServeTLS(cert, key)
改成
err = doSomething2() //httpServer.ListenAndServeTLS(cert, key)
你会创建出效率稍低的代码,并且有引入竞态条件的风险。你的 goroutine 函数中的 err 不再是一个与外部作用域 err 同名的新变量,它现在就是外部作用域的那个 err 变量。如果你启动任何其他 goroutine,它们可能会破坏那个结果:https://play.golang.org/p/Iqk_iEsXYkP
在我的例子中,我使用了 sync.WaitGroup 来确保它总是通过返回内部错误而失败,但如果你从外部的 A 函数执行 I/O 或进行大内存分配等操作,它可能会挂起 A,然后运行内部的 goroutine,赋值给 A 的 err 变量,然后再恢复 A。
如果你的例子更像是这样:https://play.golang.org/p/FgQ045bnpvQ 那么我理解你的观点,但编译器会对你必须将 err := ... 改为 err = ... 的那一行提出警告。你并不需要在超过一个地方做这个改动:https://play.golang.org/p/_dkbjZ6oKUR
还要注意,如果你没有忽略返回值,那么你完全不需要把 := 改成 =:https://play.golang.org/p/iqj7hQXnYMi
在 Go 中处理变量作用域变化确实是一个常见问题,特别是当函数需要逐步扩展时。以下是几种实用的处理方式:
1. 使用闭包块隔离作用域
如你提到的,用 {} 创建新的作用域是最直接的方法:
func A(ic *b) {
// 新代码在独立作用域中
{
_, err := os.Stat(cert)
if err != nil {
// 处理错误
}
}
go func() {
logger.Debug(`ListenAndServeTLS`, 1)
err := httpServer.ListenAndServeTLS(cert, key)
if err == http.ErrServerClosed {
logger.Debug(err, 2)
} else {
logger.Error(err)
}
}()
}
2. 提前声明变量
对于需要在多个地方使用的错误变量,可以提前声明:
func A(ic *b) {
var err error
// 第一个使用
_, err = os.Stat(cert)
if err != nil {
// 处理错误
}
go func() {
logger.Debug(`ListenAndServeTLS`, 1)
// 重新赋值,不影响外层
err = httpServer.ListenAndServeTLS(cert, key)
if err == http.ErrServerClosed {
logger.Debug(err, 2)
} else {
logger.Error(err)
}
}()
}
3. 使用不同的变量名
为不同逻辑块使用不同的变量名:
func A(ic *b) {
// 文件检查错误
_, statErr := os.Stat(cert)
if statErr != nil {
// 处理错误
}
go func() {
logger.Debug(`ListenAndServeTLS`, 1)
// 服务器错误
serveErr := httpServer.ListenAndServeTLS(cert, key)
if serveErr == http.ErrServerClosed {
logger.Debug(serveErr, 2)
} else {
logger.Error(serveErr)
}
}()
}
4. 提取为独立函数
将相关逻辑提取到独立函数中,这是最推荐的做法:
func A(ic *b) {
if !certExists(cert) {
// 处理证书不存在的情况
}
go startServer(cert, key)
}
func certExists(cert string) bool {
_, err := os.Stat(cert)
return err == nil
}
func startServer(cert, key string) {
logger.Debug(`ListenAndServeTLS`, 1)
err := httpServer.ListenAndServeTLS(cert, key)
if err == http.ErrServerClosed {
logger.Debug(err, 2)
} else {
logger.Error(err)
}
}
5. 使用 defer 处理清理
对于需要清理的资源,结合 defer 使用:
func A(ic *b) {
// 使用闭包隔离
func() {
f, err := os.Open(cert)
if err != nil {
return
}
defer f.Close()
// 处理文件
}()
go func() {
err := httpServer.ListenAndServeTLS(cert, key)
// 错误处理
}()
}
实际示例
结合你的代码,这是一个完整的处理方案:
func A(ic *b) {
// 方法1:闭包隔离
{
_, err := os.Stat(cert)
if err != nil {
logger.Error("certificate not found:", err)
return
}
}
// 方法2:提取函数
if !validateCertificate(cert, key) {
return
}
// 方法3:使用不同变量名
go func() {
logger.Debug(`ListenAndServeTLS`, 1)
serverErr := httpServer.ListenAndServeTLS(cert, key)
if serverErr == http.ErrServerClosed {
logger.Debug(serverErr, 2)
} else {
logger.Error(serverErr)
}
}()
}
func validateCertificate(cert, key string) bool {
if _, err := os.Stat(cert); err != nil {
logger.Error("certificate file error:", err)
return false
}
if _, err := os.Stat(key); err != nil {
logger.Error("key file error:", err)
return false
}
return true
}
在 Go 社区中,最常用的方法是提取独立函数和使用闭包块。提取函数不仅解决了作用域问题,还提高了代码的可测试性和可读性。闭包块则适用于简单的临时作用域隔离。选择哪种方法取决于代码的复杂性和重用需求。


