V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2EX  ›  kuanat  ›  全部回复第 5 页 / 共 9 页
回复总数  164
1  2  3  4  5  6  7  8  9  
129 天前
回复了 bocchi1amos 创建的主题 Python 为什么 Python 会有.venv 虚拟环境的概念?
@frostming #54

我又看了一遍自己写的东西,其实不太准确,我这重新描述一下。

版本选择之所以是 NP 问题,源于好几个假设。其中有几个假设是没法改变的,能够改变的还有两个:

- 一个包可以声明自己依赖零个或多个特定版本的包。
- 不允许同时选择同一个包的两个不同版本。

第一条过于严格了。如果把特定版本换成一个范围,这个问题就简单多了。第二条也可以适当放宽,和第一条一样,都是建立在 semantic versioning 主动向后兼容的假设之上。虽然不能引入同一个包的 1.X.X 和 1.Y.Y ,但是可以同时引入 1.X.X 和 2.Z.Z 。

这两个条件放宽之后,NP 问题就不存在了。但是对 APT/RPM/Node/Python 这些起步较早的社区来说,这两个要求依然太难了。

至于主包覆盖依赖的限制,这是用来解决菱形依赖的,不是规避 NP 问题的核心措施。还是之前的例子,只要保证 C 符合 semantic versioning 向后兼容就好了。这个措施实际体现的是解决问题的思路转变。

具体的来源比较分散记不清了,我凭印象做个总结。第一个做 NP 规避的应该是 Rust/Cargo 。之后 Go 做了一个叫 Dep 的类 Cargo 实现,实践了一段时间,吸取经验教训,重做之后形成了正式的 Go mod 方案。Rust/Go 用相同的方法解决菱形依赖,但是基础思路是不一样的。这一点从 Rust 总是选择最高版本,而 Go 总是选择最低版本能看出来。

这里想象一个场景:A==1.1.0 依赖 B==1.5.0 ,之后 B 发布 B==1.6.0 ,由于 B 引入了非兼容改变,导致 A==1.1.0 无法依赖 B==1.6.0 构建。这个时间节点,按照 Rust 的设计,所有依赖 A 的用户(包括依赖 A 的老用户)都会受到影响,而 Go 这边只有同时依赖 A==1.1.0 和 B==1.6.0 的新用户(老用户不受影响)才会受影响。

之后 A/B 至少有一方要打补丁发新版。Rust 认为要么 A 在 B 没有提供补丁的情况下,主动发新版声明不兼容 B==1.6.0 ,要么 B 发新版修复对 A 的支持。实际上在开源社区这两个事情都是很难的。Go 的设计是除非用户主动升级,都会保持作者发布时的最低依赖版本,(毕竟发布那个时刻的版本依赖几乎是都可以构建的)这样就给 A/B 争取了非常多的修复时间。

Rust 的设计者更希望为开发者提供最好的体验,希望一己之力解决所有问题。Go 的设计思路是依靠社区合作,依赖所有人主动去帮助解决对自己来说比较容易,而别人不好解决的问题。所以你能看到,Go 官方一直不遗余力地推动向后兼容,因为 Go 的整套实现逻辑都强依赖整个社区对于兼容性规范的共识。



最后赞美 pnpm ,感谢!
129 天前
回复了 hankli 创建的主题 程序员 试一下用 VersionFox 替代 asdf-vm?
@hankli #7

我一开始没注意到 VersionFox 是跨全平台的。为了兼容 Windows/PS 的话,确实单可执行文件比脚本靠谱。(我觉得理智一点的开发者,在 Windows 环境不用 WSL ,也应该用 MSYS2 吧哈哈)
129 天前
回复了 hankli 创建的主题 程序员 试一下用 VersionFox 替代 asdf-vm?
支持一下~

asdf shell 一直都有,global/local/shell 都支持。我记得 shell 的实现比较粗暴,就是类似 ENV=xxx cmd 的方式。

shim 机制的问题在于需要 reshim ,比如 python pip 安装了某个可执行文件,需要 reshim 才能在当前 shell $PATH 索引到。极小概率的情况,比如某些程序运行后释放执行脚本,又硬编码 #!env python 这样会导致出错。

asdf 有个 direnv 插件,可以管理环境变量,解决每次执行都要 shim 查找的问题。(话说慢真是个问题吗?)

asdf 的优点是插件可以比较方便复用已有的构建脚本,本地构建安装而不是下载二进制文件。

另外我个人比较喜欢这种脚本都用 git 做管理的模式。
129 天前
回复了 bocchi1amos 创建的主题 Python 为什么 Python 会有.venv 虚拟环境的概念?
@kuanat #47

再做一点补充。

Python 3 是 2008 年左右发布的。这个时间点上,如果 Python 决定重写 import hook ,将版本号纳入成为包名的一部分,支持安装同一个包的多个版本,就没有今天虚拟环境什么事了。

这个改动和“不做官方包管理”不冲突。不做官方包管理是正确的选择,在那个时间点上,很难做得好,特别是这些实现都要用 C 来写。

但是底层限制所限,同一个解释器环境不能安装同一个包的多个版本,那包管理器是永远无法摆脱虚拟环境的。
129 天前
回复了 bocchi1amos 创建的主题 Python 为什么 Python 会有.venv 虚拟环境的概念?
“为什么要有虚拟环境”这个问题比较好回答,就是为了隔离。我觉得楼主真正想问的是,或者说这个问题有意义的点在于,为什么 Python 没有“现代”一些的包管理机制?

楼上已经有不少人提到了,Python 不支持同时安装一个包的多个版本。要解释“为什么”不支持,那就比较麻烦了。

这要回到 Python 最早设计包这个概念的时候,当初的决定导致了,不可能在不做 breaking change 的情况下,改变这个行为或者说添加版本机制。这个设计决策的时间点在 1.X 版本,估计大部分人都没有用过,好在 PEP 文档还是可以一窥设计思路的。


我这里提个看上去无关的问题,为什么 Python 需要 __all__ 来支持 from XXX import *?

答案是 Python 的 import 机制是基于文件系统的,包名就是路径名。基于文件系统就意味着,包名无法区分 XXX/Xxx/xxx 的,因为文件系统不确定是否是大小写敏感的。

你可能会觉得包名就是路径名,那给包名后面加上版本号不就行了吗?理论上是的,很多现代一点的语言的包管理就是这么做的。那 Python 为什么做不到?主要原因是 Python 流行起来之后,不可能在不影响生态的情况下做这样的改动了。理论上 Python 2/3 的时候有这个机会,但是 Python 没有做这个变动,这与 import 的实现机制有关。

Python 的 import hook 包括标准库,都涉及到自举( bootstrap ),所以是用 C 写的。更重要的是,增加版本号意味着 Python 要官方实现包管理机制,而包管理是个理论和实践都非常难的事情。Python 选择继续采取社区的“虚拟环境”方案,我个人认为这是个正确的选择。


这个问题到这里其实就差不多说清楚了,不过我打算再补充一些内容。

包管理问题的难点在于版本选择,目的是要找到某个包的完整的(所有)、兼容的(不能包含相冲突的,比如同一个包的两个版本)依赖。这个问题有多难?我印象大概 2017 年才有证明,这是一个 NP 完全问题。NP 完全的意思就是,不知道是否存在一个可以稳定在多项式时间内,完成这个解析过程的算法。

所以几乎所有的传统包管理软件只能做个二选一,要么选择正确但是有可能很慢,要么选择时间可控但是可能出错。实际上几乎所有的包管理都选择正确但是慢,或者说正确但是不清楚要花多长时间。前面的证明给了包管理器新的设计思路,要增加限制条件或者说妥协,不去追求完美解决 NP 问题,只做工程上“好用”的解决方案。

具体理论不展开了,我这里简单说一下“现代”包管理的两个基础假设。一是高版本总是向后兼容低版本,不兼容的情况使用 semantic versioning 的 MAJOR 区分。二是主包可以改写间接依赖的版本。第二条不好理解,举个例子:A 是开发者要构建的包,这里叫主包,它直接依赖 B 和 C==1.2.0 ,其中 B 又依赖 C==1.1.0 。这时候要构建 A ,那么 A 对于 C 的要求就会覆盖 B 对于 C 的要求,又因为 C 的 MAJOR 版本没有变,理论上 C==1.2.0 是同时满足 A/B 需求的。当然单独构建 B 的时候,B 作为主包,依旧会使用 C==1.1.0 的版本。这一条放宽了之后,NP 问题的限制就不存在了,这个问题就有了多项式级别的解法。

我接触过的包管理,似乎只有 Rust(cargo)/Go 是基于新理论(即主动规避 NP 问题)的。区别是 Rust 总是选最高版本,而 Go 选最低版本。Go 的实现比 Rust 简介很多很多。其他的包管理都还在跟 NP 问题作斗争。
129 天前
回复了 Goooooos 创建的主题 Java 吐槽下 Google 开源的组件
很多时候不兼容的改动都是“无意识”的,开发者以为修改是向后兼容的,但实际上不是。这种错误我犯过不知道多少次了……


关于向后兼容这个话题,我推荐两个讲座,都有视频和讲义:

- How to design a Linux kernel interface, https://archive.fosdem.org/2016/schedule/event/design_linux_kernel_api/

- Detecting incompatible changes, https://sourcegraph.com/blog/go/gophercon-2019-detecting-incompatible-api-changes

简单做个总结。Linux kernel-userspace API 早期的失误造成的影响已经持续了几十年,很多还将继续影响下去。这些失误的来源多数是设计者想象不到下游开发者会以什么样的方式去使用这些 API 。

规避的措施就是自动化测试,而自动化测试需要对 specification 进行规范化。实际上推动了开发流程的改变,即先公开规范和 API 设计,提供实例代码,接受反馈并修改设计方案,最后再实现功能和测试。而不是过去的先做实现,然后公开 API 接口的模式。

(这个事情做到极致大概类似于 amazon ,先开发布会,然后再开发)

第二个讲座虽然是 Golang 的,但指导意义很大。核心是如何从技术上自动判定某个改动是否会影响兼容性,进而对如何编写“面向未来可兼容扩展”的 API 接口提供理论支持。Golang 的开发团队为 Go1 的兼容性做了很大的努力,有非常多的案例经验可以参考借鉴。
131 天前
回复了 rivercherdeeeeee 创建的主题 骑行 自行车选择合理推荐
我推荐个迪卡侬 Gravel 120 ,官方价 3999 的样子。迪卡侬自有品牌性价比比较高。

这个型号一般叫瓜车,通过性介于山地和公路之间,城市间骑行偶尔有烂路什么的问题不大,比公路好多了。

车架几何比较舒适,只有单盘,齿比只能说 40 速度够用。再就是这个价位因为套件什么的都比较省成本,装配看脸,多让师傅调一调。
我提供一点额外的视角,有关面试的。这个活动前两天上了 Hacker News 的头条,长期看 HN 的话会注意到类似的活动还有其他语言的版本。

很早之前我司有个内存远小于数据集的版本的同类面试题,后来我主张给砍掉了。原因有两个方面,一是区分度不高,用在校招上,绝大部分人难以回答应用相关的优化,都集中在算法上;用在社招上,脱离了主要应用场景,和八股文没什么差别。另一个方面是开放式问题对面试官要求太高,对于不在预设方向的答案难以量化打分。



回到这个题目本身。

纸面上的问题有纸面上的解法,现实里的问题有现实的应对策略。可能直觉上这是个算法题,但实际上作为比赛,那就是算法、经验、工程、原理和细节多个方面的事情了。

- 经验

社招的技术面试更看重经验,但是经验这个东西很难准确量化,甚至连标准化量化的手段都很少。

我个人对于经验的定义是“分析、定位问题的能力”,这个能力是在技术之外各行各业通用的。一般来说,能准确地用技术语言描述问题,用客观可验证的手段确认问题的核心所在,那问题就已经解决了大半。

所以我在面试里更喜欢问一些类似 profiling/debug 工具、流程的细节,因为我觉得愿意用工具、数据辅助分析,水平是要高出凭印象去解决问题的,当然这是我的一家之言。

这个投递第一天上 HN 的时候,最高的讨论是有关 IO 瓶颈的,然而这个方向是错误的。即便是 HN 这个水平相对较高的社区,低水平的讨论也是大量存在的。当然现在再去看经过社区筛选的高赞结果,就比较有指导意义了。

- 工程

考虑测试环境是 Linux 而且 RAM 32GB > DATASET 12GB ,整个测试跑五次,去掉最高最低结果。除非这个程序特别离谱,占用了超过 20GB 的内存,那么绝大多数情况下,这个文件已经在 page cache 里面了。所以跑一次是 IO 问题,跑很多次就不是 IO 问题了。

一个非常 naive 的单线程实现大概是 240s 左右,不考虑其他因素的话,8 核心测试机上可以预期到 30s 的水平,观察一下榜单,基本上 60s 以内的方案都用到了并行处理。

把工程因素放到最前面说是因为它以最低的成本实现了最大程度的优化。但是这个事情作为面试题是不合适的,因为掌握多线程是最基础的,面试的目的并不是为了区分会不会多线程。

- 原理

这个场景确实存在 IO 瓶颈,但它发生在 kernel/userspace 之间,而非 RAM 与外置存储之间。用户态 read() 需要 syscall 将 kernel 管理的 page cache 里的内容复制到用户空间。这个行为越多,效率就越低。而 MMAP 机制绕开了这个 context 切换,所以提供了真正的 IO 层面的优化。

如果再观察一下榜单里的实现,基本上 30s 上下的方案里,都用了 MappedByteBuffer/FileChannel 之类的调用。换句话说,理解 MMAP 可以在多线程的基础上,稳定将结果优化到多线程的理论上限。

但这个筛选条件略微苛刻了,要么需要比较好的科班基础,要么就要恰好做过类似的业务。最终在进入技术面的数量有限的人群里,能准确回答的寥寥无几,导致缺乏区分度,反正大家都不会。(不是水平问题,而是几率问题)

再往下就是字符串处理了,我相信如果跑个火焰图看一下,30s 上下的方案性能热点应该在自定义的 parser 上。

因为这个竞赛是 Java 的,开发者更习惯的是用 Java 提供的高级抽象,相关的实现都是内存/线程安全的,但是会因为各种检查变慢。

这里不好说到底能优化多少,但是 25s 以内的实现方案里的 parser 基本都是扫描特定字符,而不是用标准库字符串方法了。10s 左右的方案几乎都考虑到了内存分配、变量重用等等技巧。但是这样非常具体的案例在面试里是没有可考察性的,除非应聘者自己主动提出了这个方案。

- 算法

终于轮到算法出场了。对于这样一个场景,算法唯一的要求就是 one pass ,意思是边读取边计算已经读取过的结果里的最大、最小和平均,而不是完全缓存之后再统一计算。

如果是求中位数的话,我觉得倒是可以拿出来讨论。我不清楚是不是有完全准确的 one pass 中位数算法。回到现实里,对于这么大的数据集,求解目标要不要完全精准有可能在数量级上影响实现效率。所以真正考察算法的时候,我会偏好问 bloom 过滤器之类的方向。

这其实也是整个问题里我相对认可的一个点,与其空泛的问 MapReduce/Streaming 之类的,完全不如考察在解决其他问题时,展现出来的触类旁通的能力。只是这个问题又太简单了,而且性能提升很小,甚至性能提升的主要来源是避免了内存分配和 GC 。

多数时候我们都希望开发者有算法基础,但是实际应用里却不希望开发者手撸算法。面试过程里考察对于算法的理解更多体现在应用层面,知道并理解常见算法的复杂度上下界,以此可以来指导方案选择。

但现实的问题是,要么大家都会要么大家都不会,就比如这个场景里,所有人都会选择 hash 结构用来存储结果,所以没有区分度。

- 细节

进入到 10s 水平,再往后的优化几乎就是抠实现细节了。

这个层面的优化,和问题场景强相关,意思就是这些手段是难以复用的。对于 C/C++ 背景的开发者比较好理解,算法逻辑是算法逻辑,数据检查/判断分支是另外的事情。所以剩下的操作无非就是:用 unsafe mmap 替代安全的映射调用,用简化的 hash 方法替代标准实现等等。

还有一个方向是向运行环境做匹配,考虑指令集、cache line 等等微观特征。考虑这里的瓶颈不在 cpu ,我怀疑 SIMD 的方案的有效性。到了这一步,看结果的话又提升了 30%,但实际生产环境可能没人会这么奔放吧。作为面试题来说,能聊到这一步应该 100% 过关了。

这里再补充个插曲,之前有别的面试官面过一个 OI 出身的应聘者,提了一个基于状态机的方案,然后当场把面试官说懵了。后来几个人回看面试记录,证明他说的方案是没问题的。这就是我说的开放性问题对于面试官的要求太高了。

行业里的开发者,一般非科班出身很少会接触状态机。主要原因是业务上需要状态机相关算法的场景非常优先,以状态机作为优化手段的可移植性又太差,除非特别核心的逻辑不得不用,很少会主动选择。



反正现在的就业环境就这样,面试造火箭。我写这些东西就是提供个思路,其实把视角拉远一点,这个具体的题目没有多少值得讨论的东西,解决问题的逻辑更加重要。
141 天前
回复了 Cooky 创建的主题 Linux Arch 大危机, Gentoo 开始提供二进制包
Gentoo 提供二进制预编译内核大概一年多了吧,这个比较重要。相对来说软件包二进制版本没那么必要,毕竟把 Gentoo 当 GUI 主力的用户,基本都有自己的编译机。

我用 Gentoo 的时间也有十几年了,但我的主力桌面系统是 Fedora ,Gentoo 更多是个 meta 系统,用来完成特定任务和功能的。

放到十年前,我会把 Gentoo 当作服务器的主力系统跑,现在更多是用它来作为基础构建系统,来打包特定的软件,做成容器镜像使用。

另一个用途也还是当作构建机器使用,用来配置交叉工具链,编译一些嵌入式设备或者不同架构上的应用。主要是考虑到它基于源代码的特性,一方面比较好排查依赖相关的问题,另一方面 use flag 比较好做版本和特性 pinning 。

还有一个重要特性是 prefix ,用来在 mac 或者主力 linux 上,管理那些需要自行编译、不在官方源当中的软件。
单纯映射上下左右有很多方法,一般要么是 asdw 要么是 hjkl ,这个改键可以从系统层面全局做。如果只是在编辑区用,多数都是类 vim 的插件方式。

但是 IDE 层面,没有哪一家真考虑过对纯键盘做支持。IDEA 不行,VS 也不行。即便它们都有类似切换显示界面的功能(比如开启、关闭文件列表区、内置终端),但是都没有输入焦点的设计,展示了对应的界面,输入焦点不一定能切换过去。

再就是缺少统一的快捷键逻辑,比如现在的输入焦点在内置终端里面,那很多 ctrl 的快捷键组合就会和 IDE 本身冲突了。
反向端口映射

adb reverser tcp:80 tcp:8080

第一个 80 是模拟器里面的,浏览器访问 localhost 80 会转向 host 8080 。
143 天前
回复了 shuiguomayi 创建的主题 Linux 这是对 Linux 正确的使用方法么?
分区话题比较复杂,一般性的建议是用发行版默认的文件系统和分区表。

另外 ESP 比较特殊,取决于 firmware 支持,大部分要求是 FAT32 ,在苹果电脑上要苹果的那个格式。挂载点现在推荐 /efi 或者 /boot ,特别是多系统引导环境,绝大多数情况下 /boot/efi 一样用。

发行版的 Point/Rolling Release 现在的分界线不是特别明显了,往严格里面说 Debian 这种算 Point Release ,而 Arch 这种算 Rolling ,像 Ubuntu/Fedora 介于二者之间。可能看 LTS 支持会更靠谱一些。我个人的分类是看官方维护的内核是只 backport 补丁,还是会追 mainline 版本。Rolling Release 也可以假装当 Point Release 来用,只要把官方内核版本锁了就行。

如果你希望系统层面上支持回滚,可以考虑 Fedora Silverblue 这类基于 ostree 的,或者 NixOS 这种声明式的,核心思想都是 Immutable system partition ,升级过程类似安卓设备上常见的 A/B partitions 。
145 天前
回复了 Befehishaber 创建的主题 Linux 请教下不同系统间的硬中断响应时间
这个事情优先看 jitter 容忍阈值,其次才是 latency 。如果几百毫秒延迟可以接受的话,我估计 RT-Preempt 内核是可以做到的。RH 系还有 Ubuntu 都有相应的发行版,只是都是收费的。

音频相关对实时性要求比较高,一般现场回放延迟容忍大概在 20ms 以内,如果是舞台耳返的话,大概是 5~10 毫秒左右。目前还是很难用 linux RT 做实时效果合成的。

我印象 x86 RT 大概 99% 的话可以做到 30ms 上下,这个要看具体硬件情况。
如果没有硬件级别的漏洞,一般来说 TEE Attestation 是无法伪造的。

但是能用 TEE 的场景很少,因为你要在设备出厂的之前就把私钥写入 TEE 里面,所以基本上只有手机厂商和微信(支付)这种会用。使用的方式主要是用 TEE 内置密钥在 TEE 内进行加密解密,或者在 TEE 内用内置密钥对一段信息进行签名。

你说的这个场景,属于 TEE 签名应用的一种,这个结果是无法伪造的。

在无法伪造 TEE 结果的情况下,要伪造 BL 设备信息等等,用的方法是降级。某些早期设备并不具备 TEE 硬件,所以能够通过 hook 这个校验请求,欺骗系统对于硬件型号的识别,使其认为该设备不具备 TEE 能力,于是降级到非硬件的检测方式上,从而绕过相关的检测。
@kuanat #1

这个方法不一定全适用,看堡垒机是什么样的,能 ssh 的话才可以。
145 天前
回复了 sampeng 创建的主题 程序员 一个疑问,现在是人均一台开发机了?
我翻了一下楼主的提问记录,没有别的意思,只是想判断你是不是有能力自己回答这个问题。

我觉得你想问的只需要几看一遍官方简介就够了,这个文档甚至不满一页。使用场景、延迟这些关键点都回答到了,完全不需要思考。

https://code.visualstudio.com/docs/remote/remote-overview
应用自己不体面,操作系统想让它体面可太难了。

我用折叠屏两个月了,这个功能说实话除了保持前台有点意义,其他应用场景都是产品经理想象出来的,聊胜于无。
我用 GoLand 非常少,印象里有几个方面比 VS Code 要好:

1. 自动补全更加“智能”,或者说排序、展示形式更人性化更符合预期
2. 重构支持 interface/implementation ,而 gopls 还在 symbol 层面
3. 代码跳转( navigation )更舒适
4. Debugger 更好用?
5. 自带 vim 键位比 VS Code 插件好用

这里面 1 是短时间 VS Code 追不上的,这个需要投入比较多的人工打磨,不是个技术问题。

第 2 点 gopls 是可以做功能支持的,但是需要 VS Code 插件端提供一套 UI 流程,确认哪些要改哪些保留,我个人觉得是无所谓的。第 3 点也差不多,VS Code 是个 editor ,UI/UX 是不如 IDE 的,但这一点说实话我觉得不影响,但是还是列出来了。第 4 点,我周围的人说 GoLand 好用,但我个人觉得是个习惯问题,现在来说 Delve 做得越来越好了。最后第 5 点,现在都用 VS Code 集成一个 neovim 实例,我觉得已经反杀了。

除了第 1 点,剩下的优势其实是源于插件功能受限于 VS Code 界面机制,不如 IDE 那么灵活。



我是 old school 派的,所以对 editor+plugin 的好感远大于 IDE 。自从 VS Code 出来,标准化了 LSP/DAP 之后,我的就开始 vim/VS Code 并用,原因是我可以花时间为开发用的语言做配置,但是很多时候要阅读那些我不写的语言的代码,VS Code 提供了很好的默认配置。再后来微软的 Remote 开发成熟了,我就彻底转向了 VS Code ,同时用 neovim 替代了 vim 。

GoLand 我自己在 2021 年前后短暂用过。个人观点是,在 2022 年之前,GoLand 优势还是比较明显的,但是 2022 年之后,GoLand 的优势只在细节层面了。技术层面无论是当下还是未来,可能都追不上 VS Code 了。

2021 这一年,VS Code Go 体验有了质的飞跃。一是实现了 Testing API ,二是和 Delve 合作支持了 DAP 。没有多长时间,又实现了 Testing/Benchmark 的可视化,DAP 也支持了远程 attach 。测试 Debug 成了真正可用的特性。

之后 2022 年都是些细节改善,比较大的功能就是 inlay hints 。到现在体验已经非常好了。
我觉得单独拿出来一个语法细节做比较是没有意义的,异常处理的模式要放到语言设计层面去考虑。没有什么优劣之分,更多是一种取舍。

在我看来,Go 的整体设计思想是降低开发者的认知负担。前段时间 V2EX 就有个帖子,讨论并发模型里的内存可见性。对于 Go 开发来说,不知道内存可见性这个概念一点不影响写出正确的多线程代码,因为这个模型非常符合直觉。但是对于写 C 的来说,不了解这样的抽象就会踩很多坑。

但是做类似的抽象是有代价的,Go 的主要思路是“协作”(这个概念是我自创的),就是在无关紧要的地方都做一点小妥协,换取整体的简洁。比如 goroutine 就是这样,没有机制让你主动 kill 一个 goroutine ,但是可以提前沟通一个 context 机制,gouroutine 可以在获知意图的时候主动退出。

回到错误和异常处理的话题上,Go 选择了简化控制流,从机制上减少因为错误处理又引入新的错误的风险。代价是,异常处理流程的参与者都要主动确认自己的职责。本质上 if err!=nil 不是异常处理,而是判断这个事情要不要我来处理。这样设计的前提是 Go 从思想层面重新定义了“错误”和“异常”,能恢复的都叫错误,不能恢复的都交给 panic/recover ,楼上也有不少人提到了,Rust 的设计思路也是这样,这里就不展开讨论了。

再说一下语法层面的事情。前两天还有帖子讨论为什么 Go 直到 1.21 才提供 Min/Max 的标准实现。当一门语言的设计目标包含了简洁、高效和向后兼容的时候,增加新特性就变成了需要慎重考虑的事情,泛型这个事情 Go 讨论了接近十年。设计者知道 Min/Max 是个常见需求,泛型也是很必要的特性,在没有标准库实现之前,还是通过 math 包提供了浮点类型的 Min/Max 功能。其他类型需要开发者自己写,毕竟浮点比较很容易出错,但其他类型几乎写不错的。还是那个“协作”的思想,在可以接受的地方做些妥协,是为了整体更高效。

所以说到底这是个取舍的问题,Go 自始至终就是忠于“总体简洁”这个思路的,不够理想的错误处理就是代价。一些看似简单的语法变动,在编译器层面就是极其复杂的问题。Rust 内存安全,但是开发者要理解复杂的编程范式,编译器又慢又复杂。Java 写起来语法糖够多,虚拟机也自由,代价就是效率不够高。Go 相对平衡,开发者和编译器各自妥协一下,花一点小代价为对方解决一些棘手的问题。在这个事情上,不能既要又要,所以我才说单独比较错误处理这个点没有意义,一定要放到语言设计的整体层面上来讨论。

关于一些改善错误处理的思路:

1. 为 error 设计额外的类型

这个方案其实有过非常多的讨论,包括 Go 没有 enum 类型说到底是同一个问题。核心原因还是增加复杂的类型和 Go 的设计思路不符,而且实现起来和接口冲突,所以放弃了几乎所有非算术类型的支持。对于 error 类型的需求建议用接口和类型断言来实现。代价是类型断言是运行时特性,不如编译时实现安全。

2. 函数式 monad

除非换一种语言,几乎没有可能从底层支持函数式编程范式。另一个思路是基于泛型来模拟,这个方案只能说有希望,进标准库估计要讨论的东西很多。

3. Go 现在推荐的思路

就因为错误处理这个事,Go 很长时间标准库里都没有结构化日志。和基于泛型的 cmp 一样,slog 也是 1.21 才进的标准库。

对于错误类型的定义,用接口和类型断言。像 call stack 这类信息,从 runtime 取回来按照需求用接口封装一下。
对于错落处理的传递,用自定义 Logger 或者 Context 。比如 tracing 这类信息追加一下。

这里额外提一句,Context 就是非常好的“协作”机制,调用方的举手之劳,整个调用链都受益的。让被调用方绞尽脑汁汇报……只能说一将无能累死三军。

最后做个总结,不要脱离语言背景谈语法细节。用一门语言就学习它的思想,然后把精髓用在最合适的场景里。
1  2  3  4  5  6  7  8  9  
关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   4501 人在线   最高记录 6679   ·     Select Language
创意工作者们的社区
World is powered by solitude
VERSION: 3.9.8.5 · 33ms · UTC 09:51 · PVG 17:51 · LAX 02:51 · JFK 05:51
Developed with CodeLauncher
♥ Do have faith in what you're doing.