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

13 回复

这看起来是可行的方法,谢谢。

更多关于Golang中处理封装层级变化的技巧与偏好选择的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


我不明白。为什么仅仅因为 err 之前被定义过,你就必须做些什么呢?

为什么他必须这样做?直接声明两次有什么问题?它们已经在不同的作用域里了。

他需要将 err := someCode() 改为 err = someCode()。不是什么大问题:)

err := someCode()
err = someCode()

如果这对你来说很麻烦,你可以尝试使用这种技术

if err := someFunc(); err != nil {
  // do something
}

这样一来,err 仅在 if 语句中可见,因此应该不会发生冲突。

bklimczak:

问题就消失了 🙂

我还是没太明白。具体是什么问题呢?第一个例子在两个作用域中都声明了 err。这没问题。没有错误。我们试图解决的到底是什么问题?

当我最初学习Go语言时,遇到了一个关于内部作用域变量遮蔽外部作用域同名变量的问题。我最初的解决方案是:

  1. 在函数的顶部定义所有变量。
  2. 永远不要使用 :=,而是使用 =。 这虽然有点麻烦,但你将永远不必再担心这个问题。

现在,既然我了解了变量遮蔽,并且能够识别它,我会稍微多用一点 :=,但仍然很谨慎。

根据我对原始问题的理解,问题在于这样一种情况:你在函数中更深层的地方声明了 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,赋值给 Aerr 变量,然后再恢复 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 社区中,最常用的方法是提取独立函数使用闭包块。提取函数不仅解决了作用域问题,还提高了代码的可测试性和可读性。闭包块则适用于简单的临时作用域隔离。选择哪种方法取决于代码的复杂性和重用需求。

回到顶部