此篇文章流传甚广,其实里面没啥干货, 而且里面很多观点是有问题的. 这个文章在 golang-china 很早就讨论过了. 最近因为 Rust 1.0 和 1.1 的发布,导致这个文章又出来毒害读者. 所以写了这篇反驳文章,指出其中的问题.
原文链接:http://www.voidcn.com/article/p-dhylvfso-bae.html
确实是非常主观的结论,因为里面有不少有问题的观点(用来忽悠Go小白还行).
第0节:我的Go语言经历
在2009年Go刚发布时,确实是因为“Google公司制造”的光环而吸引了(包括文章作者和诸多IT记者)很多低级的尝鲜者. 还好,经过5年的发展,这些纯粹因为光环来的投机者所剩已经不多了(Google趋势). 目前,真正的Go用户早就将Go用于实际的生产了.
说到 其语法中的分号和花括号不满,我想说这只是你的 个人主观感受,还有很多人对Go的分号和花括号很满意,包括水果公司的的 Swift 的语言设计者也很满意这种风格(Swift中的分号和花括号和Go基本相同).
如果只谈 个人主观感受,我也可以说 Rust 的 fn
缩写也很蛋疼!
这个到是事实,在 golang-china 有不少吵架的帖子,感兴趣的可以去挖下,我就不展开说了.
真的不清楚楼主说的可以在 Go1.0 之前短时间内能实现的 重大改进和诸多明显缺陷 是什么.
如果是楼主说前面的 其语法中的分号和花括号不满 之类的重大改进,我只能说这只是你的 个人主观感受 而已,你的很多想法只能说服你自己,没办法说服其他绝大部分人(不要以为像C++或Rust那样什么特性都有就NB了,各种NB特性加到一起只能是 要你命3000,而绝对不会是什么 银弹).
Go 1.1的Release Note,发现语言层面没有太大改变. 语言层没有改变是是因为 Go1 作出的向后兼容的承诺. 对于工业级的语言来说, Go1 这个只能是优点. 如果连语言层在每个版本都会出现诸多大幅改进,那谁还敢用Go语言来做生产开发呢(我承认Rust的改动很大胆,但也说明了Rust还处于比较幼稚和任性的阶段)?
说 Go语言社区里的某些人固执 的观点我是同意的. 但是这些 固执 的人是可以讲道理的,但是他们对很多东西的要求很高(特别是关于Go的设计哲学部分). 只要你给的建议有依据(语言的设计哲学是另外一回事情),他们绝对不会盲目的拒绝(只是讨论的周期会比较长).
关于楼主提交的给Go文件添加BOM的文章,需要补充说明下.
在Go1.0发布的时候,Go语言的源文件(.go
)明确要求必须是UTF8编码的,而且是无BOM的UTF8编码的(G公司的Protobuf也不支持带BOM的UTF8编码).
注意: 这个 无BOM的UTF8编码 的限制仅仅是 针对 Go语言的源文件(.go
).
这个限制并不是说不允许用户处理带BOM的UTF8的txt文件!
我觉得对于写Go程序来说,这个限制是没有任何问题的,到目前为止,我还从来没有使用过带BOM的.go
文件.
不仅是因为带BOM的.go
文件没有太多的意义,而且有很多的缺陷.
BOM的原意是用来表示编码是大端还是小端的,主要用于UTF16和UTF32. 对于 UTF8 来说,BOM 没有任何存在的意义(正是Go的2个作者发明了UTF8,彻底解决了全球的编码问题).
但是,在现实中,因为MS的txt记事本,对于中文环境会将txt(甚至是C/C++源文件)当作GBK编码(GBK是个烂编码),为了区别到底是GBK还是UTF8,MS的记事本在前面加了BOM这个垃圾(被GBK占了茅坑),这里的bom已经不是表示字节序本意了. 不知道有没有人用ms的记事本写网页,然后生成一个带bom的utf8网页肯定很有意思. 这是MS的记事本的BUG: 它不支持生成无BOM的UTF8编码的文本文件!
这些是现实存在的带BOM的UTF8编码的文本文件,但是它们肯定都不是Go语言源文件!
所以说,Go语言的源文件即使强制限制了无BOM的UTF8编码要求,也是没有任何问题的(而且我还希望有这个限制).
虽然后来Go源文件接受带BOM的UTF8了,但是运行 go fmt
之后,还是会删除掉BOM的(因为BOM就是然并卵). 也就是说 带 BOM 的 Go 源文件是不符合 Go语言的编码风格的, go fmt
会强制删除 BOM 头.
前面说了BOM是MS带来的垃圾,但是BOM的UTF8除了然并卵之外还有很多问题,因为BOM在String的开头嵌入了垃圾,导致正则表达式,String的链接运算等操作都被会被BOM这个垃圾所污染. 对于.go
语言,即使代码完全一样,有BOM和无BOM会导致文件的MD5之类的校验码不同.
所以,我觉得Go用户不用纠结BOM这个无关紧要的东西(语言源文件不是文本编辑器,没必要支持各种文件格式).
第1节:我为什么对Go语言不爽?
1.1 不允许左花括号另起一行
我觉得Go最伟大的发明是 go fmt
,从此Go用户不会再有花括弧的位置这种无聊争论了(当然也少了不少灌水和上tiobe排名的机会). 只给用户一条路,不给任何走歧途的机会, 确保正确、高效。
是这优点,Swift 语言也使用和 Go 类似的风格(当然楼主也可能鄙视swift的作者).
1.2 编译器莫名其妙地给行尾加上分号
又是楼主的 个人主观感受,不过我很喜欢这个特性. Swift 语言也是类似.
1.3 极度强调编译速度,不惜放弃本应提供的功能
编译速度是很重要的,如果编译速度够慢,语言再好也不会有人使用的. 比如C/C++的增量编译/预编译头文件/并发编译都是为了提高编译速度. Rust1.1 也号称 比 1.0 的编译时间减少了32% (注意: 不是运行速度).
当然,Go刚面世的时候,编译速度是其中的一个设计目标.
不过我想楼主,可能想说的是因为编译器自己添加分号而导致的编译错误的问题. 我觉得Go中 {
不能另起一行是语言特性,如果修复这个就是引入了新的错误.
其他的我真想不起来还有哪些 调编译速度,不惜放弃本应提供的功能 (不要提泛型,那是因为还没有好的设计).
最重要是的保持Compiler的靠谱、精简、高效,而不是功能花哨,bug一堆。这样有利于做流水优化、指令集精减、易于跨平台、降低维护负担。
1.4 错误处理机制太原始
话说,软件开发都发展了半个世纪,还是无实质性改进. 不要以为弄一个异常的语法糖就是革命了.
我只能说错误和异常是2个不同的东西,将所有错误当作异常那是SB行为.
try..catch原理是jump/longjump,这种东西会增加底层复杂性,并且容易滥用,不好维护,而且可能会增加10W数量级别goruTine上下文swich负担。
正因为有异常这个所谓的银弹,导致很多等着别人帮忙擦屁股的行为(注意 shit
函数抛出的绝对不会是一种类型的 shit
,而被其间接调用的各种 xxx_shit
也可能抛出各种类型的异常,这就导致 catch
失控了):
@H_301_186@int @H_4_131@main() { try { shit(); } catch( /* 到底有几千种 Exception ? */) { ... } }
Go的建议是 panic - recover 不跨越边界,也就是要求正常的错误要由pkg的处理掉. 这是负责任的行为.
再说Go是面向并发的编程语言,在海量的 goroutIne 中使用 try/catch
是不是有一种不伦不类的感觉呢?
1.5 垃圾回收器(GC)不完善、有重大缺陷
这是说的是32位系统,这绝对不是Go语言的重点应用领域!! 我可以说Go出生就是面向64位系统和多核心cpu环境设计的. (再说 Rust 目前好像还不支持 XP 吧,这可不可以算是影响巨大?)
32位当时是有问题,但是对实际生产影响并不大(请问楼主还是在用32位系统吗,还只安装4GB的内存吗). 如果是8位单片机环境,建议就不要用Go语言了,直接C语言好了.
而且这个问题早就不存在了(大家可以去看Go的发布日志).
Go的出生也就5年时间,GC的完善和改进是一个持续的工作,2015年8月将发布的 Go1.5将采用并行GC,每次 "stop the world" 时间低于 10 毫秒,具体请参考 GopherCon2015: Go GC: Solving the Latency Problem in Go 1.5.
关于GC的被人诟病的地方是会导致卡顿,但是我以为这个主要是因为GC的实现还不够完美而导致的. 如果是完美的并发和增量的GC,那应该不会出现大的卡顿问题的.
当然,如果非要实时性,那用C好了(实时并不表示性能高,只是响应时间可控).
对于Rust之类没有GC的语言来说,想很方便的开发并发的@L_674_79@程序那几乎是不可能的.
不要总是吹Rust能代替底层/中层/上层的开发,我们要看有谁用Rust真的做了什么.
1.6 禁止未使用变量和多余import
这个问题我只能说楼主的吐槽真的是没水平.
为何不使用的是错误而不是警告? 这是为了将低级的bug消灭在编译阶段(大家可以想下C/C++的那么多警告有什么卵用).
而且, import
即使没有使用的话,是用副作用的,因为 import
会导致多个init函数
和全局变量的初始化,导致程序不可控. 如果某些代码没有使用,为何要执行 init
这些初始化呢?
如果是因为调试而添加的变量,那么调试完删除不是很正常的要求吗?
如果是因为调试而要导入fmt
或log
之类的包,删除调试代码后又导致 import
错误的花,楼主难道不知道在一个独立的文件包装下类似的辅助调试的函数吗?
import (
@H_404_266@"fmt" @H_404_266@"log" ) func logf(format String,a ...interface{}) { file,line := callerFileLine() fmt.Fprintf(os.Stderr,@H_404_266@"%s:%d: ",file,linE) fmt.Fprintf(os.Stderr,format,a...) } func fatalf(format String,a...) os.Exit(1) }
import _
是有明确行为的用法,就是为了执行包中的 init
等函数(可以做某些注册操作).
将警告当作错误是Go的一个哲学,当然在楼主看来这是白痴做法.
1.7 创建对象的方式太多令人纠结
C++的new
是狗屎. new
导致的问题是构造函数和普通函数的行为不一致,还有加不加(),行为不一致, 这个补丁特性真的没啥优越的.
我还是喜欢C语言的 fopen
和 @H_19_24@malloc 之类构造函数,构造函数就是普通函数,Go语言中也是这样.
C++中,除了构造不兼容普通函数,析构函数也是不兼容普通函数. 这个而引入的坑有很多吧.
1.8 对象没有构造函数和析构函数
defer
可以覆盖析构函数的行为,当然 defer
还有其他的任务. Swift2.0 也引入了一个简化版的 defer
特性.
1.9 defer语句的语义设定不甚合理
前面说到 defer
还有其他的任务,也就是 defer
中执行的 recover
可以捕获 panic
抛出的异常. 还有 defer
可以在 return
之后修改命名的返回值.
楼主说的 defer
是类似 Swift2.0 中 defer
的行为,但是 Swift2.0 中 defer
是没有前面2个特性的.
Go中的defer
是以函数作用域作为触发的条件的,是会导致楼主说的在 for
中执行的错误用法(哪个语言没有坑呢?).
不过 for
中 局部 defer
也是有办法的 (Go中的defer
是以函数作用域):
for { @H_301_186@func(){ f,err := os.Open(...) defer f.Close() }() }
在 for
中做一个闭包函数就可以了. 自己不会用不要怪别人没告诉你.
Swift 的块级 defer
也不方便实现以下的场景:
func (t *T) Serve() {
if Debug { log.Println(t,@H_404_266@"starTing") defer log.Println(t,@H_404_266@"exiTing") } // stuff }
Nigel Tao 给的 解释:
The longer answer is that while there@H_404_266@‘s benefit of a scope-scoped defer,there‘s also benefit in a @H_301_186@function-scoped defer. This code: func foo(filename String) error { var r io.Reader if filename != @H_404_266@"" { f,err := os.Open(fileName) if err != nil { return err } defer f.Close() r = f } else { r = Strings.NewReader(fakeInput) } // More code that reads from r. etc }
1.10 许多语言内置设施不支持用户定义的类型
说到底,这个是因为对泛型支持的不完备导致的. 记得1.5以后可以自定义strct来支持 for,chAnnel,map等。
Go语言是没啥NB的特性,但是Go的特性和工具组合在一起就是好用.
这就是Go语言NB的地方.
1.11 没有泛型支持,常见数据类型接口丑陋
Go有自己的哲学,如果能有和目前哲学不冲突的泛型实现,他们是不会@L_675_136@的.
如果只是简单学学(或者叫抄袭)已经开源的语言的语法,那是C++的设计风格(或者说C++从来都是这样设计的,有什么特性就抄什么),导致了各种脑裂的编程风格.
编译时泛型和运行时泛型可能是无法完全兼容的,看这个例子:
type Adder<T> interface { Add(a,b T) T }
请问 Adder<int>
和 Adder<float>
是一个接口吗?
type Adder interface { Add(a,b interface{}) interface{} }
对于这种场景, interface{}
虽然性能不是最好,但是接口却是一致的:
而且,目前已经有 go generate
可以弥补范型和宏部分的不足.
golang-china 关于该文的讨论中有涉及到泛型的讨论.
感觉Go即使真有泛型,也得等到Go2.0了(猜测Go2.0能在2020年诞生10周年发布).
1.12 实现接口不需要明确声明
Go是面向组合的,和UNIX的哲学类似. 使用Go你要知道 io
放的是什么, fmt
包放的是什么,习惯之后会很方便.
你不能说UNIX的命令行工具sort
没有实现强的接口依赖检测会有很多问题. 如果你非要乱用sort
的捣蛋话当然有很多问题.
但是Go给想组合和合作的人使用的,组合优于继承.
不要提 老赵 那个文章了,我发了反驳文章后他已经闭嘴了: http://my.oschina.net/chai2010/blog/122400
对于IDE环境,Go的工具 go Oracle
可以回答某类型实现了哪些接口这类问题.
1.13 省掉小括号却省不掉花括号
“代码比较简洁”,谁告诉你是这个原因了? 不懂别瞎说!
必须花括弧的原因是C语言中 if else
的悬挂问题:
if(1) ....; if(2) ...; else ...;
请问上面的 else
是属于哪个 if
的?
必须加花括弧可以避免上面的问题.
而小括弧又不是必须的因此就去掉了(Swift同样用了Go的设计).
至于 x?a:b
虽然是简洁,但是容易泛滥 (x?a:b)?(x?a:b):(x?a:(x?a:(...)))
.
Go不是因为 简洁 的 x?a:b
而禁止三元操作符,而是为了防止泛滥使用而禁止三元操作符.
1.14 编译生成的可执行文件尺寸非常大
C语言的0.04MB程序如果崩了(Windows64环境TDM-GCC生成128KB),你就只能知道它崩了.
而Go1.0的4MB程序如果崩了,你可以知道在哪个文件的哪行代码崩了,这就是差别!
对于Go1.5,Windows64环境,使用 fmt.Println
, Hello world
生成的 exe 有 2.4 MB.
对于 Rust1.1,生成的 exe 有 2.3 MB.
做了一个数组越界导致崩溃的测试,Go生成的2.4MB的程序可以输出导致崩溃的文件名和行号:
panic: runtime error: index out of range goroutIne 1 [running]: main.main() D:/path/to/main.go:7 +0x1b9
相当于CXX/C 加 -g -O 参数使生成文件含debug/trace信息,这样会增加文件大小。-w 去掉DWARF调试信息,得到的程序就不能用gdb调试了
不建议s和w同时使用。也可以压缩生成文件。
thread @H_404_266@‘<main>‘ panicked at @H_404_266@‘index out of bounds: the len is 2 but the index is 100‘,C:/bot/slave/stable-dist-rustc-win-gnu-64/build/src/libcollections\vec.rs: 1359
关于exe大小的问题可以关注 Issue6853.
1.15 不支持动态加载类库
假设系统由100多个exe组成了,那总共也就是不超过1GB的磁盘空间,没觉得有多大.
而且DLL依赖的地狱难道忘记了吗.
Go 1.8及以后支持plugin, 还可以玩玩hot-plugin,https://github.com/campoy/golang-plugins。
1.16 其他
-
导入pkg的import语句后边部分竟然是文本(import ”fmt”)
-
没有enum类型,全局性常量难以分类,iota把简单的事情复杂化
-
定义对象方法时,receiver类型应该选用指针还是非指针让人纠结
-
定义结构体和接口的语法稍繁,interface XXX{} struct YYY{} 不是更简洁吗?前面加上type关键字显得罗嗦。
-
测试类库tesTing里面没有AssertEqual函数,标准库的单元测试代码中充斥着if a != b { t.Fatal(...) }。
-
语言太简单,以至于不得不放弃很多有用的特性,“保持语言简单”往往成为拒绝改进的理由。
-
版本都发展到1.2了,goroutIne调度器依旧默认仅使用一个系统线程。GOMAXPROCS的长期存在似乎暗示着官方从来没有足够的信心,让调度器正确安全地运行在多核环境中。这跟Go语言自身以并发为核心的定位有致命的矛盾。(直到2015年下半年1.5发布后才有改观)
-
import
导入文本绝对是优点,因为可以支持很多以特殊字符命名的路径:import "_-aa/bb~/dd/xx"
,只有包名满足ID命名规则就可以了,前缀部分可以很随意 -
receiver
就是普通函数:func(self T,...)
和func(self *T,...)
的差别不是很明显吗 -
type
开始规则更统一,和var x int
和func Add(a,b int) int
类型后缀的规则是一致的(Rust中的变量和函数也是类型后置吧),比如type MyInt int
,type MyFunc func(...)
,而且也非常便于解析和查找(正则^type
就可以定位了) -
如果要加
AssertEqual
的话,那么什么叫equal
呢? 2个map或struct如何才是叫相等,chan成员呢? 别总是想着增加功能,增加功能的同时带来的问题和复杂性难道不需要考虑吗? -
语言太简单难道不是优点吗? C++语言够复杂,建议楼主深入学习
-
标准库的一大原则就是基本可用,标准库不是一个大杂烩,我想"少即是多"的哲学你是不会理解的
-
Go1.5默认N个系统线程,N为cpu核心数目. 默认值并不是没有信心,而是对于不同的程序,需要几个线程最好是一个比较困难的事情(比如gui程序为何不用多线程呢).
给Go提交的CL的要求是非常高,楼主的BOM提法我觉得可以讨论,但是不要以为CL增加了特性就必须得通过.
还好,Go团队没有接受你上面的诸多建议,要不然我估计我现在已经放弃Go了.
第2节:我为什么对Go语言的某些人不爽?
对于一,固执的G员工,你要通过逻辑来说服他们,如果自己都没有干货,别人凭什么要采纳你的建议(上面的绝大部分建议我就@L_675_136@)?
对于二,脑残粉丝谁都烦,希望楼主下次吐槽能给点干货,别把自己也整成了脑残粉.
对于技术而言,我更喜欢独裁者. 所谓的开源烂民主那是活稀泥的.
你可以尝试去提一个Linux内核也增加GUI模块的建议试试.
Go1.5的地基已经非常的牢固,这个你不用担心.
缺少使用Go语言的亲身经历,楼主也真的敢信口开河. 你不是以为G公司开发Go真的是用来玩的吧.
再说Go1.5已经完全没有C代码了,这下你该闭口了吧.
你的很多批评和改进意见都是狗屎(包括BOM那个). 你这样的用户没有融入社区时好事情,Go语言只要在生产环境好用就可以了.
可惜世界不是以你的意志改变的,Go还将继续快速发展,你是很难受吧?
第3节:还有比Go语言更好的选择吗?
不就是号称银弹的 Rust 吗,但是 然并卵. 我也断言一句: Rust 最终只能是小众语言,想代替 Go语言/C语言 根本是没戏的(Swift开源后基本可以秒杀Rust).
第4节:写在最后
走好,不送!
@H_403_846@Liigo 2014-4-29 补记1:在工作中都已经用上了,你还在想象别人是在盲目推崇,是你自己在梦游吧.
Liigo 2014-4-29 补记2:
王的垠语言出来了吗? 等着10年后他再次扇自己的脸(参考Windows无用那篇).
Liigo 2014-4-29 补记3:
不需要NB的特性,只需要简单/好用/实用就行.
@H_133_674@Liigo 2015-1-31 补记4:就是数万个“赞”又怎么样? 关键是很多地方Go已经用起来了.
Liigo 2015-4-1 补记5:
Liigo 2015-5-29 补记6:
原来这是你的功劳!
Liigo 2015-6-2 补记7:
放弃Go语言很正常,也有很多放弃X语言投奔Go语言的例子.
关于对作者倾向性质疑的声明:
不需要NB的特性,只需要简单/好用/实用就行.
关于对作者阴谋论的声明:
CL被据而怀恨至今真的是没冤枉楼主,希望下次抹黑Go能来点干货.
当然Go语言也不是完美的,作为Windows下的Go用户,说下我比较希望的改进.
首先在下面的bug修复前,我并不十分关心Go的性能改进。
- 修复 Issue11058,让 Go 可以生成 dll,这样我就可以基本抛弃 C++ 了
- 修复 Issue9510,这样 cgo 才可以放心地静态链接 c++ 库
上面2个bug是支持go和c库双向合作的关键(Linux和Darwin已经支持生成动态库). 然后就是 cgo 调用 c 函数的参数传递的性能能改善下.
在go1.5中,新引入了 vendor 的试验性的特性,因此go的包依赖管理算是基本解决.
长远看希望语言方面能有以下的特性:
- 范型支持,可以简陋些,但是不要破坏go已有的风格
- 希望能有不支持嵌套的三元表达式的支持
- 大小写的导出规则对中文能友好一些
- 接口瘾式转换导致的一些坑(
error
和nil
) - 官方的leveldb库和基于其封装的sql数据库
- os 的文件系统做成接口,提共自定义文件系统的挂载功能
- image 包能增加 GrayA/GrayA32/RGB/RGB48 之类的类型支持
- 性能改进
可有可无的: