V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
fire5
V2EX  ›  Python

看了一个 go 语言,感觉语法略为不习惯。

  •  
  •   fire5 · 2016-01-27 09:42:17 +08:00 · 21449 次点击
    这是一个创建于 3004 天前的主题,其中的信息可能已经有所发展或是发生改变。
    还是大 python 舒服。。
    第 1 条附言  ·  2016-02-04 14:43:15 +08:00
    这个话题的讨论方式偏离了很多。 如果为了讨论 golang ,@Livid 能否开一个 golang 的节点。感谢
    151 条回复    2016-02-09 21:30:09 +08:00
    1  2  
    BurNFans
        101
    BurNFans  
       2016-01-29 13:28:15 +08:00
    @noli
    师傅,理解不了你说要手工分析 err ,漏了的话,上线后不定期 crash ;
    师傅,我写不出返回不定的多个 err 的代码,你教我好不好
    noli
        102
    noli  
       2016-01-29 13:41:32 +08:00
    @BurNFans

    假设你当我是白痴…… 估计你也不会跟白痴说话吧?不分析

    假设你没有当我白痴,那么一个按照预期返回了结果的函数会 crash 吗?
    你认为我会连这么基本的问题都没弄清楚的话…………嗯嗯,我和你之间必定出了一个自大狂。

    为什么是可能导致?这就是我想批评 golang 的问题所在了。

    回到那个讨论场景, do_transaction 会调用不定多个其他函数(例如在循环体中调用),这些函数都是可以返回 err 的。

    现在其中执行到某一步的时候,某个函数返回 err 了, 那 do_transaction 中要怎么判断是否终止事务?获得这个 err 之后要怎么执行回滚?

    仔细想象这个问题,你就会发现,当你调用的代码并不是直接通过源码明文调用(就是说代码中直接通过函数名调用),而是间接地调用的时候, err 的信息就是模糊的,被隐藏在调用接口之下的。那这个时候 err 信息实际上是没用的。

    --

    问题是如何进一步扩散变得严重的呢? 现在我的某个间接调用返回了一个 err , 那么一般来说是不是意味着这个函数没有返回程序上下文期望可以继续执行下去的返回结果?

    就是说 ret, err = do_sth() 总是 要么 err 为 nil 要么 ret 为 nil ,这也是 golang 为什么能歪打正着使用这种设计的前提 convention 。

    但是因为 我在 do_transaction 中并不真的 清楚 调用的函数是 do_sth1 还是 do_sth2 ,那么我的 这个 err 是不是只能靠手工穷举所有的可能性?但是又因为 do_transaction 中你所间接调用到的子过程实际上是动态的,所以所有你现在源代码中的静态穷举显然是徒劳的。

    那么,总是会有没被检查到的 err 或者 ret 会扩散到后续的代码去,你总不能在所有的地方都准备好 panic 和 recover 吧?

    那你现在明白为什么会不定期 crash 了没?
    codeaqua
        103
    codeaqua  
       2016-01-29 13:44:03 +08:00
    错误返回 和 异常机制 就别争了,没有结果的,讨厌 错误返回? 你没写过 C 么?
    zhangchioulin
        104
    zhangchioulin  
       2016-01-29 13:45:29 +08:00
    @TangMonk 谢了哥们
    noli
        105
    noli  
       2016-01-29 13:48:27 +08:00
    @BurNFans

    按照 golang 的设计方式,要调用不定多个可能产生 err 的函数的例子,这不是很常见嘛。

    见你这么谦虚承认自己见识少,那我就给你举个例子吧,典型的 map_reduce 类型工作。

    假设你要写一个爬虫,现在有一个要爬的 url 任务列表,你要根据这个列表去获取对应的内容并且进行分析,并且分析的时候也是调用别处的代码来分析的,分析结果也跟网络一样是有可能出错的。

    那么,不管你串行也好并行也好,你总是得循环地执行这个

    result, err = crawl_and_analyze(url)

    现在由于这个任务列表是外部读入的,动态的

    你来告诉我,你打算怎么在循环体里根据 err 找出哪里处问题了?
    BurNFans
        106
    BurNFans  
       2016-01-29 13:49:20 +08:00
    @noli
    连错误返回和异常机制都没分清楚,确实没有跟你继续讨论的必要了。另外,你居然会觉得要在所有的地方准备 panic 和 recover ,所以不在一个频道上,没办法聊。
    noli
        107
    noli  
       2016-01-29 13:51:25 +08:00
    @codeaqua

    “没有结果”只是你的信念。
    实际上,软件工程的经验就是 exception 的代码结构比 返回 err 可重用性和健壮性高得多。
    noli
        108
    noli  
       2016-01-29 13:52:52 +08:00
    @BurNFans

    你觉得你很清楚,那你倒是说嘛。
    不过看你的发言风格,打一耙就走,你倒是有真小人之风。
    BurNFans
        109
    BurNFans  
       2016-01-29 13:55:11 +08:00
    @noli
    1. 我没有做任何人身攻击
    2. 我没必要跟你讨论和解释,不在一个频道上
    3. 我已经浪费了时间跟你纠缠这事了
    再见
    noli
        110
    noli  
       2016-01-29 14:57:21 +08:00
    @BurNFans

    你也确实没有人身攻击,我只是认为你说的东西 not helpful 。
    祝你在以后的道路上少浪费生命。
    cloudache
        111
    cloudache  
       2016-01-29 15:29:49 +08:00
    go 语法还是蛮好用的
    bombless
        112
    bombless  
       2016-01-29 19:44:08 +08:00
    其实无论怎样你都要包装错误的类型的,这个是没法避免的。
    比如说像 C++异常这样的机制,你会 catch 一种预期的错误,然后向外抛一个你这个层面的错误,也就是中间有个转换错误类型的过程。
    这种思路你换到 C 或者 Go 都是类似的,所以就这点怪罪 Go 的错误处理方式没有什么道理。

    实践中倒是 C++代码用异常并不很流行,比如 Google 就在它的风格中禁止使用异常。
    noli
        113
    noli  
       2016-01-29 23:45:31 +08:00
    @bombless C++ 不推荐用异常主要是因为没有 GC 。

    返回错误,和抛出异常,根本就是不一样的结构;既不是顺序也不是分支,而是跳转,而返回错误依然是一个顺序结构之中。

    异常确保不会继续向下执行,无需异常所在的上下文来保证。返回错误得自己完成这件事情。

    结构的差异大概有点像 python 的 yield 和 return 。
    codeaqua
        114
    codeaqua  
       2016-01-31 20:59:39 +08:00
    @noli 你的意思是 C 没有异常机制, 现在的操作系统还没有你写的健壮? 还是说操作系统不是你口中的软工程?(另外,我是支持异常机制的,因为我的大多数代码很多都是统一处理错误的,但是不代表我认为 错误返回 和 异常机制 孰优孰劣,场景不一样而已,所以我说的争论没有结果,还请阁下别摆出 sibi 的态度)
    noli
        115
    noli  
       2016-02-02 10:13:30 +08:00
    @codeaqua 如果你觉得你写任何代码都有操作系统的代码质量,加上足量的测试,那当然用什么语言健壮性都会很好。所以,我的说法是在软件工程经验来说,使用异常机制健壮性*更*容*易* 提高,可重用性更好。

    我不觉得你举操作系统的例子能说明错误返具有不可替代性甚至优越性,事实上,很多操作系统也是内部实现了异常抛出机制的,只是在开源的里面很少这样做而已。

    你认为错误返回和异常机制的优劣只是场景问题。那我们来说说吧,在 go 语言常用的场景,你认为什么情况下,用错误返回优与用异常 ?
    codeaqua
        116
    codeaqua  
       2016-02-02 16:24:07 +08:00
    @noli

    void foo() throws Exception {
    SomeResource r = openSomeResource();
    r.do1(); // exception
    r.do2();
    }
    这个例子正是异常机制使用不当的后果,如果项目够复杂,很多时候这种错误会难以发现。
    而 error code 不会改变正常的代码 flow, 代码的走向清晰明确。
    还有 错误 和 异常 是有区别的,但同时它们的界限有时又比较模糊,在不同的业务里面,有些错误是异常,有些可能是正常的结果(仅仅是他的类型里面有 Error 这个字样而已,但它应该被视为一个普普通通的类型)。
    exception 有好处有坏处,而好处和坏处都是来自于同一个特性----改变正常的代码 flow

    另外程序界到现在还没有得出的结论你是怎么得出来 xxx 比 xxx 更好的?
    我不想再跟你争了,因为我一开始就说了没结果的!
    codeaqua
        117
    codeaqua  
       2016-02-02 16:28:02 +08:00
    @noli
    "我不觉得你举操作系统的例子能说明错误返具有不可替代性甚至优越性",阁下别老是自己 yy ,我有表达你所说的这种观点吗?
    codeaqua
        118
    codeaqua  
       2016-02-02 16:47:16 +08:00
    c#我也在写,别拿 c#的 async 和 await 来和 golang 比,没有可比性, golang 真正的优势在于解决了异步模型的侵入性问题,什么是侵入性自己去 google 。
    上面某楼从别人网站挖的测试图,代码实现都不一样,根本没有参考价值,我写过一个 1000W 长度的随机整形数组排序测试, rust 是 0.9s , go 是 1.1s , vc 是 1.8s ,算法完全一致,我不是要表达 go 比 c 还快, vc 的编译器问题罢了,比如用 clang ,但是这个测试可以说明 go 的速度问题
    noli
        119
    noli  
       2016-02-02 18:30:48 +08:00
    @codeaqua 如果返回错误并不具有不可替代性和优越性,那么按照简单的逻辑,抛出异常的做法就是具有不可替代型和优越性——这是我的观点。逻辑上是这么说当然是不够的,但是我也在此前的回复中说明了,什么情况下,抛出异常是比返回错误更合理更符合实践的做法。

    你举的那个例子叫做 checked exception 的语法——声明了这种异常就一定要 catch ,这个只是脑残 Java 的特例, C# C++ 的 exception 都不会要求声明阶段指定 exception ;所以才会导致程序员不得不用偷懒的办法——是的,这就是偷懒,你说的这种情况——声明抛出任意异常——依然是程序员偷懒而不是异常机制有什么问题,只要程序员想“偷懒”,返回一个 err 一样可以绕过——更重要的是,返回一个 err 更容易导致 callsite 出现疏漏的问题,情形也是我此前所说的,在一个方法调用中可能出现不定多种 err ,,处理对应 err 的缺失会导致 panic 出现在预期以外的位置,更难调查。

    --

    golang 无非就是有 channel 这种东西吧,这也能够叫做非引入地解决异步模型问题?那什么语言不能呢?这本来就只是一个编程语言的库就可以解决的问题而已。

    别卖弄你的术语,侵入式 intrusive 写过 C++ 的人都知道是什么意思,倒是你把 intrusive  这个词用在同步异步模型,倒是创举了。

    如果你是想说“异步代码的传染性”,譬如 C# 里面的 async 关键字会传染得到处都是,而 golang 没有这个问题。 那你真是太小看 C# 了。

    golang 在这个问题上唯一的优势就是,语言规定死了这样解决异步问题,不需要发明其他的框架——然而在我看来,这样的语言也就适合代码流水线上的工人去用了。跟 Java 没什么两样。
    codeaqua
        120
    codeaqua  
       2016-02-02 18:40:31 +08:00
    @noli
    "如果返回错误并不具有不可替代性和优越性,那么按照简单的逻辑,抛出异常的做法就是具有不可替代型和优越性",你的逻辑我实在是看不懂。。。。

    “ golang 无非就是有 channel 这种东西吧,这也能够叫做非引入地解决异步模型问题?那什么语言不能呢?这本来就只是一个编程语言的库就可以解决的问题而已”, 侵入性。。。。哎,我无法和你沟通了,懂的人自然懂,不懂的我懒得解释了,解决了异步模型侵入性问题的目前有 3 个, go, erlang, python 的 gevent 框架, gevent 是通过替换标准库实现的。

    还有,从你的对话里面看的出阁下对自己排斥的事物会一黑到底,俗称偏执狂
    noli
        121
    noli  
       2016-02-02 19:05:48 +08:00
    @codeaqua

    对排斥的东西当然是一黑到底了,人之常情。只不过令你等不高兴了,那你来啊,我从来不介意你们说我黑子,喷子,无理取闹——反正我又不掉一根猫。

    对待排斥的东西,有的人斯文一点就呵呵,心里暗笑你个傻逼;像我这样粗鄙一点的就开喷,然后增加点谈资——心胸狭隘的人自然会抵制会反对,有眼光的人可能会看到别的不同的观点或者没接触过的领域;然后我继续很爽;我没觉得这有什么不好。

    就算你说我偏执狂,也不会动摇我说的事情本身具有的合理性——如果恰好我说的是对的,那么我对我说的偏执不是有另外一种叫法叫做“对真理的追求”?呵呵。

    我排斥 go 语言就是因为它是编程语言的倒退,当然我不排除它在某些领域确实很使用;这也算就算了,但是叫嚣什么“解决了异步模型侵入性问题”—— 这个本来就是一个 setjump longjmp 问题的扩展而已——我就觉得你们这帮 go 语言粉真是无知者无畏啊……是不是你们只听说过 goroutine 没听说过 coroutine 啊?
    codeaqua
        122
    codeaqua  
       2016-02-02 19:09:49 +08:00
    @noli “有的人斯文一点就呵呵,心里暗笑你个傻逼”, 我有什么话喷你了吗?还是你自己 yy 的?还有 coroutine 我比你知道的多,我也是软粉, c#和 typescript 我都在用,你别再给我们其他软粉招黑了可以吗?
    codeaqua
        123
    codeaqua  
       2016-02-02 19:12:18 +08:00
    还有 6L 也是,简直给 gopher 招黑,
    noli
        124
    noli  
       2016-02-02 19:15:52 +08:00
    @codeaqua 为了帮你们科普,我这么热情主动,你们还说我偏执狂。你们自己说说到底谁才值得被 BS ?

    https://en.wikipedia.org/wiki/Coroutine

    “ According to Donald Knuth, the term coroutine was coined by Melvin Conway in 1958, after he applied it to construction of an assembly program.[1] The first published explanation of the coroutine appeared later, in 1963.[2] ”

    看到没有, 1963 年就有这 coroutine 这种概念了。再看看下面的 coroutine 支持的语言列表?有没有什么感觉?

    go 语言所解决的问题,只不过就是把 io 相关的 API ,跟 coroutine 和线程池 结合在一起,做成了一个语言。然后,再顺便按照发明人自己的特殊癖好,把 exception 砍一下,加了个 gc 。只要你喜欢,随便哪个语言封装一下库都可以做好这种功能。

    我能理解你们为什么这么兴奋——因为以前你们用别的语言不知道怎么解决这类问题,现在有了,感觉自己的能力好像强了很多——是的, golang 降低了一点这些门槛,然而 golang 没有解决而你们也没有遇见过的问题依然存在。
    codeaqua
        125
    codeaqua  
       2016-02-02 19:22:18 +08:00
    @noli 你真以为就你知道 coroutine ?玩异步的谁不知道?,动不动就嘲讽,别再丢人现眼了, MD ,实在是忍不住喷你了,再见
    fire5
        126
    fire5  
    OP
       2016-02-02 19:37:54 +08:00
    同学们,这个话题结束了吧。
    noli
        127
    noli  
       2016-02-02 19:38:20 +08:00
    @codeaqua 哈哈,如有误伤,实属无奈,记得下次正确评估对手,毕竟跟疯子或者喷子开战,肯定是掉身价的,哈~ 走好不送。

    我相信多数人都知道 coroutine 是什么鬼。
    但是从 形式上 或者 实践上,怎么把 coroutine 和 处理大量 IO 的问题结合起来,不知道的人肯定更多——不然也不会有那么多人就在瞎吹 golang 了。
    zrinthect
        128
    zrinthect  
       2016-02-02 19:44:51 +08:00
    哈哈哈。恨铁不成钢。
    zhujinlong
        129
    zhujinlong  
       2016-02-02 20:04:07 +08:00
    不喜欢处理 err 的同学,你们写 C 的时候是怎么办的?
    aheadlead
        130
    aheadlead  
       2016-02-03 08:59:19 +08:00 via iPhone
    @noli 逻辑推断错误

    人身攻击很讨厌啊…
    lazydao
        131
    lazydao  
       2016-02-03 09:17:07 +08:00 via Android
    虽然有人身攻击,但认真讨论技术还是要👍。
    goool
        132
    goool  
       2016-02-03 13:02:53 +08:00
    @noli

    很惊讶的看到,并且完全不同意 107 楼的说法:“实际上,软件工程的经验就是 exception 的代码结构比 返回 err 可重用性和健壮性高得多。”

    基于我的浅薄理解, Go 的设计者认为程序中出现执行分支的大多数情况,都是 *需要* 编码者 *分析* 并 *解决* 的。如果 F 调用了 G ,现在 G 返回了一个 err ,说明 F 交给 G 的任务,已经不在 G 的 *正常* 设计之内了,简单来说, F 给了一个 G 能力之外的工作,这是 F *必须* 解决的问题。这种设计迫使你关注每一个 err ,进而迫使你: 1 、 KISS ; 2 、模块化; 3 、分层。

    而在极少数的情况用到的 panic/recover ,从字面来说,就不是用于业务逻辑的,而是用于防御和恢复, panic/recover 建立的是一个系统边界,用于处理类似这样的问题: 1 、出问题了你完全不懂吗,按一下机箱上的 Reset 按钮; 2 、我被打了一拳晕倒了,让我重新冷静一下再站起来; 3 、机械卡住了,让我们回到第一步,先抖动几下(也许就不卡了),重新开始。
    goool
        133
    goool  
       2016-02-03 13:12:40 +08:00
    大量使用 exception , try/catch 的代码,具有这样的气质:我给你一个任务,你完成就好,你搞不定?好吧,这事我也没办法了,向上抛吧。

    大量使用 if err != nil 的代码,是这样的:我给你一个任务,你完成就好,你搞不定?好吧,帮你擦屁股吧。
    noli
        134
    noli  
       2016-02-03 14:38:04 +08:00
    @goool 关注点并不是返回 err 或者 try catch 的出发点是什么,先别忙着讨论气质。

    让一个 stream socket 发送若干字节算不算 stream socket 能力之外的事情?不算吧? stream socket 尝试发送之后发现 tcp 连接已经因为超时断掉了,再算不算 *正常* 设计内应该考虑的事情? 算的吧?

    好的,既然 stream socket 发送字节和要处理超时连接中断这种事情,都负荷你说的,可以使用 err 的场合。那么现在,我要通过 send_x_target(x, targets) 并发地启动 x 个 stream socket 发送到 targets 列表所指定的目标地址,然后找出有哪些地址发不过去了。

    请问你要怎么设计 send_x_target 的返回值?

    如果你说,这种场合不适合用返回值:请问要怎么解决这类多个 IO 复合的问题?

    没想明白这类问题的抽象形式,我觉得是没有资格来讨论 try catch / err 是怎么设计的吧。
    goool
        135
    goool  
       2016-02-03 15:25:43 +08:00
    @noli 就你的具体问题,看我理解的对不对?

    你要有一个函数 send_x_target ,向多个目标 socket 发送数据。那么我可以合理的假定,对于调用者来说,每个目标 socket 是无差别的,也就是说,其中的 socket s1 超时了,与 socket s2 超时了,对于调用者来说是无差别的,采用同样的策略处理(如忽略、报错、丢弃、重试、日志等)即可。那么在这个假定之下, send_x_target 函数再加一个 failurePolicy 参数,用于决定某种失败发生时应采用何种策略。好,这是 send_x_target 函数的功能。

    然后是返回值,我想它返回一个 promise 列表即可,其中每个 promise 都是某个 socket 的处理结果,由调用者决定什么时机在 promise 上 wait 以取得相应的结果。
    noli
        136
    noli  
       2016-02-03 18:41:25 +08:00
    @goool

    返回一个 promise 列表并不是 send_x_target 的初衷:找出有哪些地址发不过去了。

    我再具体一点来说吧,这个地址发不过去,从 connect 到 send 完成,是有几种可能的情况的:

    1. 构造 socket 出错,例如因为 打开文件数太多之类的
    2. connect 出错,可能是 Connection Refused 或 Timed out
    3. send 出错,可能是等待回复超时,也可能是连接中断

    然而,这里只有情况 2 里面的 Connection Refused ,才属于 “这个地址发不过去” (因为 Timed out 可能只是本机或者远程机器响应不过来)

    3 这种情况,可能需要一点复杂的重试策略来完成任务
    如果遇到 1 , 根本不在设计的范围内, send_x_target 也不知道该怎么办

    我不知道 golang 要怎么弄才够简洁,如果是 C#,我会这样来表达:

    https://segmentfault.com/n/1330000004411185
    noli
        137
    noli  
       2016-02-03 18:53:08 +08:00
    @goool

    我预计 go 的做法会一点都不 KISS
    我期待你可以给我一点惊喜。
    bombless
        138
    bombless  
       2016-02-03 19:44:16 +08:00
    > 返回错误,和抛出异常,根本就是不一样的结构;既不是顺序也不是分支,而是跳转,而返回错误依然是一个顺序结构之中。

    这个理解应该是有点问题的,异常也是一个逐步返回的过程,这个过程我们叫 unwinding

    其实我不太理解这段回复想表达的要点是什么……

    异常和返回错误的比较显著的差异在于它破坏了类型检查,你无法预期被调用者会抛出什么异常。你写代码的时候被调用者可能返回某些异常你都考虑到了,等被调用者被修改后这部分预期也许就不再成立。
    它不像函数返回值那样是可以通过类型来限定然后通过工具(包括编译器)来检查。
    即使是 Go 这样比较弱的类型系统都可以受益于通过返回值来表达异常这个设计。
    noli
        139
    noli  
       2016-02-04 11:39:30 +08:00
    @bombless 我不理解你想说的是什么。“无法预期调用者会抛什么异常” 这个难道是你说的 “破坏了类型检查的原因?

    可是异常之所以叫做异常,就是因为它存在  没有被捕获 的可能性,才叫异常的吧?异常没有被捕获就会在抛出点导致 crash ; 而 err 被意外地忽略了,然后继续执行,程序被预期以外的数据影响到别的地方的代码然后导致 crash ,这种情况不是更应该被警惕吗?

    “返回错误,和抛出异常,根本就是不一样的结构;既不是顺序也不是分支,而是跳转,而返回错误依然是一个顺序结构之中。”

    这句话你可以这么来理解: 在一个 try catch block 中间的代码,你可以认为是用一个独特的 goroutine 来运行的;这个 goroutine 与 try catch block 之外的上下文之间有一个 channel 用来通知异常。所以,当异常发生的时候, try catch block 中间的代码只是被中止了执行并没有自行退栈,而异常被传递了出去;返回 err 则不一样,函数被认为已经完成了随着 return 会退栈。

    在局部上,你可以认为,在 try catch block 之间的每一行代码之间,编译器都会自动地加上等价于 golang 的 if err != nil { notify_exception(...) } 这样的代码(事实上 C++ 的编译器就是用类似的原理来实现的,会记住代码帧执行到哪一步) —— 或许这也是某些人认为 返回 err 和 try catch 除了手工增加防御性的代码之外,并没有本质的区别 ,然而 ——

    notify_exception 把控制权交给了上层代码,这个上层代码并不仅仅是 try catch block 所在的上下文代码,还包括了调用栈上更靠近栈底的所有函数帧上的代码,就是说,那个用于通知异常的 channel 的 scope 是跨越 多个调用层次的;而没有 exception handling 机制的语言,例如 go 例如 C ,要做到这样的效果,就必须在相关的代码上层层防御——有没有嗅到一阵代码耦合的气味?

    用 C 反而会更安全一点,因为 C 只能返回一个值,而调用者 *总*是* 有责任去判断这个返回值是否符合预期,譬如 bsd socket 的 recv 返回类型是 ssize_t , recv 的简单形式是告诉调用者接收了多少个字节(理应是非负数)然而 ssize_t 也说明了它会返回负数值 (异常发生),在这种情况下,其实就是一个弱化版的 checked exception 也就是非常接近 Java 的那种做法的本质——你*应该* 在调用的现场就处理好所有的异常,所以层层防御并没有在 C 里面成为现实,但即使是这样, C 代码依然非常容易耦合抽象程度很低。而 Java 的 checked exception 得益于其标准库的完备性,在编译的时候就已经强制保证了所有 exception 都要正确地被 catch 。 C 还是要看程序员个人修养。

    然而 golang 用多值返回来描述异常,就打破了这种类似于 checked exception 隐含的强制性。多了一个 err 返回出来,就意味着懒惰的程序员总是有办法把责任往上扔,并且不怎么影响函数的返回值的设计,因为可以多值返回嘛——这跟懒惰的程序员总是接住所有的 exception 又不处理简直是一样的;

    你甚至可以自己去看看 golang 自己的官方例子,产生了一个 err 之后,有什么东西可以阻止偷懒程序员或者做得昏头昏脑的程序员,继续去使用无意义的返回值去做任何事情吗?难道把 err print 出来事情就完了吗?

    更坏的是,经过多层往上推卸责任之后,上层代码已经无法知道 扔上来的 err 到底是个什么鬼了——越是靠近调用栈栈底的函数,这个麻烦就越大,因为调用链越长,可能调用到的其他函数的范围越大。这个时候你必须猜这个 err 的所有可能性,想小心翼翼地处理这个 err 只能看运气,遗漏了处理某些具体类型的 err 的风险总是存在。
    从这个角度来说, C++ 可以 throw 任何东西上去实际上也不是什么好的设计,尽管 C++ 还可以用 catch(...) 的语法来保证接住抛上来的任何东西,在需要阻止异常扩散的时候还有最后一招。

    如果使用类似 C# python 等等的这种异常机制,首先是解除了处理 err 的耦合,并且总是保证,如果异常没有被恰当的接住就立即 crash , crash 的时候还可以保留现场,告诉 debug 的人是哪里导致的 crash ( golang 是做不到的,想好好地 log error 只能靠所有人都自觉),实际上这样才能更严格的要求调用者考虑清楚异常的类型;如果写的时候没有考虑清楚,那么跑的时候总是会的。

    就算懒人程序员用类似于 catch(...) 这样的方式来偷懒绕过所有的检查和避免 crash ,它也没有办法把错误数据的影响扩散到其他地方,不会危害到其他地方的代码安全性。

    综上所述, golang 的多值返回 err 的设计,就是事情没干得更好,倒是更容易让人做出更坏的事情出来——简直就是开历史的倒车。
    noli
        140
    noli  
       2016-02-04 11:51:27 +08:00
    想起 @codeaqua 说 try catch 会破坏正常代码的 flow ,我想想也是笑了,好像 golang  的 defer 不会改变 flow ? python 的 yield 难道也不改变 ?
    goool
        141
    goool  
       2016-02-04 12:24:02 +08:00
    @noli 我看了你的代码,不是很懂你的 shouldStopTrying 是在干嘛,另外代码中那么多 Exception 不明白是在表示什么。

    但鉴于你 *真的* 写出了代码,那么我也要写出代码以示尊重: http://pastebin.com/czVfkiuL
    goool
        142
    goool  
       2016-02-04 12:27:59 +08:00
    我认为你后续的讨论有诸多问题,我要滚回家了,简单说一下:

    try/catch 强迫程序处理错误,而返回值没有强迫,所以返回值不好—— NO ,绝对的误解!
    golang 的 defer 会改变代码的执行流程—— NO !
    noli
        143
    noli  
       2016-02-04 14:13:32 +08:00
    @goool

    那个 shouldStopTrying 其实目的很简单,就是不停地重试。最后应该加个 finally 判断一下什么时候不需要再重试了,但是我没有加,这要看需求,但是不影响 SendContentToTarget 。由于在 C# 里面 class 类型( HashSet 是一个 class )的参数传递都是 by reference 的,所以可以通过多次调用同一个方法,来不断地移除 HashSet 里面的元素。

    我先说说我对你的代码的理解:

    你的 send_data 目标应该是类似于我代码里面 29-53 行的 try block 里面做的事情。
    然后,你的 main 函数里面做的,目标应该是我的代码里面 SendContentToTargets 做的事情。
    你用一个 channel 来把“发不过去”的地址传出来。

    然后,我俩的代码都回避了返回值的设计,我感觉我俩都没到点上。

    如果对你的代码没有理解错的话,我认为有以下几点造成我和你的代码的实际功用是不一致的:

    1.
    发消息或者等回复的失败,并不应该视作“地址发不过去”,而是应该重试。
    所以在我的代码里第 66 行会把这个事情扔给 SendContentToTargets 的调用者来处理,譬如说如果重试次数超过了多少,就不试了,认为它不发送不过去。 shouldStopTrying 就是为这个用的。

    2.
    如果目标地址的列表太长了,导致打开文件数量太多,那么运行到一定程度之后,我的代码会在第 31 行或者 32 行抛出异常,但无论是在 54 行接还是 92 或者 96 行 catch ,都不会导致判断 “该地址发不过去”。

    但是在你的代码的第 12 行 看起来并不会做这个区分(连接超时还是打开文件数到达上限),并且我很好奇,如果其实根本没有连接上,那么 defer 的时候又手动 Close 了一下,是不是里面会自动判断有没有真的连接上? C# 的 try catch 机制在这个 socket Dispose 的时候会做合适的处理,然而 golang 里面虽然有 GC ,但是这资源的释放跟 GC 似乎是两回事,不知道 golang 会怎么处理?

    如果是自定义的对象而不是 golang 标准库的对象呢?
    当然,以上的这些你都可以认为是无关重要的细节……然而,正是这些细节会影响你的代码的可重用性和健壮性。



    其实我希望看到的是,用 golang 做这个事情的时候,是不是能够把不是 send_x_target 该管的东西放在 send_x_target 之外的代码来处理。

    譬如,在 ** 不更改 send_x_target 的签名前提 ** 下,我可以这么改:

    https://segmentfault.com/n/1330000004414242

    把控制什么应该重试,什么应该排除的条件指定放在外面,这样, send_x_target 就变得更加纯粹了。
    cloudzhou
        144
    cloudzhou  
       2016-02-04 14:36:11 +08:00
    golang 的 error 目前是我比较困惑的地方,因为迫使你不得不认真的思考错误处理,否则容易漏掉。

    @noli 我看了你的代码是在 exception 做 switch ,实际上,这和 Golang 的 err 设计类似的,
    你可以理解 Golang 里面的 err 就是枚举,对应一个个的 exception
    对于 network 的 error , golang 的开发者只是做的比较简单,比如 net package 的 error :

    var (
    // For connection setup and write operations.
    errMissingAddress = errors.New("missing address")

    // For both read and write operations.
    errTimeout error = &timeoutError{}
    errCanceled = errors.New("operation was canceled")
    errClosing = errors.New("use of closed network connection")
    ErrWriteToConnected = errors.New("use of WriteTo with pre-connected connection")
    )
    也许还有其他几种错误。

    所以按照刚才 @goool 的代码,在 channel 的遍历阶段,把 error 做 switch 决定是否算错误就可以了。
    但实际上,单纯从你们写的这两代码来看,我觉得, Golang 显然容易理解多了。
    我阅读一些开源代码,确实是存在需要关注指定 error ,而不得不使用 string match 的方式来匹配。是比较难看的方式,但这是可以改进的。

    但是从我以前 Java 的经验来看, Exception 改变了程序流程的运行是一个很大的问题。
    noli
        145
    noli  
       2016-02-04 14:36:59 +08:00
    @goool

    “ try/catch 强迫程序处理错误,而返回值没有强迫,所以返回值不好”
    这不是我的观点。

    我的观点是, golang 通过多值返回 err 很容易导致 “推卸责任的行为难以收拾”,所以 golang 通过多值返回处理 err 是个逗逼的设计。 而不是“返回值不好”。


    “ golang 的 defer 会改变代码的执行流程”
    这个也不是我的观点。

    我的观点是: defer 会使得看代码的顺序和代码执行的顺序不一致,十分烧脑。
    不信请自己看 https://tour.golang.org/flowcontrol/12

    package main

    import "fmt"

    func main() {
    defer fmt.Println("world")
    defer fmt.Println("Awesome")
    defer fmt.Println("Golang suck")

    fmt.Println("hello")
    }

    试试猜猜输出顺序是什么?
    如果再嵌套多几个别的资源,你觉得这不是一个更加反人类的设计吗?

    另外多说一句 defer 会使重要资源的释放不必要的推迟(必定是到最后才回收),又是一个脑残设计
    cloudzhou
        146
    cloudzhou  
       2016-02-04 14:42:04 +08:00
    对于大型开发中,可能需要引入自己的 error list ,比如
    https://github.com/go-sql-driver/mysql/blob/master/errors.go 的 MySQLError
    然后和 mysql error number 做对应,调用的时候决定错误类型已经怎么处理。
    noli
        147
    noli  
       2016-02-04 14:48:52 +08:00
    @cloudzhou 你说的这一点就是我为什么特意用 C# 的 SocketException 来做对比的地方了。

    事实上, SocketException 就应该为每一个不同的 ErrorCode 独立写成一个 SocketException 的子类。这样根本就没有什么 switch case 的需要,编译器帮你做了。 C# 这么做纯粹是历史遗留问题。

    而没有 exception 就必须总是用类似与 switch case 这样的分支结构来处理这些错误,也就是我说的为什么容易导致疏漏的原因。

    再况且,只要它是一个 exception ,不 catch 程序就要死掉。不会让错误扩散, golang 呢?骗过了编译器以后,运行时的错误结果还是会扩散出去的。

    你非要说 exception 改变程序流程,那是因为 Java Exception 的脑残设计,滥用 Exception 把它当作类似于 yield 这样的东西来用。
    难道 golang 的 defer 就不会被滥用了? 到时候情况会比 Java 好很多吗?
    cloudzhou
        148
    cloudzhou  
       2016-02-04 15:06:38 +08:00
    @noli 按照例子,你说要对不同的错误做针对的处理,那么无论如何避免不了 switch ,只是以各种不同的 exception 表现而已。

    对于错误的检查,如果忽略,确实会存在问题,错误会一层层的传递而不处理。
    defer 在 Golang 共识是不能滥用,这和不处理 exception 一样的道理。

    Golang 的一些设计,不是说刻意这么做,开发者自己说了,“没想好怎么做”
    以后 Golang 语言层面如果有什么变化,也不奇怪。

    我有很长的 Java, Python, 和半年 Golang 经验,但是在 Exception vs Error as Value 比较中,我自己也想不出什么合适,只能从语言 taste 来理解。
    noli
        149
    noli  
       2016-02-04 15:56:11 +08:00
    @cloudzhou

    switch case 和 exception 的作用肯定不一样啊。

    1. 如果 SocketException 的各种 ErrorCode 都以 SocketException 的子类出现,要么我用 SocketException 一个 catch 把各种子类全部捕获了,在这种情况下,你才需要根据子类细分再手动 switch case 来处理 —— 这时候很有可能会漏掉某种子类,这个弊病跟用返回值处理是一样的

    2. 然而你也可以 catch 每一个 SocketException 的子类,这样就是编译器替你做了 switch case
    这种情况下你一样可以针对细分情况分别做处理:

    再然后
    2.1 如果我的疏漏了 SocketException 的子类没有捕捉,并且也没有 catch SocketException 的本尊,那么,会 crash

    2.2 如果我最后有捕捉 SocketException 本尊,甚至直接 catch Exception (所有 Exception 的共同父类),那么尽管我依然遗漏了情况没有处理,但是因为 catch 了, exception 发生后的代码就不会被执行了,程序不会 crash 并且后续的代码不会意外地使用了无效的返回值。

    也就是说,像 golang 里面这样的错误, 也是 @goool 代码里面的问题:

    func send_data(n int, target string, content string, result chan Pair) {
    conn, err := net.DialTimeout("tcp", target, time.Duration(1 * time.Second))
    if err != nil {
    result <- Pair{n, err}
    return
    }
    defer conn.Close()

    DialTimeout 的作者不必在异常发生时硬要给 conn 塞一个值,
    而调用 DialTimeout 的时候也不必防御性地 return
    要一遍又一遍地重复写这些 resut <-Pair{n, err} return 这种代码,我真的不能认为它 DRY

    再况且,我本来出这个题目,就是要考验 golang 处理错误的能力。
    他的代码根本就无视了这一部分的逻辑,你看起来当然简洁易懂咯。
    实际上,就以这段为例,至少要检查一下 err 的具体情况,才能决定 Pair 是不是应该送出去。
    这时候你就会发现谁的代码里面到处都是 switch case 了


    再看看 抛异常的方案, send_x_target 的主要逻辑 就这几行:

    var client = new TcpClient();
    await client.ConnectAsync(endpoint.Address, endpoint.Port);

    var stream = client.GetStream();
    await stream.WriteAsync(content, 0, content.Length);

    var reply = new byte[1024];
    var respLength = await stream.ReadAsync(reply, 0, reply.Length);

    两个版本里面都没有变过

    你觉得“ Golang 显然容易理解多了”
    我只能说你的口味和我的太不一样了
    cloudzhou
        150
    cloudzhou  
       2016-02-04 17:38:16 +08:00
    @noli
    他的代码例子, switch 是在外层处理的。

    按照我阅读 Golang 官方仓库的理解, Golang 希望的是开发者这么处理错误:
    1 统一的错误返回方式
    2 开发者自己要去处理 error 结果, error as alue 指的是 error 和 return value 一样重要,不可以忽略,在返回之后就要立刻处理
    3 根据 error 的严重程度定义级别,封装,比如 net.Error 里面引入 Temporary() 接口
    4 如果要对 error 进行详细处理, cast 然后 switch ,或者可以引入 error number (比如 mysql driver)

    作为开发者而言,你要考虑封装自己的 error ,而不是一概的抛出去。
    对于我目前的体会就是,两种方式我都能接受,因为 error 不管那种方式,都是需要自己去仔细考虑的。
    xxxcat
        151
    xxxcat  
       2016-02-09 21:30:09 +08:00
    C#粉战斗力有两颗猴腮雷,但还是比不上 PHP 粉的三颗猴腮雷!
    1  2  
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   1253 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 29ms · UTC 17:55 · PVG 01:55 · LAX 10:55 · JFK 13:55
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.