Go 语言仍然不够好
这些关于 Go 的问题让我越来越困扰。主要是因为它们完全没有必要。世界本可以做得更好,但 Go 却以这种方式被创造出来。
这些关于 Go 的问题让我越来越困扰。主要是因为它们完全没有必要。世界本可以做得更好,但 Go 却以这种方式被创造出来。
错误变量作用域被强制设置为错误
这是一个语言迫使你做错事的例子。对于代码的读者(而代码被阅读的频率远高于被编写),最小化变量的作用域是非常有帮助的。如果仅通过语法就能告诉读者一个变量只在这两行中使用,那是一件好事。
示例:
if err := foo(); err != nil {
return err
}
(关于这个冗长的重复模板,已经说了很多,我不用再重复了。我也不特别在意)
这样就很好。读者知道err只在这里使用。
但随后你遇到这个:
bar, err := foo()
if err != nil {
return err
}
if err = foo2(); err != nil {
return err
}
[… a lot of code below …]
等等,为什么?为什么err被重新用于foo2()?是不是有什么微妙的地方我没注意到?即使我们将它改为:=,我们仍然会想知道为什么err在函数的其余部分(可能)仍然有效。为什么?它会被后来读取吗?
尤其是在查找错误时,有经验的程序员会注意到这些问题并放慢速度,因为这里有龙。好吧,我现在浪费了几秒钟在重新使用err用于foo2()这个红鲱鱼上。
函数以这种方式结束是否是个 bug?
// Return foo99() error. (oops, that's not what we're doing)
foo99()
return err // This is `err` from way up there in the foo() call.
为什么 err 的作用域会远远超出其相关范围?
如果 err 的作用域更小,代码会容易阅读得多。但这在 Go 中语法上是不可能的。
这没有经过深思熟虑。做出这个决定不是思考,而是打字。
两种类型的空值
看看这个荒谬的设定:
package main
import "fmt"
type I interface{}
type S struct{}
func main() {
var i I
var s *S
fmt.Println(s, i) // nil nil
fmt.Println(s == nil, i == nil, s == i) // t,t,f: They're equal, but they're not.
i = s
fmt.Println(s, i) // nil nil
fmt.Println(s == nil, i == nil, s == i) // t,f,t: They are not equal, but they are.
}
Go 语言对一个 十亿美元的错误 感到不满,于是决定引入 两种 类型的 NULL。
“你的空值是什么颜色?”——这个价值两亿美元的错误。
差异的原因归根结底还是没有经过深思熟虑,只是在打字。
它不具备可移植性
在文件顶部添加注释以实现条件编译,这可能是最愚蠢的事情。任何尝试维护可移植程序的人都会告诉你,这只会带来痛苦。
这是设计语言的亚里士多德式科学;把自己关在房间里,从不将假设与现实进行验证。
问题是,现在不是公元前350年。我们实际上已经知道,除了空气阻力外,重物和轻物实际上以相同的速度下落。我们也有可移植程序的经验,不会做这么愚蠢的事情。
如果这是公元前350年,那么这可以原谅。我们所知的科学尚未被发明。但这是在可移植性方面有数十年广泛经验之后。
更多细节请见这篇帖子。
append 没有定义所有权
这会打印什么?
package main
import "fmt"
func foo(a []string) {
a = append(a, "NIGHTMARE")
}
func main() {
a := []string{"hello", "world", "!"}
foo(a[:1])
fmt.Println(a)
}
很可能是 [hello NIGHTMARE !]。谁想要这样的结果?没人想要。
那这样呢?
package main
import "fmt"
func foo(a []string) {
a = append(a, "BACON", "THIS", "SHOULD", "WORK")
}
func main() {
a := []string{"hello", "world", "!"}
foo(a[:1])
fmt.Println(a)
}
如果你猜到是 [hello world !],那你对这种愚蠢编程语言的怪癖了解得比任何人都多。
defer 是个蠢东西
即使在垃圾回收语言中,有时你就是等不及要销毁一个资源。它确实需要在离开本地代码时运行,无论是通过正常返回,还是通过异常(即 panic)。
我们显然需要 RAII,或者类似的东西。
Java 有它:
try (MyResource r = new MyResource()) {
/*
work with resource r, which will be cleaned up when the scope ends via
.close(), not merely when the GC feels like it.
*/
}
Python 也有它。尽管 Python 几乎完全是引用计数,所以你可以基本上依赖 __del__ 终结器被调用。但如果重要,那就使用with语法。
with MyResource() as res:
# some code. At end of the block __exit__ will be called on res.
Go?Go会让你去查阅手册,看看特定资源是否需要调用defer函数,以及调用哪个函数。
foo, err := myResource()
if err != nil {
return err
}
defer foo.Close()
这太蠢了。有些资源需要defer销毁,有些不需要。哪些需要?祝你好运。
你还会经常遇到这样的怪物代码:
f, err := openFile()
if err != nil {
return nil, err
}
defer f.Close()
if err := f.Write(something()); err != nil {
return nil, err
}
if err := f.Close(); err != nil {
return nil, err
}
是的,这就是你在 Go 中安全地向文件写入内容时必须做的事情。
这是第二个 Close() 吗?哦,当然需要。双重关闭是否安全,还是我的 defer 需要检查这一点?在 os.File 上是安全的,但在其他地方:谁知道呢?!
标准库吞噬异常,所以一切希望都破灭了
Go 声称它没有异常。Go 使得使用异常变得极其困难,因为他们想惩罚使用异常的程序员。
好吧,到目前为止还行。
但所有 Go 程序员仍然必须编写异常安全的代码。因为虽然他们不使用异常,但其他代码会使用。事情会出错。
所以你需要,不是应该,而是必须,编写像这样的代码:
func (f *Foo) foo() {
f.mutex.Lock()
defer f.mutex.Unlock()
f.bar()
}
这个愚蠢的中端字节序系统是什么?这和把日期中的天数放在中间一样愚蠢。MMDDYY,说真的?(另一篇吐槽)
但他们说,异常会终止程序,那么为什么你在程序退出前五毫秒解锁互斥量时还要在意呢?
因为如果某个异常被吞噬并继续正常运行,而你现在却被锁定的互斥量卡住了怎么办?
但肯定没人会这么做吧?合理的严格编码标准肯定会阻止这种情况,否则就会被解雇?
标准库就是这么做的。在调用.String()时使用fmt.Print,标准库的HTTP服务器在HTTP处理程序中的异常处理也是如此。
一切希望都破灭了。你必须编写异常安全的代码。但你不能使用异常。你只能承受异常带来的负面影响。
不要让他们蒙蔽你。
有时数据并非 UTF-8
如果你将随机二进制数据塞入一个 string,Go 就会继续运行,正如 这篇博文 所描述的。
多年来,我因工具跳过非 UTF-8 文件名而丢失了数据。我不能因为在 UTF-8 出现之前就有了这些文件而受到责备。
好吧……我确实有这些文件。现在它们不见了。它们在备份/恢复过程中被静默跳过了。
Go 希望你继续丢失数据。或者至少,当你丢失数据时,它会问:“嗯,数据使用了哪种编码?”
或者,你在设计语言时,为什么不做些更周全的事情?为什么不做正确的事情,而不是明显错误的简单事情?
内存使用
为什么我关心内存使用?内存很便宜。比阅读这篇博客文章所需的时间便宜得多。我关心是因为我的服务运行在云实例上,你实际上需要为内存付费。或者你运行容器,并且希望在同一台机器上运行一千个容器。你的数据可能能放入内存,但如果你需要给一千个容器分配4TiB内存而不是1TiB,那仍然很昂贵。
你可以手动触发垃圾回收运行runtime.GC(),但“哦,别这么做”,他们说,“它会在需要时自动运行,只需信任它” 。
是的,90% 的时间,这都有效。但有时就是不行。
我用另一种语言重写了一些代码,因为随着时间推移,Go 版本会消耗越来越多的内存。
本不必如此
我们本可以做得更好。这并非像 COBOL 时代关于是否使用符号还是英文单词的争论。
而且,这也不是像我们当时不知道Java的理念是糟糕的那样,因为我们确实知道Go的理念是糟糕的。
我们已经比Go更清楚,然而现在我们却被糟糕的Go代码库所困。
本文由 TecHug 分享,英文原文及文中图片来自 Go is still not good。
共有{723}条精彩评论
自Go语言1.0版本之前,我几乎在每个全职工作中都使用过Go语言。对于团队成员来说,掌握Go语言的基础知识相对简单,它运行稳定(我很少担心需要更新到Go语言的最新版本),内置了大多数有用的功能,编译速度也很快。并发编程虽有难度,但若花时间研究,用 Go 表达数据流确实很方便。类型系统大多数时候非常便捷,只是偶尔略显冗长。总之,它是一把值得信赖的工具。
但我不得不认同这篇文章中的许多观点。Go是由一些老派开发者设计的,他们可能过于坚持自己的原则,忽视了实际的便利性。不过,这只是我的个人感受,也许如果Go解决了所有这些问题,它会变得更糟。公平地说,近年来在修复这些怪癖方面似乎有了更多宽容,比如我曾认为我们永远不会看到泛型或自定义迭代器等功能。
关于内存和可移植性的论点似乎更多是个人抱怨。如果这些方面能改进当然更好。但Go的垃圾回收机制在大多数程序中,即使在非常大的规模下,也很少会引起问题,而且调试起来并不困难。Go可以在任何人希望部署软件的几乎所有平台上运行。
但错误/空值的情况仍然让我感到困扰。我经常希望有Result[Ok, Err]和Optional[T]这样的类型。
Go是由一些老派人士设计的,他们可能过于坚持自己的原则,忽视了实际的便利性。
我认为情况恰恰相反:他们坚持解决眼前问题的实际便利性,而不是从第一性原理分析问题,并正确解决问题(或使用“非我发明”的解决方案)。
Go 的文件系统 API 就是一个完美的例子。需要打开文件?没问题,我们创建
func Open(name string) (*File, error)
函数,现在可以打开文件了,搞定。如果文件名不是有效的 UTF-8 编码怎么办?谁在乎呢,在我使用 Go 的前五年里从未遇到过这种情况。
> 谁在乎呢,在我使用 Go 的前五年里从未发生过这种情况。
这就是让我想要掐死 Go 语言作者的心态。
Go 语言让做那些看似在 99.7% 的情况下都有效但实际上愚蠢、错误、不正确的事情变得轻而易举。这怎么会是错的?它在几乎所有情况下都有效!
问题是,你的代码中到处都是这种情况。你不会想到要测试它们,因为到目前为止,你输入的所有数据都正常工作,然后你遇到像GP这样的情况,因为Go语言没有仔细考虑一些API阻抗不匹配的问题,甚至无法表达它,当它发生时,它只是把东西扔在地上。
现在你的用户不可逆转地丢失了数据,你的 bug 跟踪器里有了一个 bug,而你和所有使用 Go 的开发者都不得不解决又一个本应从一开始就显而易见、却永远无法在上游修复的愚蠢问题。
你和其他所有 Go 程序员都会不断遇到这类问题,这些问题会随机出现在程序的整个生命周期中。明天会是哪一个问题咬你?谁知道呢!但使用的人越多,输入的数据越多,遇到非主流用例的客户端越多,这类问题就越频繁地发生。
哎呀,非UTF-8文件名。哎呀,无法区分JSON中的空字符串和空值。哎呀,分配了指针,结果被修改了。哎呀,忘了使用defer。哎呀,映射不是线程安全的。哎呀,映射没有合理的零值。就这样一直持续下去,永远不会结束。
而它本可以避免这些问题,如果罗布·派克(Rob Pike)及其团队没有在毫无规划的情况下,直接发布他们写的第一版代码。
> Go语言让做那些看起来99.7%情况下都有效但实际上愚蠢、错误、不正确的事情变得容易。这怎么会是错的?它在几乎所有情况下都有效!
我最喜欢的例子是Go语言作者拒绝将单调时间添加到标准库中,因为他们自信地误解了其必要性
(可能是因为谷歌的时钟从未跳过)
然后在经历了一些重大故障(由于闰秒)后,他们终于添加了它
现在库变得一团糟,因为最初的时钟/时间抽象没有考虑多个时钟的概念
而且每个用 Go 编写的程序都布满了由于使用了错误的时钟而导致的可怕 bug
https://github.com/golang/go/issues/12914 (https://github.com/golang/go/issues/12914#issuecomment-15075… 可能堪称有史以来最糟糕的评论)
这个问题可能是我的最爱。问题确实存在,反馈是:“你不应该那样运行硬件。像谷歌一样运行服务器,不要跳过时间。”这与最初对代码版本控制的立场相似。只需运行一个单一仓库!
我也是这么想的,为什么他们不直接避免犯错呢?
这不是关于零错误,而是关于从之前犯过错误的语言中学习并避免重复这些错误。我早早决定不使用Go,因为我意识到他们正在重复多少错误,这些错误最终会困扰维护者。
要是每个人都像你一样聪明就好了。
这不是智力问题,GP也没有暗示这是智力问题。
在过去10多年的Go职业生涯中,我被这类问题困扰的次数屈指可数,而仅仅在过去三周内,我就被半吊子的Java问题困扰了多次。
关于Java有很多话要说,但无论是标准库还是流行的第三方库,都经过了实战考验,所以我很难相信你的说法。
它们可能确实如此,因为Java的时间处理几乎总是很糟糕。最初有java.util.Date,它设计得非常糟糕。Sun试图用java.util.Calendar来修复它。这在一段时间内有效,但仍然很繁琐,有人记得Calendar.getInstance()吗?之后有人坐下来编写了Joda-Time,这确实非常出色,我认为它是JSR-310和新java.time API的基础。所以你说的有道理,但他们花了15年才把它做好。
在Date“统治”的时代,是否有其他语言拥有更好的时间库?而Calendar并非Date的替代品,因此它有点脱离主题。
Joda Time是一个优秀的库,它基本上是Java时间API的基础,也是几乎所有现代语言时间API的基础,但考虑到历史——Java基本上总是拥有当时最好的时间库。
你可以相信你喜欢的东西,当然,但“经过实战考验”并不意味着“不容易被滥用”。
ROFL,真的吗?
Go语言比Java更好吗?当然,可以,也许吧。我不是Java专家,所以对此没有立场。
Go语言本可以比现在好得多吗?如果Pike及其团队考虑过Google以外的使用场景,或者稍微向外寻求灵感,Go语言会更好吗?毫无疑问是的,而且这些改动无需牺牲语言简洁性、编译速度等核心优先级。
认为Go比某些前辈语言更好是完全可以的,同时对它错失的那些显而易见、本可以大幅提升的低 hanging fruit 感到彻底沮丧,这也是可以理解的。
虽然关于字符串编码的普遍问题是可以接受的,但在一个通用且跨平台的语言中,一个强制要求Unicode正确性的文件接口是积极的缺陷,因为世界上存在一些文件,它将无法与之交互。如果你的语言强制要求这一点,而它没有一个字节袋作为备用方案,那么它就是有缺陷的,你只是还没有遇到它。Go 在这个特定的 API 上是正确的。我在此并不庆祝这一事实,也不认为 Go 的设计者会庆祝,但它仍然是正确的。
这是让我感到困扰的事情之一,比如 Rust 中的 OsStr / OsString。从理论上讲,这是一种非常优雅、原则性的字符串处理方式(必须是 UTF-8)和文件名处理方式(在 Linux 和 Mac 上几乎可以是任意字节)。但在实际使用中,OsStr 的操作体验非常糟糕。它们缺少普通字符串的大部分 API……似乎对它们的操作只是事后才考虑的,而且假设人们会将它们视为不透明的(这是错误的)。
Go 允许字符串包含非 Unicode 内容的混乱方法在我看来更具人体工学性。你在关心字符串是否为 UTF-8 的地方验证它们是否为 UTF-8。(所以我同意。)
主要问题不是无效的 UTF-8,而是无效的 UTF-16(在 Windows 等系统上)。据我所知,Go 在这方面曾存在严重 bug(https://github.com/golang/go/issues/59971),直到最近采用 WTF-8 编码(该编码实际上是为 Rust 的 OsStr 设计的)。
WTF-8 编码有一些不便的特性。连接两个字符串需要特殊处理。Rust 的不透明类型可以弥补这一缺陷,但我猜 Go 的 WTF-8 处理方式会暴露一些反直觉的行为。
有人希望为OsStr添加一个普通的字符串API,但具体细节尚未确定。例如:是否应该允许使用OsStr needle对OsStr进行分割?这可以实现,但需要切换到OMG-WTF-8编码(https://rust-lang.github.io/rfcs/2295-os-str-pattern.html),该编码包含更多特殊情况。(我已经用 OsStr::slice_encoded_bytes() 参与了这场讨论。)
是的,我刻意避免提及Windows,因为Windows并非使用UTF-16,而是使用“int16字符串”,与Unix文件名使用int8字符串的方式相同。
依我之见,Windows与其他平台的差异使得我对WTF-8更加不满。C++确实有很多令人不快的地方,但至少我可以做类似以下的事情:
#if _WIN32
using pathchar = wchar_t;
constexpr pathchar sep = L'\';
#else
using pathchar = char;
constexpr pathchar sep = ‘/’;
#endif
using pathstring = std::basic_string<pathchar>;
请注意,这有很多糟糕的地方,其中一个主要原因是,你直接暴露在不同操作系统中路径表示的差异中。尽管这(上面)有很多糟糕的地方,但我仍然更喜欢它,而不是 Go 或 Rust 的方法。
> 你在关心字符串是 UTF-8 的地方验证它们是 UTF-8。
与任何缺乏静态类型系统的情况一样,问题在于你现在必须在每个关心UTF-8的地方进行验证,或者仔细跟踪值是否已验证过,而不是只验证一次并让编译器检查是否已完成验证。
实际上,验证通常发生在转换为 JSON 或使用 HTML 模板等场景中,因此并非在所有地方都需要验证。
验证固然不错,但 Rust 的原则性方法有时让我束手无策。也许 Rust 终将完善 OsString 接口,届时我们可以称 Rust 在这场讨论中“胜出”,但目前尚未实现,且已过去多年。
> 验证通常发生在
除非验证没有发生,然后你不得不面对该死的克苏鲁,因为每个人都认为他们可以随意做出错误假设,而这些假设实际上在任何地方都没有被强制执行,因为“哦,那永远不会发生”。
这不是工程学。这是靠运气编程。
> 也许Rust会最终搞清楚OsString接口
OsString难以使用的原因正是这些问题真实存在。Go语言将这些问题抛诸脑后,迫使你在某个倒霉的用户丢失数据时收拾残局。Rust则迫使你直面这些问题,尽管它们令人不快。这种痛苦只发生一次,之后问题便在未来无限期得到解决。
Rust 还提供了 OsStrExt,如果你不在乎可移植性,这将大大减少许多问题。
我不知道这为什么不理想:错误是困难的,但如果你不需要可移植性,你可以选择更好的 ergonomics。如果你后来需要可移植性,编译器会告诉你你不能使用你选择的捷径。
你能举个例子说明 Go 的方法如何导致数据丢失吗?博客文章中提到了这一点,但没有解释任何内容。
似乎 GGGGGP 帖子中存在一些混淆,因为即使文件名不是有效的 UTF-8,Go 也能正确工作……也许这就是他们没有注意到任何问题的原因。
假设你正在编写一个函数,用于遍历目录将文件复制到其他位置,然后删除该目录。不幸的是,你遇到了这个
https://github.com/golang/go/issues/32334
哎呀,看起来有些文件对你来说是不可访问的,你无法复制它们。
幸运的是,当你尝试删除源目录时,Go 的标准库会进入无限循环,从而保存你的数据。
https://github.com/golang/go/issues/59971
更严重的问题是,API允许同时返回错误和有效的文件句柄。这可能在文档中注明不会发生。但看看Read方法。它会同时返回错误和长度,你需要同时处理这两者。
Read() 方法显然是个例外而非常规。常见的惯例是在遇到错误时返回 nil 值,除非返回两个值确实有实际意义,例如在部分读取最终失败但仍产生了一些非空结果的情况下。这种情况确实很少见,但如果你必须处理这种情况,你可以这样做。否则,通常在 err!=nil 时忽略结果。这确实很混乱,但现实世界也同样混乱,Go 语言承认这一点
Go 语言并不承认这一点。它选择回避。
大多数情况下,如果有结果,就没有错误;如果有错误,就没有结果。但请务必每次都进行检查!并且确保在检查时不要犯错,不小心使用了该值,因为尽管从技术上讲它是无意义的,但它仍然名义上是一个有意义的值,因为零值本应是有意义的。
哦,还有,一定要仔细检查文档,因为语言无法告诉你哪些情况下两个返回值都是有意义的。
现实世界本就是混乱的。而Go语言不会提前警告你混乱所在,也不会努力防止你陷入其中,只会一直站在你身边批评你,让你自己清理这些混乱。“你不再使用那个变量了,也把它清理掉吧。” “现在没有新变量了,所以用err =代替err :=。”
> 如果文件名不是有效的 UTF-8 格式怎么办?
没什么?我认为 Go 和操作系统都不要求文件名必须是 UTF-8 格式。
> 没什么?
会出错。这很奇怪,因为你可以创建一个无效的 UTF-8 字符串(例如 “xbdxb2x3dxbcx20xe2x8cx98”)并正常打印它;只是不能将其传递给例如 `os.Create` 或 `os.Open`。
(Bash 和其他多种工具也会抱怨它不是有效的 UTF-8;Neovim 不会保存以该名称命名的文件;等等。)
这听起来像是你的内核拒绝创建该文件,与 Go 无关。
$ cat main.go
package main
import (
“log”
“os”
)
func main() {
f, err := os.Create(“xbdxb2x3dxbcx20xe2x8cx98”)
if err != nil {
log.Fatalf(“create: %v”, err)
}
_ = f
}
$ go run .
$ ls -1
‘’$'275262'‘=’$'274'‘ ⌘’
go.mod
main.go
我有点困惑,Go是否仅限于使用UTF-8文件名?因为它可以读写任意字节序列(这是字符串可以存储的内容),这应该足以处理其他编码吧?
Go并不受限,因为字符串只是惯例上使用UTF-8,但并不局限于此。
那么我很难理解帖子中的问题,它似乎相当模糊,有没有人知道具体发生了什么问题,是他们使用 Go 的方式有问题,还是 Go 本身存在实现问题,特别是这些代码行:
如果你将随机二进制数据放入字符串中,Go 就会继续运行,正如这篇帖子中所描述的。
多年来,我因工具跳过非UTF-8文件名而丢失数据。我不应因拥有在UTF-8出现前命名的文件而被责怪。
让我翻译: “我决定不喜欢某件事,现在我将各种之前的负面经历与它联系起来”
是的,这个投诉相当奇怪,或者至少不清楚。
> 这听起来像是你的内核拒绝创建该文件
是的,当 bash 等工具也遇到问题时,这就是我的假设。
听起来你是在文件系统中发现了 bug,而不是 Golang 的 API,因为你可以将那个字符串传递给那些函数并成功打开文件。
嗯,当使用 8 位文件名时,Windows 确实是个奇怪的系统。如果直接处理,即使使用损坏的 UTF-8 也无法表示所有有效的文件名,而非有效的 Unicode 文件名无法在不丢失数据或采用某些奇怪约定的情况下编码为 UTF-8。
你可以使用类似 WTF-8(这不是拼写错误,可惜)的方法使其双向兼容。Rust 在底层实现了这一点,但未暴露内部表示形式。
你所说的“当使用8位文件名时”是什么意思?你是指-A API,比如CreateFileA()吗?这些API不支持UTF-8,除非你使用的是允许进程以UTF-8代码页运行的较新版本的Windows。
一般来说,Windows 文件名是 Unicode 的,你可以通过使用 -W API(如 CreateFileW())来表示这些文件名。
Windows 文件名在 W API 中是 16 位(A API 基本上通过转换为活动的老式代码页来封装),并且通常是格式正确的 UTF-16。但它们并不一定需要如此——我认为 NTFS 本身只关心 0x0000 和 0x005C(反斜杠),而栈中的所有层都接受无效的 UTF-16 代理字符。别让我开始谈论正常的 Win32 路径处理(Unicode 规范化、“COM” 仍是一个特殊文件等),其中部分操作可以通过在 NTFS 中使用“\?”前缀来绕过。
归根结底,由于这些值并非总是 UTF-16,因此没有标准方法可以将它们转换为单字节字符串,使得有效的 UTF-16 转换为有效的 UTF-8,而其余部分仍可进行往返转换。这就是像 WTF-8 这样的混杂编码所解决的问题。Rust Path API 是目前我见过的处理此类问题最佳方案,不会因不良 Unicode 编码而崩溃。
我认为这取决于底层文件系统。Unicode(UTF-16)在 NTFS 中是原生支持的。但 Windows 仍支持 FAT,我猜在 FAT 中可能存在多种 8 位编码:所谓的“OEM”代码页(437、850 等)或“ANSI”代码页(1250、1251 等)。我尚未核实近期 Windows 版本如何处理无法用 Unicode 表示的 FAT 文件名。
我认为 Linux 也是如此,它只关心 0x2f 字节(即“/”)。
Windows 路径在文件系统级别上并不一定是格式正确的 UTF-16(按某些人的定义是 UCS-2)。如果它们始终格式正确,您可以通过简单的 Unicode 重新编码将其转换为单字节表示形式。但由于它们并非如此,如果您希望将它们转换为单字节字符串(使其与 UTF-8 编码匹配),则需要对格式不正确的 UTF-16 进行处理。
在 Linux 中,它们是 8 位几乎任意字符串,如你所指出的,通常是 UTF-8。因此它们始终具有方便的 8 位编码(即保持原样)。然而,如果你想将它们转换为 UTF-16,你将面临与 Windows 相同的问题,只是方向相反。
以及 0x00,如果我记得没错的话。
以及 0x00。
> 但是如果文件名不是有效的 UTF-8 编码怎么办?
它们可以支持将文件名作为 `string | []byte` 传递。但等等,Go 甚至没有联合类型。
需要注意的是,Go 中的字符串可能包含无效的 UTF-8 编码。在 1.0 版本之前,遇到无效的 UTF-8 字符串时会引发 panic。
这正是问题的核心。如果 string 类型无法对内容做出任何超出 []byte 的额外假设,那么拥有 string 类型有何意义?答案是,他们原本计划在遇到无效的 UTF-8 时让转换为 `string` 时抛出错误,并假设 `string` 是有效的 UTF-8,但这在其他地方引发了问题,因此他们为了立即的实用性而放弃了这一设计。
Rust在发布1.0版本时,似乎已经非常接近于不将&str作为原始类型,而是仅提供一个指向&[u8]的库别名。
这再次彰显了Rust的安全文化。虽然将&str作为&[u8]的别名会很方便,但如果允许这种错误,Rust现在集中进行的所有安全检查都将由每个用户永久承担。 instead of a few dozen checks overseen by experts there'd be myriad sprinkled across every project and always ready to bite you.
它不会是别名,而是 struct Str([u8])。安全机制本身不会有任何变化。
https://github.com/rust-lang/rfcs/issues/2692
我喜欢这种历史知识。感谢分享!
即便如此,你还是会遇到像 len 这样的“纸割伤”,它返回字节数。
字符串长度的问题在于,可能至少有四个概念可以被称为“长度”,而当没有一个是 len 时,很少有人会满意。
就我所知,按计算难度排序:字节长度、字符集点数、字符/字形数量、显示高度/宽度。
也许最好让Str根本不包含len。它可以包含bytes、code_points、graphemes。这样每个用法都会非常精准。
> 字符串长度的问题在于,可能至少有四个概念可以被称为“长度”。
解决办法不是放弃选择,随便选一个,其他情况不管。而是要_全部暴露_,让工程师自行选择。为了不重复讨论Rust,我指出Ruby在这方面也做得很好。
* String#length # 统计 Unicode 码点数量
* String#bytes#length # 统计字节数量
* String#grapheme_clusters#length # 统计字母群数量
同样,这些“视图”允许你自然地对这些概念进行切片、索引等操作。Go语言的字符串是其中最糟糕的。它们名义上是UTF-8编码,但实际上没有任何机制强制执行。实际上,它们只是字节桶,除非你将它们发送到要求UTF-8编码的API,否则这些API会静默地忽略它们或在编码不正确时出现异常。
高度/宽度显示取决于字体,因此不能直接作用于“字符串”,而需要一个带有额外上下文的对象。
你也可以使用代码单元数量,这是C#选择的方案。
当你尝试从字符串中切取一段并最终选择一个索引(可能基于长度)时,可能会导致代码点被分割。String/str 通过 chars 迭代器对 Unicode 标量(代码点)提供抽象,但基于字节的抽象几乎成为默认选项,这让人感觉有些凌乱。
顺便说一句,文档中指出,与字符集群(grapheme clusters)相关的工作永远不会进入标准库。
你可以轻松地将 `&str` 视为字节,只需调用 `.as_bytes()`,即可得到 `&[u8]`,无需任何疑问。你不希望默认将 &str 视为字节的原因是,这几乎总是错误的做法。更糟糕的是,这种错误在 99% 的情况下都能正确工作,因此你可能直到为时已晚才意识到存在 bug。
如果你的 API 接受 &str,并尝试进行基于字节的索引,它几乎肯定应该接受 &[u8] 而不是 &str。
如果你的 API 接受 &str,并尝试进行基于字节的索引,它
几乎肯定应该接受 &[u8] 而不是 &str。
字符串是按字节索引的。这就是问题所在。
> 但将基于字节的抽象作为默认选项感觉有些混乱。
我的意思是,两者都不应作为默认选项。您应该在使用时选择字符或字节,但我认为这不可行;大多数语言已选择其中一种作为首选形式。或者有些语言在 90 年代具有前瞻性,围绕 UCS-2 构建并后来扩展到 UTF-16,因此您拥有 16 位“字符”,其中某些代码点对应两个字符。当然,处理操作系统意味着要处理它们提供的内容以及语言的偏好(或者,如本帖其他地方讨论的,假装它不存在以让简单的事情更简单,困难的事情更困难)
.(清晨的脑子不灵光——我需要咖啡)
从技术上讲,原始类型是str,确实可以创建&mut str,但很少有人会想可变地借用字符串切片。
然而,&str 并不是“&&String 的别名”,我实在无法想象你是如何得出这个结论的。String 不存在于 Rust 的核心中,它是来自 alloc 的,因此如果你没有分配器,它就不会可用。
str 并不是真正的“基本类型”,它仅作为类型构造器的参数抽象存在——将 & 运算符视为该目的的“类型构造器”,但包括 Box<>、Rc<>、Arc<> 等。因此你可以有 Box<str> 或 Arc<str>,以及 &str 或 &mut str,但不能单独使用 ‘str’。
为什么不在需要的地方使用 utf8.ValidString?为什么让最基本的数据类型承担高度特定的格式检查?
在处理混乱数据时遇到一些 � 符号,总比应用程序拒绝运行并到处出错要好。
依我之见,UTF-8 并非高度特定的格式,它是文本的通用编码。你在 C 或 C++ 中编写的每个 ASCII 字符串本质上已经是 UTF-8。
这意味着对于 99% 的场景,char[] 与标准 UTF-8 字符串之间没有区别。它们具有相同的数据表示和内存布局。
问题出现在人们开始像在PHP中使用字符串那样使用字符串时。他们只是用它来存储随机字节或其他二进制数据。
这与字符串类型不相符。字符串是文本,但现在我们没有文本。这就是问题所在。
我们应该使用byte[]或其他类型来替代字符串。这是对字符串的滥用。我认为允许字符串不代表文本并不太过限制——这就是字符串的本质!
你提倡的方法是上世纪70年代在Unix文件系统和80年代在Perl中被放弃的方法,而且是有充分理由的。
Unix 的一个重大进步在于,无需对二进制数据和文本数据进行单独处理;它们可以存储在同一类型的文件中,并包含在同一类型的字符串中(遗憾的是,C 语言除外)。有时你需要进行一些特定的文本处理,但其余时间你可以保持所有代码的 8 位兼容性,以便安全地处理任何数据。
采用你所倡导方法的语言,如 Python,经常会出现一些 bug,例如无法打印异常 traceback(因为 stdout 被设置为 ASCII)或无法打开通过命令行传递的文件名(因为它们不是有效的 UTF-8)。
如我在https://news.ycombinator.com/item?id=44991638中所示,在Rust等语言中很容易遇到这个问题。
并非所有文本都是 UTF-8,而在某些实际应用场景(如 Windows)中,这一点至关重要。
是的,Windows 中的文本以它自己的特殊方式存在问题。
我们可以尝试将它转换为适用于其他文本的对象,但在边界情况下这不会奏效。
例如,如果我将Linux上的文本写入Windows文件,文本会损坏。反之亦然。
Go语言允许你这样做。但在Rust或使用大多数语言的库时,你不能。你必须专门进行转换。
这就是我所说的“将随机二进制数据存储为文本”的意思。当然,Windows 的 UTF-16 变种某种程度上算是文本,但其实并非如此。它有自己的特性。这需要使用不同类型的字符串,或者将其转换为普通字符串。
即使在 Linux 上,文件名中也不能包含 ‘/’,而在 macOS 上不能包含 ‘:’。这还不包括与字符串中的空字节相关的问题。拥有一个单独的 Path 对象来表示文件名或路径 + 文件名是有意义的,因为在每个平台上,路径都有其独特的特殊要求。
这可能是由于系统/操作系统级别上设计决策不周而导致的遗留垃圾,但我们不得不接受它。而一种没有提供必要工具来安全地处理这种混乱的语言,在我看来,并不是一个值得信赖的开发平台。
确实存在一些语言,它们明确在易用性(例如统一的字符串类型)与正确处理许多现实世界边界情况之间做出权衡。但这些语言不应用于备份系统等严肃场景,因为边界情况可能导致数据丢失。Go语言在追求语言简洁性的同时,却被定位为用于编写严肃程序的严肃语言,这与其实际情况不符。
> 即使在 Linux 上,文件名中也不能包含 ‘/’,而在 macOS 上不能包含 ‘:’
是的,这就是为什么所有专业的库都不使用字符串来表示路径。它们有自己的路径数据类型,因为这实际上是不同的数据类型。
再次,你可以像 Go 那样直接使用有缺陷的字符串,但这很愚蠢,你不应该这样做。他们应该参考 C++ 的 std::filesystem,它在这方面实际上做得相当不错。
> 一种没有提供必要工具来安全地处理这种混乱的语言,在我看来,并不是一个严肃的开发平台。
我同意,即使PHP在这方面也比Go做得更好,这真的说明了一些问题。
> Go在追求语言简洁性时做出了妥协,而它被定位为一种用于编写严肃程序的严肃语言,但实际上并非如此。
我同意。
> 这就是为什么所有专业的库都不使用字符串来表示路径。它们有自己的路径数据类型,因为这实际上是不同的数据类型。
它有什么不同?我没有看到这里有任何与不同类型相关的限制。请注意,这个线程已经混淆了问题,因为他们说的是文件名,而你说的是路径。路径可以包含/,它只是碰巧有特定含义。
如果你想要对磁盘上文件位置的更好抽象,那么你根本不应该使用路径,因为如果文件被移动,路径就会失效。
字符串可以包含路径无法包含的字符,这取决于操作系统。因此,只有某些字符串是有效的路径。
通常的做法是让路径构造函数进行验证,或者使用静态函数 path::fromString()。
此外,当文件被移动时路径失效是正确的行为。例如 openFile() 或 moveFile() 函数需要路径。路径也可以是相对位置。
> 字符串可以包含路径无法包含的字符,这取决于操作系统。因此,只有部分字符串可以作为有效路径。
可以吗?如果你想打开一个文件名中包含无效UTF-8字符的文件,那么路径必须包含这些字符。
路径可以包含路径分隔符——但文件名不能包含它。
> 例如,openFile()或moveFile()等函数需要路径。
macOS 有一种称为书签 URL 的功能,其中可以包含诸如 inode 编号或网络挂载地址等信息。应用程序会利用它来记住如何找到最近打开的文件,即使你重新组织了磁盘或挂载点已断开连接。
据我所知,它确实会解析为路径,因此最终可以使用 open() 函数,但你可以想象一种替代方案。当然,安全问题暂且不提。
Rust 允许在字符串中使用空字节。大多数(所有?)操作系统不允许在文件名中使用空字节。
我一直认为字符串类型的目的是用于索引。字符串的每个索引始终对应一个字符,但字符有时由多个字节组成。
没错。但需要明确的是,在 Unicode 中,字符串索引的是码点,而非字符。例如,一个表情符号可能由多个码点组成,某些语言中的特定字符也是如此。Unicode 中此类字符的名称为“字形”(grapheme),而字形拆分操作极为复杂,通常应由专门的 Unicode 库处理,而非通用字符串对象。
你无法以高效的方式实现这一点,而且走这条路可能会导致问题,因为字符(在Unicode中称为“字母”)通常不会像开发人员假设的那样行为。
字符串只是一个不可变的[]byte。Go语言中字符串可以包含无效的UTF-8字符,这是我最喜欢的功能之一,这样就不会像Rust那样陷入String、OSString、PathBuf和Vec<u8>的混乱之中。在Go中,这一切都只是字符串
Rust 的 &str 和 String 专门用于 UTF-8 有效的文本。如果你在处理任意字节序列,那么在 Rust 中就是使用 &[u8] 和 Vec<u8>。这并不是“混乱”,只是与 Go 的做法不同。
如果有什么能让 Rust 程序在任何奇怪的文本输入下都可能正确运行,而 Go 可能只是处理 ASCII 输入的理想情况。
这类细节在标准库层面至关重要。
我一直不明白这种类型在实际中有什么用处。在什么情况下你真的需要将其限制为有效的 UTF-8?
你应该始终能够遍历字符串的码点,无论它是否是有效的 Unicode。迭代器可以默默地用替换字符替换任何错误,或者通过返回例如 `Result<char, Utf8Error>` 来标记错误,具体取决于用例。
据我所知,所有尝试限制Unicode的语言最终都不得不添加工作绕过机制,因为现实世界中的“文本”有时会包含编码错误,而保留错误往往比通过替换字符破坏数据更好,或者干脆拒绝接受某些输入并导致程序崩溃。
在 Rust 中有 bstr/ByteStr(目前正在添加到 std 中),选择使用哪种字符串类型有些尴尬。
在 Python 中有 PEP-383/“surrogateescape”,它之所以有效是因为 Python 字符串不保证有效(它们可能是格式不正确的 UTF-32 序列,带有范围限制)。确定何时实际使用它有些尴尬。
在 Raku 中有 UTF8-C8,这可能是所有解决方法中最奇怪的(留给读者自行尝试理解……哦,它还会干扰未规范化的有效 Unicode,因为这是另一个愚蠢的限制)。
与此同时,Unicode标准本身将Unicode字符串定义为代码单元序列[0][1],因此Go是少数几个真正实现Unicode(8位)字符串的现代语言之一。值得注意的是,Go的三位发明者中至少有两位也基本上发明了UTF-8。
[0] https://www.unicode.org/versions/Unicode16.0.0/core-spec/cha…
> Unicode字符串:包含特定Unicode编码形式的码单位序列。
[1] https://www.unicode.org/versions/Unicode16.0.0/core-spec/cha…
> Unicode 字符串在所有情况下不必包含结构良好的代码单元序列。这相当于说,特定的 Unicode 字符串不必采用 Unicode 编码形式。
Rust 处理此问题的方式完全合理。字符串类型保证其内容为有效的 UTF-8。当你从字节数组创建字符串时,你有三个选项:1) ::from_utf8,这将迫使你处理无效的 UTF-8 错误,2) ::from_utf8_lossy,这将用替换字符代码点替换无效的代码点,以及 3) from_utf8_unchecked,这不会进行有效性检查,并且明确标记为不安全。
但没有选项可以直接用无效字节构建字符串。3) 并非为此目的而设;它适用于您已知字符串有效的场景。
如果你使用 3) 方法从无效字节创建 &str/String,你不能安全地使用该字符串,因为标准库不幸地假设仅存储有效的 UTF-8 字符。
https://doc.rust-lang.org/std/primitive.str.html#invariant
> 构建一个非 UTF-8 字符串切片并非立即导致未定义行为,但对字符串切片调用的任何函数都可能假设其为有效 UTF-8,这意味着非 UTF-8 字符串切片可能在后续操作中导致未定义行为。
> 如果你使用 3) 方法从无效字节创建 &str/String,你无法安全地使用该字符串,因为标准库不幸地假设仅存储有效 UTF-8。
是的,这是件好事。它允许每个获取 &str/String 的代码假设输入是有效 UTF-8。另一种选择是,每次你编写一个接受字符串作为参数的函数时,你必须分析你的代码,考虑如果参数不是有效的 UTF-8 字符串会发生什么,并适当处理。你每次修改函数时也必须重新进行整个分析。这是一种可怕的时间浪费:最好是:
-
尽早将内容转换为 String,并在后续假设其有效性,以及
-
对于明确不关心有效性的函数,使用 &[u8] 代替。
当然,这正是 Rust 所做的:我不知道 &str 允许你做而 &[u8] 不能做的事情,除了那些需要假设它是有效 UTF-8 的情况。
> 当然,这正是Rust的做法:我不知道有任何事情是&str能做而&[u8]不能做的,除非这些事情确实需要你假设它是有效的UTF-8。
这难道不正是我的观点吗?如果使用 &[u8] 就能完成所有操作,那么验证 UTF-8 的意义何在?它只是一个通用性较低的字符串类型,而你的程序在进行不必要的验证时浪费了 CPU 周期。
任何库函数如何处理完全随机的字节?比如,它如何遍历码点?它可能需要假设 UTF-8 的标准规则,例如知道在某个字节前缀之后,下一个字节也是同一码点的一部分(如果我用词不当请见谅),但现在你需要在每一行都进行复杂的错误处理,而如果你只是让你的类型只表示有效的实例,这些处理就是不必要的。
再次强调,这是简单化与恰当抽象的区别,前者只是将复杂性分散到更大的范围。
如果你有一个未经过UTF-8编码的字节数组,那就直接使用字节数组。
二进制字符串上有许多有效且定义明确的操作,例如排序、哈希、写入文件、测量长度、使用它们构建Trie、按分隔符字节或子字符串分割、连接、子字符串搜索、作为消息发送到ZMQ、作为ZMQ前缀订阅、在LevelDB中用作键或值,等等。对于不包含空字节的二进制字符串,我们可以添加将其作为命令行参数传递以及将其用作文件名。
UTF-8(顺便说一下, 由设计 Go 的团队设计)的目的是以一种方式编码 Unicode,使得这些字节字符串操作对应于 Unicode 操作,这样您就不必关心字符串是 Unicode 还是简单的 ASCII,因此您不需要任何错误处理,除非在极少数情况下,您想对字符串语义上表示的文本做一些操作。唯一无法直接映射的操作是测量长度。
>二进制字符串上存在许多有效且定义明确的操作,例如 (…) 等。
你列出的每一项都由 &[u8] 类型支持。这就是重点:如果你想在不假设数据是有效 UTF-8 的情况下操作数据,只需使用 &[u8](或分配 Vec<u8>),标准库提供了你通常需要的操作,除了那些假设字符串是有效 UTF-8 的函数(如遍历码点)。如果你需要这种功能,你需要将 &[u8] 转换为 &str,而转换过程会迫使你检查转换错误。
问题在于,有太多函数不必要地接受 `&str` 而不是 `&[u8]`,因为人们期望文本内容应该使用 `&str`。
因此,你自然会编写另一个接受 `&str` 的函数,以便将其传递给仅接受 `&str` 的另一个函数。
本质上,没有人真正需要验证(即在前面额外遍历字符串一次),我们只是将其作为合同的一部分,因为其他地方已经将其作为合同的一部分。
情况远比这糟糕——在许多情况下,例如在 Linux 命令行中将文件名传递给程序时,正确的行为要求_不_进行验证,因此当验证失败时抛出错误会引入 bug。我已在https://news.ycombinator.com/item?id=44991638中对此进行了更详细的解释。
从语义上讲这没问题,但给 &str 这样一个短名字会让人产生危险的冲动,将其用于文件名、标准输入输出(stdio)和命令行参数等场景。这种转换过程会向本应可靠运行的代码中引入错误,而这种错误在 Go 中是不会出现的。如果将其命名为 ValidatedUnicodeTextSlice 之类的名字,可能就没问题了。
实际上,要引入此类问题非常困难,恰恰因为Rust的标准库设计得非常出色。你能举个具体场景说明这会成为问题吗?
例如,在Linux和FreeBSD等鲜为人知的操作系统上,将命令行参数传递给程序的_极端特殊_场景; https://doc.rust-lang.org/book/ch12-01-accepting-command-lin… 建议:
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
...
}
当我运行这段代码时,一个_来自官方手册的字面示例_,使用我这里文件名,它会引发 panic:
$ ./main $‘200’
线程 ‘main’ 在调用 `Result::unwrap()` 时因 `Err` 值 “x80” 引发 panic,库文件 library/std/src/env.rs:805:51
提示:请使用 `RUST_BACKTRACE=1` 环境变量以显示调用堆栈
($‘200’ 是 Bash 中表示值为 128 的单个字节的记法。我们在下面的 strace 输出中会看到它。)
因此,任何使用 Rust 编写的程序,如果尝试传递该文件名,并且使用手册推荐的方式接受命令行参数,都会崩溃。它可能在各种测试中运行良好,但在生产环境中遇到一个文件名不符合有效 Unicode 规范的文件时,就会崩溃。
我刚写的这个 C 程序处理得很好:
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
char buf[4096];
void
err(char *s)
{
perror(s);
exit(-1);
}
int
main(int argc, char **argv)
{
int input, output;
if ((input = open(argv[1], O_RDONLY)) < 0) err(argv[1]);
if ((output = open(argv[2], O_WRONLY | O_CREAT, 0666)) < 0) err(argv[2]);
for (;;) {
ssize_t size = read(input, buf, sizeof buf);
if (size < 0) err(“read”);
if (size == 0) return 0;
ssize_t size2 = write(output, buf, (size_t)size);
if (size2 != size) err(“write”);
}
}
(我可能应该使用 O_TRUNC。)
这里可以看到它成功复制了该文件:
$ cat baz
cat: baz: No such file or directory
$ strace -s4096 ./cp $‘200’ baz
execve(“./cp”, [“./cp”, “200”, “baz”], 0x7ffd7ab60058 /* 50 vars */) = 0
brk(NULL) = 0xd3ec000
brk(0xd3ecd00) = 0xd3ecd00
arch_prctl(ARCH_SET_FS, 0xd3ec380) = 0
set_tid_address(0xd3ec650) = 4153012
set_robust_list(0xd3ec660, 24) = 0
rseq(0xd3ecca0, 0x20, 0, 0x53053053) = 0
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=9788*1024, rlim_max=RLIM64_INFINITY}) = 0
readlink(“/proc/self/exe”, “.../cp”, 4096) = 22
getrandom(“xcfx1fxb7xd3xdbx4cxc7x2c”, 8, GRND_NONBLOCK) = 8
brk(NULL) = 0xd3ecd00
brk(0xd40dd00) = 0xd40dd00
brk(0xd40e000) = 0xd40e000
mprotect(0x4a2000, 16384, PROT_READ) = 0
openat(AT_FDCWD, “200”, O_RDONLY) = 3
openat(AT_FDCWD, “baz”, O_WRONLY|O_CREAT, 0666) = 4
read(3, “foon”, 4096) = 4
write(4, “foon”, 4) = 4
read(3, “”, 4096) = 0
exit_group(0) = ?
+++ 退出状态为 0 +++
$ cat baz
foo
上述链接的Rust手册页面解释了为什么他们认为默认在所有程序中引入此错误是个好主意,以及如何避免它:
> _注意,如果任何参数包含无效的Unicode,std::env::args会引发panic。如果你的程序需要接受包含无效Unicode的参数,请使用std::env::args_os。该函数返回一个迭代器,该迭代器生成OsString值而非String值。我们在此选择使用 std::env::args 以保持简单性,因为 OsString 值因平台而异,且比 String 值更复杂。
我不清楚 OsString 的“复杂性”体现在哪里,但目前我先接受手册的解释。
因此,Rust 的设计显然使得在即使是最简单的程序中也极难避免引入此类问题。
Go 的设计则不存在这个问题;这个程序与 C 程序一样运行良好,且没有 Rust 的陷阱:
package main
import (
“io”
“log”
“os”
)
func main() {
src, err := os.Open(os.Args[1])
if err != nil {
log.Fatalf(“open source: %v”, err)
}
dst, err := os.OpenFile(os.Args[2], os.O_CREATE|os.O_WRONLY, 0666)
if err != nil {
log.Fatalf(“创建目标文件:%v”, err)
}
if _, err := io.Copy(dst, src); err != nil {
log.Fatalf(“复制:%v”, err)
}
}
(O_CREATE 让我发笑。看来 Ken 确实把 “creat” 拼成了 “creat”!)
这个程序生成的 strace 输出要杂乱得多,所以我不会包含它。
你可能会好奇,除了故意攻击外,这样的文件名是如何产生的。最常见的情况是文件名使用非 Unicode 编码(如 Shift-JIS 或 Latin-1)编码,随后发生磁盘损坏,但故意攻击的情景也不容小觑。你不想让攻击者能够创建你的工具无法识别或在检查时变成石头的文件名,就像美杜莎一样。
请注意,错误日志消息中也包含了格式错误的 Unicode 文件名:
$ ./cp $‘201’ baz
2025/08/22 21:53:49 open source: open ζ: no such file or directory
但它并没有显示ζ。它实际上输出了一个值为129的字节,导致错误消息成为格式错误的UTF-8。这显然可能带来风险,具体取决于日志文件的存储位置,因为它可能包含任意终端转义序列。但请注意,Rust的UTF-8验证并不能保护你免受此类问题的影响,也无法防范类似以下情况:
$ ./cp $‘n2025/08/22 21:59:59 oh no’ baz
2025/08/22 21:59:09 open source: open
2025/08/22 21:59:59 oh no: 没有此文件或目录
我不是在贬低Rust。Rust有很多优点。但它的字符串处理并不是其中之一。
如果它是&[bytes]或其他什么,我同意。但&[u8]与&str并没有太大区别。
难道 &[u8] 不正是你应该用于命令行参数和文件名等场景的类型吗?在这种情况下,你希望它的名称简短,比如 &[u8],而不是像 &[bytes] 或 &[raw_uncut_byte] 那样冗长。
OsStr/OsString 是在这种情况下使用的。Path/PathBuf 专门用于文件名或路径,我认为它内部使用 OsStr/OsString。我从未查看过 OsStr 的内部实现,但我不惊讶它可能是 &[u8] 的封装。
注意,&[u8] 允许包含空字节,以及可能的其他边界情况。
你无法从命令行参数中获取空字节。根据 https://news.ycombinator.com/item?id=44991638 的内容,在接受命令行参数时通常不使用 OsString,因为 std::env::args 返回的是字符串,这意味着大多数在命令行接受文件名的 Rust 程序可能都存在这个 bug。
Rust字符串可以包含空字节!Rust使用显式字符串长度。不过,大多数操作系统在参数中传递空字节时确实无法处理。
没错,但它不能包含无效的 UTF-8,而 UTF-8 在 Linux、FreeBSD 和其他普通 Unix 系统中的命令行参数和文件名中都是有效的。请参阅我上面的链接,了解这如何导致 Rust 程序中的错误。
那么 [u8] 肯定可以实现这些函数。
我不明白这个抱怨。(3)听起来正是你所要求的。而且,做不安全的事情确实不安全。
> 我不明白这个抱怨。(3)听起来正是你所要求的。而且,做不安全的事情确实不安全
你应该使用 `unsafe` 作为限制安全推理范围的方式。
一旦你使用 `from_utf8_unchecked` 构建一个 `&str`,你就不能安全地将其传递给任何其他函数,除非查看其代码并推理它是否仍然安全。
另请参阅实际文档:https://doc.rust-lang.org/std/primitive.str.html#method.from…
> 安全性:传入的字节必须是有效的 UTF-8。
> 我一直不明白这种类型在实际中有什么用处。在什么情况下你真的需要限制它为有效的 UTF-8?
因为 99.999% 的情况下你希望它是有效的,并且希望在它无效时出现错误?如果你想处理无效的 UTF-8,那应该是一个有意识的选择。
您希望当文本文件中包含部分写入的字符时,grep 程序会崩溃吗?99.999% 的概率似乎非常高,而且您并未提供该限制的实际使用场景。
崩溃?不。但我可以安全地处理错误发生时的情况,因为语言本身通过返回适当的 Result 类型来帮助我处理这种情况。因此,我必须明确检查我拥有的是哪种“变体”,而不是在 go 情况下忘记调用 validate 函数。
Rust在遇到错误时不会崩溃,除非你告诉它崩溃。你必须选择如何处理错误,否则代码无法编译。如果你不在乎读取文件时丢失信息,可以使用优雅处理无效字节的丢失函数。
我认为你可能忘记了 rune 类型。rune 确实会做出假设。
[]Rune 用于 UTF 字符序列。rune 是 int32 的别名。string,我认为,是 []byte 的别名。
`string` 不是 []byte 的别名。
考虑:
for i, chr := range string([]byte{226, 150, 136, 226, 150, 136}) {
fmt.Printf(“%d = %vn”, i, chr)
// 注意,s[i] != chr
}
该循环遍历 6 个字节的次数是多少?答案是它会遍历两次,分别是 i=0 和 i=3。
还有一些标准 API 在字符串不是有效的 UTF-8 时会表现异常,而如果它只是一个字节集合,就不会出现这种情况。
如果文件名不是有效的UTF-8,Go语言仍然可以打开文件而不会有问题,只要你的文件系统不试图变得聪明。Linux ext4文件系统和Go都将文件名视为二进制字符串,只是它们不能包含NUL字符。
这是帖子中的一个小错误。
> 如果文件名不是有效的 UTF-8 格式怎么办?
那就让它成为有效的 UTF-8 格式。如果你试图在库中一个常用函数中解决所有长尾问题,那会带来很多麻烦。这种方法更好。如果有人遇到像文件名包含无效字符这样的奇怪问题,他们可以自己解决,甚至发布一个包。嘿,为了解决0.01%的问题而让100%的使用场景变得复杂?
> 那么将其转换为有效的UTF-8格式。
我认为你误解了。对于一个存在于磁盘上并试图读取的文件,你如何做到这一点?为他们重命名文件?他们可能不喜欢这样。
> 他们坚持从实际便利性出发,快速解决眼前的问题,而不是从第一性原理分析问题,并正确解决问题(或使用“非我发明”的解决方案)。
我之前说过,Go的大部分设计看起来像是模仿了谷歌的C++风格。我看到有人说他们喜欢Go的某些地方,往往是因为这些特性最初出现在C++宏或工具中。
我离开谷歌前曾检查过这一点,我相信随着时间推移,这种情况会越来越少。但在我看来,Go 的核心理念似乎是“如果我们创建一种类似 Python 的编译型语言,它比 C++ 更易于上手,但仍保留 C++ 的使用体验,会怎样?”
Go 难道不是源自为 Plan9 编写的语言,因此早于 Rob Pike 在谷歌的工作?
是的,Go语言表面上几乎与Pike的Newsqueak完全相同。
我不记得具体细节,但可能记忆有误。
不过,任何人都会将之前的经验带入项目,因此其中必定存在Plan9的影响。
他们实际上使用了Plan9的C编译器和链接器。
是的,我清楚这一点
直接从 Plan 9 源代码构建项目,这与“将之前的经验带入项目,(…) 其中一定存在一些 Plan9 的影响”相去甚远
这是一个C编译器。你的意思是Go受C影响吗?…
他们是从那里开始的,但现在是由Go自身编译的。
我认为你应该升级到一个量化程度较低的神经网络模型。
我不明白你为什么一直如此不礼貌地回复。我曾试图给你一个机会,但看来我只是浪费了时间。
这显然不是我看到的。
好吧,祝你好运,在网上对人发脾气,或者做其他什么事。
我最近开始为一份新工作编写Go语言代码,这是在20年没有接触过编译型语言用于严肃项目之后(我曾以业余爱好开发过DevKitArm)。
我知道这主要是个人喜好问题,但天啊,感觉糟透了。而且没有默认参数值,错误处理看起来很糟糕,生产环境中也没有真正的堆栈跟踪。还有那个“面向对象”语法,每个函数都得加个丑陋的引用。还有指针……
这让我想起了我学习C/C++的时光。就像用25年前的技术编程,那时候我还在1999年的大学里。
然后人们对它实现的编译时间感到惊讶,而编译型语言早在运行于10 MHz的PC上,在640 KB的限制下就已经实现了(如TB、TP、Modula-2、Clipper、QB)。
> [一些]编译型语言早在运行于10 MHz的PC上,在640 KB的限制下就已经实现了
然而,许多编译型语言的编译速度非常慢,尤其是对于大型项目,C++和Rust就是典型的例子。
将C++和Rust归为一类是奇怪的。我使用过Rust代码库,其编译时间仅需2-3分钟,而C++编译器则需要数小时才能完成。
我认为抱怨rustc编译时间的人一定是刚开始使用编译型语言……
不过,有一种方法可以让C++超越Rust。
利用二进制库、导出模板、增量编译和多核链接,如果使用VC++或clang最新版本,还可以使用模块。
虽然它仍然没有Delphi那么快,但变得更容易管理。
确实,但除了C++和Rust,还有其他编程语言。
嗯,输出几乎未优化的机器码并拥有超弱类型系统确实有助于提升速度——就像Go语言一样!
这对某些人来说是个合理的权衡,不是吗?有很多工作可以忍受偶尔的运行时错误和不够顶尖的性能,尤其是如果这意味着在其他领域(编译速度、工具链)有所提升。拥有多种语言可供选择对我来说感觉是个不错的事情。
但 Go 的工具链很糟糕。真的很糟糕。
当然,与 C++ 相比,它还不错。Go 真的在与 C++ 竞争吗?从我的角度来看,不是。
但与 Go 实际用于的场景相比……工具链很糟糕。PHP 的工具链更好,.NET 的工具链更好,Java 的工具链更好。
Go的工具链是最好的之一。你见过npm吗?
Go在一定程度上是对C++的回应,如果我记得它被介绍时是如何描述的。但似乎事情并没有这样发展。也许“系统编程语言”对不同的人意味着不同的东西。
我个人更希望有一个更强大的类型系统(例如,Java 的编译速度同样快,且其类型系统并不孱弱),但当然可以。
从开发者角度来看,这当然是受欢迎的,但从生态系统角度来看,更多语言并不一定更好,因为这会增加所需的努力。
你说Java编译速度一样快是什么意思?你是说Java到字节码的转换很快吗?当然,它几乎什么都没做。Go编译生成机器码,你不能把它和字节码生成相提并论。
Java的AOT编译时间和Go一样快吗?
> 当然,它几乎什么都没做。Go编译生成机器码,你不能把它和字节码生成相提并论
为什么不能?机器码本身并不特殊——C++和Rust变慢是因为优化,而不是因为目标是机器码本身。Go“几乎什么都没做”,只是几乎原样输出机器码。
Java 的 AOT 编译通过 GraalVM 的原生映像实现,速度较慢,但其工作方式不同(完成所有 Java 类加载和初始化,并将这些内容“烘焙”到原生映像中)。
从 Go 的角度来看,Java 的类型系统堪称语言知识的博士水平,这多少有些讽刺。
尤其考虑到该语言在1996年曾遭到批评。
遗憾的是,Go语言缺乏抽象和简单的类型系统,使得我编写代码的速度远比Rust等语言慢。
这对现代编译器有点不公平——现在有更多标准需要遵守,更多(微)架构,前端需要与中间表示(IR)集成,再与优化器和代码生成器集成,等等。其中一部分是自找的:你需要又一个0.01%的优化吗?以可维护性甚至正确性为代价?(你好,UB。)但其中大部分只是计算机技术的演进。
但这些并非规则。如果你是出于兴趣在做这些事情,可以看看QBE <https://c9x.me/compile/> 或Plan 9 C <https://plan9.io/sys/doc/comp.html>(Go语言就是从中衍生出来的!)
> 这对现代编译器有点不公平
其实不然。普罗布斯特定律适用。
基于此,编译器/语言应首先优化程序员生产力,其次才是代码速度。
但事实并非如此——Go 是一种彻头彻尾的现代语言,只是缺少了讨论中提到的某些特性。但它非常出色,我曾为多家企业客户使用它编写了大量 API,效果都非常不错。
如果你想要一种_不错的_现代编译型语言,可以试试Kotlin。它并非完美无缺,但非常易于使用,且编译时间相当合理(针对JVM,我没有尝试过原生编译)。人们也称赞Nim对开发者友好,但我没有亲身体验过。
我只在JVM上使用过Kotlin。你说有办法绕过JVM并用它构建二进制文件?得去查查。Kotlin的问题不在于语言本身,而是使用它找工作可能不太容易。“Kotlin专家”这个职位几乎不存在。你能找到的Golang和Python工作比Kotlin多。
> Go是由一些老派人士设计的,他们可能过于坚持自己的原则,忽视了实际的便利性。
感觉他们坚持的两个原则是“什么能让编写编译器更容易”和“什么能让编译更快”。这些目标不错,但它们几乎不以开发者为中心。
不确定是否仅此而已。我记得在相关讨论中经常出现“我们不是Java”的说法。我总觉得,他们拒绝某些概念(如异常和泛型)更多是出于原则,而非实际分析。
比如,是的,这些概念确实经常被过度使用,导致了它们自己的痛点。但人们也似乎经常重新发现,完全去除它们也会带来痛点。
Ian Lance Taylor,泛型概念的坚定支持者,曾大量撰写关于将泛型添加到Go语言的困难。我猜最初的开发团队不得不缩小范围,打造一个尽可能简单但仍具实用性的工作语言。易于并发是目标,因此他们基本上采用了Modula-2的大部分特性,加上Oberon(及其他来源)的理念,去掉了所有“冗余”(如可通过枚举类型索引的数组等),添加了垃圾回收机制,这已绰绰有余。
我记得他们构建Go的主要原因之一是Google的C++代码编译时间达到了半天之久。
唉,你知道的,年轻人想要新东西。他们其实并不关心能否完成工作。
当我看到“几乎不面向开发者”时,我想到这是来自谷歌,他们以令人难以置信的规模运行计算和编译器。他们优化编译器速度和简洁性似乎并不奇怪。
在拥有大量代码库和构建时间的地方,编译速度快是一个很好的目标。但在只有几万行代码的小型初创公司中,这可能不太适用。
> 并发是棘手的
Go语言及其运行时是我所知唯一能在语言内部无缝处理多核CPU并发的系统,它使用类似CSP(goroutine/channel)的形式化方法,这种方法易于理解。
Python 因全局解释器锁(GIL)和难以理解的异步库而显得混乱。C、C++、Java 等语言需要外部库来实现线程,而这些库无法在语言本身上下文中进行推理。
因此,Go 语言是 HTTP 服务器(或服务)用例的完美选择,根据我的经验,没有其他语言能与之匹敌。
> 因此,Go 语言是 HTTP 服务器(或服务)用例的完美选择,根据我的经验,没有其他语言能与之匹敌。
Elixir 在 2015 年就实现了在单台机器上处理 200 万个 WebSocket 连接,这值得一提。[1] 这主要得益于其底层的 Erlang 运行时环境。
我曾编写过一些复杂的 Go 代码(我为一门课程实现了 Raft 协议)以及大量 Elixir 代码(专业开发),根据我的经验,Go 的并发模型在某些场景下可行,但在其他场景下表现不佳,而且在 Go 中编写潜在风险代码的难度远低于预期。
[1]: https://phoenixframework.org/blog/the-road-to-2-million-webs…
我曾在Elixir和Go两者中工作过。我仍然认为Elixir在并发方面是最好的。
我最近意识到,没有简单的方法可以“将goroutine错误向上传递”,于是我写了一些代码来确保这是可能的,而这时我意识到,像往常一样,我正在重写OTP库的一部分。
整个监督机制对并发处理非常有价值。
> Java 等需要外部库来实现线程
Java 无需外部库即可实现线程,因为线程功能已内置于语言及其标准库中。
> Java 等需要外部库来实现线程,而这些库无法在语言本身上下文中进行推理。
你对Java的这一说法是什么意思?该库是随Java一起提供的运行时环境,尽管底层是操作系统线程,但抽象层并不算泄露,且使用时并不感觉它们实际位于JVM之外。
与它们_协作_时可能会有些笨拙。
此外,Java 是少数几种开箱即用就具备良好并发数据结构的编程语言之一。
我认为“父级”意味着它们(大多)不支持通过关键字实现。但你可以使用 Kotlin 来实现。
尽管如此,仍有许多流行语言能够实现这一点,在许多情况下甚至比 Go 语言做得更好。
我认为这是你所了解的唯一系统。但它绝非唯一。
> 许多流行语言都能实现这一点,在许多情况下甚至比 Go 语言更好
我希望能看到这些语言的列表,并附上相关参考资料。
Erlang、Elixir、Ada,还有许多其他语言。Erlang 和 Ada 比 Go 语言早了几十年。
你想要参考资料,这里是Ada语言参考手册(LRM)中关于任务和同步的章节:http://www.ada-auth.org/standards/22rm/html/RM-9.html
对于Erlang和Elixir来说,并发编程几乎是它们的专长,所以你可以随便拿一本关于它们的书或教程,就会了解到它们是如何处理并发问题的。
Haskell也是其中之一。它支持事务内存,这使得程序员无需显式地考虑锁定问题。
请详细说明或提供一些例子来支持你的观点?
这样的语言并不多。C/C++和Rust都映射到操作系统线程,并且没有内置CSP类型的并发机制。
在Go的类别中,有Java、Haskell、OCaml、Julia、Nim、Crystal、Pony…
动态语言更可能使用绿色线程,但并非 Go 的替代品。
> 没有那么多。
你列出了三个不支持的语言,然后又列出了七个支持的语言。
是的,没有多少语言像 Go 那样支持并发…
在这七种语言中,有多少是主流的?只有一种……
所以实际上是Go与Java的对决,或者你可以接受性能损失使用Erlang(对于某些任务是合理选择但并非万能),或者冒险尝试新型范式/未受支持的语言。
如果你想要主流语言,Java和C#是主流语言,且使用频率远高于Go。Clojure不算太小众,尽管不如Go流行,但其内置并发支持至少与Go相当。Ada在其特定领域仍被广泛使用,且自1983年起就内置了比Go更强大的并发功能。此外,Erlang和Elixir也应列入此列表。
这是 6 种语言,一个不完整的列表,它们要么是真正的主流且比 Go 更受欢迎,要么至少是广为人知且易于获取和入门。所有这些语言都内置了并发支持且支持良好(与 C 不同)。
EDIT:还有一点,除了Elixir外,其余语言都比Go更早出现,尽管Clojure仅比Go稍晚。因此,已有先例可供学习。
Erlang(或Elixir)绝对可以替代Go,用于那些需要CSP(通信安全协议)的软件类型。
来源:过去几周在工作中用Elixir替换了一个Go程序。
我还会再次使用Go(毫无疑问),但它并非万能药。它应是命令行工具和许多服务器的默认选择,但认为它是唯一具备类似CSP特性的可用语言的观点是荒谬的。
除非我们将 JDK 视为外部库。说到库,Java 的并发容器确实强大,且能被众多工程师安全使用。我认为 Go 的生态系统还远未达到这个水平。
> 使用类似 CSP 的形式化系统(goroutine/channel),这种系统易于理解
我以为在Go中很少有人提到这样一个事实:CSP系统在非玩具项目中几乎无法进行推理,因此大家普遍使用互斥锁等机制进行系统级协调。
我甚至不确定是否在生产应用中见过通道被用于除停止goroutine、收集工作组结果或类似局部操作之外的其他场景。
还有原子操作(sync/atomic)以及基于原子操作和/或互斥锁的高级抽象(如信号量、sync.Once、sync.WaitGroup/errgroup.Group等)。我使用过这些,也见过他人使用。
但没错,CSP 模型基本上已经过时了。我认为语言设计者坚持认为 goroutine 不应在用户代码中可寻址或可抢占,这使得这种情况不可避免。
Go 的并发实现更多依赖于其绿色线程和无色函数,而非通道。
> 无色
函数是带颜色的:那些接受 context.Context 的函数和那些不接受的函数。
但我同意,与异步实现相比,这种着色非常微弱。你可以自由地使用 context.Background()。
Go 非常适合多核环境,尤其是它在数据竞争情况下甚至不具备内存安全性的特点。
在实际应用中很少遇到这种情况,而且它会被种族检测器捕获(你需要主动启用该功能)。但语言设计者选择不解决这个问题,所以我认为这是合理的批评。[1]
不过,一旦了解了这一问题,避免它就变得容易了。我认为,尤其是考虑到Go语言的CSP特性如今已被淡化,这一问题应在文档中更突出地提及,并提出更现实的解决方案(如原子操作、互斥锁)。
这也可以通过使用 128 位原子操作来解决,至少对于字符串和接口而言(而切片太大了,占用 3 个字)。添加通用 128 位原子支持的想法已经在他们的雷达上 [2],并且已经存在一个包来实现它 [3], 但我认为字符串或接口不符合对齐要求。
[1]: https://research.swtch.com/gorace
[2]: https://github.com/golang/go/issues/61236
[3]: https://pkg.go.dev/github.com/CAFxX/atomic128
Erlang.
Swift?JavaScript?
JavaScript?怎么用?Web Workers?JavaScript 是单线程的。你无法在不使用用户空间 IPC 的情况下利用多个核心。
我不想过多争论(因为我本人并未使用过此功能),但 Node.js 自 v12 版本起确实支持真正的多线程:https://nodejs.org/dist/latest/docs/api/worker_threads.html。我不确定你所说的“M:1 线程”是什么意思,但我确实很想了解更多,如果你愿意提供更多细节的话。
还有像 Hermes(主要用于 React Native)这样的运行时,支持将图形线程与其他线程的操作分离。
尽管如此,我不会否认原帖关于“在语言内部处理并发”的观点——多线程和并发在Go语言中以更根本的方式内置,而JavaScript则不然。但值得指出的是,至少几个主要运行时都支持多线程,且开箱即用。
就连 Node.js 的作者也承认其并发设计存在问题,并转而使用 Golang。Libuv 虽然很棒,但无法处理所有场景,你仍然需要应对瓶颈、隔离性差以及单线程事件循环等问题。在 Node.js 中,背压问题会变得非常明显且令人头疼,尤其是在大规模应用时。
当然,许多人根本不需要处理那种吞吐量。这取决于应用程序和施加在其上的负载。很多人没有意识到这一点。这没关系!如果它能工作,那就工作吧。但如果你确实需要并发,那么你可能不希望使用 Node.js——即使是新版本。选择Go语言绝对不会更糟。我们能有这些选择是件好事。
我一直强调的一点是,语言和技术的选择不应由个人决定,而是要根据当前的软件和团队来决定。我经常根据负责开发和维护的团队来选择语言、框架和工具。如果你能让团队成功,因为某种语言提供了Rust提供的类型安全或内存安全,或者一个良好的工具链,无论团队需要什么——那真是太好了。事实上,这可能是成功企业和失败企业之间的区别。如果公司倒闭了,没有人使用软件,那么没有人会在乎软件有多神奇。
是的,这些是需要手动管理共享内存/传递内存的 worker:
> 在 worker 线程中,worker.getEnvironmentData() 返回传递给创建线程的 worker.setEnvironmentData() 的数据的副本。每个新创建的 Worker 都会自动获得环境数据的独立副本。
M:1 线程意味着用户空间线程映射到单个内核线程。Go 是 M:N 线程:goroutines 可以任意调度到各种底层操作系统线程。其基本组件(goroutines 和通道)使并发和并行处理比大多数语言简单得多。
_> 但值得指出的是,至少几个主要的运行时环境都支持多线程,开箱即用。
我个人在这种情况下不同意。几乎每种语言都有类似 pthread 的原始并发原语。本帖的背景正是 Go 与常规线程接口的区别。引用 gp:
_> Go 语言及其运行时是我所知唯一能在语言内部无缝处理多核 CPU 并发性的系统,它采用类似 CSP(goroutine/通道)的形式化表示,这种表示方式易于理解。
其他语言确实支持线程,但在 Go 中,并发和并行处理都比大多数语言更简单。
(但不是Erlang 🙂 )
我不得不查看 M:1 线程的实现方式——它是这样的:https://en.wikipedia.org/wiki/Thread_(computing)#M:1_(user-l…
基本上,原帖作者指出 JavaScript 可以同时运行多个任务,但由于所有任务都映射到 1 个操作系统线程,因此不存在并行性。
所以……不是并发。
不。参见[并发与并行](https://stackoverflow.com/questions/1050222/what-is-the-diff…)。
任务是并发执行的,但不是并行执行的。
Erlang?
这是一种恶魔般的做法
我的感觉是,在开发者体验方面,它完美地抓住了“非常有主见、非常标准、一种做事方式”的部分。在大型微服务架构上工作是一件令人愉悦的事情,因为每个仓库不需要采用不同的风格,也不需要避免格式讨论,因为这些都已经包含在内。
问题在于,它在选择“哪些”功能作为Go的标准方式时有些过时。人们期望使用map/filter方法而非存在off-by-one风险的循环,期望类型系统具备TypeScript的智能性(尽管功能较少且强制性更强),错误处理令人头疼,等等。
我明白实现这些功能而不打开通向大量“创造力”(在负面意义上)的大门是困难的。但我觉得Go有时很难被接受,尤其是对于那些母语是JavaScript而非C的年轻开发者。
> 问题在于,在选择哪些内容作为Go的标准做法时,有些内容已经有些过时
我同意这一点。我认为 Go 作为一种新语言,选择以简单实用为目标,拥有优秀的工具链,而非在任何特定方向上过于实验性或雄心勃勃,而是依赖于经过验证的编程模式,这是一个非常明智的选择。只是奇怪的是,他们错过了 2009 年左右已经相当成熟的一些东西。
Map/filter等就是一个完美的例子。我记得大约在2000年,普通程序员认为Map和Filter是毫无意义的怪异和异国情调。为什么不使用一个普通的for循环呢?十年后,普通程序员会说,for循环难以阅读,是隐藏 bug 的完美场所,我无法相信我们曾经甚至在简单的事情上使用它们,比如Map、Filter和foreach。
到2010年,就连Java也决定添加其“流API”和lambda函数,因为无论它们在Java上看起来多么糟糕,它们仍然在清晰度和简洁性方面有所改进。
不知何故,Go 错过了行业迈出的这一步,反而坚持使用“for”。Go 的各种 for 循环形式相较于 C/C++/Java 的 for 循环确实有所改进,但我认为,如果 Go 能够采纳行业正在趋同的成熟解决方案,这将更符合其保守、务实的哲学。
Go泛型提供了所有这些功能。在泛型出现之前,你可以使用filter、map、reduce等函数,但需要在库/包中自行实现这些函数,并且针对每种类型分别实现。
在Go 1.18版本中添加泛型后,你可以直接导入他人实现的这些函数的泛型版本,并在整个代码中使用它们,无需再考虑实现细节。这个问题不再存在。
虽然语言现在允许这样做,但它并非为此设计。我认为如果 Go 设计者原本打算让 map、filter 等函数取代大多数 for 循环,他们会设计一种更简洁的匿名函数语法。比如:
colors := items.Filter(_.age > 20).Map(_.color)
而不是
colors := items.Filter(func(x Item){ return x.age > 20 }).Map(func(x Item){ return x.color })
据我所知,这是在 Go 中表达相同功能的方式,如果容器类型定义了 Map 和 Filter 方法。
> 人们期望有一个 map/filter 方法
他们真的期望吗?经过太多函数式编程的斗争后,我开始实践我开玩笑称之为“调试驱动开发”(Debugging-Driven Development)的方法。就像 TDD 从一开始就考虑设计决策以确保可测试性一样,这让我编写出易于调试的代码(特别是通过 printf 引导的调试和逐步执行调试)
比如,在 for 循环中间添加一个 printf,甚至不需要理解循环的逻辑。只需添加一行代码并写入 printf。我厌倦了那些代码链条紧密、迭代优雅,但到了周日凌晨 3 点匆忙调试时却难以拆解和调试的代码。
我并不是功能编程的坚定拥护者,这一点你得明白。
只是在现实世界的问题中,大量繁琐的步骤往往可以归纳为“重新整理这些数据”、“给我这个集合的一个子集”或“按这个字段对这些数据进行聚合”。
在我看来,循环结构在简洁明了地表达这些常见概念方面表现不佳。它们占用大量屏幕空间,通常需要额外的变量,而且仅从一个 for 循环块中无法立即看出你要做什么——“我即将进行迭代”对我这个读者来说并不是有用的信息,你是正在转换数据、选择数据还是聚合数据?
结果是,你通常会得到大量类似的代码行:
userIds = getIdsfromUsers(users);
其中函数只是封装了一个循环。相比之下:
userIds = users.pluck(‘id’)
你可以将这个封装的实用函数放在其他地方。
Rust 提供了 .inspect() 用于迭代器,这可以满足你的 printf 调试需求。当然,这对实际调试器来说有点困难,但目前支持得相当不错。
我同意显式循环更容易调试,但这要以更难编写 _and_ 阅读(需要在脑海中保持状态)_and_ 更容易出错(因为可变性)为代价。
我认为这是一个糟糕的权衡,大多数语言都在逐渐放弃这种设计
其实还有一个不太明显的优势:for循环允许进行单次内存访问,而非多次。如果你处理的是足够大的列表,这确实会产生显著差异,因为内存访问相对昂贵(差异并非微不足道,仅通过优化内存访问即可使循环性能提升10倍)。
因此,对于大型循环,如下代码:
for i, value := source { result[i] = value * 2 + 1 }
将比以下循环快2倍:
for i, value := source { intermediate[i] = value * 2 }
for i, value := intermediate { result[i] = value + 1 }
这取决于你的迭代器实现(或缺乏迭代器实现),功能上等同于你的第一个示例。
例如,Rust 的迭代器是懒惰评估的,并且在过滤数据时会提前退出,因此它与你的第一个形式相同,但经过了尽可能的优化。另一方面,Python 的 map/filter 等函数可能会每次都返回完整的列表,就像你的中间变量一样。[EDIT] Python 返回生成器,所以这是合理的。
我认为,任何允许函数式数据操作的合理语言,其性能都应与手动 for 循环相当。(这就是为什么 Rust 会用 .iter()/.collect() 让你头疼)
Python 的 map/filter/zip 等函数返回生成器,因此它们是懒惰评估的。
谢谢,我之前不确定,所以用了“可能”。评论已编辑 🙂
Clojure 的转换器也是如此。
这是一个非常有道理的观点。循环还允许你通过调整迭代过程本身来优化性能,例如在满足某个条件时跳过 n 个步骤。
我每隔几年在准备 Leetcode 面试时都会遇到这些优势,因为这种优化对于实现可接受的结果是必要的。
然而,在日常生活中,大多数需要转换的数据块都属于以下类别之一:
– 数据量较小,此时可读性和可维护性比性能更重要
– 数据存储在数据库中,并通过查询而非代码进行过滤/重塑
– 用于队列或类似场景的原子处理(通常在导入大量数据时使用)
– 操作本身是标准算法,你只需从标准库中调用,内部已处理循环逻辑。
就像树和递归一样,我们很少锻炼这块肌肉。当然,具体情况因领域而异。
Rust 在 map/filter/reduce 操作上进行了大量编译器优化,且在许多情况下可轻松实现并行化。
直接使用真正的调试器。你可以逐步调试闭包等内容。
我猜是这样。也许 Go 的调试器不太好用,我不确定。但在 PHP 中使用 xdebug,你可以使用所有花哨的 array_* 方法,然后用调试器逐步调试闭包或可调用对象。
这取决于语言和 IDE。Intellij 的 Java 调试器在流调试方面非常出色。
Go中缺乏堆栈跟踪简直令人发狂,我们不得不手动传递每个错误
> 那些提出整个许可证话题的人不能被当真
那么你的意思是,那些在新的许可证制度下被迫花费大量资金购买许可证的大学和其他机构也不能被当真?你是说他们都没有听取建议,也没有员工告诉他们OpenJDK的存在吗?
关于你的Linux评论,我们中的一些人还记得SCO事件。
遗憾的是,甲骨文的财力比SCO更雄厚,可以雇佣更多律师……
> 你的意思是,那些在新制度下被迫花费大量资金购买许可证的大学和其他机构也不值得认真对待?你是说它们都没有听取建议,而且内部也没有人告诉它们OpenJDK的存在吗?
这些信息对我来说相当意外,因为我所了解的机构从一开始就切换到了基于OpenJDK的替代方案。在甲骨文试图玩弄许可把戏后,没有理由继续使用甲骨文的版本。
为什么这些机构继续使用甲骨文JDK并最终为此付费?OpenJDK是直接替代方案,切换后没有任何价值损失……
TL;DR:无法确定校园内是否有人下载了Oracle Java……Oracle会监控下载行为并派遣审计人员……
参见我之前回复中的链接/引用。
许可问题纯属FUD(恐惧、不确定性与怀疑)。甲骨文是一家糟糕的公司,这绝不是不使用Java的正当理由。
> 甲骨文是一家糟糕的公司,这绝不是不使用Java的正当理由。
奇怪,在我看来这是个有力论据。选择你的守护者。
我提出了一系列客观事实。基于此,基本逻辑推断表明你可以完全自由地使用Java。其他任何说法都无关紧要。
我不知道你提到的大学是哪所,但肯定他们也曾被“迫不得已支付$$$”来缴纳水费等费用。如果他们决定选择“付费支持”,那么……你就得为此付费。作为交换,你可以 a) 在出现问题时指责第三方(政府机构喜欢这样做,且往往在法律上必要)b) 在需要时获得实际的实时支持,即使是在圣诞夜。
简而言之:无法确定校园内是否有人下载了Oracle Java……
引用自本文:[1]
*他告诉《注册表》杂志,甲骨文正在“在各国部署专门的Java销售团队,然后识别那些似乎在下载Java的公司,并要求进行审计。这种策略目前似乎正在全球范围内真正实施。”*
[1] https://www.theregister.com/2025/06/13/jisc_java_oracle/
同样的情况也适用于盗版Photoshop、Microsoft Office等软件。
此外,作为另一个话题,甲骨文进行审计的具体原因在于,其软件并不具备“回传”功能来检查许可证等信息——而这正是其目标客户群体(如大型政府机构、安全关键系统等)的必要要求。一个国家的医疗系统或核电站基地不能因为有人忘记付费而停摆。
因此,甲骨文只是访问与他们有许可协议的公司,检查正在使用的内容,以确定是否符合现有合同。此外,我也听说过几起案例:某公司未严格按照合同条款使用软件(例如意外启用了某些功能),但在审计时,甲骨文销售人员表示若订阅更大套餐即可忽略该错误,大多数管理者会欣然接受以避免责任,这种商业行为虽有争议,但与OpenJDK无关。
> 引用自本文:[1]
文章极力试图将大学的许可费用与甲骨文随机审计Java下载联系起来,但实际上没有人明确指出这就是发生的事情。
历史费用的豁免可以追溯到上一次许可变更,当时甲骨文更改了许可费的计算方式。因此,甲骨文针对这些客户采取行动似乎是合理的,因为他们是未能支付虚高费用的付费客户。
我认为标准库中包含 HTTP 就是“死电池”问题的完美例证:标准库的 HTTP 是否也应支持 QUIC 和/或 WebSockets?如果你选择包含这些功能,就意味着标准库要支持非常具体的用例。如果你选择不包含它,那么 QUIC crate 是否应该扩展或包含标准库的 HTTP 实现?如果你选择包含,你就创建了一个“死电池”。如果你选择扩展,你就会因为在标准库和外部 crate 之间引入依赖关系而制造一个维护噩梦。
> 我的意思是,到了 2025 年,当每个人都在使用加密技术时,标准库中没有加密支持?或者当每个人都在调用 REST API 时,标准库中没有 HTTP 支持?
我不是,而且我很高兴核心团队不需要维护一个 HTTP 服务器,可以花时间在低级功能上,这就是我选择 Rust 的原因。
抱歉,但对于大多数编程任务,我更倾向于使用具有实际功能的数据容器而非 HTTP 库:Set、Tree 等类型。这些是计算机科学的基础构建块,却缺失于 Go 标准库中。(虽然它们最近才被添加,但功能仍远不及 Rust 的 std::collection。)
此外,如另一条评论所提,HTTP 或加密库可能迅速过时。HTTP3 呢?后量子加密呢?安全修复呢?标准库与语言版本绑定,即与语言发布版本绑定。拥有独立于语言的代码使我们能够更快演进、更精简且更具可组合性。因此,尽管该库维护良好,但它与 Go 版本绑定。
此外,它允许在绝对必要时进行破坏性 API 更改。我可以列举两个先例:
– 在 Rust 中,chrono 中的时间 API 不得不进行过几次更改,而 Rust 维护者对它未被纳入标准库表示感谢,因为这使得大规模更改成为可能
– 另一方面,在 Go 语言中,人们发现 net.Ip 的设计极其糟糕(它只是 []byte 的别名)。Tailscale 编写了一个替代实现,现在该实现已作为 net 包的子包存在,但旧版的 net.Ip 已经固定下来。(https://tailscale.com/blog/netaddr-new-ip-type-for-go)
> 集合、树等类型。这些是计算机科学的基本构建块
如果你从事计算机科学研究,Go 可能是你应该避免使用的最后一种语言。然而,如果你感兴趣的是_编程_,那么基础数据结构是数组和哈希表,Go 已经内置了这些。其他内容都是小众的。
> 此外,如另一条评论所提,HTTP 或加密库可能会迅速过时。HTTP3 呢?后量子加密呢?安全修复呢?标准库与语言版本绑定,因此与语言发布版本绑定。拥有这样的独立代码允许我们更快地演进、更轻量级,并且更具可组合性。所以,是的,库维护得很好,但它与 Go 版本绑定。
整个重点是拥有一个_良好支持_的加密库。Go 确实做到了,并且始终保持最新。包括安全修复。
关于后量子加密:https://words.filippo.io/mlkem768/
> – 另一方面,在 Go 中发现 net.Ip 的设计极其糟糕(它只是 []byte 的别名)。Tailscale 编写了一个替代实现,现在作为 net 包的子包存在,但旧的 net.Ip 已成定局。(https://tailscale.com/blog/netaddr-new-ip-type-for-go)
是的,那又怎样?这在我看来是处理问题的完美方式——始终有一个经过验证的高质量库可供使用。随着其设计缺陷逐渐被发现,新版本会每隔约 10 年开发并发布一次。
一个由用户群体分裂使用的、几乎不受支持的库集合,就是一团糟。
Go的标准库维护良好且功能丰富,因为谷歌非常重视它在这些用例中的表现
这对Go和谷歌来说效果很好,但我不确定这在Rust或其他语言中是否容易复制
你认为C和C++的标准库应该包含HTTP或加密功能吗?
嗯,crypt(3) 是 POSIX 的一部分,所以……
还有其他不基于OpenJDK的JVM,但总体而言你的观点成立。
是的,我明白,但人们难以理解OpenJDK周围的许可问题,让我们不要提及替代实现(实际上,这使得整个平台从长期发展的角度来看成为更好的目标!没有多少语言拥有多个完全独立的实现标准)。
> 这是一个值得信赖的工具
我同意。
Go的标准库非常出色。
而且Go没有依赖地狱的问题,不像Python。只需打包一个即用型二进制文件。
那么替代方案是什么?
Java?需要使用不同分支的许可证问题。而且 Go 更容易使用,尤其是在服务器端部署时。
Zig?Rust?学习曲线复杂。而且选择例如 Rust crate 会重新引入依赖地狱和供应链攻击的潜在风险。
> Java ? 涉及使用不同分支的许可问题。此外,Go 更易于使用,尤其是在服务器端部署时。
是的,这些只是许可问题,因为服务器端基本上只有一个完全免费的实现,那就是 OpenJDK,它是由 Oracle 完全开源并作为参考实现的。基本上,Corretto、AdoptOpenJDK 等都是同一个仓库的 构建版本。
那些提出整个许可证话题的人不能被当真,这就像说 Linux 是专有软件,因为你可以付费获得 Red Hat 的支持。
你忘了D。在一个D存在的世界里,很难理解为什么需要创建Go。这篇帖子中的每一条批评在D中都不存在。如果谷歌将用于Go的努力用于改进D,我认为今天的D将是最好的语言。但事实上,D几乎没有得到投资(我指的是实际开发人员花在改进它、清理它、编写工具上的时间),这点很明显。
我认为这两种语言无法直接比较。Go 试图保持简单(无论这意味着什么),而 D 则是一个功能齐全的语言。
> Rust 故意选择拥有一个小的标准库,以避免“死电池”问题。
“小”和 Rust 的标准库之间是有区别的,后者在实际应用中几乎不存在。
我的意思是,到了 2025 年,当每个人都在使用加密技术时,标准库中没有加密功能?或者当每个人都在调用 REST API 时,标准库中没有 HTTP 功能?
正如另一位回复你的人所说,Go 允许你快速上手并立即开始工作。
不同的权衡,两者都可行。
标准库较小的缺点是选项繁多,你突然发现(或说,已经有一段时间了)为Tokio编写的异步包无法在async-std上运行,等等。
Go中也经常出现这种情况——直到log/slog出现之前,很多人选择了一个结构化日志器并将其作为API的一部分,强加给其他人。
我认为这是因为 Go 社区紧密围绕标准库展开:
例如,据我所知,Rust 有多种处理字符串的方式,而 Go(在很大程度上)只有一种(得益于垃圾回收)
字符串/OsSfeing 与垃圾回收有什么关系?
uv 加上在注释中添加所需包的新方式相当不错。
你可以运行 `uv run script.py`,它会自动下载库并在虚拟环境中运行脚本。
不过仍无法与 Go 媲美,打包单一跨平台二进制文件的体验堪称完美。通过一些技巧,甚至可以将整个静态网站打包进去 🙂 这种方式在构建带有简单 UI 的业务逻辑时效果极佳。
我已有一段时间未关注 Python 领域,但看到市场上又出现新的工具来处理这类问题并不意外。
当这些功能直接内置于语言本身时,你才会真正体会到其价值。Go 二进制文件将始终运行,但 Python 项目可能在几年后就无法构建了。
除非它使用了 CGO 并具有动态依赖项,否则“始终”这个说法有点夸张。
或者导入路径是某人的博客域名,该域名包含一个指向实际 GitHub 仓库的 <meta> 引用(连同标签,如果我记得没错的话),而源代码真正存放在那里。疯狂
我从未理解将 SCM 网址直接作为包导入放在源代码中的心态。
嗯,这就是我强调的_问题_——Go语言不知怎么的决定了兼具两者最糟糕的特点:导入路径中包含任意域名,然后将源代码的实际引用放在……其他地方
import “gopkg.in/yaml.v3” // 现在这段代码是做什么的?
curl https://gopkg.in/yaml.v3?go-get=1 | grep github
<meta name="go-source" content="gopkg.in/yaml.v3 _ https://github.com/go-yaml/yaml/tree/v3.0.1{/dir} https://github.com/go-yaml/yaml/blob/v3.0.1{/dir}/{file}#L{line}">
哦,好的 :-/
我认为只有 go.mod 文件中的条目会明确指定是 v3.0.0 还是 v3.0.1
另外,为了未来版本,不要使用那个包 https://github.com/go-yaml/yaml#this-project-is-unmaintained
uv 现在是新潮流。让我们五年后再看看…
> 你可以运行 `uv run script.py`,它会自动下载库并在虚拟环境中运行脚本。
是的,但你仍然需要将 `uv` 安装为先决条件。
而且你最终还是会得到一个充满依赖地狱的虚拟环境。
当然,我们都记得 Python 2 过渡到 Python 3 的那段混乱时期,然后又推迟了,再推迟了……
你说的有道理,当然从技术上讲可以让它(稍微)“干净”一点。但我还是选择 Go 二进制文件吧。 😉
安装 uv 是必需的,而且非常简单。
不,虚拟环境中没有依赖地狱。
Python 2 到 3:你还在纠结这个老问题吗?它已经死了……请继续前进。
这让我更加沮丧。Go语言的优势更多在于工具链和生态系统,但语言本身并不出色。我希望这些努力能投入到更好的语言中。
Go语言拥有透明的异步I/O和非常优秀的M:N线程模型,这使得使用epoll编写HTTP服务器变得简单高效。
对于这个用例,其使用体验比我用过的任何语言都更好。
不过,实现 HTTP 服务器并不是软件开发中常见的用例。
> 我希望这些努力能投入到更好的语言中。
但这些努力正在进行中。阅读《Go 博客》、《Go 周刊》等新闻简报。它一直在不断改进。语言变更需要大量时间才能做好,但语言正在演进。
> Go 的标准库非常出色。
我见过更糟糕的,但考虑到这是一种相对较新的语言,本可以做得更多,我仍然不会称其为优秀。
我将忽略许多最受欢迎的库(甚至由谷歌维护的基本库)中大量荒谬且完全错误的内容,因为你只在讨论标准库。
就我所知,结构体的标签管理不一致(JSON默认值、omitzero与omitempty),甚至没有对标签拼写错误的错误提示,读写模式迫使你编写自定义连接器在两者之间,bzip2有读取器但没有写入器,键值对的上下文链表。只需看看“encoding”包中接口的一致性,就会让人感到沮丧,hash包实际上应该叫checksum。为什么strconv.Atoi/ItoA仍然存在?Time.Add()与Time.Sub()的区别……
它充满了不一致性。每次我超过几天不使用某个功能时,都不得不查看文档。不,自动完成功能中的两行文档并不包括仅在包顶部解释的潜在陷阱。
而且请别让我开始谈论我不得不为 net 库中的某些内容编写包装函数,以使其更加一致或至少不那么明显错误。net/url.Parse!!! 我说别让我开始谈论这个包!nil 与 NoBody!啊!
这些都不是语言层面的问题(语言层面本身就有很多值得讨论的地方)。
这些都不是致命缺陷,但它们会累积成问题,最终导致“千刀万剐”般的痛苦。
我甚至不再信任用 Go 编写的任何解析器,我总是试图想出边界案例来测试它们的反应,而大多数情况下我都会感到惊讶。
当然,还有更糟糕的语言和库。但这仍然不是我在 2025 年为新项目选择的语言。
> 标准库
是的,我最喜欢的是`time`包。它只是一个数字,名义上的类型系统真正闪耀。使用它是一种享受。你是什么意思,我可以做`+= 8*time.Hour` 😀
不幸的是它没有错误处理,所以当你执行 += 8 小时而失败时,它不会返回 Go 错误,不会抛出 Go 异常,它只是默默地做错事(截断时长)并希望你不会注意到……
它很简单,这对小型工具或脚本来说很好,但随着规模的扩大,它变得非常脆弱,因为没有处理任何边界情况
这种情况何时会失败——如果计算出的时间早于最小时间或晚于最大时间?
我幸运地在编写单元测试时发现了这个问题,而不是在生产环境中。在 Go 语言中,time.Time 的范围远大于 time.Duration,因此在计算时间差时很容易发生溢出。但通常在操作时间时不会返回错误。Duration 在每次操作时都必须仔细检查,以确保不会超出范围。
内部实现中,time.Duration 是一个 64 位计数器,而 time.Time 则包含两个更复杂的 64 位字段加上一个位置字段
为什么容易发生溢出?据我所知,time.Duration 的范围是 ±290 年。
只要你不需要做 `hours := 8` 和 `+= hours * time.Hour`。令人惊讶的是,要让这种乘法生效的唯一方法是将 `hours` 转换为 `time.Duration`。
在 Go 中,`int * Duration = error`,但 `Duration * Duration = Duration`!
这确实一致。常量类型根据上下文确定,因此 8 * time.Hour 的类型为 time.Duration。
如果你有一个整型变量 hours := 8,你必须在乘法前将其转换类型。
对于简单的整数和浮点数运算也是如此。
f := 2.0
3 * f
是有效的,但 x := 3 需要 float64(x)*f 才能有效。加法等运算也是如此。
如果你做过任何科学工作,这完全是疯狂的。
Go默认解析时间字符串的方式确实疯狂,就连维护者都后悔了。这是典型的“聪明反被聪明误”的教材案例。
是选择默认值而不是模板化值吗?
除了需要定期记住什么是0填充毫秒之类的问题外,这并不是什么大问题。
我不是原帖作者,但第一次看到 time.Parse(“2006-01-02 03:04:05”) 时也感到困惑,心想这到底是怎么回事?!
https://pkg.go.dev/time#Layout
人们常提到Discord用Rust重写部分栈代码,因为Go的GC暂停导致问题。
这段代码位于其中央路由服务器的热点路径上,每秒处理数十亿(B)条消息,或者类似的疯狂场景。
你不是在构建 Discord,GC 几乎不会在你的指标中留下任何痕迹。GC 完全没问题。
我明白你可以专门编写不使用 malloc 的代码,但我好奇在规模化场景下,是否存在与 GC 暂停问题相当的堆管理/碎片化和压缩问题。
我对大规模使用malloc语言的经验不多,但我知道热碎片化和GC碎片化是非常相似的问题。
在GC语言中,有一些技术可以避免GC,比如 arena 分配之类的东西,通常被认为是非标准的。
“并发是棘手的”
这对于大多数语言来说都是正确的,即使是那些并发支持更简单的语言。正确使用并发才是棘手的部分。
我对可移植性没有太大问题。我认为Go在AWS Lambda等场景中表现出色,因为你希望快速执行且无需将代码分发到用户系统。
> 类型系统大多数时候非常方便
在哪个世界里?
在我这里。它很好用。
它是最好的、最健壮的,还是能做花哨的事情?不
但它足够好,可以发布可靠的软件,再加上基于 Go 构建的庞大代码检查框架。
> 庞大的静态分析框架
我好奇为什么最终需要这样一个框架…… 😉
我认为Result[]和Optional[]有点被高估了,但nil确实让我困扰。然而,nil不会消失(指针和接口的默认值还能是什么,又不破坏现有代码?)。我认为Go只需要一种非空类型注解/声明就足够了。
是的,也许它们被高估了,但它们似乎是避免空值并标准化错误处理的公认类型集(同时支持一些像Rust的?运算符这样的语法糖)。
我经常看到开发者在其他语言如TypeScript中引入这些类型,但当它们在用户空间引入时效果并不理想(通常只是代码库中的一小部分遵循这一标准)。
TypeScript 处理空值/未定义值的另一种方式是将其纳入类型定义中,且无法使用可能为空值/未定义值的类型。在我看来,在 TypeScript 中使用 Optional<T> 有些奇怪。TypeScript 还支持异常处理……
我认为它们只有在语言本身围绕这一机制设计时才有效。在 Rust 中,这行得通,因为你无法在不进行模式匹配的情况下解引用 Optional 类型,而模式匹配机制比这要通用得多。但在其他语言中,这只是一个瑕疵。
如我所说,某种类型注释会更符合Go语言的风格,例如:
func f(ptr PtrToData?) int { ... }
你只能在 if ptr != nil { … } 语句块内访问 *ptr。Uber 有一款名为 nilaway 的代码检查工具,其工作原理与此类似,只是缺少类型注解。该提案会破坏现有代码,因此或许需要一个显式的非空指针标记(但这并不太方便,可惜)。
是的,默认值是 Go 语言的原罪之一,现在要回滚已经太晚了。我认为默认值并没有太多好处——int i; 与 int i = 0; 并没有实质性区别。如果他们担心的是结构体初始化,那么只需编写一个构造函数即可。
Go 在除初始化外的所有场景都选择了显式而非隐式——而初始化正是我真正需要“显式”的地方。
这确实让类型非常可预测:一个 var int 无论何时何地如何使用,始终是一个有效的 int。如果没有默认值,你将如何设计围绕初始化和声明的类型系统和语义?是否像 C 语言一样允许未初始化的值?这基本上是默认值加上额外步骤和额外的安全漏洞。扩展类型系统以支持PossiblyUndefined<t>?这感觉像是显著的复杂化,但也许有人让它工作了……
Golang非常适合那些你真的、真的无法放弃跟踪GC的问题场景。这可能是罕见的情况,但它确实存在。大多数支持 GC 的语言都没有像 Golang 那样开箱即用的高性能并发 GC,而且其最低内存要求也相当低。(当然你可以提供更多内存来尝试提升整体吞吐量,而且你可能应该这样做——但你并不一定非要这样做。这使得它非常适合在内存资源紧张的小型云虚拟机上运行。)
Java的GC在吞吐量导向和延迟敏感的工作负载中都领先一代[1]。尽管Go的GC也进行了一些改进,现在比几年前好多了。
[1] ZGC 基本上将堆大小与暂停时间解耦,此时来自操作系统调度器的暂停时间会比 GC 更长。
你有这个的来源吗?我认为 Go 的 GC 在低延迟优化方面要好得多。
> 我经常希望有 Optional[T] 这样的类型。
只要你不关心与广泛生态系统的兼容性,你可以自己实现一个完美的 Optional:
type Optional[Value any] struct {
value Value
exists bool
}
// 创建空对象。
func New[Value any]() Optional[Value] {}
// 创建值对象。
func Of[Value any](value Value) Optional[Value] {}
// 创建指针对象。
func OfPointer[Value any](value *Value) Optional[Value] {}
// 获取值的唯一通用方式。
func (o Optional[Value]) Get() (Value, bool) {}
// 获取值或引发 panic。
func (o Optional[Value]) MustGet() Value {}
// 获取值或默认值。
func (o Optional[Value]) GetOrElse(defaultValue Value) Value {}
// JSON 支持。
func (o Optional[Value]) MarshalJSON() ([]byte, error) {}
func (o *Optional[Value]) UnmarshalJSON(data []byte) error {}
// 数据库支持。
func (o *Optional[Value]) Scan(value any) error {}
func (o Optional[Value]) Value() (driver.Value, error) {}
但你可能更关心与其他人的兼容性,所以……是的,Go 处理可选性的方式确实很糟糕,因为它需要到处传递指针。
你可以写 Optional,当然,但你无法取消 nil,而这正是我真正想要的。我在 Java 中尽可能使用 Optional<T>,但它并未让我免于 NullPointerException。
你对具体问题描述得不够精准。在 Go 中,nil 并不是像在 Java 中那样是个大问题,因为并非所有东西都是对象的引用。结构体不能为空,等等。在 Java 中,你可以直接返回 null 而不是 Optional<T>,但在 Go 中不行。
一旦消除滥用指针来表示可选性的自我伤害,Go 中几乎没有空指针错误的可能性。
还有其他一些问题。
对于 JSON,你无法将 Optional[T] 编码为空。它必须编码为某种值,通常是 null。但当你解码时,字段的缺失意味着 UnmarshalJSON 根本不会被调用。这通常会导致默认值,当然你随后会将其重新编码为 null。因此,如果你对 JSON 进行往返处理,输出的结果与输入会存在实质性差异(这对某些其他语言/库来说很重要)。也许新的 encoding/json/v2 库修复了这个问题,我还没查看过。
此外,我通常希望 Optional[T]{value:nil,exists:true} 在任何情况下都不可用,无论 T 的类型是什么。但 Go 的类型系统过于有限,无法表达这一限制,甚至无法表达一种让函数强制执行这一限制的方法,除非使用反射,而反射又存在类型擦除问题,使得即使使用反射也难以正确实现!因此,你不得不编写大量不同的构造函数:一个用于所有基本类型和字符串;分别用于指针、映射和切片;三个用于通道(chan T、<-chan T、chan<- T);最后一个用于接口,必须使用反射。
对于 JSON,我只是进行序列化/反序列化:
{
“value”: “value”,
"exists: true,
}
对于空值,这很有趣。我从未遇到过相关问题,因此从未考虑过。
理想情况下,我希望 Optional[T] 在值存在时与 T 编码相同,而在值不存在时以可配置的方式编码。诚然,*T 也有“空值到 null”的问题,即使使用 *T 和 `json:“,omitempty”`,也会出现相反的问题(null 变成空值)。当时我没有考虑到这一点,所以这更多是编码/JSON的问题,而非Optional[T]本身。不过据我所知,你无法实现MarshalJSON并输出空值。
> 但整个错误/空值的情况仍然困扰着我。我经常希望有 Result[Ok, Err] 和 Optional[T]。
我在面试中提到这一点时,面试官立即拒绝了我。
他们说“面试结束了”,并示意我离开(虚拟的门)。我当时愣住了,哈哈。那是在Go语言热潮的高峰期。不确定RancherLabs后来发生了什么。
天啊,你真是躲过了一劫。
他们可能认为你不太适合编写符合Go语言规范的代码。很多人称赞Go语言的一点是其代码库中的一致性风格,如果你不喜欢这种风格,可能会尝试使用不同的编程模式,这会让所有相关人员感到痛苦。
有些公司会明确测试员工与公司工作哲学(语言、架构等)的文化契合度。
这既是为了保持共同方向,也是担心对技术的不满会导致员工很快离职。
我不同意这种做法,别误会,但我见过这种情况,这或许能解释你的经历。
无需粉饰。有些地方就像邪教,最好避而远之。这对GP有利。
> 并发性很棘手,但
罗布·派克听到了吗?哈哈。他多年来一直贬低Java,真是令人恼火。(是的,幸灾乐祸/g)
对我来说,Go语言令人惊叹之处在于,它相对较新,而我们行业的集体智慧本应更清楚这些问题。这就像今天发明一款现代唱片机,搭配不会损坏且永不磨损的全新唱片。不错……但我们为什么要这么做?我们不应该用这么多冗余代码、冗长语句和潜在陷阱来编写低级代码。开发性能媲美低级语言的高级语言。
我不会责怪创建者。他们做了他们该做的事,这很好。我更震惊于它被广泛采用的方式。
希望看到Go语言的CoffeeScript版本。
> 希望看到Go语言的CoffeeScript版本。
虽然不实用,但:https://github.com/borgo-lang/borgo
我仍然不明白为什么defer在函数作用域内有效,而在词法作用域内无效,而且没有人能向我解释其中的原因。
事实上,这对我来说非常令人惊讶,因为我是在编写处理文件的循环代码时才发现的,当文件列表变得太大时,程序开始崩溃,因为 defer 直到函数返回后才关闭句柄。
当我问其他 Go 程序员时,他们告诉我将循环体包裹在一个匿名函数中并调用它。
除此之外(以及一些其他小问题),我发现 Go 是一种愉快、紧凑的语言,具有高效的语法,这种语法并不鼓励人们试图耍小聪明。我开始学习 Go 时,重新编写了一个相当庞大的 C# 项目,惊讶地发现尽管它只有 C# 功能的 10%,但代码最终变得更小。它还鼓励高性能的默认设置,比如不强制在每个转折点进行垃圾回收分配,对序列化等功能有非常好的内置代码生成支持,而且不像C#那样坚持要“吞噬世界”,比如通过ORM展示你可以用C#代替SQL来操作关系型数据库,或者通过注解C#对象来实现GRPC。在Go中,你通过编写SQL来做SQL,通过编写protobuf规范来实现GRPC。
有时你需要词法作用域,有时需要函数作用域;例如,也许你在循环中打开了一堆文件,并且需要在函数的剩余部分中保持所有文件打开。
目前它是函数作用域;如果你需要词法作用域,你可以将其包裹在一个函数中。
假设它是词法作用域,而你需要函数作用域。那么你该怎么办?
将其设为词法作用域可以解决这两个问题,并且对任何阅读代码的人来说都清晰明了。
在合理的语言中,你可以随时使用{}引入新作用域,以控制所需的行为。
在 Go 中,你可以用 `{}` 开始一个新作用域。如果我有许多临时变量,我会将最终结果声明在花括号之外,然后在内部进行操作。但最近我更倾向于直接写一个函数。这样更清晰且更易于测试。
目前,你可以这样写
if (some condition) { defer x() }
当它是词法作用域时,你需要添加一些变量。虽然这种情况并不常见,但词法作用域的defer也不常被需要。
有什么例子需要这样做吗?
我记不起来有过这种需求(但这可能只是因为我习惯了使用词法作用域来处理defer类型的构造/RAII)。
我经常使用这种模式。例如,类似于以下代码:
a := Start()
if thingEnabled {
thing := connectToThing()
defer thing.Close()
a.SetThing(thing)
}
a.Run(ctx)
> 假设它是词法作用域,而你需要函数作用域。那该怎么办?
在函数作用域级别 defer 一批操作,并在打开文件后将它们追加到数组中。
这似乎比添加额外函数更麻烦,且可读性更差。
不过能同时拥有两种选项就好了。为什么不创建一个“defer”包呢?
我从未想要函数作用域的defer,不确定有什么用例,但如果有的话,你可以按照其他评论的建议去做。
真的吗?我发现情况恰恰相反。如果我需要词法作用域,我就会这样写,例如
f.Close() // 不使用defer
我可能希望使用函数作用域的defer,是因为该函数可能存在多个不同的退出点。
在词法作用域下,安全跳出作用域的方式仅有三种:
1. 到达过程末尾,此时无需defer)
-
通过‘return’退出,此时也同时退出函数作用域
-
通过‘break’或‘continue’跳出,虽然这些情况确实可能受益于词法作用域的defer,但它们通常也容易被拆分为独立函数;而且如果你的代码复杂到需要defer的程度,那么这些分支本就应该被拆分。
如果 Go 语言支持其他控制流结构(如 try/catch 等),那么词法作用域 defer 的必要性会更强。但这其实对大多数人来说都不是问题,除非你还在寻找 Go 语言不支持的其他特性。
我不用 Go 编程,但读完这段内容,感觉其实根本不需要函数式作用域。在 Java 中,我会直接写:
try (SomeResource foo = SomeResource.open()) { method(foo); }
或
public void method() { try(…) { // 所有业务逻辑都包裹在 try-with-resources 中 } }
对我来说,词法作用域似乎可以实现函数式作用域几乎所有功能,只是没有意外行为。
一个小问题,但第二个示例本质上仍然只是函数式作用域(即不是字面意义上的,而是更务实意义上的),因为你在方法中没有在 try 块之外包含任何分支。
但这无关紧要,因为我明白这只是一个示例。更重要的是,Go 本身并不支持你所描述的控制流(如我在之前的帖子中所说)。
这里关于‘defer’的大部分评论在其他语言中是有意义的,因为这些语言有与 Go 不同的惯用法和特性。但它们不能直接应用于 Go,因为你需要先对语言进行其他改动(例如实现 try 块)。
你做的是编译器在底层需要做的事情:在函数开头创建一个打开文件的列表,然后使用一个 defer 语句循环遍历该列表并关闭所有文件。这其实并不是一个复杂的构造。
defer { 关闭集合中的所有文件 }?
如果在打开其中一个文件时发生错误,你在 for 循环内部返回错误,却忘记关闭已经打开的文件,会发生什么?
你在打开文件时将它们放入集合中,并在打开任何文件之前注册 defer。这样工作正常。defer 应该具有词法作用域。
目前,你可以两者兼得。如果你想这样做,可以将主体包裹在一个函数中。不要为了获得更广泛的作用域而进行包裹。
-
它避免了在未将代码包裹在函数中时使用缩进
-
该机制与调用栈/栈展开相关联
-
对于从 C 语言过渡而来的开发者来说,这感觉很自然(类似于 C 语言中的
goto fail)
(是的,当我希望在循环中延迟执行时,现在循环体必须是一个函数,这让我感到困扰)
我认为你一针见血——Go语言设计者让panic可恢复的决定是愚蠢的。这迫使栈展开机制,意味着即使栈中发生panic,defer语句仍需执行。
由于他们不愿采用“正统”的RAII栈展开机制,这便是他们妥协出的糟糕方案。
我曾使用过同时具备这两种特性的语言,发现自己在使用块级语言时,希望能在条件语句中使用函数级别的defer。
可能没有深层原因,这重要吗?
是的,函数作用域的defer需要动态数据结构来跟踪待处理的defer操作,因此并非零成本。
这也会成为 bug 的来源,因为你可能会比预期更长时间地持有某个资源——考虑到 Go 中没有阻塞操作的提示,你可能获取一个互斥锁,延迟释放,然后在某个函数调用意外阻塞时,整个程序会卡顿一秒。
我认为这只有在从具有不同规则的语言转换过来时才是真正的问题。块作用域(因此无法在函数结束时条件性地删除临时文件)对来自 Go 的人来说同样令人惊讶。
但我确实同意,defer 的动态特性及其非块作用域设计可能并非最佳选择
将循环体包裹在立即调用的函数中似乎会让代码更难阅读。尤其对于以“简单”和“直观”为卖点的语言而言。
你可以用 C# 编写 SQL 或使用 protbuf 规范。你只是还有其他选择。
词法作用域没有栈来放置 defer。
词法作用域中的所有 defer 位置都是静态的,你可以直接定位这些位置,或在帧中添加一个固定大小的栈。
我过去五年几乎完全专注于一个大型 Go 项目,这点我深有同感。该项目的一个组件要求尽可能少地使用内存,因此我花了很多时间在 Go 语言上解决相关问题。我们遇到了许多问题,例如垃圾回收器无法快速清理内存,或者出现堆碎片化问题(因为 Go 语言在设计时决定不使用紧凑型垃圾回收器),因此我们不得不尽量避免内存分配。哦,而且当我们遇到这些问题时,调试起来极其困难。你可以获取堆内存剖析,但这些剖析只告诉你堆中活跃的对象。它们不会告诉你所有垃圾和所有碎片的情况。因此,诊断问题就成了读懂这些模糊线索的过程。例如,堆栈剖析显示函数 X 仅分配了 1KB 内存,但它被调用在一个热点循环中,因此该函数可能生成了 20MB 的垃圾,而这些垃圾在剖析中是不可见的。
我们预先分配了一批静态缓冲区并重复使用它们。但这导致了大量所有权问题,如文章中提到的 append footgun。我们甚至不得不重新实现标准库的部分内容,因为它们会分配内存。我明白我们有非标准用例,大多数程序员不需要对内存使用如此较真。但我们需要,而且如果能不觉得自己在与语言对抗,那将非常理想。
我发现,当你需要这样做时,将东西移出堆内存会更容易,尽管显然在垃圾回收语言中这并非完全简单,而且肯定会产生很多不平滑的边缘。如果你发现自己正在用Go语言编写本质上类似C++或Rust的代码,那么当你可以时,最好直接用相应的语言重写那部分代码 🙂
或许新的“绿茶”GC能提供帮助?它被描述为“一种并行标记算法,虽然不是以内存为中心,但至少是内存感知型的,即它努力将彼此相邻的对象一起处理。”
https://github.com/golang/go/issues/73581
我看到了!我非常有兴趣尝试一下,看看它是否对我们的用例有帮助。当然,目前我们已经将内存分配减少到极致,GC 几乎没有太多工作要做,除非我们在某个地方出错(这种情况确实发生过)。我可能需要在热点路径中故意添加一些内存分配作为压力测试。
我最希望的是一个紧凑型垃圾回收器,但据我所知,Go语言无法在不破坏向后兼容性的情况下添加此功能,因此很可能永远不会实现。
> 该项目的一个组件需要尽可能少地使用内存,因此我在Go语言的这一方面遇到了很多困难。
你选择的语言并不适合这个问题。C/C++/Rust/Zig会是更好的选择。
我想你可能会对arena实验感兴趣,不过目前它似乎处于暂停状态
嵌入本地Redis或SQLite实例?
在Go中使用SQLite同样不是件容易的事。
我知道这条评论可能没什么帮助,所以抱歉,但听起来Go完全不适合这个用例,你和团队可能被迫使用它,是因为公司只在生产环境中使用部分主流编程语言。
我听说过“既定路径”这个术语,指的是组织选择使用的语言,并禁止使用其他语言。
不,Go 在我公司其实并不广泛使用。最初的开发者选择 Go 是因为他们认为它适合我们的用例。我们特别寻找一种编译型语言,能够生成依赖性极少的二进制文件,无需手动内存管理,并且相对成熟(我认为当时Rust还不到1.0版本)。我们知道需要限制内存使用,但这更多是“锦上添花”而非必要条件。而Go表现得相当不错。它在生产环境中运行了几年后,我们才开始遇到这些问题。我们正在考虑将该项目迁移到Rust,但这将是一项巨大的工程。这是一个经过实战检验的5万行以上代码库。
> 原始开发者选择Go是因为他们认为它适合我们的用例。
我有点不明白。如果内存要求严格,这对我来说似乎毫无意义。20年前我曾为诺基亚设备编写J2ME游戏。我们试图将游戏压缩到50-128KB内存中,而这一切都是用Java语言实现的。没有哪个理智的Java开发者会不晕倒地查看那段代码——没有动态分配,一切都是静态的,字节和字符是最常用的数据类型。游戏中的图像被压缩到极致,没有头部信息,什么都没有。如果你在目标设备上遇到内存限制,你真的需要仔细考虑。
我们的内存要求严格性直到多年后才显现出来。作为背景,该应用程序是一个在终端用户服务器上运行的系统守护进程。因此,我们分配的每个字节、使用的每个CPU周期,都是从客户那里夺走的字节或周期。我们不提供关于内存使用的明确保证或正式的SLA,但我们确实会尽量减少内存使用。因为我们不希望有人升级代理版本后,我们的守护进程突然使用显著更多的内存并引发内存不足(OOM)错误,或使用更多CPU导致客户不得不扩展其服务器集群。目前我们的p99 CPU和内存使用率与两年前大致相同 (RSS 低于 40MB,我上次检查时是这样)
所以我们并不是在只有几千字节内存的机器上运行,但我们确实希望尽量减少内存使用。
哇。我敢打赌这需要一些相当巧妙的思路,这种思路通常只会在 C 或 C++ 中考虑。
Go 确实有其不足之处,但我认为它在服务器端语言中占据了一个独特的优势地位。
它比 Node 或 Python 更快,且拥有比两者更优的类型系统。它的学习曲线比 Rust 更平缓。它拥有良好的标准库和工具链。语法简单,通常只有一种实现方式。错误处理存在一些问题,但我仍然更喜欢它,而不是Node,因为在Node中,一个catch子句可能会接收几乎任何东西作为“错误”。
我是否遗漏了其他也具备这些特性的语言?我并不是Go的狂热粉丝,职业生涯中主要使用Node进行后端开发,但最近一直在探索Go。
> 它比 Node 或 Python 更快,且拥有比两者更优的类型系统。其学习曲线远比 Rust 更平缓。它拥有优秀的标准库和工具链。语法简洁,通常只有一种实现方式。错误处理虽有不足,但我仍更倾向于它而非 Node,因为在 Node 中,catch 语句可能接收几乎任何类型的“错误”。
我觉得这段描述同样适用于 Java 或 C#。
“简单语法且通常只有一种实现方式”与Java的特性恰恰相反。
Java和C#都是功能极其丰富、需要学习内容极多的语言。而Go语言用户可以在一天内掌握80%的语言核心内容。
仅仅因为你可以学习某件事,并不意味着你必须去学习。C# 现在提供了顶级程序,这些程序在快速浏览时与 Python 脚本几乎无法区分。无需命名空间、类或主方法。只需你想要执行的代码和一个简单的文件。
https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals…
该语言体积小巧且遵循常规编程规范,标准库功能足够强大,因此人们很少需要为其能处理的功能导入其他依赖库。这使得它成为我使用过的最易于阅读依赖关系的语言。
如果我在其他任何语言中遇到需要这样做的情况,我会想方设法暂时避免这样做(包括寻找其他依赖项来替换它),因为这几乎总是会耗费大量时间,而且可能最终一无所获,除非花费完全不合理的时间。因此,我可能会尝试一段时间后放弃努力。
在 Go 中,我毫不犹豫地直接跳进去,几乎每次都能快速找到我需要的东西。
这与 JavaScript(或 TypeScript——它也没有避免这个问题)完全相反,在 JavaScript 中,你可以有三个库,而这三个库不仅读起来像与你正在编写的语言完全不同的语言,而且彼此之间也像完全不同的语言。唉。这个库最初是在“一切都应是高阶函数”的潮流下编写的,它为了避免将隐式实例化的对象视为对象而自寻烦恼……这个库大量使用“类”……这个库过度依赖原型继承的特定特性,天啊,快让我死吧…… 这个代码导入了Lodash,唉,又来了……等等。
我的意思是,这没问题,但这几乎不适用于将新开发者快速投入到一个非常大的C#代码库中,以及他们能多快掌握这门语言。
任何大型代码库由于其规模,都会有较长的学习曲线。而大型代码库可以通过开发自动化工具来绕过大型语言(如Java)初始化设置的繁琐流程。这似乎不是优化方向。作为替代小型服务(本应使用Python或Node.js实现)的方案,Go的快速部署和简洁性确实有意义。这也是为什么使用Go且之前使用过其他语言的开发者中,Python工程师和希望构建小型网络服务的人群占比最高。
在我曾任职的采用Go语言的大型代码库公司中,普遍结论是:Go语言不如Java。公司最终本应直接使用Java。
Java和C#与Go语言相比,历史包袱过于沉重。它们在不同版本下几乎看起来像是不同的语言。
他们将永远被这20%的遗留问题绊倒。
Java 是一种非常小的语言。我不认为学习它会显著耗时更长。
现在再加上 Spring,这是大型 Java 网页代码库中的标准。这非常非常令人望而生畏。
嗯,你在 Go 中也不会只是在加两个数字。
给我一个苹果和橘子的比较。包括路由、cookie、身份验证/授权、SQL 注入、跨站脚本保护等。
我基本上同意你的观点,除了简单语法和唯一实现方式这一点。据我所知,Java至少支持两种不同的并发编程范式,可能更多。我不确定C#的情况。如果我错了请纠正我。
Java的两种不同并发编程范式是什么?
但那是因为它们更早出现,而且在现代并发编程被发明之前就已经存在了。
例如,在C#中,有多种方法,但你通常应该使用现代的async/Task方法,这种方法非常容易学习,并且在示例中被独家使用多年。
这可能有点吹毛求疵,但当人们将“Node”称为编程语言时,我感到困扰。它不是语言,而是 JavaScript 运行时环境。对此你可能会说“当人们提到 Node 时,他们只是指 JavaScript”。但这可能也不准确,因为现代许多在 Node 上运行的项目都是用 TypeScript 编写的,而非 JavaScript。因此,说“Node”并不能明确指出你指的是哪种编程语言。(此外,如今还有许多非 Node 的方式可以执行 JavaScript/TypeScript)
无论如何,假设你指的是 TypeScript,我听到你更喜欢 Go 的类型系统而不是 TypeScript 的类型系统,这让我感到惊讶。确实有某些情况下,你可能会对 TypeScript 的类型系统过于依赖,但正因为这种表达能力,我认为它比 Go 的类型系统高效得多(我对 Rust 和 Go 的比较也会做出同样的论点)。
我的本意只是强调,我是在将 Go 与为 Node 运行时编写 JavaScript 进行比较,而非在浏览器中使用,仅此而已,但你说的没错。
关于 TypeScript,我其实是它的忠实拥趸,现在几乎不再写原生 JavaScript 了。我认为我的团队使用得当,并通过代码审查解决了相关问题。然而,我的主要抱怨是,我无法信任其他团队也能做到同样的事情,而TS支持绕过或谎报类型的逃生 hatch。
我参与的一个项目中,代码库由多个团队共享。仅在本周,我就多次因变量被显式类型断言为非其本类的类型(如foo as Bar)而感到沮丧。在这些情况下,它比原生JS更糟糕,因为它会误导人。
没错,但没有人直接使用 V8,尽管从技术上讲你可以这样做。Node.js 之于 JavaScript,就像 LuaJIT 之于 Lua,或 GCC 编译 C 语言一样。
公平地说,但我认为 JavaScript 生态系统是独特的,因为语言与执行/编译它的东西非常分离。当你编写 Go 时,99.9% 的情况下你是在为 Go 编译器编写代码。
当你编写 JavaScript(或被转译的 TypeScript)时,你不能轻易假设目标是 Node(V8)。它可能是 Bun(JavaScriptCore)、Deno、浏览器等。
这有点吹毛求疵,大家都知道“Node”在这个上下文中的含义。
显然不是,因为我最初以为他在谈论 TypeScript,毕竟 JavaScript 的类型系统与之相比并不完善。
是的,最大的问题是大多数语言都有其不足之处。Go 语言性能出色且便于移植,拥有良好的运行时环境和生态系统。但它也存在空指针、零值、没有析构函数以及没有宏等缺陷。(在有人指出宏不好之前,我得说代码生成更糟糕,而 Go 语言不得不大量使用代码生成来弥补缺乏宏的不足。)
确实存在缺陷更少的语言,但它们通常更复杂(例如Rust),因为Go的大部分问题都源于其设计者对“简单至上”的执着追求。
我以为代码生成比宏更好——至少比文本宏更好。你不能告诉我 Ken Thompson 没有在 Go 设计中使用宏,因为他没有使用过带有宏系统的语言!
即使是基于抽象语法树(AST)的宏系统,也存在诸如无限循环和变量捕获等棘手问题。要调试编译器为何陷入无限宏展开循环往往非常困难。解决这些问题的宏系统,如R⁵RS语法规则系统,又存在其他缺点,如实现复杂且表达能力有限。
而且,通常没有简单的方法来查看经过宏处理器处理后的代码,这使得由有缺陷的宏引入的生成代码中的错误难以追踪。
相比之下,如果你的代码生成器卡在无限循环中,你可以像正常调试程序一样调试它;它不会因变量捕获而产生棘手的错误;而且很容易查看其输出。
我只用 Go 做过一个小项目,但听到有人认为它的类型系统比 Node 更好,我感到惊讶,毕竟 Node 的默认类型系统是 TypeScript!
同意关于 Node/TS 错误处理的观点。那简直糟糕透顶
>比两者都更好的类型系统
鉴于Python最近的重大改进,我个人认为它在结构化类型方面远超Go。
是的,Python在这方面遥遥领先。最大的缺点是类型可能与实际实现不一致,导致运行时出错——但Go的any和反射机制也存在类似问题。
Python 已经拥有结构化 (!) 模式匹配、解包、内置类型检查,以及 exhaustiveness 检查(取决于你使用的类型检查器)。所有这些都在“类型检查时”生效。
它还可以通过类方法实现类型状态编程。
像 Pydantic 这样的库在 ergonomics 和类型安全性的结合上非常出色。
缺失的关键部分是总和类型,这需要语言级别的支持才能良好工作。
相比之下,Go 语言较为简单。
只要你十年前写的代码毫无价值,且不期望十年后使用今天写的代码,Python 适合用于原型开发。
使用像Pydantic这样的库,Python并不差——但我不会认为基础Python能达到Go的水平,尽管你可以通过库将其提升到一个不那么痛苦的程度。
Go(以及许多其他语言…)在依赖管理和部署方面做得不好。:-/ 正如俗话所说,“发明Docker比修复Python的工具更容易”。
是的,我认为,考虑到 Python 的渐进式类型系统,任何关于 Python 类型系统质量和实用性的讨论都假设你正在使用当前最好的类型检查器之一。
我直到最近几年才开始大量使用它。它曾经功能有限且令人厌烦地冗长。现在它很棒,你甚至不需要显式标注协变/逆变类型,而过去需要通过从 typing 导入进行笨拙注释的很多内容,现在只需用普通 Python 即可指定。
最重要的是,越来越多的库将类型支持视为重中之重,因此通常无需再下载类型模拟库和注释包并担心保持同步。有些库会做一些令人讨厌的事情,比如在所有返回类型后添加“ | None”以允许自己粗心大意,但至少它们在某种程度上提醒你它们可能粗心大意,而不是让你感到意外。
现在使用类型注释已经足够好和容易,即使是对于小型脚本,使用类型注释也能节省时间,因为它能快速发现拼写错误或返回类型错误。
正如你所说,Pydantic 往往是让它真正有用处的魔法。它足够简单,并且添加了足够的价值,因此值得不携带数据在字典或元组中。
我对 Go 类型系统的主要不满在于,我认为其接口的结构化类型虽然方便,但其实与鸭子类型(duck typing)的方便性如出一辙。就像猎人使用鸭叫器与鸭子本身具备鸭叫能力一样,F16 战斗机和录像机(VCR)都具备弹射功能。
也许是 Nim。但它并未真正流行起来,因此生态系统相对不成熟。
我认为 Go 的类型系统极其乏味。
你能详细说明吗?
真正的亮点是几乎不需要维护。我15年前写的代码至今仍能正常运行
这就是对我来说的卖点。如果我需要处理一段无人维护的遗留代码,我希望它是Go语言写的,因为这样只需升级编译器和常用库就能继续正常运行。
我对Go语言有着深深的厌恶,因为它缺乏许多功能,包括一个可用的类型系统(如果我无法编写类似SomeClass<T where T extends HorsePlay>的代码,那么这个类型系统对我来说是不可用的)。
对于NodeJS开发,通常会使用TypeScript进行编写,它拥有非常优秀的类型系统。
我个人也曾编写过服务器端的 C# 代码,这在当今是一次非常愉快的体验。不过,C# 如今已经成为一门大型语言。
它确实击中了甜点。在生产环境中,几乎没有其他比 Go 更快、更广泛使用的编程语言主要用于网络服务。你可以争论 Rust,但我就是看不到它在招聘信息中出现。几乎没有人直接用 C 或 C++ 编写 Web 服务。
我曾短暂参与过为客户扩展一个 Go 静态网站生成器的项目。代码非常清晰易读,但由于语言本身存在许多不完善之处,扩展起来较为困难。简单的修改往往需要修改大量代码,且修改方式并不直观。为了追求“简单性”,封装和抽象的能力受到了限制。抽象是我们实现简单且易于扩展代码的主要方式。约翰·奥斯特豪斯将复杂程序定义为难以扩展的程序,而非单纯指规模庞大或难以理解的程序。平均而言,Go程序似乎经常违反这一原则。程序看似“简单”,但扩展起来却困难重重。
Go 就像皇帝的新衣。告诉人们他们只是不理解,或者这是另一种做事方式,这并不能说服我。它唯一值得称道的是简单的开发体验。
我发现人们谈论 Go 的方式非常奇怪。如果有人提出批评,人们几乎总是回应说这门语言“没问题”,并暗示你提出问题是可耻的。人们说 Go 更简单,但不得不编写一个 for 循环来获取映射的键列表并不简单。
我同意你的观点,但你需要更新一个 Go 无法完成的示例
> 需要编写一个 for 循环来获取映射的键列表
现在我们有了标准库的“maps”包,你可以这样做:
keys := slices.Collect(maps.Keys(someMap))
借助泛型的魔力,终于可以实现这一点了。
如果 Go 在方法和函数的用法上能保持一致,也许我们就能用 “keys := someMap.Keys()” 而不是像 `http.Request.Headers.Set(“key”, “value”)` 这样奇怪的混合用法,但 `map[‘key’] = “value”`
或者 ‘close(chan x)’ 但 ‘file.Close()’,等等等等。
我自2024年以来就没再用过Go,但我想说类似的话——似乎我用Go做函数式编程时挺开心的。问题是客户不让我们用Go。我们只能在Java(天啊)和Python之间选择来构建API。我们选择了Python,因为我交叉双臂、咬紧嘴唇,拒绝在容器时代继续编写Java代码。我从未真正喜欢过Java,或者说我从未真正喜欢过使用Java所能获得的_工作类型_?<– 那个
公平地说,我在泛型出现前就停止使用Go了,所以现在已经有些过时了。我记得我们曾讨论过泛型,当时有一个反对泛型的群体。泛型现在好多了吗?我担心很多库代码都是在泛型出现之前写的。
泛型只是对泛型概念的弱化模仿,几乎就像在说“我们做到了”,却没有让语言变得更具表达力。
例如,你不能写以下代码:
type Option[T any] struct { t *T }
func (o *Option[T]) Map[U any](f func(T) U) *Option[U] { ... }
这会失败,因为方法不能有类型参数,只有结构体和函数可以。这大大影响了泛型的易用性。
而且,正如你正确指出的,标准库大多是泛型之前的,所以现在有很多重复的函数,比如“strings. Sort“ 和 ”slices.Sort“,”atomic.Pointer“ 和 ”atomic.Value",可能很快就会有 sync/v2 https://github.com/golang/go/issues/71076 等。
旧的非泛型版本通常也没有被废弃,它们只是用来 trap 那些不知道“永远不要使用 atomic.Value,一定要使用 atomic.Pointer”的人。
> 要是 Go 在方法和函数上能保持一致就好了
这也会影响可发现性。`slices`、`maps`、`iter`、`sort` 都是顶级包,你必须了解这些才能高效地进行迭代操作。你不能直接使用 `items.sort().map(foo)`,通过自动完成功能来引导和发现这些方法。
> 要是 Go 在方法和函数的区分上能保持一致就好了
泛型只能应用于函数而非方法,这是由于其类型系统所致。因此别抱太大希望,修改这一点将导致兼容性问题。
哦!还记得当很多人因为 Rob 提到语法高亮会分散注意力而自诩为高人时吗?或者当我坚持认为 GOPATH 是 Google 傲慢的产物(恰逢 `godep` 出现且 Kubernetes 正在费尽心思处理 GOPATH 问题时)而遭到群起而攻之?
很高兴不在那个社区,很高兴现在不用写(或读)Go。
坦白说,大多数时候我看到人们对Go赞不绝口,都是因为那些在非C语言中本就存在的特性,或是完全主观的评价如“它很简单”(而忽略了现实)。
你只用过 Go 语言一次,而且只是短暂地用过,却觉得自己有资格如此轻易地做出这样的判断?
作为一个从 2015 年就开始使用 Go 语言的人,参与过数十个大型代码库的开发,总行数可能达到百万,跨越多个团队,你的批评并不成立。
在可扩展性方面,Go 语言并不比 C 语言差,也不比 C# 或 Java 差。Go程序的可扩展性取决于开发者是否正确设计代码库。确实,Go在表达力上比某些语言更注重明确性。你被鼓励减少抽象层,更加具体和明确。但这绝不会阻碍代码的扩展性。编写模块化、可扩展程序的能力是一种需要学习的技能,而非编程语言免费提供的。
听起来你是在一个结构不良的代码库上工作,并错误地认为这是 Go 的问题。
我个人不喜欢 Go,它确实有许多缺点,但它受欢迎是有原因的:
Go 是一种性能合理的语言,它使得编写可靠、高度并发的服务变得相当直观,这些服务无需依赖繁重的多线程——这都要归功于 goroutine 模型。
当 Google 推出 Go 时,几乎没有其他流行、静态编译的语言可供选择。
至今仍几乎没有——唯一真正与之竞争且处于类似领域的是 Java 的新虚拟线程。
支持异步/等待的语言也承诺了类似的功能,但在实际应用中却面临诸多复杂性(如避免异步任务阻塞、函数着色等)。
这里不考虑Erlang,因为它是一种截然不同的语言类型……
因此,尽管Go语言存在诸多不足,但凭借goroutines和Google项目的声誉,它依然广受欢迎。
Java 虚拟机(JVM)正逐步缩小与 Go 的差距。通过虚拟线程、ZGC、Lilliput、Leyden 和 Valhalla 等技术,JVM 正在缩小这一差距。
从 Java 8 到 Java 25 的变化堪称天壤之别。未来前景光明。Java正逐步引入更多语言特性,使其使用起来更加便捷。
我至今仍对早期职业生涯中的Java心有余悸。那些奇怪的模式、FactoryFactories、Spring框架和ORM,90%的时间还能正常工作,但剩下的10%则是纯粹的痛苦。
无论语言如何演进,我都不愿再回到 Java。
对我来说,C# 已填补了 Java 在企业/游戏环境中的空白。
C# 是一门被严重低估的语言,过去十年间它迅速演进,融合了面向对象编程与函数式编程的精髓。
它足够快,足够简单(现在与TypeScript非常相似),足够灵活,文档完善(因此大语言模型(LLMs)表现出色),拥有广泛且维护良好的官方库,而且团队多年来一直致力于提升语言的简洁性(模式匹配和switch表达式是我在C#和TS之间切换时非常想念的功能)。
EF Core 也是最佳 ORM 之一:成熟、稳定、文档完善、性能优异、易于使用且表达力强。过去一年在 Node 生态系统中工作后,我发现使用 EF Core 构建快速应用时几乎没有“纸割伤”(Prisma、Drizzle 等工具都存在大量这类问题)。
遗憾的是,我与许多人交谈时发现,他们对 .NET Framework(遗留技术、仅限 Windows)有负面印象,可能之前在 C# 仅限 Windows 的时代工作过,之后就再也没关注过它。
虽然C#很棒,但编程语言的问题在于,你不仅在选择一种语言,还在选择使用这种语言的公司类型,以及编写这种语言的人的类型。
这意味着,如果你使用C#编程,你会遇到大量来自企业、银行或政府背景的开发者,他们认为使用四层企业架构、数据传输对象(DTO)和五行代码类是编写CRUD应用程序的唯一方式,而最糟糕的是,你会遇到大量在十年前大学里学习过C#的人,他们拒绝学习其他任何东西。
EF很棒,但大多数人使用它是因为他们不需要学习SQL和数据库。
Blazor很棒,但大多数人使用它是因为他们不想学习前端开发和JS框架。
我认为你关于资源类型的观点有道理,但根据我的经验,使用一些简单的启发式方法(尽管现在有了 AI 和作弊行为,情况可能大不相同)来区分优劣并不难。
“现代 C#”(如果我们能区分的话)在建模方面有很多不错的特性,比如不可变的 record 类型和命名元组。我认为EF真正发光的地方在于,它允许你轻松地用持久化模型来建模领域,然后将DTO纯粹用作投影(这就是我使用DTO的方式)到视图中(例如REST API端点)。
我无法代表整个生态系统发言,但至少在我自己的用例中,EFC主要用于_写入_场景和一些基本的读取场景。但在几乎所有项目中,我最终都会在读取侧使用 CQRS 与 Dapper 进行更复杂的查询。因此我认为这不是人们在回避 SQL,而是团队更注重生产力。
关于 Blazor,我不会推荐它替代 JS,除非是内部工具(曾在一家初创公司尝试过,后来切换到 Vue + Vite)。但公平地说,现代JS前端开发确实是一团复杂的乱麻。
C#之所以成为移动游戏的通用语言,是因为Unity。
而当前端使用C#时,后端也必然使用C#。
我认为,Unity C#几乎不是“真正的”C#,因为它使用了完全不同的编程模型,包括不同的对象生命周期、编程和对象模型。我知道执行部分是相同的,但为Unity编程在各个方面都与编写ASP.NET代码感觉非常不同(甚至比从ASP.NET迁移到Spring Boot更不同)
我认为可以通过创建一个`instructions.md`文件,并明确指定要使用的语言版本/功能来解决这个问题。
我仍然为Silverlight[0](以及Moonlight)的消亡感到遗憾,因为当时人们对微软的厌恶情绪非常强烈。
它实际上在当时非常出色,比Flash领先了数光年。
但人们宁愿使用各种黑客手段让Flash在Linux和OSX上运行,也不愿使用Moonlight。
[0] https://en.wikipedia.org/wiki/Microsoft_Silverlight
我非常高兴它消失了。它是一个奇怪的专有Flash替代品,而Flash本身就是一个奇怪且专有的技术,只不过新的版本由一家公开表示要摧毁Linux及其盟友的大公司所有。
当时他们战略的核心是如何完全掌控网络。每次他们的尝试失败,我都会庆祝。
作为当时使用它进行开发的人,我认为它消亡的原因是他们每次Windows新版本发布时都会推出新版本,且新版本与旧版本略有不兼容。
久而久之,人们对频繁更新感到厌倦。
公平地说,那些“奇怪的模式”并非Java本身,而是当Java成为“企业级”技术后,围绕它形成的疯狂文化。
而且实际上是从C++转过来的!
令人惊讶的是,有多少人认为GoF与Java有关,却从未读过这本书。
我花费了无数个小时和数年时间掌握控制反转、晚绑定以及所有那些在面试中如此重要的设计模式,结果却从未真正利用过这些东西,因为一旦应用程序建成,它们很少甚至从未被重新配置以完成除最初设计目的之外的其他任务。我们本可以省去这些麻烦,直接编写模块化、可测试的非面向对象代码,然后就此打住。经过大约25年,我回想起自己花费大量时间使用Spring Framework、Struts以及其他老旧产品,不禁摇头叹息。那不过是别人用来牟利的手段。
我还想起Tomcat从一个需要部署的应用程序转变为运行时嵌入式库的那段经历!那一刻仿佛所有人都意识到,Web容器不过是虚有其表的幌子。这并没有阻止雇主强迫我继续使用WebSphere/WAS,因为“他们为此付了钱,天啊,他们一定要用!”与此同时,这些产品早已过时,被Docker容器彻底取代。
我好奇现在的“WebSphere管理员”都在做什么?曾经能管理那些Jython配置可是个高薪职位,哈哈。
那不是Java,而是Spring。
不过,如果在JVM上,就用Kotlin吧。
或者Clojure、Scala、Groovy。
还有GraalVM,支持JavaScript/Node、Python、R和Ruby。
以及许多其他语言。
此外,认为你只会使用“新Java”范式似乎有些乐观,因为大多数企业软件仍停留在旧版本。就像Python一样,理论上你可以创建出色的全新项目,但行业中80%的工作都基于旧版或遗留组件。
作为Java开发者,如今抱有希望是合理的。
现代Java社区正逐步采用函数式编程的常见实践“使非法状态不可表示”,并将其称为“数据导向编程”。这对我们这些积极使用ADT的人来说很不错。我不再需要反复解释“什么是Option<?>?”或“为什么使用ADT?”,只需指向这些新资源即可。
希望这种转变能引导Java社区走向比当前“ cargo cult”(即以“贫血领域模型”为幌子使用可变C结构体+垃圾回收器来伪装面向对象编程)更理性的方向。
是的,你可能会被分配一台真空管主机……
比如,有1000万Java开发者,任何语言都有大量全新的开发项目,更不用说在如此庞大的语言中。
> 大多数企业软件仍停留在旧版本
这根本不正确。最新调查显示,生态系统中已有60%的项目超越了Java 8。
我对JVM的批评是,它已不再有用,因为我们不再通过这种机制实现可移植性。我们构建的应用程序可以在容器中运行,并可以在它们将要运行的确切环境中编译,我们控制所有这些。Sun Microsystems和Java需要在Solaris、DEC、HP、可能的SGI以及后来的Linux上运行的日子已经一去不复返了。然而,我们仍然在可移植性内部使用可移植性,原因可追溯到很久以前。
这很好,但你还在使用Maven和Gradle吗?我希望看到一个不那么糟糕的流行包管理器,才会考虑回归。
(类似于Python终于通过uv工具理顺了自身。)
这可能是真的,但面对30年来积累的冗余代码、碎片化的生态系统和工具链,以及不断演变的语法和规范,足以让任何人望而却步。我个人再也不想经历类路径地狱了,尽管自上次接触Java大约15年前以来,情况可能有所改善。
Go语言尽管存在诸多缺陷,却极力避免复杂性,而我多年来发现,这是编程语言最重要的品质。我并不需要功能繁多的语言,我想要的是一门具备核心功能且设计稳健、具备一定灵活性、并且不会给我添麻烦的语言。在这一点上,Go语言比我使用过的任何语言都做得更好。
我很有可能在最新版本的Java上运行一个30年前编译的.jar文件。Java是向后兼容和向前兼容的典范,这门语言经过精心设计,语法不会过于冷漠,即使有人从Java 7时代开始休眠,也应该能够阅读Java 25的代码。
> Go 尽管存在诸多缺陷,但它极力避免复杂性
整个领域本质上是关于管理复杂性。你不能回避复杂性,而是要为人们提供管理它的工具。
而 Go 则走向了复杂性管理的低端,它提供的功能不足以管理这种复杂性——它过于简单化,而非真正简单。
我认为最佳平衡点实际上在Java——它是一门非常简单的语言(与Scala等相比),但又具备足够的表达力,能够提供高效且易于使用的各类库(例如完全类型安全的SQL DSL)
> 我可以合理地运行一个30年前编译的.jar文件在最新版本的Java上。
很好,几乎所有语言都能做到这一点。这不是大家讨论的重点。
> Java是向后兼容和向前兼容的典范,
使用Java 8的公司比例是否已低于50%?[1]
[1]: https://www.jetbrains.com/lp/devecosystem-2023/java/
> 很好,几乎所有语言都能实现类似功能。这不是大家讨论的重点
Go语言已经引入了破坏性变更。
> Java 8
是的
你避免了不必要的复杂性。
如果你认为Java中不存在这种情况,可以花些时间阅读Maven文档或Spring文档https://docs.spring.io/spring-framework/reference/index.html https://maven.apache.org/guides/getting-started/然后想象自己是一个编程新手,试图理解这些文档
你尽量让简单的事情保持简单,并努力让复杂的事情变得更简单,如果可能的话。简单并不容易
我不再讨厌Java( anymore),它有许多实用之处(比如说…Jira)。但当我编写Go语言时,我几乎从未想过“哦,我希望现在正在编写Java”。不用了
嗯,Spring是一个完整的框架,它为你提供了大量功能,但当然,复杂性必须存在于某个地方——从根本上来说是这样。
没有它,你要么自己编写这些复杂功能,要么甚至无法意识到为什么这些功能是必要的,例如未能意识到SQL注入、跨站脚本攻击等存在。后端有一些常见的要求,而你的问题很少不需要这些基本功能,所以作为初学者,我建议你学习框架,就像你在尝试飞行之前学习如何驾驶飞机一样。
对于其他事情,没有必要使用Spring——纯Java有许多工具,你可以随意修改任何你想要的东西!
> 整个领域都围绕着管理复杂性。你不会回避复杂性,而是为人们提供工具来管理它。
复杂性存在于计算的各个层次,从硅片开始。虽然我们无法避免现实世界问题的复杂性,但我们可以肯定地减少其解决方案所需的复杂性。有无数的问题主要是由我们软件堆栈和运行它们的硬件的自我诱导的复杂性引起的。选择一种刻意避免这些问题的编程语言,是我在这件事上唯一能做的主张,因为我既没有能力也没有耐心去重新做那些比我聪明的人已经完成的数十年艰苦工作。
仅仅因为一种语言倡导简单性,并不意味着它不提供解决现实世界问题的工具。Go语言的作者在选择合适的权衡方案方面做得非常出色,而大多数其他语言的作者则不然。_大多数_时候。我仍然认为泛型是一个错误。
仍然有大量地方在运行旧版本的Java,比如JDK 8。
如果坚持使用最新版本并定期更新,Java是非常优秀的。但很多公司讨厌自己的开发人员。
能够创建一个能够快速启动且内存占用与等效的Go语言应用相同的自包含Kotlin应用(JVM)将非常令人惊叹。
Graal Native Image 能够实现这一点(尽管编译时间较长,但你可以直接在 JVM 上进行开发,支持热重载等功能,仅在发布时进行原生编译)
据我所知,如果使用不兼容的库,Graal 仍然会带来不少麻烦,但可能这些信息已经过时。
这个问题仍然存在。主要问题是,进行原生编译时必须提前声明反射目标。如果你的框架不支持这一点,这可能会带来麻烦。
你可以通过使用 AppCDS 和压缩对象头来获得 Graal Native 提供的绝大部分功能。
这是关于所有这些内容的最新 JEP。
只有那些无法通过直接代码访问的反射目标。如果你有代码访问类的“stringLiteral”字段,那么它会自动注册。但如果你基于用户输入访问它,则需要手动注册。
此外,许多库现在都带有标记这些额外反射目标的元数据。
不过你总体上是正确的,但具体取决于你的用例。
嗯,谷歌现在并没有推出太多新的(成功的)服务,所以引入新语言的潜力相当小,可惜了 :)。此外,Go缺少一个非常重要的功能,即在运行时服务中实现类似 HotSwap 的能力,这对调试大型复杂应用程序而无需关闭它们非常有用。
谷歌100%在开发大量新服务,而Go语言已有13年历史(在谷歌内部甚至更久),因此它肯定有足够的机会去实现。
至于热插拔,我还没听说它被用于生产环境,那主要是为了加快开发周期——虽然我可能错了。通常来说,更安全的方法是启动新版本,将请求直接切换过去,然后关闭旧版本。直接热插拔类存在问题,例如若在某个类中新增字段,缺乏该字段的旧实例将如何行为?
热插拔确实有助于进行小范围调整,例如在某处添加日志语句以验证假设。但用于显著改变行为则可能不安全,尤其在生产环境中 🙂
在你看来,哪种现代语言更适合新项目?
也许有点奇怪,但我会选择Swift。
Swift是我对下一代服务器语言的期待。优秀的类型系统,优秀的错误处理。
Elixir,带类型系统
我喜欢Elixir,但它无法编译成单一二进制文件,虽然它支持大规模并发,但单线程运行较慢,部署也仍具挑战性。
而且列表比数组慢,即使它们提供了功能性保证(一切都是权衡……)
不过,我认为它其他方面几乎都令人惊叹,而且拥有几乎在其他地方找不到的独特特性
那还未实现。此外,Elixir绝非 Go 的替代品。
它无法在性能上与 Go 匹敌。没有可变数组,几乎所有数据都是链表,且数据共享只能通过消息传递实现。
我在日常工作中主要使用 Elixir,但最近需要编写一个高性能的数据迁移工具,所以我选择了 Go。
我也支持 Elixir,但它并非竞争对手,原因有很多。在这个领域确实有一些语言,尽管它们太小且不够成熟,比如 Crystal、Nim。仍在等待更好的选择。
附言:Swift,有人用过吗?
我上次检查时,Crystal的编译时间对我来说太慢了,无法用于任何超过玩具项目的任务。
Swift在Linux上的体验相当糟糕,但语言本身看起来很有前途。
是的,如果要求是“能够轻松编写可靠、高度并发的服务,且不依赖于复杂的多线程”,那么Elixir是完美的选择。
即使没有类型(类型即将到来,而且看起来不错),Elixir 的模式匹配也比 Go 的错误处理好上千倍。
这个我支持。
Clojure
对于网页前端:JavaScript
对于机器学习/数据处理:Python
对于后端/通用软件:Java
我们所知的唯一万能解决方案是基于现有库进行开发。这些语言也恰好是任何值得参考的排名中前三大最受欢迎的语言。
我随时愿意用Go替换Java。我一直不喜欢Java中需要大量“代码填充”的public static void main。
关于计划在几周后发布的Java 25:
—– https://openjdk.org/jeps/512 —–
首先,我们允许主方法省略臭名昭著的 boilerplate 代码 public static void main(String[] args),这使得 Hello, World! 程序简化为:
class HelloWorld {
void main() {
System.out.println(“Hello, World!”);
}
}
其次,我们引入了一种紧凑的源文件格式,让开发者可以直接进入代码,无需冗余的类声明:
void main() {
System.out.println(“Hello, World!”);
}
第三,我们在 java.lang 包中添加了一个新类,为初学者提供基本的行导向输入输出方法,从而用更简单的形式替换了神秘的 System.out.println:
void main() {
IO.println(“Hello, World!”);
}
这样就更接近 Go 的语法了,尽管有些人可能不喜欢。 🙂
除了每个语句都需要额外的 3 行 if err 代码外…
也许它比你想象的来得更早… 这一切都始于添加值类型,现在又有了类似 Go 的语法改进… 谁知道呢? 🙂 你会非常开心的。
编辑:等等,Java 还没有值类型…… /开玩笑
> Go 风格的语法优化
我听过的最矛盾的词汇。
???!
精炼:从物质中去除杂质或不需要的元素的过程。
精炼:通过微小改动来改进或澄清某事。
public static void 在一个包含AbstractFactoryBuilderInstances工厂的类中…? 对吧?… 说再一遍?
我们讨论的是去除不必要的语法结构,而不是像某些人用注解那样添加东西,目的是什么?精炼类型或许? 🙂
> public static void 在一个包含 AbstractFactoryBuilderInstances 工厂的类中
这不是语法问题。工厂构建器与语法无关,而与代码风格密切相关。
这种矛盾的表述暗示语法精炼会受到 Go 语言的启发,而 Go 语言以语法简单著称。我不是说基本语法不好。但显然现代Java的语法要精致得多,这并非因为它看起来更接近Go。
总听到Go开发者用“Java太冗长”作为新手论点,但Go那边有那么多冗余代码,而Java那边却处理得很好。
Go中的每个函数调用都需要3到5行代码。对于需要处理错误的问题,Go代码的行数通常是Java的两倍以上。Go是一种特别容易出现“代码填充”问题的语言。
从Go的角度抱怨冗长性真是讽刺。
不过,Java已经简化了PSVM要求,甚至不需要显式声明类,一个void main方法就足够了。[1] 当然,这对于非脚本代码来说并不重要。
[1] https://openjdk.org/jeps/495
Java,哈哈。企业级语言,抽象层过多且对面向对象编程的理解有误。绝对不行。
那么PHP/Ruby用于网页开发呢?
一位优秀的 Ruby 程序员可以创造奇迹并极具生产力,但我认为从某个规模开始,它就不再能像以前那样良好地扩展(无论是从性能还是从更大团队的角度来看)。
PHP 的框架非常出色,它们隐藏了这个语言中原本布满地雷的部分(尽管多年来一直在稳步改进)。
如果这是你或你的开发人员所熟悉的,两者都是不错的选择。
但它们不会是我个人的首选。
绝对不要选择Java。即使核心语言在过去几年中有所改进,选择Java几乎肯定意味着你的团队将被迫使用专有/企业工具(如IntelliJ),因为在每个Java/C#开发环境中,本地环境都与IDE配置绑定。更不用说Spring了——现在每次代码审查都会在Github上显示“大型差异默认不显示”,因为Java中的一个简单模块必须是一个至少超过500行代码的新类。
你上次接触Java是在2000年之前吗?
本地开发环境与集成开发环境(IDE)完全无关,但如果你不使用一个不错的IDE(无论使用哪种语言),那你就是在自讨苦吃——IDE能极大地提升工作效率。
你还在用XML时代的技术吗?Spring Boot的效率极高——事实上,Go语言比Java要冗长得多,而且充满了不必要的if语句。
> 你上次接触Java是什么时候,2000年之前吗?
2025年8月22日。
本地开发环境并非严格意义上与IDE绑定,但在任何规模较大的项目中,它们实际上是绑定的。原因在于,大多数 Java 开发团队确实认为“如果你不使用一个不错的 IDE,无论使用哪种语言,你都在自讨苦吃”。我在 Deno、Lua 和 Zig 中使用文本编辑器 + CLI 工具也能很好地工作。只有当我进入 Java 世界时,最明智的人才会说“是的,有 CLI,但我真的不了解它。我建议你下载 IntelliJ 并运行这些配置。”
是的,Spring Boot 很有生产力。Ruby on Rails 或 Laravel 也是如此。
任何生产级项目都会使用 Maven 或 Gradle 进行构建。有 CI/CD 管道、代码检查等,如果只能通过 IDE 进行构建,这些功能如何实现?
当然,有些非常落后的公司仍然通过邮件发送修改后的文件,没有任何版本控制,我敢肯定其中一些公司仍然使用IDE配置,但说实话,我最常看到这种情况是在Visual Studio项目中,而不是Java项目。尽管你可以在任何其他语言中找到这些情况,你只需要扩大用户基数。一种尚未达到1.0版本的语言,其技术能力较强的用户比例会更高,这并不令人意外。
>只有当我进入Java世界时,那些最聪明的人才会说:“是的,确实有命令行界面,但我并不真正了解它。我建议你下载IntelliJ并运行这些配置。”
显然他们对工具并不熟悉,我也不敢称一个初级开发者为“最聪明的人”
我知道,既是_专有_又是_企业级_,对吧?https://github.com/JetBrains/intellij-community/blob/idea/20… (我本可以链接到Apache 2版本的PyCharm,但这对那些喜欢贬低专业工具的人来说无关紧要)
那是社区版。虽然是个俏皮又带点讽刺的评论。
异步/等待确实存在一些痛点,但我认为对它的批评往往被夸大了。如果完全采用异步编程,大多数问题都会消失,但将旧的同步代码与异步代码混合使用则要困难得多。
我的经验主要来自C#,但在我的经验中,异步/等待在C#中工作得非常出色。你需要了解一些基础知识以避免问题,但这适用于几乎所有类型的并发。它们都有潜在的风险。
别忘了Rust。从我所见,它在微服务领域正变得非常流行。不难理解原因。多线程处理轻松自如。内存使用量低。延迟表现出色。
Rust 的异步功能让你很容易以多种方式自掘坟墓。
大多数编写基本异步 CRUD 服务器的用户可能不会注意到,但如果你编写复杂、高度并发的服务器,你就会非常清楚。
这可能是一个可行的权衡,对许多人来说确实如此,但它远不如 Go 那样傻瓜式安全。
一种具备Rust特性但去除内存和生命周期管理、Go的垃圾回收和标准库的语言,可能就是我一直在等待的语言。
我使用Go多年,虽然它能快速搭建小型项目,但大型项目很快就会陷入千疮百孔的困境。
调试是一场噩梦,因为如果代码中存在未使用的X(而你在调试和测试“如果我注释掉这部分会发生什么?”时,总会存在未使用的X),它甚至拒绝编译。
官僚主义令人厌烦。魔法文件名令人厌烦。魔法字段名令人厌烦。标准库中隐藏的秘密 panic 令人厌烦。背后偷偷进行的堆复制令人厌烦(而且速度很慢)。Go 中的所有魔法最终都会变得令人讨厌,因为它们通常是天真地重新利用了某些东西(它们依赖于为不同目的和假设设计的组件,但天真地决定依赖其副作用来支持自己略微不兼容的机制——比如特殊文件名和大写字母,尽管并非所有字符都有这样的特性…… 难道真的那么麻烦,要为想要暴露的东西输入“pub”吗?
现在人工智能已经很强大了,我开始喜欢 Rust,因为我可以快速询问人工智能为什么我的类型不匹配,或者为什么会发生棘手的可变借用——而不是花几个小时翻阅文档和 Stack Overflow 问题。
自从人工智能变得强大后,我就再也没做过严肃的Rust开发,但去年12月我确实有过短暂的尝试,结果发现它们在Rust上的表现令人震惊。那种冗长的语法和到处都是显式信息的设计,竟然能让它们轻松解决那些会让人类纠结许久的问题。
> 调试简直是一场噩梦,因为如果存在未使用的X,它甚至拒绝编译。
Go语言的开发者会指责你没有使用正确的工具。
是的,Go语言对原则的坚持过于僵化。
我曾向其中一位創作者描述這個「除錯」問題,但他甚至不理解問題所在。這簡直太業餘了,讓人懷疑他們是否曾踏出學術界一步。
順便一提,AI 在 Go 上的表現很差。人們本以為這種簡單的語言會適合 ChatGPT。結果 ChatGPT 在 Java、C#、Python 等多種語言上的表現都比 Go 更好。
我在AI和Go上的成功率并不比其他语言(JavaScript、Python、Terraform、Swift)更差。
我认为Terraform是最糟糕的。但考虑到它的利基市场,这并不令人意外。
毕竟他们创造了Unix和C,所以他们并非完全学术化。
总体而言,我从Go 1.0之前一直到今天都非常喜欢它。当然可以挑剔一些细节,但说“它仍然不够好”是一种奇怪的看法。
我认为,随着创建者逐渐退出项目,Go很难保持其核心愿景,这将使语言变得更差(并使权衡变得毫无意义)。
我认为让Go被贴上“用于编写服务器的语言”的标签,已经并且将继续导致重要的人才流失,这些人才可能会转向Rust或继续留在Python等语言中。
也许这只是有趣,就像一直抱怨Visual Basic有多糟糕,虽然这是事实但无关紧要,因为需要做Visual Basic擅长的事情的人们继续做他们该做的事。
Go确实存在问题,但至今我尚未看到任何经得起严格审查的Go负面评论。
通常,如本文所述,对Go的反对意见往往以技术上正确但最终过于吹毛求疵的论点形式出现。
Go的优势如此显著,以至于那些微小问题根本不足以让人放弃这门语言。
Go 足够好,足以在等待版本间缓慢但稳步的改进以提升使用体验的同时,继续使用它。
没错,作者抱怨的大部分问题都是在任何语言中都能找到的琐碎问题。相比之下,Go 的一些真正深层次的语言设计问题包括:
– 零值,缺乏对构造函数的支持
– 对空值的处理不佳
– 默认可变性
– 没有为泛型设计的静态类型系统
– int 不是任意精度类型 [1]
– 内置数组类型(切片)的所有权语义考虑不周 [2]
值得一提的其他问题:
– 没有总和类型
– 没有字符串插值
[1]: https://github.com/golang/go/issues/19623
[2]: https://news.ycombinator.com/item?id=39477821
我写了一本关于Go的书,所以我的观点可能有些偏颇。但当我十多年前开始使用Go时,它确实让我耳目一新。它让编程重新变得有趣,比Java少了很多冗余代码,足够简单易学,而且性能通常也很好。
没有单一的“最佳语言”,这取决于你的具体用例。但我认为,对于许多典型的后端任务,Go 是一个你不会后悔的选择,即使你对这门语言有些不满。
当我遇到一些家庭 DIY 或木工问题时,我总是会拿起我信赖的 Dremel:
* Dremel易于使用:我不用担心用电锯切断手,也不用为圆锯设置夹具。我不用把工件搬到车库。
* Dremel简单:一个滑块控制速度。将旋转刀头应用于工件。
* Dremel有趣:它握在手中非常舒适。它噪音不大。我不用担心会伤到自己。它能非常令人满意地削去物体表面的碎屑。
在许多方面,Dremel 都是一个伟大的工具。但 90% 的时间,当我使用它时,它最终会让我花五倍的时间(但是一段愉快的五倍时间!),而最终结果是一个摇摇晃晃、刮痕累累的 mess。我责怪自己没有在前期投入足够的意志力去使用合适的工具。
我发现自己在使用各种硬件和软件工具时都会这样:为了追求乐趣和易用性而过度优化,却忘记了最终结果的价值,以及使用合适工具的重要性。
我将此称为“Dremel效应”,并在选择工具时会刻意留意这一点。
这是一个有趣的比喻。
我目前的工作性质决定了我的编程大多属于“为了乐趣”的范畴。所以我宁愿花5倍时间,也要享受其中。
话虽如此,我认为Go不仅仅是有趣,它也是许多后端项目中的一种可行选择,这些项目传统上会使用Java或C#。而且在我看来,它肯定比最近流行用JS/Python驱动后端微服务要好。
同意如果 Go 是一把 Dremel(我不确定它是否是),那么 JS 就像是把生锈的非锁定折叠刀。
如果你不喜欢 Go,那就放手吧。我希望没有人强迫你使用它。
一些批评确实有道理,但其中一些听起来像是他们没有花时间去理解这门语言。这都是权衡取舍的问题。例如,我喜欢Rust的很多地方,但它仍然不是我最喜欢的语言。
不同意。我读过的关于Go的大多数批评都很弱。这个还算不错。而我这么说,是因为我非常喜欢Go。
不过,我真的很希望有一个重构,让他们在空值、作用域规则等方面做得更好。然而,他们承诺永远不会破坏现有程序(值得称赞,可以理解),因此设计空间极其有限。我宁愿处理局部的不便甚至过度的冗余,也不愿面对系统性问题。
生活中很少有事情是真正强加给我的,但放弃所有我不喜欢的东西是愚蠢的。妥协无处不在,我认为进入权衡并不意味着我没有资格对权衡的事物发表意见。
我认为这篇文章听起来不像有人没有花时间去理解这门语言。它听起来更像是那种只有在你认真使用这门语言一段时间后才会让你感到烦躁的事情。
这篇文章是经过深思熟虑的,显然是来自一个真正用Go语言构建过实际项目的人。
我非常喜欢Go语言,并在可能的情况下使用它。然而,我希望有一种像Go语言一样的东西,但没有这些问题。值得讨论这一点。例如,我认为大多数这些批评是公平的,但我对其中几点有异议:
1. 错误作用域:是的,这使得代码审查比必要时更加复杂。这是导致微妙且不必要的错误的地方。
-
两种类型的 nil:是的,这非常令人困惑。
-
它不具备可移植性:Go 的可移植性不如 C89,但它相当可移植。例如,它足以用于编写通用预构建的命令行工具,这大约是我对“实用可移植性”的标准。
-
切片的所有权问题及其他怪异行为:是的。
5. 未强制执行的`defer`:是的,与`err`类似,这会引入一些微妙的 bug,只能通过文档、仔细审查和 boilerplate 处理来克服。
6. 异常_叠加_在 err 返回上:是的。
7. UTF-8:还没有给我带来麻烦,但我不知道这种批评是否合理。
8. 内存使用:我认为垃圾回收是该语言的卖点,而非缺点。
在我看来,关于数据所有权的章节包含了 Go 语言最严重且不可原谅的缺陷示例。该示例中 append 的行为属于那种可能引发 bug 或过于晦涩的行为,绝不应出现在任何编程语言中。作为一名常写 Go 代码的开发者,我理解该语言为何存在这一特殊设计,但希望自己永远不会真正“理解”到足以原谅它的程度。
我惊讶于评论中的人们没有更多关注 append 的示例。
Go确实存在一些问题。但依我之见,本文中描述的任何问题都不成立。
而且,所有有效的抱怨加起来也无法构成“不好”的结论,依我之见。
当然,生活选择是一回事,但这种批评仍然有价值。我学到了一些东西,也认为Go可以改进(我明白这是因为我不太懂这门语言,但我仍然更喜欢在循环中使用map而不是append)
这不禁让人问:你最喜欢的语言是什么?
“喜欢就用,不喜欢就别用!”
他们正在强迫人们像在Go语言中那样编写TypeScript代码(我目前所在的团队就是这样,还有其他极其愚蠢的决策——只对服务边界进行单元测试,不将逻辑提取到纯函数中,不编写UI测试等)。我真的必须记住,在加入一家公司之前,要让他们展示他们的代码。
(我明白这不是招聘方,但邮箱在个人简介中)
我这样做,觉得效果很好……
myfunc(arg: string): Value | Err
我尽量不再使用TypeScript抛出异常,而是像Go一样进行错误检查。当与Go后端配合使用时,这使得上下文切换非常轻松……
他们仍然会抛出异常,而且在几乎每个函数周围都有成千上万的try-catch块重复出现 :-/
你见过Java开发者写Python吗?感觉一样 🙂
让我想起了这篇经典演讲 https://www.youtube.com/watch?v=o9pEzgHorH0
当然有:https://youtu.be/wf-BqAjZb8M?t=831
啊,是的。我喜欢在那些雇佣专家只是为了告诉他们如何做好自己擅长的工作的地方工作。
从技术上讲,1965年提出的“十亿美元错误”这一术语,到2025年将变成“十亿美元错误”。或者,如果以住房成本来衡量,它将是一个“二十亿美元错误”。
:^/
“十亿美元的错误”是在1965年犯下的,但这个术语是在2009年提出的,定义如下:
> 我无法 resist 诱惑,在代码中加入了一个空引用,仅仅因为实现起来非常简单。这导致了无数的错误、漏洞和系统崩溃,这些问题在过去四十年里可能造成了十亿美元的损失和损害。
– 我在这里看到很多关于Go语言问题(如空值处理或错误作用域)与Rust语言优势的讨论。
– 作为一名使用过C/C++和Fortran的开发者,我认为所有这些语言都有各自的挑战——例如,Go语言的简洁性与Rust语言的安全性保证之间存在权衡。
– 能否分享一个现实世界中的例子,说明Go的设计导致了生产环境问题,而Rust或其他语言本可以避免?
– 我很好奇这些权衡在实际中是如何体现的。
抱歉,我不做Go/Rust编程,仍在使用C/C++/Fortran。
> Go 的设计导致了生产问题
一个简单的例子,如果你在 Go 中创建两个独立的库并尝试与应用程序链接,你会遇到大麻烦。
我遇到过同样的问题:https://github.com/golang/go/issues/65050
https://www.youtube.com/watch?v=xuv9A7CJF54&t=440s
> 导致了生产问题
这不是生产问题,而是一个非常小众的使用场景
使用软件加载插件本身就是一个小众场景,而这些插件刚好是用Go语言编写的?不,这并不是小众场景。如果你在Go语言中只做Web服务器开发,那当然不会遇到这个问题。
我不同意文章的大部分内容,但我认为我明白它从何而来。
Go语言最大的缺点在于,它与底层硬件的交互不够直观。它提供了许多高级特性,营造出一种“我们为你考虑周全”的氛围,却未能向用户提供足够的教育,让他们明白自己将面临实际操作的挑战。
以切片为例:即使在命名上它意味着“一部分”,但实际上它更接近“一盒指针”。当你修改指针+1时会发生什么?或者“两种类型的空值”;拥有两个字节(简化说法),一个是结构体类型,另一个是该结构体的地址,与仅仅拥有一个空值(NULL)是有区别的——这就像知道房子不存在,与确信房子存在并说它位于海洋底下的火山中央一样。
Foo99 的批评是另一个例子。如果你想实现不是99个循环而是100亿个循环,每个循环仅占用10字节,你需要100GiB的内存才能退出。如果你复用地址块,你只需使用… 10字节。
我建议尝试在C中实现词法作用域延迟,并将它们放在线程中。那将是一大堆乐趣。
我认为这最终取决于你想要成为怎样的工程师。我不喜欢手把手指导,更愿意独自面对代码,让单元测试如雨般跟随我的代码,因此 Go、Zig、C(从低级语言)对我来说很合适。有些人更喜欢 Rust 或高级抽象。这也很好。
但在我看来,嘲笑Go语言不隐藏抽象层,就像嘲笑足球是儿童游戏一样,因为它不仅没有马,而且球员用腿而不是球杆踢球。
> 我认为我知道它来自哪里 […] 嘲笑Go语言不隐藏抽象层
作者在此。
不,这不是问题的根源。我编写C语言已有30多年,Go语言大约12-15年,目前更倾向于Rust。我喜欢C++(是的,真的)以及将所有这些没有句柄的刀具拼凑在一起。
不,我对Go的批评在于它没有吸取数十年来理论研究的教训,即什么有效、什么无效。
我并不责怪 Go 在切片中的泄露抽象,例如。我责怪它在最初创建了糟糕的抽象 API,发放了本可以避免的陷阱。我知道如何避免在切片上追加元素时,同一数组的其他切片可能在其他地方仍可访问的陷阱。但我认为在 Go 创建的那一年,创建这样的陷阱是不可辩解的。
活得够久,任何人都可能会犯傻。 “不要犯错”并不是一个选项。这就是为什么编程语言的 API 和语法很重要。
至于裸机环境;Go 既未能充分利用高级语言的优势,同时又不适合裸机环境。
这是一个错失的机会。因为是的,在 2007 年,我无法指出某种严格来说更适合某些特定用例的解决方案。
我没有关于不适合裸金属的经验。但我有过使用高级语言通过“创新”思维实现类似功能的经验。我见过 Rust 中整数溢出。我见过用 Elixir 实现的库,在发送另一个 UDP 数据包前会等待该数据包被重新广播。
没有图灵完备的语言能阻止人们犯傻。
这不仅仅是编程语言的API和语法问题。这是概念复杂性,Go在这方面非常低。这是重构难度,Rust在这方面非常高。这是由JS/TS库堆叠在一起带来的隐式行为。这是工具的易用性、生态系统的规模以及API的可用性。而Golang在这些方面都表现出色。
你文章中提到的所有例子对我来说都是“嗯?这不是显而易见的吗?”。以你在C语言中的经验,我实在想不通你为什么不愿重复使用相同的内存分配,反而要将它们分别保存,同时预留可能用不到的内存空间。
即使假设所有内容都应放在栈上,你仍然会因栈外隐式分配而导致内存泄漏或崩溃。
添加200个goroutine,这(双关语)会如何堆叠?
修复这些所谓的“陷阱”真的是一个错失的机会吗?Go语言每年都在变得更加强大,尽管它被一些人讨厌(我理解,有些人更喜欢Rust的实现方式,但这没关系),它正被越来越多地作为一种成熟稳定的语言使用。
许多应用程序甚至不需要担心垃圾回收。如果你在开发一些关键应用程序,可以与 Zig 搭配使用,享受跨编译的便利,尽可能接近裸机,同时满足所有所需的管道。
> 基于你在 C 语言中的经验,我完全不明白你为什么不想重复使用相同的分配,而是将它们分别保存,同时为可能不需要的分配空间预留空间。
你指的是哪一部分?
> 即使假设所有内容都应放在栈上,你仍然会因栈外隐式分配而导致内存泄漏或崩溃。
你的意思是什么?我不是想冒犯,但如果你理解内存的工作原理,这听起来很困惑。你指的是栈外分配会导致内存泄漏吗?
> 你在文章中展示的所有示例对我来说都是“嗯?这不是显而易见的吗?”
是的。这些对我来说都不是新鲜事。在C++中,在类层次结构中定义非虚构造函数对我来说也不是新鲜事,但也可以对为什么“允许”这样做提出合理的批评。我认为C++可以从第一性原理出发为这一点辩护,而Go无法做到。
我不确定你所说的foo99是什么意思。我猜这是关于在循环中使用defer?
> 修复这些所谓的“陷阱”真的是一个错失的机会吗?
在我看来,非常是的。
实际上,文章中提到的这些问题对我来说从未成为过问题。(尽管如此,我还是投了赞成票)
对我来说真正成为问题的是与 GitHub 之外的私有仓库合作(我必须澄清这一点,因为在 GitHub 上与私有仓库合作是不同的,因为 Go 有专门的硬编码设置来确保 GitHub 的兼容性)。
我曾寄希望于GOAUTH环境变量,但要么(1)我比自己想象中更笨更瞎,要么(2)目前仍无法强制Go在不先尝试HTTPS请求的情况下通过SSH获取模块。而且,`GOPRIVATE=“mymodule”` 和 `GOPROXY=“direct”` 无法解决问题,即使与 Git 的 `insteadOf` 结合使用也不行。
绝对不只是你。在我之前的工作中,我们需要从 GitLab 获取私有 Go 模块,后来又需要从自托管的 Forgejo 实例获取。CTO 和我花了一整天时间进行试错,最终找到一个干净的解决方案。如果我记得没错,我们最终让每位开发者在环境中添加 `GOPRIVATE={module_namespace}`,并在他们的 `.netrc` 中添加以下内容:
“` machine {server} # 例如 gitlab.com login {username} password {read_only_api_key} # 必须是实际密钥而非环境变量 “`
虽然能稳定工作,但这不是我们理想中的解决方案。
使用 Go 语言已有两年,之前使用的是 C 语言。这些观点完全合理。Go 语言的一些特性有时会让人感觉更像是地雷而非设计决策,尤其对于那些习惯于使用 RAII、错误作用域或更优雅地处理空指针的语言的开发者来说。但 Go 语言的魅力(也是其诅咒)在于其毫不妥协的极简主义。它并不追求优雅,只是在可扩展性上追求可预测性和可维护性。说“没有理智的人会选择X”可能让人感到痛快,但这会阻碍我们理解为什么理性的团队会选择Go并常常在使用它时取得成功。Go并非适合所有人,但它确实有其存在的理由。
最近我参加了一个会议,讨论是否要更广泛地采用Go作为后端服务语言,但几位架构师提到了“两种空值类型”的问题,最终否决了这一提议。我感觉他们有点小题大做,但令我惊讶的是,到了2025年,团队仍然没有修复这个问题。如果在语言设计中,你唯一看重的是永远不破坏现有代码,即使按任何定义,现有代码已经破损,最终使用你语言的只有现有代码。
这已经解释过很多次了,但这么有趣我再解释一遍。 🙂
所以:Go 的呈现方式令人困惑,但这种行为是有意义的、正确的、永远不会改变的,并且无疑被正确的程序所依赖。
对于习惯于 C++、C#、Java、Python 或大多数其他语言的人来说,令人困惑的是,在 Go 中,空指针(nil)是一个完全有效的函数方法的接收者。方法解析查找在编译时静态进行,只要方法不尝试解引用指针,一切正常。
即使你将值赋给接口,它仍然有效。
package main
import “fmt”
type Dog struct {}
type Cat struct {}
type Animal interface {
MakeNoise()
}
func (*Dog) MakeNoise() { fmt.Println(“bark”) }
func (*Cat) MakeNoise() { fmt.Println(“meow”) }
func main() {
var d *Dog = nil
var c *Cat = nil
var i Animal = d
var j Animal = c
d.MakeNoise()
c.MakeNoise()
i.MakeNoise()
j.MakeNoise()
}
这将打印
bark
meow
bark
meow
但接口方法的查找无法在编译时进行。因此,接口值实际上是一对——指向类型的指针和实例值。类型不为空,因此接口值在每种情况下都类似于(&Cat, nil)和(&Dog, nil),这并非接口的零值,零值应为(nil, nil)。
但这非常令人困惑,因为Go会将空结构体值强制转换为非空的(&type, nil)接口值。可能有一些命名或语法方式可以让这更清晰。
但这种行为完全合理。
你提到的根本原因在于,在 Go 中(与 Python、Java、C#……甚至 C++ 不同),“对象”的“类型”并未与对象本身一同存储。
一个 struct{a, b int32} 占用 8 字节内存。它不会额外占用字节来“知道”其类型、指向“方法”的虚函数表、存储锁,或任何其他对象“头部”。
Go 中的动态分派使用接口,接口是胖指针,同时存储类型和对象指针。
基于此设计,自然可以存在空指针、空接口(无类型且无指针)以及指向空指针的类型化接口。
这可能是个糟糕的设计决策,也可能令人困惑。它是导致数据竞争破坏内存的原因。
但作者称“差异的根本原因在于,只是在打字,而非思考”这种说法,纯属偷懒。
与声称 Go 不利于可移植性一样懒惰。
我曾编写过大量使用系统调用的 Go 代码,并在二十多个不同平台上运行,发现其设计远比 C 语言更合理。
是的,我完全同意——考虑到 Go 的设计,这种行为是有道理的(而仅仅为了让使用根本不同语言的用户感到熟悉而改变行为是愚蠢的)。
然而,nil 的非直观解释是不幸的。
我不确定理想的设计会是什么。
也许只是让接口不可与 nil 比较,而是类似于 `unset`。
type Cat struct {}
type Animal interface{}
func main() {
var c *Cat = nil
var a Animal = c
if a == nil { // 编译错误,无法将接口与 nil 比较
;
}
if a == unset { // 假,希望直观
}
}
不过,这只是一个你碰一次就会明白的尖锐问题。我惊讶于人们会为此如此困扰……毕竟,一旦你熟练掌握语言,这并不会影响你的使用。
(例如,对nil的存在本身或错误处理的抱怨,就更容易理解了!)
(附带说明,Go 确实修复了 for 和 range 循环中捕获变量的作用域问题,这是一个向后不兼容的更改,但他们通过实证证明它修复的 bug 比引发的更多(非常合理)。C# 之前也进行了相同的更改,并给出了相同的理由,这为 Go 提供了灵感。)
这个问题在 Lisp 语言中已存在超过 50 年……如果我们能从其他语言的错误中吸取教训就好了。
当我第一次得知 Go 存在这个问题时,我感到非常震惊——毕竟,人们已经为此绊倒过很多次!不过看到他们修复了这个问题,我感到非常欣慰。
我深信,你应该写下“这非常令人困惑”这句话,思考片刻,然后就此打住。它确实非常令人困惑。仅此而已。其他都不重要。我明白为什么会是这样。我不是傻子。正如你所说:这真的很让人困惑,而这在选择公司其他人(实习生、初级员工)需要使用的编程语言时确实相关。
> “关键点在于,我们的程序员是谷歌员工,他们不是研究人员。他们通常比较年轻,刚从学校毕业,可能学过Java,可能学过C或C++,可能学过Python。他们无法理解一种精妙的语言,但我们希望利用他们来构建优质软件。因此,我们给他们的语言必须易于理解且易于掌握。”
这是一个你一不小心就会绊倒的尖锐边缘,但一旦你思考它,它就说得通了,你不需要博士学位来理解它!
我认为可能会有一些更优雅的语法或命名约定来减少混淆。
Architect-level在抱怨语言的怪癖?这在我对语言的优先级中排名很低。我更担心的是语言的成熟度、工具支持、库支持、学习难度以及开发人员可用性。
我记得我们的最终决定是扩大TypeScript的使用范围;它在您列出的所有方面都优于Go语言。更成熟、工具支持更好、库资源更丰富、更容易招聘开发者等。
不过,回想起来,有人应该提到TypeScript至少有三种不同的方式来表示空值(undefined、null、NaN,还有其他几种)。在TS中至少好一点,因为与Go不同,类型检查器不会主动欺骗你关于undefined可能存在的不同状态。
> 我觉得他们有点小题大做,但令我惊讶的是,到了2025年,团队仍然没有修复这个问题。
你说得对,这是一个小众问题,因此基本上无关紧要。他们完全可以因为Python的“显著空格”而拒绝它。
精彩的文章!
我喜欢Go和Rust,但有时觉得它们缺乏其他语言拥有的工具,仅仅是因为它们想与众不同,而没有实际好处。
每次阅读Go代码时,我都会看到比平时更多的错误处理代码,因为该语言没有异常处理机制……
而且有时Go/Rust代码更复杂,因为它们也缺乏一些面向对象编程工具,且没有工具可以替代它们。
因此,Go/Rust 的冗余代码比我预期中现代语言要多得多。
例如,在 Delphi 中,接口可以通过属性实现:
type
TMyClass = class(TInterfacedObject, IMyInterface)
private
FMyInterfaceImpl: TMyInterfaceImplementation; // 包含实际实现的字段
public
constructor Create;
destructor Destroy; override;
property MyInterface: IMyInterface read FMyInterfaceImpl implements IMyInterface;
end;
在 Go/Rust 中无法实现这一点。而我阅读的 Go 文档强烈建议使用组合模式,但缺乏相应的工具支持。
这种“新方法就是最佳方法,过去的好东西可以忽略”的观点很常见。
当 MySQL 没有事务时,文档中提到“以原子方式执行操作”,但并未明确说明具体实现方式。
MongoDB 直到 4.0 版本才支持事务。他们认为这并不重要。
当 Go 没有泛型时,出现了一系列“模式”来替代泛型……但实际上并未真正替代。
Go/Rust缺乏继承机制让我有同样的感受。新的模式无法替代继承或其他工具。
“我们不提供这个语言工具,因为人们在旧语言中使用它的方式有问题。”别担心,人们也会错误地使用新工具!
这些并不是理想的工具,我认为Go的解决方案有些不够直观,但它们解决了我在其他语言中会通过继承来解决的问题。我很少使用它们。
谢谢。我之前在为一个项目寻找这个解决方案,但在Go或Rust中都找不到。在发帖前,我咨询了ChatGPT,它说这不可能……
我喜欢Go,但让我最头疼的是决定何时使用指针、何时不使用指针作为变量/接收者/参数。如果是接口变量,它会指向接口'struct'中的具体实例。有些东西默认以指针形式传递,比如上下文。
这感觉很粗糙,我担心自己会犯错。
这让我也很困惑。因为有时复制数据比使用指针更高效,但没有明确的界限来判断何时适用。我得到的建议是“对代码进行性能分析,并基于数据做出决策”。这让我很不满意。
现在我总是为了可读性而一致地使用指针。
我主要将其作为某种可变性的信号。
当我需要一个具有稳定身份的值时,我会使用指针。
将指针用作可选类型是使用Go语言最糟糕的部分。
…你想要副本还是原始对象?
没错,就是这样。如果你要修改接收者的字段,或者想按引用传递字段,你就需要指针。否则,值类型就够了,除非…那个奇怪的接口要求你这样做。我猜这就是问题所在?
到处都用指针?谁在乎。
但不要使用指向接口的指针。
需要考虑是否在处理接口类型还是具体类型,这很烦人。
如果到处都用指针,为什么不直接设为默认?
我总是用指针来处理结构体。
我80%的时间都用结构体。常见误解:指针与值接收器在性能上没有区别(Go编译器为两者生成相同代码,结构体接收器不会被复制)。大多数结构体本来就很小,复制是安全的。Go还会自动在值接收器和指针接收器之间进行转换。如果我看到指针,我会认为这是可以被修改的(或非常大的)。事实上,如果我看到指针,我会想“这里要修改了”。我用Go编写了40万行代码,很少遇到这个问题。
我基本上同意帖子中的所有内容。我曾被“两种空值”问题困扰过一两次。不过,我工作过的最愉快且最高效的代码库都是用 Go 编写的。
一些经验。不要将切片的部分内容传递给会修改它们的函数。匿名函数需要 recover 语句。了解所有 goroutine 的返回方式。
一如既往,让我们回顾一下Pascal在1976年就能实现的功能,
type
StatusCodes = (Success, Ongoing, Done)
Go在2025年,
type StatusCodes int
const (
Success StatusCodes = iota
Ongoing
Done
)
在Pascal版本中,注释应该放在哪里?
随你喜欢。
Pascal现在在哪里?
哎呀!!Pascal的不受欢迎绝不是因为它支持如此优雅的枚举类型(或集合)。我认为他只是指出,这类优雅的特性早已存在(且为人所知),而新语言未能借鉴这一特性实属奇怪。
位于Go之下,Perl居中。所有语言均高于Fortran,低于Visual Basic。
https://www.tiobe.com/tiobe-index/
被这些团队使用,https://www.embarcadero.com/
如果你愿意,我可以提供相同的示例代码,支持C、C++、D、Java、C#、Scala、Kotlin、Swift、Rust、Nim、Zig、Odin等语言。
请务必提供。这将非常有趣。
Pascal演变为Modula-2,随后Wirth将其简化为Oberon。他的学生Griesemer在博士论文中探讨了将Oberon扩展用于超级计算机并行编程的可能性。与此同时,Pike将Modula-2作为灵感来源,创作了80年代和90年代的一些语言。他与Griesemer和Ken Thompson合作,将其中一种语言Newsqueak重新设计为Golang。这就是Pascal今天的位置。
它依然活跃且充满活力,对吧? 🙂 https://www.freepascal.org 他们甚至有一个可以编译为 WASM 目标的游戏引擎:https://castle-engine.io/web
如果Pascal没有强制要求全面的模式匹配,那么在这一点上它并不比Go或C#更好。
Go才是被讨论为忽视历史的那个。
C# 幸好是由一位重视类型系统的人设计的,也许你应该重新考虑一下它。
仅仅添加 sum 类型或 exhaustively pattern matching 是不够的……现在 F# —— 这是由一位重视类型系统的人设计的。
Pascal 的实现方式是否与 Go 类似?
func f(x StatusCode) {
}
f(728347) // 没有这样的状态码。不管怎样,没有编译错误。
你有多常将字面量传递给函数?
f(728347) // 没有这样的状态码。不管怎样,没有编译错误。
a := 728347
f(a) // 编译错误。
[删除]
人们想要总和类型,因为总和类型可以解决大量设计问题,而这一概念早在20世纪80年代的SML中就已出现。我见过对Go设计最精辟的批评之一是,Go语言团队忽视了30多年的编程语言设计,因为该语言似乎引入了早在其开发工作开始前就已解决的设计问题和陷阱。
Rust在1976年并不存在。
ML在1973年就存在了,并且拥有……合类型!
是的,但这并不改变Go搞砸了的事实。
合类型与上述简单的例子不同。合类型实际上是有用的,至少有一点。
没有人要求使用 sum 类型,Pascal 的做法已经是一个巨大的改进。
但我想 Go 开发者喜欢编写他们钟爱的 boilerplate 代码,这让他们感到温暖。
这根本不会产生任何影响,除了那些喜欢挑剔的人。
具体来说,我希望 Go 支持 sum 类型。我也希望 C# 和其他可能需要使用的语言支持 sum 类型。
每种语言都有其问题;尽管如此,我认为 Go 还是相当不错的。我不是说文章中提到的观点无效,你确实需要小心,我对“nil 接口不一定是 nil”的问题也深恶痛绝。
很难找到一种能满足所有人需求的语言。我认为 Go 更适合小型、专注的应用程序/工具……当然也能理解它在“企业级”代码库中可能引发的问题。
流行语言总会招致一些批评。这类讨论也有助于语言的演进。
但每个人心里都清楚,Go 语言的一些小缺陷绝对无法抵消其简单性和便利性。我当然希望它有代数数据类型,但这并不是决定性因素。它正是因其受欢迎而存在的完美例子。
它无疑是生产力最高的语言之一。无需繁琐,直接高效完成任务。
每种语言都有其缺陷。我尊重Go保持相对简单的特质。它在并发方面也表现不错(对我来说足够了)。
如今,似乎各种语言都在追逐范式并过度适应不断变化的目标。
看看Rust和Swift变成了什么样子。C#不知怎么的保持了相对的理性,但这并不意味着它独立于潮流。
我既同意这些观点,也认为这完全无关紧要。Go 是需要快速交付且性能稳定的最佳语言。此外,Go 与 AI 的结合效果惊人。因此,在某些方面,与 Node 和 Python 等语言相比,Go 实际上能更快地推进项目。
2015年我曾撰写一篇文章《如何抱怨Go语言》,旨在讽刺那些完全忽视大局和“不完美”语言实际影响的文章。很高兴它至今仍具相关性 🙂
这一直是我对 Go 的看法。一种不完美的语言,为不完美的开发者而生,被组织(而非个人)选择,以确保从初级到高级工程师的基准实用性。我喜欢它吗?不。我会主动选择它吗?不。但当当时的选择是 JavaScript 或无类型 Python 时,它可能看起来更具吸引力。当时Python正经历一个糟糕的2到3版本升级,与Go语言的自动格式化和升级机制相比,显得相当愚蠢。
> 不完美的语言,为不完美的开发者
万物皆有裂缝,那是光照进来的地方。
选定的示例:
bar, err := foo()
if err != nil {
return err
}
if err = foo2(); err != nil {
return err
}
这听起来更像是吹毛求疵。
如果你真的关心作用域,同时希望以后能使用bar,代码应该这样写:
bar, err := foo()
if err != nil {
return err
}
err = foo2() // 直接重用 `err`
if err != nil {
return err
}
这实际上是覆盖了 err,而不是“遮蔽”它。
这里令人困惑的是,`if err != nil` 和 `if err = call(); err != nil` 之间的区别不仅仅是风格问题,后者还会引入一个作用域,捕获在 `;` 之前创建的任何变量。
如果你真的真的想使用相同的 `if` 风格,可以尝试:
if bar, err := foo(); err != nil {
return err
} else if bar2, err := foo2(); err != nil {
return err
} else {
[在此使用 `bar` 和 `bar2`]
return ...
}
这就是为什么有 Goo 语言:Go 加上语法糖和内置功能
https://github.com/pannous/goo/
• 错误由真值条件或 try 语法处理 • 所有 0 和 nil 均为假值 • #if PORTABLE put(“;}”) #end • 修改方法如 “hi”.reverse!() • 垃圾回收可暂停/禁用 • 许多其他提升使用体验的 QoL 增强
Go是否已成为新的PHP?时不时会看到有人抱怨Go的不足。
不,这种情况自Go出现以来就存在,通常是C或C++开发者有特定需求,这很正常,Go并非适合所有人。
我认为对于C或C++开发者来说,那些住在玻璃房子里的人不应该扔石头。
我会从更现代的语言的角度批评Go,这些语言拥有强大的类型系统,如ML家族、Erlang/Elixir,甚至新兴的Gleam。这些语言成功地提供了强大的原始类型和模型,用于创建良好的、封装的抽象。ML语言可以帮助开发者完全避免某些错误,并准确理解代码更改对其他部分的影响——而像Erlang这样的语言则提供了处理运行时错误的有趣模式,无需像Go那样大量冗余代码。
这是一种以“简单性”为名束缚开发者的语言。当然,像Python这样的语言给予了过多的自由,而像Rust这样的语言则过于复杂(我个人认为),但Go充其量只是与这些语言平起平坐。如果人们从中获得乐趣或收益,那也无妨,但我们不能假装它真的是一个伟大的工具。
我对 Go 最大的不满始终是包管理。Rust 做得非常出色,而 NuGet(C#/.NET)也做得恰到好处,以至于微软将其作为 Visual Studio 的内置功能添加进来。它最初是一个插件,与微软毫无关系,现在微软完全拥有它,这没问题,而且它就是好用。
Cargo 非常出色,你可以用它做很多了不起的事情,我希望 Go 能在这方面投入更多。
提到 Python 也挺有趣的,很多 Go 开发者都是前 Python 开发者,尤其是在早期。
你觉得包管理/模块系统哪部分不够完善?
我也很好奇,因为我认为它总体上很棒。
> 我会从更现代的语言的角度批评 Go,这些语言拥有强大的类型系统,如 ML
Go 发布日期:2012
ML:1997
你忘了:CLU 1977。
“. 它们可能是任何设计中参数多态性最困难的两个部分。回想起来,我们过多地受到了没有概念的 C++ 和 Java 泛型的影响。我们本应在早期花更多时间研究 CLU 和 C++ 概念。”
https://go.googlesource.com/proposal/+/master/design/go2draf…
尽管如此,Go 仍缺乏 ML 在 70 年代就已具备的许多现代编程范式和语言特性。但这些特性存在一个致命缺陷:它们并非“我们自己发明”的。
Go被宣布为C和C++的替代品,因此将其与这些语言进行比较是合理的。
它原本是为Google的网络服务用例设计的C和C++替代品,顺便提一下。
并非如此,除了原始作者外,没有人这样认为。作者对C++的编译时间有意见,并得到了经理的赞助,从事这个Go的副项目。
谷歌的网络服务仍然用Java/Kotlin、C++编写,如今也用Rust。
这种宣传方式已经超过十年没有被推广了。
相反,PHP至少随着时间的推移而改进,并采用现代语言设计实践。
> Go是否已成为新的PHP?时不时地,我看到一篇文章抱怨Go的不足。
此类文章在Go于2013年发布1.0版本之前就已司空见惯。事实上,其中大部分(如果不是全部)抱怨内容在当时就能原封不动地写出来。这篇帖子中唯一缺失的、能让我相信它确实写于2013年的内容,就是对Go缺乏泛型功能的抱怨,而泛型功能是在几年前才添加的。
自Go语言还是谷歌内部一个鲜为人知的边缘项目时,人们就在Reddit上抱怨它。当时就连谷歌自己也不太在意这个项目,更没有投入任何资源去开发它。然而,人们仍然在使用它,并发现它很有用。
Go语言一直处于80%完成的状态,但最后那20%(最难的部分)从未被实现。
这令人沮丧,因为它本可以变得很好,但它永远无法达到那个境界——现在是因为向后兼容性。
罗布·派克(Rob Pike)关于Go语言起源的评论一针见血。
最后的20%也是故意从未完成的。这是他们喜欢管理语言的方式。我感到沮丧,但似乎对某些人有效。
Go是一个很好的例子,说明平庸的技术如果没有FAANG启动项目时的那种粉饰效应,是无法凭借自身优势取得成功的。
我完全不认同这种观点。我选择Go是因为它具有快速编译速度、生成静态二进制文件、无需大量依赖即可构建有用程序、相对易于维护,并且内置了良好的工具链。我认为这就是它相较于Dart或其他我忘记的由企业支持的语言获得采用的原因。
程序员编写的代码中80%是API胶水代码。
Go在API胶水代码方面表现出色。将JSON作为字符串获取,将其转换为结构体,应用业务逻辑,然后将JSON发送到另一个API。
所有这些功能都内置于标准库中,并且默认性能足够高,以至于在你API胶水SaaS开始真正赚钱之前,你根本不需要担心性能问题。
我曾因这些特性尝试过一个项目,但很快放弃转投Rust。类型安全不足,冗余代码过多。太多该死的“if err != nil”。
该语言处于Rust和Python之间的尴尬位置,而其中任一语言几乎总是更优选择。
但,谷歌的粉色眼镜……
> 类型安全不足
确定吗?这取决于具体用例。
> 冗余代码过多
这不会对任何事情产生实质性影响。
> 过多的“if err != nil”语句。
这是表面上的问题。
> 该语言处于Rust和Python之间的尴尬位置,而其中一种语言几乎总是更好的选择。
Rust没有垃圾回收机制,因此它只能局限于系统编程领域。如果你想要垃圾回收的便利性,Rust就不在考虑范围之内。
Python?不错,但速度慢,包管理一团糟,动态类型(你不是提到类型安全吗?),异步而非绿线程,等等,等等。
我几乎同意你的观点。如果有一种语言拥有快速编译器、优秀的工具链、强大的标准库、静态二进制文件以及类似 F# 的类型系统,我将永远不会使用其他语言。
Rust 对我来说不够好。我希望 Roc 可能成为这样的语言,但我不抱太大希望。
OCaml?可能还有Haskell?
编译器可以更快,但我猜除此之外,Rust已经具备了所有这些特性。
我认为Rust的标准库与Go相比有所欠缺,因此平均每个Rust项目都有大量依赖项。对我来说,Rust就像系统编程领域的Node + NPM。此外,上次使用时编译速度非常令人痛苦。我习惯了Zig、Hare、Go、Bun的速度。Rust让我想要用叉子戳自己的眼睛。
完全正确。
另一个令人震惊的例子是,人们将逻辑思考推迟给大型公司,比如有人为苹果焊接内存和 SSD 的行为辩护,尤其是在这个网站上,直到某个中国小伙子证明,苹果这样做的所有所谓理由都是事后诸葛亮的胡扯。
Go 也是如此,但如果你花足够多的时间,你会发现一些核心粉丝,甚至来自 Go 的核心团队,开始感到失望,并开始提出问题,但他们总是以“我知道这是 Go,神圣的理由存在,我质疑是在犯罪,但为什么 X 或 Y”开头。这真是喜剧。
这意味着很多人都在使用它。仅此而已。
Go对我来说是最好的语言,因为我用它开发速度快,没有那么多 bug,编译速度快,而且我通常对垃圾回收器没什么意见。依赖管理也很好。
Go对我来说是一个超级高效的工具。
Go 几乎让我得了腕管综合征,因为它带来了大量几乎相同但又不完全相同的重复代码模式。我再也不会使用它了。
你仍然手动输入大部分代码吗?
AI 解决了我的腕管综合征问题。
当我心情好时,甚至不用打字,只需用语音命令 AI 处理错误情况。
我还没见过 AI 产出过不是垃圾的东西。根据上下文,我完全可以使用 Rust 或 C 而不必担心腕管综合征。
我可以以技术问题为由回复。但更具建设性的建议是利用AI进行自动补全,即在同一行中自动补全你本需要手动输入的内容。这并非为了生成大量代码。
交叉编译Go语言非常简单。静态二进制文件可在任何环境下运行。加密库是Let's Encrypt等证书颁发机构(CA)的基础,表现卓越。
绿色线程非常有趣,因为你可以以极低成本创建数千个线程,这使得不同设计方案成为可能。
我认为对defer的抱怨有些微不足道。对我来说,真正的主要问题是导入机制的运作方式。它知道GitHub,而替换依赖项(包括本地依赖项)非常困难。文件的强制布局、命令目录等等。
我可以接受这一切,但模块是我浪费最多时间和最头疼的部分。
> 它知道 GitHub 的事实,以及在那里替换依赖项(包括本地依赖项)的困难。
在 `go.mod` 中使用 `replace`,或者如果你在本地进行开发,可以使用 `go.work`?
或者直接提交代码,如果你是天才,想让试图理解你代码库的潜在攻击者摸不着头脑 https://github.com/pulumi/pulumi/blob/v3.191.0/pkg/go.mod#L5 或 https://github.com/opentofu/terraform-provider-aws/blob/main…
> 文件、命令行目录等的强制布局。
你不需要有命令行目录。我在 Go 项目中经常看到它,但我不确定为什么。
哦不,Rust 太难了,Go 也不好,我该回到 Java 吗?
也许是正在开发的 Carbon 语言?听起来很有前途,但离 1.0 版本还很远。
Carbon 仅用于与 C++ 互操作和从 C++ 迁移。在 Carbon 中创建新的代码库没有意义,而且项目的读我文件明确告诉你不要这样做。
> … 而且项目的读我文件明确告诉你不要这样做。
你能引用你所指的段落吗?
据我所知,与C++代码的互操作性只是他们明确的目标之一;他们只是将这一点放在“语言目标”部分的最后一项。
> 现有的现代语言已经提供了卓越的开发体验:Go、Swift、Kotlin、Rust 等。能够使用这些现有语言的开发者应该继续使用它们。
中间有太多选择。
如果在检查 nil 时不使用 == 运算符,接口 nil 的示例是否会更清晰?例如,使用一个假设的 is 运算符:
package main
import “fmt”
type I interface{}
type S struct{}
func main() {
var i I
var s *S
fmt.Println(s, i) // nil nil
fmt.Println(s is nil, i is nil, s == i) // t,t,f: 这样还不够清晰吗?
i = s
fmt.Println(s, i) // nil nil
fmt.Println(s is nil, i is nil, s == i) // t,f,t: 仍然不混淆吗?
}
当然,这意味着你必须精确定义 is 和 == 的语义:
– is 对于接口会检查值和接口类型。
– 接口的 `==` 仅使用值而不使用接口类型。
– 对于结构体/值,`is` 和 `==` 是显而易见的,因为只有值需要检查。
如果这是最糟糕的情况,那还不错。
同意,我们大多数人不需要 C++/C 语言的特殊功能,Go 提供的功能对我们来说已经足够。
奥丁编程语言对所有问题都有有意义的回应。该语言的设计无可挑剔。
它非常出色。
Go确实存在一些问题。但本文中提到的这些问题恰恰证明了作者是Go语言的新手。
我不会炫耀资历,但或许新手就是你?
我审查过大量Go代码。我经常看到其他人犯这些错误。
在该帖子的评论中,作者声称自己拥有12至15年的Go语言经验[0]。
[0] https://news.ycombinator.com/item?id=44985378
但仍然是Go新手?
文章的观点显得过于简单/肤浅,缺乏一位经验丰富的Go程序员应有的深度。
这篇帖子中有几个小错误,但主要是有人对Golang中的一些小问题过于兴奋,而这些问题他们确实正确地指出了。
作者在此。我可能无法否认第二部分,但我很乐意听到你认为事实错误或我可能表述不清的地方。总是乐于被纠正。
(这不是一个小错误,而是别人指出的问题,即Python并不是严格的引用计数。是的,这就是为什么我强调了“几乎”和“基本上”。对于这种批评,我无能为力)
哦,我的意思是,你对处理非 UTF-8 文件名的方式有误(参见 https://news.ycombinator.com/item?id=44986040),而且在 90% 的情况下,延迟解锁互斥量会让情况更糟而不是更好。
需要互斥锁的通用原因是,你正在将某个数据结构从一个有效状态转换为另一个有效状态,但在转换过程中,数据结构会处于不一致状态。如果其他代码看到这个不一致状态,可能会导致程序崩溃或其他异常行为。因此,你需要在转换前先获取互斥锁,并在数据结构进入新有效状态后释放互斥锁。
但如果在修改过程中发生异常,数据结构可能仍处于不一致状态!但此时互斥锁已释放!因此,使用该不一致数据的其他线程将出现异常行为,此时你将面临一个非常棘手的 bug 需要修复。
这并不总是适用。也许互斥锁保护的是更系统性的一致性条件,比如“这个变量中的数字是我们收到的消息数量”,即使有些计数丢失,也不会导致崩溃或异常行为。也许它只是提供了一个内存屏障,防止读取撕裂。也许互斥锁只是保护一个比较并交换操作,写成比较 followed by 交换。但在这种情况下,我质疑在持有互斥锁时是否真的可以引发 panic!
这就是为什么 Java 废弃了 Thread.stop 方法。(但 Java 在异常处理过程中展开时会隐式解锁互斥锁,这确实会导致 bug。)
这与你讨论的 Go 语言是否优秀的问题仅有模糊的相关性。显式错误处理或许能提高你发现在持有互斥锁时可能出现的错误并正确处理它的几率,但你正确指出,由于 Go 确实支持异常,你仍然需要担心这个问题。而这类情况往往极其难以测试——也许测试根本无法让被调用的代码触发 panic。
总类型是目前最缺失的一项功能,我认为Go语言在其他方面做得非常出色
我认为很多人选择Go语言是因为Google的背书,而非其本身优秀。例如中国科技圈曾出现过大规模采用Go语言的热潮。我个人认为Rust/Go/Zig等现代语言在试图避免成为C/C++/Java的过程中,多少有些用力过猛。
Go 语言从一开始就带来了一股清新的空气,并且非常实用。它感觉像是一种整洁的小语言,终于拥有了一个现代的标准库。十五年前,这是一种令人欢迎的变化。我认为 Go 和 Node.js 几乎在同一时间起步并迅速发展并不令人意外。人们正在寻找一种现代、轻量级且简单的语言,而这两个项目都提供了这一点。
这篇文章只是为了吸引眼球而发泄不满。列出的问题都是表面现象,除非有人对该领域有深入了解。没有好的数据点来衡量这些问题与现实世界问题之间的权衡,即成本是多少。即使关于内存的问题,没有数据支持也显得薄弱。
是的,这种语言并不感觉是下一代的
我能理解人们选择它的原因,但它在便利性上的提升远大于编程语言本身进化的提升
> 它在便利性上的提升远大于编程语言本身进化的提升
你在这里做出的区分在我看来并不存在。便利性正是设计的核心。
我在$dayjob中用Go写过不少代码,我得说它就是……无聊。我知道这听起来像是在抱怨奇怪的事情,但我就是对用Go写的任何东西提不起兴趣。它就是……一般般。不确定为什么,可能它就是不像过去其他语言那样让我感到兴奋。
不过,它确实是一门适合团队协作的语言。
不,它就是被设计成无聊的。这显然是个缺点,但当你从事具有挑战性的项目时,这种设计反而能发挥作用。语言本身不干扰开发流程,在这种情况下反而很有用。
Go 语言的无聊正是我使用它的原因。
“你的空值是什么颜色?”——价值两亿美元的错误。
这简直是夸张。
令人着迷。作为一名C++开发者,我无法想象没有RAII会是怎样的。那似乎既冗长又痛苦。而那个空指针比较……真让人不适。
我不明白如何将接口赋值为结构体的指针。这该如何实现?这似乎会导致编译错误。我对Go接口了解不多。
Go语言的支持者常说的一件令人烦躁的事是,Go语言很简单。事实并非如此。即便它真的简单,用简单语言编写的代码也并不一定简单。以Kubernetes控制平面为例,其中包含一些最复杂、最臃肿的代码,而这些代码全部是用Go语言编写的。
这篇文章中有一些观点让我感觉自己像《破坏之王》中的罗伯·施奈德,说“他不知道那三颗贝壳!”但其中也有一些观点是合理的。
空值问题。当一个接口被赋予一个结构体时,即使该结构体为空,接口也不会为空——这很可能是设计上的疏漏。这是一个合理的观点。
在函数中使用 append。切片按引用传递确实是最大的问题之一。他们这样做是为了节省内存和提升速度,但 append 问题会变得难以解决,除非进行抽象处理。这是一个有效的观点。
整个函数作用域内的 err。你定义了它,当然它就在那里。与不断实例化另一个变量相比,复用一个通用变量更好。缺乏 try-catch 迫使你思考。这不是一个有效的观点。
defer。scope 块和函数块有什么区别?我等着。
我更感兴趣的是对你认为无效的论点进行反驳,而不是一个冗长的 +1,它什么也没添加。
对于我来说,Go 将复杂性从语言中移出并放入你的代码中,让你不得不面对认知负荷。如果能保持简单,这没问题,但一旦超过一定复杂度,对我来说就是硬性拒绝。
> 如果你将随机二进制数据填入字符串,Go会照常运行,正如这篇帖子所描述的。
> 几十年来,我因工具跳过非UTF-8文件名而丢失数据。我不能因为在UTF-8出现前就存在的文件名而被责怪。
嗯……为什么怪Go呢?
作者在此。
我的意思是,如果忽略无效的 UTF-8(可能是有效的 iso8859-1)而没有错误处理,或者反之,这在过去曾导致我丢失数据。
与Rust相比,Rust中的路径名与普通字符串是不同类型。如果你需要将路径名当作字符串处理且不介意它“有点错误”(因为只是用于显示给用户),那么你可以调用.to_string_lossy()。但当精确名称匹配确实重要时,更难不小心忽略这种情况。
当精确性重要时,.to_str() 返回 Option<&str>,因此调用者被迫处理文件名可能不是 UTF-8 的情况。
对文件名编码的粗心大意是数据丢失的原因。Go 对所有类型的字符串都比较粗心,包括文件名。
感谢您的回复。我理解在类型系统中显式编码字符集可以帮助发现 bug。
但强制所有字符串为 UTF-8 并不能神奇地解决您描述的问题。实际上我经常看到相反的情况:现在您必须编写两条代码路径,一条用于 UTF-8,另一条用于其他编码。而第二条路径在实际中会被忽略,因为编写起来很麻烦。例如,我构建了您在另一份提交中的 Web 服务器项目(非常酷!),并给它一个名称为非 UTF-8 的 tar 文件。没有特殊的处理发生,我只是得到“错误:在一個或多个参数中检测到无效的 UTF-8”并应用程序退出。它完全拒绝处理非 UTF-8 文件——这是否更不粗糙?
强制使用UTF-8并不能“修复”奇怪的边界情况下的兼容性问题,它只是破坏了所有兼容性。最佳做法是将数据视为不透明的字节,除非有充分理由不这样做。这就是Go所做的,因此我认为不应因这个特定原因责怪Go,而应责怪备份应用程序。
> 它完全拒绝处理非UTF-8文件——这样做是否更严谨?
你可以辩论这是否严谨,但我认为错误提示比静默破坏数据要好得多。
> 最佳做法是将数据视为不透明的字节,除非有充分理由不这样做
当处理字符串时,这种做法似乎并不合适,因为字符串不仅仅是字节的集合。它们有编码,通常你希望能够将字符串转换为大写或小写等操作。
我无法确定这里的最佳方式。但Rust在这方面的处理比我见过的任何语言都更好。
我认为无需设置两条代码路径。也许你的程序可以全程保持原始形式,无需转换。例如,从磁盘读取数据时,仅提取文件名并传递给归档库。
没有必要将它转换为“字符串”。是的,它本可以是字节数组,但提取文件名(或可能的最终目录加上文件名)是字符串操作,只是不一定是UTF-8字符串。
而且如我所说,对于所有只需向用户显示的用例,“有损”版本就足够了。
> 我只是得到“错误:在一個或多個參數中檢測到無效的UTF-8”的提示,然後應用程式就退出。它完全拒絕處理非UTF-8格式的檔案——這樣是不是更嚴謹?
哈哈,说得对。但确实更严谨。您是否希望文件被静默跳过?您已创建归档文件,启动了Web服务器,但就是无法让其显示您想要的页面。
要让tarweb支持文件名中的非UTF-8编码,程序员必须认真考虑这意味着什么。我认为这不意味着进行有损转换,因为文件名本身并非如此,且这不仅仅是为了人类显示。而且它可能也不应该是字节,因为工具很可能希望发送 UTF-8 编码。
或者它们不这样做。无论哪种情况,除非这经过设计、实现和测试,否则文件名中包含非 UTF-8 字符应被视为无效输入。对于在整个进程生命周期中使用 tarfile 的情况,这可能意味着拒绝该文件,并要求用户回滚到之前的可用版本或采取其他措施。
> 强制使用 UTF-8 并不能“修复”奇怪边界情况下的兼容性
没错。但总比静默损坏好。
与Rust内核工作相比,他们似乎不得不实现一个新的Vec等价物,因为在用户空间和内核空间中处理分配失败是不同的[1], 而Vec push不能失败。
同样,Go字符串操作不能失败。而内存分配问题有其自身的原因,字符串操作则没有。
[1] 一个独立的大话题。几乎没有人会关闭overcommit。
错误比静默损坏好,当然。
但当你将数据作为不透明字节传递时,不会有静默损坏,你只是在显示时看到一些占位符符号。这就是我在终端中看到文件的方式,我可以正常删除它。
而且,终端中的问号比应用程序完全无法工作要好得多。
非UTF-8字符被跳过的情况通常是那些不使用字节作为默认字符串类型的语言编写的应用程序的特征,而不是相反。这在Python2/3库中多次困扰过我。
无需将它转换为“字符串”。
直到你遇到一个需要文件名以字符串形式出现的 crate.
而它非常适合大多数商业软件,因为大多数企业并不专注于构建优质软件。
Go 拥有足够好的标准库,且 Go 支持“一堆 if 语句”架构。这就是你所需的一切。
大多数企业环境并未得到足够的重视,无法超越“一堆 if 语句”架构。当然,也许在代码刚开始时它有一个不错的架构,但很快最初的开发人员离开了,然后下一波开发人员来了,他们有不同的想法,梦想着一个“重写”,他们偷偷地开始了但从未完成,然后他们离开了,第三波开发人员来了,到那时代码已经一团糟,所以现在他们只是在if语句堆上添加if语句,直到Jira工单关闭, 公司就这样勉强维持着这套糟糕的软件,如果公司泄露了1亿人的个人数据,他们也不必承担财务责任。
Go语言拥有专为企业场景设计的极度强大的代码检查工具,以及gofmt格式化工具。
每段代码都遵循统一规范,可被自动、中立地分析以发现问题。
我讨厌 Go,但我还没找到其他更不讨厌的语言。
好吧,它不适合你。
那就不使用它吧,忽略所有关于“语言 X 不好”的帖子,无论你最终选择使用哪种语言?
还有很多其他语言。我不明白这种爱恨交织的言论,好像Go语言本身欠你一个道歉。
有人想尝试解释他第一个示例的意思吗?
bar, err := foo()
if err != nil {
return err
}
if err := foo2(); err != nil {
return err
}
上述代码(声明了一个仅在第二个if语句作用域内有效的err值)应该能编译通过吧?他到底在抱怨什么?
EDIT:我明白了;似乎无法让bar是函数作用域,而err是if作用域。
我的意思是,我同意他的观点关于接口。但“append”这件事对我来说只是在发牢骚。在他的例子中,`a`是一个局部变量;为什么赋值给局部变量会被期望改变调用者的值?你期望以下代码能工作吗?
int func(a *MyStruct) {
a = &MyStruct{...}
}
如果不行,为什么你期望 `a = apppend(a, …)` 能正常工作?
> 为什么赋值给局部变量会被期望改变调用者的值?
我觉得你可能需要重新阅读。我的意思是,它确实会改变调用者的值。(当然,有时会)这就是问题所在。
哦,我明白了。我的意思是,是的,切片和数组之间的关系有些微妙;但它也为你提供了某些功能。我是在使用C语言数十年后才接触Go语言的,所以对这个概念并不感到困难。
我只能认为这是个人喜好问题。
EDIT:有一点我认为不是个人喜好问题,那就是缺乏类似“const *”的机制。切片的问题在于,你有时可以改变某些东西,但实际上并不能真正改变。如果能强制要求传递切片指针(以便实际分配新的底层数组并指向它),或者传递不可修改的切片(以便确保函数不会在背后修改切片),那会更好。
这可能就是问题所在,但我对这一点以及 append 投诉也感到好奇。似乎作者对作用域规则有异议,但这些规则与许多其他语言并无本质区别。
如果有人真的不喜欢复用err,完全可以创建独立变量,例如err_foo和err_foo2。没有理由不能复用err。
编辑:关于 err 的主要抱怨是它留在作用域中,但我认为作者不喜欢这一点
你没有正确复制第一个示例中的代码。
不,第二个“if”语句是红鲱鱼。以下两种方式均可工作:
bar, err := foo()
if err != nil {
return err
}
if err = foo2(); err != nil {
return err
}
以及
bar, err := foo()
if err != nil {
return err
}
if err := foo2(); err != nil {
return err
}
他甚至明确指出:
> 即使我们将它改为 :=,我们仍然会想知道为什么 err 在函数的其余部分中仍然有效。为什么?它以后会被读取吗?
我的初步反应是:"第一个 `err` 是函数作用域的,因为程序员将其设为函数作用域;他显然知道你可以将它们设为 if 语句的局部变量,那么他到底在说什么?`
直到我尝试重写代码,将第一个 `err` 设为 if 块作用域时,我才意识到他可能遇到的问题:好吧,如何同时将两个 `err` 变量设为 if 块作用域,同时让 `bar` 保持函数作用域?你必须这样做:
var bar MyType
if lbar, err := foo(); err != nil {
return err
} else {
bar = lbar
}
这只是为了限制err的作用域而添加的大量冗余代码。
恭喜,你发现了一种语言中的几个痛点。现在作为一个科学实验,将相同的推理应用到其他几种语言中。你发现的问题数量乘以其重要性,是会大于还是小于 Go 的得分?就是这样,这就是整个问题——Go 不好,但总体上没有可行的替代方案。
> 两种类型的 nil
这在JavaScript中是什么。
如果我不太注意,就会被“空值接口”问题困扰,因为Go语言区分了“包含类型”和“接收类型”
package main
import “fmt”
type Foo struct{
Name string
}
func (f *Foo) Kaboom() {
fmt.Printf(“hello from Kaboom, f=%sn”, f.Name)
}
func NewKaboom() interface{ Kaboom() } {
var p *Foo = nil
return p
}
func main() {
obj := NewKaboom ()
fmt.Printf(“obj == nil? %vn”, obj == nil)
// 接下来这一行会引发 panic(因为方法接收了 nil *Foo)
obj.Kaboom()
}
go run fred.go
obj == nil? false
panic: runtime error: invalid memory address or nil pointer dereference
作为一名资深 Go 程序员,我之前并不理解关于两种类型 nil 的评论,因为我从未遇到过这个问题,于是深入研究了一下。
结果发现这只是对 fmt.Println() 语句实际行为的误解。如果使用更高级的打印语句,一切就变得非常清楚了:
package main
import (
“fmt”
“github.com/k0kubun/pp/v3”
)
type I interface{}
type S struct{}
func main() {
var i I
var s *S
pp.Println(s, i) // (*main.S)(nil) nil
fmt.Println(s == nil, i == nil, s == i) // true true false
i = s
pp.Println(s, i) // (*main.S)(nil) (*main.S)(nil)
fmt.Println(s == nil, i == nil, s == i) // true false true
}
本文作者指出了一個便利功能,即 fmt.Println() 會顯示介面中元素的狀態而非介面本身的狀態,卻誤將此視為根本設計問題,並撰寫了一篇關於語言問題的長篇大論,而該問題實際上並不存在。
宽容地说,我猜作者可能是在抱怨将空指针放入空接口会造成混淆。这确实令人困惑,但并不意味着存在“两种类型的空值”。空值 simply 表示空。
作者展示了s==nil和i==nil的结果,这些检查几乎在任何地方都必须进行(所谓的“十亿美元错误”)
这与Printf无关。关键在于这两种不同的空值有时会与空值相等,有时会相互相等,有时则不会
确实,这两者之间存在真实的内部差异,你可以打印出来。但_这就是作者的观点_。
这是一个人为构造的例子,我自己在代码中从未真正遇到过(到目前为止,我已经写了大量代码),我的团队的代码中也没有遇到过。
Go 语言确实存在一些设计缺陷,其中许多已得到修复,但有些无法修复。提醒人们注意这些问题是可以的。但故意制造令人困惑的例子然后抱怨它们,这几乎等同于稻草人谬误。
> 这是一个人为构造的例子,我自己在代码中从未真正遇到过(而到目前为止,我已经写了大量代码),我的团队的代码中也没有遇到过。
它足够令人困惑,以至于它有一个 FAQ 条目,而且人们试图在 Go 2 中更改它。显然,人们遇到了这个问题。(我肯定遇到了)
这就是我对这类批评的看法。
每种语言都有类似的陷阱。在我用PHP编程超过20年的时间里,我曾整理过一份Google文档,记录了所有遇到的愚蠢PHP陷阱。
这些陷阱通常是语言本身的一些愚蠢设计,加上开发者糟糕的设计,或者试图走捷径却偏离了正轨。
我相信你从未遇到过这个问题,它确实不是日常会遇到的问题。但它确实存在,偶尔会让人们中招。
这就像一个已知的锋利边缘,人们偶尔会因此受伤。没有语言是完美的,但当人们遇到这些问题时,他们有理由抱怨。
作者在此。不,我没有误解。接口变量有两种类型的 nil。未类型化的 nil 可以与 nil 比较,而类型化的 nil 则不能。
你通过打印类型试图澄清什么?我清楚类型是什么,这就是我能给出这个简洁奇怪的示例的原因。我清楚比较的结果以及原因。
而“为什么”是因为“存在两种类型的 nil,因为这是个糟糕的语言设计选择”。
我在实际代码中见过这种情况。有人将变量与 nil 进行比较,结果发现不是 nil,然后调用方法(接收者),最终因 nil 解引用而崩溃。
编辑:根据这条评论,这种“两种类型的空值”在生产环境中也咬了其他人:https://news.ycombinator.com/item?id=44983576
_> 作者在此。不,我没有误解。接口变量有两种类型的 nil。未类型化的 nil 可以与 nil 比较,而类型化的 nil 则不能。
没有两种类型的 nil。你会把一个空桶和一个空杯子称为“两种类型的空”吗?
只有一种nil,它在不同上下文中表示不同含义。你正在混淆问题,让本应非常简单的事情(接口可以包含其他内容,包括本身为空的内容)显得复杂。
_> 我在实际代码中见过这种情况。有人将变量与nil比较,结果不是nil,然后调用方法(接收者),导致因nil解引用而崩溃。
当然,我在C语言中也见过因相同原因导致的指针到指针解引用失败。这没什么特别不同的。
同意。这很好。
将“good”改为“perfect”
天啊,这种文章真无聊。Go 是一门伟大的语言,这一点从用它编写的大量优秀软件中可见一斑。有没有一些不足之处?当然有。
但这篇文章不过是作者在使用 Go 时遇到的烦人问题的列表。很好,写一篇关于 Go 问题的文章,但别说“因此它是一门糟糕的语言”。
虽然我同意文中提到的许多观点,但这些问题似乎都不值得大惊小怪,坦白说。所以你的口味和原始设计者不同。谁在乎呢?你说哪种语言更好?我也能在那个语言中找到同样多的问题。
另外,defer 很好。
Go 错误处理 bla bla 垃圾帖子
> 等等,为什么 err 在 foo2() 中被重复使用?是不是有什么我没注意到的地方?即使我们把它改成 :=,我们仍然会想知道为什么 err 在函数的剩余部分中仍然有效。为什么?它以后会被读取吗?
第一次赋值为nil,第二次在第二个函数中出错时覆盖。我看不出来作者的问题?这非常明确。
作者在此说明:我不是在讨论变量的值,而是在讨论变量的生命周期。
在检查是否为空后,err 变量本不应继续处于作用域内。这就是为什么建议使用 if err := foo(); err != nil 的原因,因为这样一来,就不会意外地引用 err 变量。
我举的例子是 Go 语言在语法上不允许你限制变量的生命周期。这里指的是变量本身,而非其值。
你描述的是发生的情况。我对发生的情况没有意见,但对语言本身有意见。
为什么变量的生命周期甚至重要?
我在帖子中给出了一个例子,但明确说明:因为一个拼写错误的变量没有被捕获,例如作为一个未使用的变量。
博客文章中的例子会失败,因为return err引用了一个已超出作用域的err。这在语法上会防止意外地写成foo99()而不是err := foo99()。
我得等会儿再读剩下的部分,但这是作者的失误。那段代码没什么不明白的。如果 err 没有被设置,那它就没被设置,而且我们已经不在函数中了。如果没有,为什么浪费一个接口句柄?
这读起来就像你对 JavaScript、C、Python 等语言的通用抱怨。
在合适的地方使用语言。你知道如果遇到语言无法解决的问题,你可以用另一种语言解决那个特定问题,然后调用那段代码吗?
我们曾经有一个 Node.js 服务。我们有一些计算密集型任务,我们将那部分代码迁移到 Go,因为它适合那件事。后来有人将那部分代码移植到 Rust,并成为一个独立项目。
我不知道。这只是代码。没人真的在乎,我们使用这些工具来解决问题。
Go 继承了学术语言理论和设计中许多流行的理念,其典型用例中,这些理念的成果是混合的,但我认为总体上是积极的。
其主要优点是认知负荷低,并鼓励以简单直接的方式做事,后者反过来又强化了前者。
拥有复杂强大类型系统和其他功能的语言在许多方面更优越,但在大多数开发者手中,它们往往成为过度复杂化的借口。初级开发者(不是新手但尚未成为资深)热爱复杂性,会抓住任何机会尽可能添加复杂性,无论是为了展示自己的聪明才智、探索新领域,还是尝试实现他们认为需要但实际上并不需要的功能。Go语言在一定程度上抑制了这种倾向,尽管开发者当然会找到其他方法。
资深开发者深知复杂性是邪恶的,而简洁才是智慧与技能的标志。高级语言特性应用于简化复杂概念的表达,而非增加简单概念的复杂性。并非所有语言特性都应被频繁使用。
哦,是的。换句话说,它抑制了“极客式设计”,而这在实践中是函数式编程和高度表达型类型系统面临的巨大问题。
你最终会创建出这些优雅的抽象,从程序员作为艺术家的角度来看非常具有吸引力,但通常会分散注意力,无法以足够好的方式完成工作。
你可以看出Go的创建者对工程师心理非常熟悉,知道什么会让他们偏离正轨。Go 拿走了所有闪亮的玩具。
Go 是一种你在工作中使用的语言,你必须设置数十个代码检查工具和自动代码质量检查来捕捉所有陷阱,比如这里与 “err” 相关的内容,而没有人会从这些中获得任何乐趣。从Go诞生以来,“所有函数都必须返回并处理错误代码”这一要求就显得荒谬可笑。早在2009年我第一次看到它时就觉得荒谬,而现在我不得不将其用于工作中的K8s相关任务时,它依然和当初一样荒谬。让大语言模型(LLMs)处理Go语言中所有冗余代码和痛苦的部分,同时Go语言完全缺乏优雅性的特点,也与大语言模型(LLMs)对代码优雅性的模糊认知相得益彰。
不,你需要一个无环结构来可能保证这一点,在CPython中。其他Python实现更正常,你不应该依赖析构函数。
我喜欢Python,但__del__的警告和注意事项数量之多,让我怀疑这个人是否读过文档[0]。我最喜欢的WTF:
> 虽然不推荐,但 __del__() 方法可以通过创建对实例的新引用来推迟其实例的销毁。这被称为对象复活。
[0]: https://docs.python.org/3/reference/datamodel.html#object.__…
这与父评论中关于“Python 中循环结构永远不会被释放”(顺便说一句,这是错误的)的声明有何关联?
当我回复时,评论中唯一的内容是“是的,没有”。我同意 __del__ 方法存在诸多风险。
阅读:循环GC,是的,我链接的章节明确讨论了这个问题,以及如何解决它。
这不是我的主张,顺便说一句。
作者在此。
是的,是的。因此使用了“几乎”和“基本上”这些词。正是出于这个原因。
这些反对意见在我看来都不算严重,然后文章结尾写道“我为什么要关心内存使用?内存很便宜。”恕我直言,内存膨胀会影响每一次操作的性能和用户体验。对软件工程的谨慎关注应避免或最小化这些问题,并强调合理使用内存的价值。
有人给这位先生发个同行奖金吧
流行和优秀很少(甚至从未)是同一回事。
Python 糟糕透顶,但它拥有的粉丝比 K-pop 偶像还多。强制空格?没有强类型?全局解释器锁?垃圾错误信息?一个无法搜索的包仓库(部分原因是因为它充满了垃圾包和不良分支),包名随机且没有命名规范?安装或配置应用程序/环境时完全缺乏标准化?运行一个命令并实时读取标准输出和标准错误需要100行代码?
大家使用Python和Go的真正原因是因为谷歌使用了它们。否则Python看起来就像带对象的BASIC,而Go则是一个被过度炒作的小众语言。
我写了一篇关于类型化与非类型化空值问题的简要说明。这是在生产环境中可能真正咬你的东西之一。在代码审查中很容易忽略它。
以下是配套的 playground:https://go.dev/play/p/Kt93xQGAiHK
如果你运行代码,你会发现对 ControlMessage 调用 read() 时会引发 panic,尽管有 nil 检查。然而,对于 Message 则不会发生这种情况。查看 Message 的 read() 实现:我们需要在指针接收结构体方法内部添加空指针检查。这是最简单的解决方案。我们为此提供了一个代码检查工具。生态系统也提供了帮助,例如 protobuf 生成的代码也在指针接收结构体内部添加了空指针检查。
在低级语言中花费了一些时间后,我认为 Go 语言更加合理。你的示例:
第一个案例——你有一个结构体的地址,你传递它,一切正常。
第二个案例:你将结构体的地址设置为“空指针”。什么是空指针?它只是另一个地址,可能是 0x000000 或其他值。从内存角度来看,它确实存在,但操作系统会阻止你访问空指针所指向的任何内容。
因为你没有访问任何内容,所以不会出错。这就像一个装有致命毒药的盒子,你没有打开它。
第三个例子与第二个相同。你有一个 IMessage,但它指向 NULL(而不是 NULL 指向致命毒药)。
而在第四个例子中,你终于打开了盒子。
这是魔法知识吗?我不这么认为,但我也不惊讶于你可以通过切片传递来修改数据。
我认为Go最大的缺点是它自诩为高级语言,而实际上它接触的底层硬件比人们习惯接触的要多。
很好的例子,确实很棘手
这是什么意思?他们只是使用 recover 并保留坏数据吗?
> 标准库就是这么做的。在调用 .String() 时,fmt.Print 会这样做,标准库的 HTTP 服务器在 HTTP 处理程序中的异常处理时也会这样做。
除了这个,大多数情况似乎都不是什么大问题,除了 `append`,这确实是一个糟糕的语法。如果你在原地 append,不要返回值。
标准库从 panic 中恢复,程序继续执行。
这意味着如果你这样做:
func (f Foo) String() string {
some.Lock()
t := get_something()
some.Unlock()
t = transform_it(t)
return t
}
而 get_something() 发生 panic,程序将继续运行,但 mutex 处于锁定状态。当然,比程序死锁更危险的事情还有很多。
使用 defer 是强制要求的,因此必须编写异常安全的代码。即使你从未使用过异常。
所有这些抱怨“Go本可以成为什么”的无聊家伙……这不是黑客新闻吗?你们为什么不自己去创建一个“更好的”语言?但你们不会,你们只会抱怨
哈哈,一开始我以为——“别再纠结这些错误了,别再纠缠不休了”,但后来我开始阅读,这是一篇很好的文章,读起来很有趣。
如果你能找到一种没有烦人缺陷的编程语言,那我就能找到一种不存在的编程语言,而且可能永远不会存在。
我真的很喜欢Go。它满足了我所有的需求。它是解决你问题的语言吗?我不知道,但很可能答案是“不”。
Go易于学习,非常简单(这是对我来说的一个重要特点),如果你想要更多功能,你可以很快实现。
当博客文章作者说出这句话时,我完全无法认同:
> 为什么我要关心内存使用?内存很便宜。
这是只有缺乏经验的人才会说的话。在规模化应用中,没有什么东西是便宜的;如果你在为规模化应用或客户编写软件,就不存在廉价的资源。往往,单个字节都至关重要。内存使用量至关重要。CPU周期至关重要。内存分配至关重要。人们喜欢假装这些不重要,因为这样能让工作更轻松,但如果你想编写高性能软件,就必须考虑CPU缓存行,而如果你考虑了缓存行,就必须考虑类型内存使用。
> 在大规模环境下,没有廉价的资源;如果你在为大规模应用或客户开发软件,就不会有廉价的资源。往往,单个字节都至关重要。内存使用量至关重要。CPU 周期至关重要。内存分配至关重要
如果你的目标是极致的性能调优,以至于单个字节都至关重要,我认为 Go 语言可能不是一个很好的选择?确实有一些语言在垃圾回收调优和缓存行友好性方面比 Go 更出色。
但坦白说,你的评论更像是门槛主义,认为别人缺乏经验,因为他们没有在你所处的规模下开发软件。你对他们的领域同样缺乏经验(且不感兴趣)。
> 之前的帖子《为什么Go不是我最喜欢的语言》和《Go程序缺乏可移植性》让我对Go的批评已持续超过十年。
我笑了
同感,我不知道这让他成为Go的最大粉丝,还是这实际上真的很悲哀。
我从未遇到过Go的问题,因为它每年为我赚取数百万美元。
我从未遇到过Enron的问题,因为我在它高点时卖掉了它。
作为一名使用Go语言编程超过10年并用其编写过大型代码库的人,我对本文的观点有以下看法:
:Error变量作用域 -> 确实,初学者可能会感到困惑,但有一定经验后就不会再在意了。是否可以缩小其作用域?`当然可以,但感觉这里被夸大成了一个“问题”,而我认为Go团队更应该重新审视其他更重要的事项。关于Go的错误处理,有人讨厌它,有人喜欢它:我个人喜欢它(是的,我真的喜欢),所以我认为这更多是个人偏好而非“糟糕”的事情。
:两种类型的空值 -> 有趣,我在使用Go语言超过10年的时间里,做了大量与指针相关的操作,却从未遇到过这种情况,所以我好奇在什么情况下会遇到这种无法避免的情况。虽然有点困惑,但我承认。
:它不具备可移植性 -> 我对此没有意见,因为我只在Unix系统上工作,而且我有自己的编译二进制文件,所以在这里我也看不到任何问题。
:append 没有定义所有权 -> 我的意思是…… seriously?你的测试用例,尽管结果可能出人意料,但这是一个非常奇怪的用例。为什么你要在中间字段 append,如果你考虑这些函数在底层做什么,你的尝试实际上感觉像是你想产生奇怪的行为,而这种行为在任何语言中都可以实现。
:defer 是个蠢主意 -> 这里我 100% 同意——从我的角度看,它会导致大量资源浪费,在某些情况下还可能引发奇怪的错误,但我没兴趣解释这些——我只想说,defer 虽然看似有用,但从我的角度看是个糟糕的设计,不应使用。
:标准库吞噬异常,所以一切希望都破灭了 -> “所以一切希望都破灭了”,我意思是,你早就离开了客观性的范畴,但这真的达到了顶点。我写过一些相当大的 Go 应用程序,我从未遇到过无法通过调整代码来防止异常发生的情况。再次强调,我感觉有人只是在寻找可以避免的抱怨对象。(如果有人提出一个极其罕见、可能百万分之一概率的案例,请始终记住,语言设计并不以最罕见的情况为导向)。
:有时数据并非UTF-8编码 -> 我不会花时间阅读另一篇文章,如果重要请直接给出示例。我曾处理过不同编码(网页爬虫),并且都能妥善处理。
内存使用 -> 你描述的情况是我在设计决策中不太满意的一点,即内存管理。不过,我的一个 Go 语言项目是一个内存中图数据库,在某个场景下连续运行了约 2 年未重启,其中存储了大约 18GB 的数据集。它包含大量互斥锁处理(关于你之前提到的异常问题,我从未遇到过),而且它作为面向互联网服务的后端运行,因此不仅处理内部数据。
——————–
最后我想说:很多时候这取决于个人偏好。我可以花几天时间抱怨JavaScript、Java、C++或其他语言,但有什么意义呢?选择适合你用例和喜好的语言,不要选择不适合的语言并抱怨它。
此外,为了证明我不是一个盲目崇拜“Go语言是最好的”的粉丝,因为它确实有值得批评的地方,比如之前提到的内存管理。
虽然我仍然认为你在应用程序中创建了内存泄漏,但Go语言有一个“内存池”的概念,这使得代码能够部分自行管理内存,从而开发出更加内存高效的应用程序。最近这个功能进展缓慢,我真的希望Go团队能重新拾起它,使其成为一个稳定的功能。如果这样,我可能会用它来更新我所有的较大代码库。
另外——这让我非常烦恼,因为它让我浪费了大量时间——Go语言的插件系统。我设计了一个架构来协调处理流程,出于某些原因,我希望将这些协调的“组件”实现为插件。但当前的插件系统只能用“地狱般的折磨”来形容。我折腾了大约三年,直到最近放弃了插件功能,直接添加了相关实现。插件本身是非常强大的工具,一个良好的插件系统可以带来巨大价值,但目前状态下我绝不推荐任何人触碰它。
以上只是两个例子,我还能列出更多,但我想强调的是:应该批评的是真实存在的问题,而非自己创造的问题或单纯不喜欢的语言设计决策。我不确定这类文章是出于无聊的发泄,还是为了吸引眼球而故意引发争议。无论如何,这都无助于解决问题。
作者在此。
:两种类型的空值
其他评论者有过类似经历。我也有过。但并非所有人都经历过。这并不意味着它是好的。
:无所有权的append操作
我见过这种情况。当然可以选择“不这么做”,但如果能通过语法直接禁止就更好了。
:这不具备可移植性(“仅限Unix”)
我只在Unix系统上工作。但如果你只在amd64 Linux上工作,那么可移植性就不是问题。支持BSD和Linux时,我才会遇到这种混乱。
:所有希望都破灭了
所有希望都破灭了,特别是关于无需编写异常安全代码的想法。如果 panic 总是导致程序崩溃,那倒也无妨。但没有编码规范能让你逃脱标准库的限制,所以,关于可以假装 panic 退出问题的希望,确实破灭了。
你不需要阅读我的博客文章。期待阅读你更精彩的评论。
我每天都用Go工作,同时使用Dart和Python。
我认为切换到Go就像一种不同的禅修。需要时间来适应并进入Go的节奏……与其他语言不同,LSP很快,但开发者却不那么快。一旦你失去了活下去的意志,你就会变得非常熟练。/s
自从Go的次要版本号小于10以来,我就一直在为自己编写小型Go工具。
我仍然可以查看这些工具的代码,打开它们,它们看起来与现代代码相同。我还可以使用最新编译器(1.25?)编译所有这些工具,它们都能正常运行。
无需研究五年来的包管理器变更和新框架。
我写Go代码时也会唱《Fade to Black》:D
我心想“我之前真的听过这首歌吗?”答案是“没有”,所以现在我听过了(这是 Metallica 的一首关于自杀念头的歌曲,至于在编写 Go 时听这首歌是否是个好主意,我无法确定,因人而异)。
我的开发者体验与Rust类似,但由于类型系统过于宽松,更加令人沮丧。
如果因为分享我的观点而被点赞,我可能会放弃生活。
[删除]
没有人比Gophers更关心Rust。
通常情况恰恰相反……
[删除]
我遇到过库代码导致段错误的情况。我认为这更糟糕。
我遇到过Go的垃圾回收器释放了它从未分配过的内存。
还是主动一点好,嘿嘿嘿
defer 并不比 Java 的 try-with-resources 更差。两者都不是真正的 RAII,因为在两种情况下,你,调用者,都需要记住要使用冗长的形式(“try (…) {” 或 “defer …”)而不是简洁的形式(“…”),后者虽然能编译通过,但会默默地做错事情。
当然,真正的 RAII 会比这两种方式更好,但作者的观点是,Java 比 Go 更好,因为资源获取是词法作用域的,而不是函数作用域的。想象一下,如果 Java 的 try (...) { } 在 try 块结束时没有释放资源,而是在包裹方法返回时释放。这就是 Go 的 defer 机制的工作方式。
在 Go 中不能创建新的块作用域吗?如果不能,我同意。如果可以,那只要这样做就能实现词法作用域?
defer 在 Go 中不是块作用域的,而是函数作用域的。因此,如果你想延迟释放互斥锁,它只会在大函数结束时执行,即使它被放在一个块中。这意味着你不能这样做(草图):
func (f *Foo) foo() {
// 关键部分
{
f.mutex.Lock()
defer f.mutex.Unlock()
// 对共享资源的操作
}
// 此时锁仍被持有,但你可能并不希望这样
}
你可以直接调用 Unlock,但如果发生 panic,它就不会像上面那样被解锁。如果调用栈中较高的位置阻止了 panic 导致整个程序崩溃,这可能会使系统处于不良状态。
这是 defer 的关键问题。它的工作方式与 finally 块类似,但仅在函数退出时触发,因此实际上并不适合此任务。
正如兄弟指出的,你可以使用立即调用的匿名函数,但这只是不方便,即使它已经成为惯用做法。
你必须创建一个匿名函数。
仍然比Rust更快(编译速度)。
仍然不在同一水平线上。只有其中一种是“系统语言”,借用Go的不恰当营销术语。
还有什么比开发者对自己的工具产生优越感更能让他们感到安慰的?然后是那股无法遏制的想要贬低“下层”的欲望?我认为这简直可悲至极。
我仍然对Go中没有“do while”循环感到震惊。
Python也没有
> 可能 [hello NIGHTMARE !]. 谁想要那个?没人想要那个。
我不在乎你想要那个。每个人都应该知道切片就是这样工作的。没有更多,也没有更少。
我真的不在乎那件事,我只是知道切片的行为方式,因为我学习了这门语言。这就是你在使用它进行编程(专业地)时应该做的事情
没错。如果你用 Go 编程,你就应该知道这一点。
就像每个 PHP 程序员都应该知道三元运算符的结合性与其他语言相反一样。
如果你用一种语言编程,你就应该知道这种语言的缺点。这并不意味着这些方面就不坏。
注意,从PHP 8.0开始,三元运算符是非结合性的,尝试在没有显式括号的情况下嵌套它会导致硬错误。
作者显然也清楚这一点,否则就不会专门提及。所有这些问题只是语言本身的特性,而这正是问题所在。
如果你能接受这一点,那么接下来的示例应该会让你感到不满,因为按照你自己的定义,“这根本不是切片的工作方式”。
我对后续的例子也无异议。如果你查阅过切片的相关资料,就会知道它们就是这样定义和工作的。我并非在评判,只是按照语言本身的规定使用它。
对于感兴趣的人,这篇文章解释得非常清楚,我认为:https://go.dev/blog/slices-intro
那么你似乎对不一致的所有权以及对底层数据而非结构的行为依赖感到满意。
你真的看不出来为什么人们会指出一个在你脚下不断变化的定义是“糟糕的定义”吗?他们并不是在争论文档是错误的。
这个定义是完全一致的。如果容量足够(程序员可以直接使用cap()函数进行检查),append操作是就地进行的,否则它会分配一个新的底层数组。
是的,它既一致又复杂且反直觉。
“一致性”是必要的,但不足以构成“良好”。
> 因为我学习了这门语言
如果这是你的论点,那么任何语言都不存在糟糕的设计决策。
这篇文章很有趣,对我来说也非常有教育意义,但每次我读到一篇批评编程语言的文章时,写这篇文章的人自己并没有做出更好的东西。
这真可惜,因为这和对着风撒尿一样无效。
如果你认为一个人在没有设计过编程语言的情况下无法对编程语言进行有说服力的批评,那么我要求你展示你对编程语言批评的成果,这样我才能知道你在编程语言批评领域是否“做出了更好的东西”。
当然,按照你的逻辑,这也意味着你自己设计过编程语言。
如果你没有做过这些事情,我就不重复你那些花哨的言辞了。
> 如果你认为一个人在没有设计过编程语言的情况下,无法对编程语言进行有说服力的批评
实际上,我认为这是一个合理的论点。我本人没有设计过编程语言(除了些许实验性尝试),因此我对贬低他人的设计选择持谨慎态度,因为即使以我有限的经验,我也清楚设计过程中总会存在权衡取舍。
同样,我对那些写作水平平庸的文学批评家也不屑一顾。
谁才有资格评判那些批评家的写作是好是坏?那些被认定为好作家的批评家?是谁审核了他们?这必须是一条由认证的好作家组成的长链。
不,我坚持我的立场。我可能无法做得更好,但我能分辨出什么是不好。
(我对Go没有意见。我几乎没用过它。这只是基于一个普遍原则:能够评判自己无法做到的事情。我的意思是,奥运会也有体操裁判,他们并不是金牌得主。)
我从未当过摇滚明星,但我认为Creed很糟糕。
我真的不喜欢你的逻辑。我不是米其林大厨,但我有资格说一家餐厅毁了我的甜点。虽然我可能做不出比他们更好的焦糖布丁,但我仍然能看出他们做得比隔壁的竞争对手差。
例如,我喜欢Python,但它在某些地方天生就很慢,因为sum(list)必须检查每个元素的类型,以确定调用哪个__add__函数。即使所有元素都是整数,也没有办法向解释器证明字符串没有混入其中,所以解释器必须每次都检查。
看吧?我从未编写过编程语言,更不用说像 Python 这样流行的语言了,但我仍然有资格指出它与其他语言相比的不足之处。
它不仅非常出色,对于人工智能来说更是如此。
يجمع 888starz.bet بين ألعاب الكازينو والمراهنات الرياضية في موقع واحد مخصص لمستخدمي مصر.
888starz [url=http://swsi.co.kr/board/bbs/board.php?bo_table=free&wr_id=144378]888starz[/url]
يحتوي الكازينو على أكثر من 4000 لعبة سلوت من كبار المزودين العالميين.
يتيح 888starz الرهان على عشرات الرياضات بينها UFC و Dota 2 و CS:GO.
يطرح 888starz مكافآت منتظمة تشمل الاسترداد النقدي والترقيات.
يقدم الموقع خدمة عملاء على مدار الساعة بالعربية إضافة إلى تطبيق apk ونسخة آيفون.
True Fortune casino has become a go-to online casino for many players in the United Kingdom.
True Fortune regularly adds new titles and jackpot games to the lobby.
Frequent players climb a VIP ladder that unlocks better rewards and faster withdrawals.
Topping up an account is instant with no fees on most payment methods.
All games run on certified random number generators for provably fair results.
Help is always at hand thanks to round-the-clock live chat support.
true fortune casino home [url=http://true-fortune-casino17.com/]true fortune casino home[/url]

发表回复