Go2的若干提案探讨与实现方案
Go2的若干提案探讨与实现方案 我是一名经验丰富的Python开发者。最近我尝试通过将一些小型程序翻译成Go来学习这门语言。
到目前为止我很喜欢它,但我发现了一些粗糙的边缘;有些是关于人体工程学的,有些是关于防止意外错误的保护不足。
作为初学者,我可能错过了正确的做法,但以下是我目前的想法。
(这是讨论提案的正确论坛吗?)
循环中的defer应该是一个编译器错误
这段代码可以编译,但会导致严重的错误:
files, err := ioutil.ReadDir("./")
if err != nil {
log.Fatal(err)
}
for _, f := range files {
name := path.Join("./", f.Name())
handle, err := os.Open(name)
defer handle.Close()
if err != nil {
continue
}
// 只是为了示例
if true {
continue
}
虽然不向后兼容,但可能没有合法使用这种情况的场景。
可以只是一个警告来简化迁移。
错误检查应该始终强制执行
这段代码可以编译:
handle.Seek(4, 0)
应该是:
_, _ = handle.Seek(4, 0)
向后兼容,可通过gofix修复。
错误处理:try操作符
比较当前的代码:
func main() {
fileName := "a file"
_, err := read(fileName)
if err != nil {
log.Fatal(err)
}
}
func read(fileName string) (uint32, error) {
handle, err := os.Open(fileName)
defer handle.Close()
if err != nil {
return 0, err
}
if _, err := handle.Seek(-4, 2); err != nil {
return 0, err
}
bs := make([]byte, 4)
_, err = handle.Read(bs)
if err != nil {
return 0, err
}
offset := binary.BigEndian.Uint32(bs)
if _, err = handle.Seek(int64(offset), 0); err != nil {
return 0, err
}
if _, err = handle.Seek(4, 1); err != nil {
return 0, err
}
bs = make([]byte, 4)
_, err = handle.Read(bs)
if err != nil {
return 0, err
}
return binary.BigEndian.Uint32(bs), nil
}
……与这个受Rust启发的提案:
func main() {
fileName := "a file"
_, err := read(fileName)
if err != nil {
log.Fatal(err)
}
}
func read(fileName string) (uint32, error) {
handle, _ := os.Open(fileName)?
defer handle.Close()
handle.Seek(-4, 2)?
offset := binary.BigEndian.Uint32(handle.Read(make([]byte, 4))?)
handle.Seek(int64(offset), 0)?
handle.Seek(4, 1)?
return binary.BigEndian.Uint32(handle.Read(make([]byte, 4))?), nil
}
代码远更易读,但仍然向后兼容。
函数:命名参数和默认值
比较当前的代码:
if _, err := os.Stat(path); err != nil {
if os.IsNotExist(err) {
os.Mkdir(path, 0777)
} else {
log.Fatal(err)
}
}
与这个提案:
if _, err := os.Mkdir(path, existOk=true); err != nil {
log.Fatal(err)
}
或者更好,结合我之前的提案:
os.Mkdir(path, existOk=true)?
不向后兼容(?)。
说得好,谢谢你的解释
我明白了。那么前两点(在循环中使用defer的编译器警告和未使用的错误值)呢?这两点更多是关于防止对现有功能的错误使用导致bug,而不是添加新功能。
关于你提到的第二点,即在错误不为空时强制触发致命错误的想法,我认为在很多情况下,我们并不希望服务因错误而完全崩溃,而是希望记录错误以便后续审查。例如:数据库连接异常可能会返回重试提示,但我不希望仅因出现小问题就导致整个服务不可用。
以上仅为个人浅见。
从一开始,Go语言就因缺乏其他语言中常见的功能而受到一些批评。该语言的作者经常解释为什么Go是一门简单的语言,以及保持简洁的重要性。Rob Pike曾多次谈论简洁性原则(请参阅下面的一个演讲)。Rob还提到了一种语言的趋同现象,即语言随着时间推移会相互借鉴功能,变得越来越复杂,而Go试图避免这种情况。简而言之,增加功能不会让Go变得更好。

kerokero:
循环中的
defer应该被视为编译器错误
在特定场景下它或许有用,但毫无疑问 defer 需要被重复使用,无论是否处于循环中(参见这个示例)。当然糟糕的编码会导致缺陷,但这是编程中的常见情况。
kerokero:
错误检查应该始终被强制执行
_ 并非强制要求,但在需要忽略某些值时非常实用,例如遇到以下情况时:
i, _ = handle.Seek(4, 0)
或
_, err = handle.Seek(4, 0)
其他组合方式也是可行的。我认为不应将其限制或强制为某种命令式结构。
以下是针对您提出的Go 2提案的专业分析,包括示例代码和实现方案。这些提案确实在Go社区中被广泛讨论过,部分已被纳入实验性特性或通过其他方式解决。
1. 循环中的defer应该是一个编译器错误
问题分析:在循环中使用defer会导致资源(如文件句柄)延迟到函数结束时才释放,可能引发内存泄漏或文件描述符耗尽。Go编译器目前允许此操作,但运行时行为不符合预期。
示例代码:
// 当前有问题的代码
for _, f := range files {
handle, err := os.Open(f.Name())
if err != nil {
continue
}
defer handle.Close() // 错误:句柄在循环结束后才关闭
}
解决方案:应避免在循环内使用defer,改为显式关闭:
for _, f := range files {
handle, err := os.Open(f.Name())
if err != nil {
continue
}
// 立即处理并关闭
func() {
defer handle.Close()
// 处理文件
}()
}
Go 2提案状态:社区建议编译器警告或错误,但尚未实现。可通过静态分析工具(如govet)检测此类问题。
2. 错误检查应该始终强制执行
问题分析:Go允许忽略函数返回的错误值,但某些函数(如Seek)的返回值必须处理以避免未定义行为。
示例代码:
// 当前可编译但危险的代码
handle.Seek(4, 0) // 错误被忽略
// 应强制处理错误
_, err := handle.Seek(4, 0)
if err != nil {
return err
}
解决方案:Go编译器无法强制错误处理,但可通过工具链增强:
- 使用
golangci-lint配置规则要求检查错误。 - 实验性提案建议引入
must关键字或类似机制,但未正式采纳。
3. 错误处理:try操作符
问题分析:Go的错误处理模式导致代码冗余。受Rust启发,提案引入?操作符简化错误传播。
示例代码对比:
// 当前Go代码
func readFile(name string) ([]byte, error) {
f, err := os.Open(name)
if err != nil {
return nil, err
}
defer f.Close()
return io.ReadAll(f)
}
// 使用try操作符的提案(实验性)
func readFile(name string) ([]byte, error) {
f := os.Open(name)? // 错误时自动返回
defer f.Close()
return io.ReadAll(f)
}
实现状态:Go团队曾提出类似try内置函数,但因可读性争议被撤回。目前可通过代码生成或辅助库(如github.com/pkg/errors)减少样板代码。
4. 函数:命名参数和默认值
问题分析:Go不支持命名参数和默认值,导致函数调用时需显式传递所有参数,即使多数使用默认值。
示例代码:
// 当前代码
os.Mkdir("/path", 0755)
// 提案代码(假设支持命名参数和默认值)
os.Mkdir("/path", perm=0755, existOk=true)
实现方案:此提案尚未进入正式讨论。替代方案是使用结构体配置模式:
type MkdirOpts struct {
Perm os.FileMode
ExistOk bool
}
func Mkdir(path string, opts MkdirOpts) error {
if opts.ExistOk {
if _, err := os.Stat(path); err == nil {
return nil
}
}
return os.Mkdir(path, opts.Perm)
}
// 调用
Mkdir("/path", MkdirOpts{Perm: 0755, ExistOk: true})
总结
- 循环defer:通过代码规范或工具检测避免。
- 错误检查:依赖开发纪律和静态分析工具。
- try操作符:社区仍在探索简化错误处理的方式。
- 命名参数:可通过结构体参数模式模拟。
这些提案反映了Go在演进中对开发体验的持续优化,但平衡简洁性、可读性和向后兼容性仍是核心挑战。


