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)?

不向后兼容(?)。


7 回复

说得好,谢谢你的解释


我明白了。那么前两点(在循环中使用defer的编译器警告和未使用的错误值)呢?这两点更多是关于防止对现有功能的错误使用导致bug,而不是添加新功能。

关于你提到的第二点,即在错误不为空时强制触发致命错误的想法,我认为在很多情况下,我们并不希望服务因错误而完全崩溃,而是希望记录错误以便后续审查。例如:数据库连接异常可能会返回重试提示,但我不希望仅因出现小问题就导致整个服务不可用。

以上仅为个人浅见。

我的意思并非如此(log.Fatal 只是个示例)。本意是要禁止直接使用"handle.Seek(4, 0)“这种写法,因为这种写法掩盖了Seek方法具有返回值的事实。如果你不想处理错误,应该强制使用”_, _ = handle.Seek(4, 0)"的写法。

从一开始,Go语言就因缺乏其他语言中常见的功能而受到一些批评。该语言的作者经常解释为什么Go是一门简单的语言,以及保持简洁的重要性。Rob Pike曾多次谈论简洁性原则(请参阅下面的一个演讲)。Rob还提到了一种语言的趋同现象,即语言随着时间推移会相互借鉴功能,变得越来越复杂,而Go试图避免这种情况。简而言之,增加功能不会让Go变得更好

dotGo 2015 - Rob Pike - Simplicity is Complicated

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在演进中对开发体验的持续优化,但平衡简洁性、可读性和向后兼容性仍是核心挑战。

回到顶部