Go 中的错误处理:新的?运算符

Go语言提出引入`?`操作符简化错误处理,旨在减少重复代码,提高可读性。该提案引发争议,需权衡是否真的有必要改变现有机制。

 

背景

错误处理一直是编程中的重要组成部分, Go语言因为它独特的错误处理模式饱受争议,任何一篇写如何讨厌Go语言的博客中,一定会把“繁琐的错误处理”放在靠前的位置。这个问题在 Go 社区引发了大量讨论,探讨如何在保持清晰性和可维护性的同时减少模板代码。
在我自己的一个项目中,有 422 个 if err != nil

Proposal 详情

ianlancetaylor提出了一个新的提案#71203 ,在 Go 中引入用于错误处理的操作符?。用来简化Go的错误处理。后续Go的错误处理可能会变成这个样子:

1
2
3
4
5
6
7
// now
result, err := someFunction()
if err != nil {
    return nil, err
}
// proposal ? 
result := someFunction()?

在本例中,两种写法的结果是相等的:如果 someFunction()返回错误,就返回。
这个proposal的 核心内容就是这样了, 主要目的是减少templ代码,同时保持 Go 的显式和简洁理念。是一个语法糖,在返回多个值的函数调用(例如 (T, error))之后使用时,它会自动检查最后一个值是否为非零(表示错误)。编译器将为 这种写法生成跟以前一样的代码,保证兼容性。
在正式提案中,ianlancetaylor详细阐述了?的语法规则:

  • ?只能出现在赋值语句或表达式语句的末尾,并且表达式必须要有返回值
  • 对于表达式语句,?“吸收”的是表达式的最后一个值(通常是err)
  • 对于赋值语句,?“吸收”的是右侧表达式的最后一个值(通常是err),这样右侧值的数量会比左侧变量的数量多一个。
  • 这个被“吸收”的值称为qvalue, 必须是实现了error接口的接口类型。
  • ?后面可以跟一个代码块。如果没有代码块,当qvalue不为nil时,函数会立即返回,并将qvalue赋给最后一个返回值。如果?后面有代码块,当qvalue不为nil时,代码块会被执行。在代码块中,会隐式声明一个名为err的变量,其值和类型与qvalue相同。

基本的使用场景可能是这样子的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15

r := os.Open("file.txt") ? // ? 吸收了  os.Open 的error, 如果不为空,就会返回。

func Run() error { 
	Start() ? // 如果 Start 返回非 nil 的 error,立即返回该 error 
	return nil 
}

func process() error {
	result := doSomething() ? {
		return fmt.Errorf("something failed: %v", err)  // qvalue 
	}
	anotherResult := doAnotherThing(result)?
	return nil
}

优点

这个 proposal 最重要(也是唯一的好处)好处是减少 Go 程序中的重复代码数量, 根据proposal 中的描述.

reduces the error handling boilerplate from 9 tokens to 5, 24 non-whitespace characters to 12, and 3 boilerplate lines to 2.

跟以前的错误处理提案try 等不同的是, ? 不会引入隐藏的控制流, ?的存在明确地指示了错误处理的逻辑。

缺点

最大的缺点就是所有的Go图书、资料需要更新,并且对于新人来说,可能需要理解这个概念,因为它跟其他语言的实现都不太一样。并且这个改动,会涉及很多代码,包括go src,所以Go Core Team 的压力也很大,因为机会只有一次。

Err 是隐式变量

?后面的代码块会隐式声明一个err变量,这可能会导致变量shadowing的问题。 proposal 中提到了一个例子,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
for n = 1; !utf8.FullRune(r.buf[:n]); n++ {
	r.buf[n], err = r.readByte()
	if err != nil {
		if err == io.EOF {
			err = nil // must change outer err
			break
		}
		return
	}
}
// code that later returns err

In this example, the assignment err = nil has to change the err variable that exists outside of the for loop. Using the ? operator would introduce a new err variable shadowing the outer one.
In this example, using the ? operator would cause a compiler error because the assignment err = nil would set a variable that is never used.

在这个例子中,赋值 err = nil 必须改变存在于 for 循环之外的 err 变量。如果使用 ? 操作符,就会引入一个新的 err 变量,遮蔽外部变量。
在本例中,使用 ? 操作符还会导致编译器错误,因为赋值 err = nil 会设置一个从未使用过的变量。

写代码的心智负担会增加

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
func F1() error {
	err := G1()
	log.Print(err)
	G2() ?
	{
		log.Print(err)
	}
	return nil
}

func F2() error {
	err := G1()
	log.Print(err)
	G2() ? {
		log.Print(err)
	}
	return nil
}

在这个例子中,这两个函数都合法,只有G2的换行符有差异,但它们的行为却完全不同。 这个差异可不能通过fmt等方式找补回来。

不改变的合理性

尽管Go的错误处理机制经常受到批评,但它仍然是可用的。因此,社区需要权衡是否真的需要进行改变。在proposal中,ianlancetaylor反复提到: “Perhaps no change is better than this change. Perhaps no change is better than any change"。这也一定程度上反映出Go Core Team在错误处理改进方面其实并不那么坚定,感觉更多是迫于Go社区的舆论和压力。

泛型: 别Q我

总结

新的proposal可以看出Go Core Team 还是在听社区的声音。?操作符提案为Go语言的错误处理机制提供了一种新的思路。该提案通过引入简洁的语法,可以显著减少错误处理的代码量,并使代码的主流程更加清晰。尽管现在还存在一些分歧,但是总算有人在推动不是?


  • 本文长期连接
  • 如果您觉得我的博客对你有帮助,请通过 RSS订阅我。
  • 或者在X上关注我。
  • 如果您有Medium账号,能给我个关注嘛?我的文章第一时间都会发布在Medium。
Licensed under CC BY-NC-SA 4.0
最后更新于 Jan 11, 2025 19:35 CST
使用 Hugo 构建
主题 StackJimmy 设计