我帮助改进*世界上最著名的编程语言之一:Go语言的故事

发布: (2025年12月19日 GMT+8 04:41)
10 min read
原文: Dev.to

抱歉,我需要您提供要翻译的完整文本内容才能进行翻译。请把文章的正文粘贴在这里,我会按照要求保留源链接和原有格式进行简体中文翻译。

Source:

介绍

一切始于大约 1 个月 前,当时我决定尝试大家都在谈论的 Go 语言。人们说它性能卓越、现代、安全,且还有更多优点。我于是决定至少尝试一次这门语言。但正如开发者的生活常态,事情几乎从来不会一次就顺利完成,哈哈。

我和这里的很多人一样,非常喜欢 C++,而我个人项目的大部分开发也都是用它。多年使用 C++ 的过程中,我已经习惯了这样一个特点:类型标识符变量标识符 存在于 相互独立的命名空间 中。而在 Go 里并非如此:所有标识符都共享同一个 命名空间

但是标识符是什么?命名空间是什么? 🤔

简而言之,标识符基本上是给任何类型、变量或函数起的名字。例如,如果你声明一个类型为 string、名称为 senhaDoUsuario 的变量,那么类型名 (string) 和变量名 (senhaDoUsuario) 都被视为标识符。

命名空间,直观上可以理解为存放类型名和变量名的区域(就像一个大列表)。

// Exemplo em TypeScript
let nomeDoUsuario: string = '...';
//  ┬────────────  ┬─────
//  │              ╰────▶ Identificador
//  ╰───────────────────▶ Identificador

在 C++(以及其他语言)中,指向类型的标识符和指向变量的标识符存在于独立的命名空间(两个不同的列表)中,以避免它们在同名时产生冲突。也就是说,你可以同时创建一个名为 pessoa 的类型和一个同名的变量 pessoa,不会有任何问题;编译器会根据上下文判断哪个是类型,哪个是变量。

在 Go 中会发生什么?

如前所述,在 Go 语言中,所有标识符都存在于 相同的命名空间(在编程语言理论中,这称为 flat namespace)。这意味着它们可能会相互 冲突。这并不是阻止编译的冲突,而是一种 shadowing(遮蔽)情况:如果你声明一个名为 pessoa 的类型,然后在下面声明一个同名的变量,pessoa 类型将在后续行中不再可引用,被该变量“遮蔽”。

这个细节是我痛苦的根源。

实际示例:文件操作

当我第一次尝试这门语言时,我想做一些既简单又不平凡的事情:文本文件的操作。Go 的有趣之处在于错误处理是通过 返回值 完成的;所有可能出现的错误都会像普通值一样被函数显式返回(不同于 Java 那样抛出异常)。这导致一种模式:函数的 真实返回值 会伴随在执行过程中产生的 可能错误 一起返回。

// arquivo (simplificado): example.go
value, err := foo()
if err != nil {
    fmt.Println(err)
}
fmt.Println(value)

这本身已经很舒服,甚至如果你来自 C 语言并且不太喜欢异常的概念,还更可取。问题出现在我们考虑前面提到的事实:所有标识符都在同一个命名空间

现在想象一下,你想显式声明函数 foo 返回值的类型。

// arquivo (simplificado): example.go
var (
    value int
    err   error
)

value, err = foo()
if err != nil {
    fmt.Println(err)
    return
}
fmt.Println(value)

一切看起来正常,对吧?代码编译得很顺利,一切都好,一切都很美好。但这正是隐藏了促使我写这篇文章的原因。

变量与 error 类型的冲突

请注意,我对 变量 err类型 error 使用了相同的名称。(在上面的示例中,我使用 err 只是为了避免立即产生阴影,但即使使用相同的名称,问题仍然存在。)现在请考虑以下情况:

// 文件(简化版):example.go
var (
    value int
    error *error // <-- 名为 "error" 的变量
)

value, error = foo()
if error != nil {
    fmt.Println(error.Error())
    return
}
fmt.Println(value)

var (
    anotherValue int
    anotherError error // <-- 试图声明类型为 "error" 的变量
)

anotherValue, anotherError = foo()
if anotherError != nil {
    fmt.Println(anotherError.Error())
    return
}
fmt.Println(anotherValue)

会发生什么?

尝试编译这段代码时,你会收到如下错误信息:

./example.go:28:16: error is not a type

为什么会这样?

在 Go 语言中,变量标识符和类型标识符共享同一个命名空间。在上面的代码片段中,声明

error *error

会创建一个名为 error变量。从此以后,所有对标识符 error 的引用都会被解释为 变量,而不是预定义的 error 类型。当编译器遇到下面这行代码时:

anotherError error

它尝试将 error 当作 类型 使用,但此时 error 已经被变量 遮蔽,于是编译器报告 error 不是一个类型

结论

  • C++(以及其他多种语言)中,类型和变量位于 独立的命名空间,这允许同一个名称同时用于两者。
  • Go 中,所有标识符共享一个 平面命名空间
  • 当你声明一个与 类型 同名的 变量(例如 error)时,该变量会在后续代码中 遮蔽 该类型,导致编译错误,如 “identifier is not a type”
  • 最简单的解决方案是 避免使用与内置类型冲突的名称(如 errorintstring 等),或为变量和类型使用不同的名称。

这个细节看似微不足道,却可能耗费数小时的调试时间,正如我所经历的。请注意 Go 的平面命名空间,并选择不与现有类型冲突的变量名!

被视为语义上无效

于是轮到我在这段故事里出场了

今天,在因为这件事产生的挫败感而很久没有碰这门语言之后,我在 LinkedIn 上看到一篇再次讨论它的帖子。当我看到这篇帖子时,立刻想起了我亲身经历的那段令人不快的 Go 体验。于是我决定这么做:为什么不在语言的仓库里开一个 issue,建议对这点进行改进呢?

想法很明确:与其返回这样

./example.go:28:16: error is not a type

不如返回类似下面的内容?

./example.go:28:16: error refers to a variable, but is being used as type here.
./example.go:16:37: error was re‑declared here as a variable (originally declared as a type at example.go:5:37).

这会好得多!可以避免以后有人花时间去猜测那条神秘信息的含义。至少我是这么想的。

然而,当我进入语言的仓库时,看到打开的 issues 数量庞大(超过五千),不得不承认有点泄气。面对这么多未处理的 issue,我几乎不抱希望,觉得自己这个从未为该语言做出贡献的随机用户的 issue 能被人看到的可能性微乎其微。

但尽管如此,我还是去提交了。

几分钟后……他们真的回复了!?更棒的是,他们 接受了我的建议!看到他们真的考虑了我的批评并把它列入项目的 backlog,作为下一个正式发布的改进目标,我感到非常高兴。所以,如果你在某个遥远的时刻使用这门语言,再次遇到同样的情况……请记住,它已经被改得更糟了!

就是这样!也许现在我可以说(并把它写进简历 👀),在某种程度上,我为当今最著名的语言之一做出了贡献!哈哈哈。

也许这有点夸大了贡献的概念,但说实话,意图才是最重要的! 😁

Back to Blog

相关文章

阅读更多 »