V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2EX  ›  kuanat  ›  全部回复第 4 页 / 共 13 页
回复总数  243
1  2  3  4  5  6  7  8  9  10 ... 13  
@Kinnikuman

如果是本地文件,缓存进度条可能和内存没有太大关系。

缓存的真实量取决于内核管理的 page cache ,假如剩余空间足够大,这个 page cache 会在第一次访问文件的时候把存储中的内容全部加载,不内存不足的话会加载一部分。在应用程序看来,fd 已经在了,stream 读取也开始了,就看你读多少。

这个 page cache 的容量主要影响拖动进度条,超出去了就会去读存储,没超的话,内核会有自己算法加载新的内容进有限的 page cache 里。
196 天前
回复了 kuanat 创建的主题 分享发现 一个好用的、纯软件的扩展屏方案
@swordsmile #1

Hyprland 目前还不支持 multi-seat 功能 https://github.com/hyprwm/Hyprland/issues/1731

这也是我一直主用 sway 的原因。其他方面 hyprland 易用性好太多了,不得已我还给 sway 写了个简陋的 master stack 布局管理器……

另外还有些 layer-shell 协议的应用(悬浮显示状态、通知和关机菜单这一类)也是硬编码了 seat0 的,有可能会导致显示位置或者输入焦点异常。对我影响比较大的是 tofi (一个 wofi 的改版)会有输入焦点问题。
201 天前
回复了 swordsmile 创建的主题 Linux archlinux hyprland swaylock-effects wayvnc 自动退出
@swordsmile #2

我看了一下,hyprland 支持创建 headless 的。

https://wiki.hyprland.org/Configuring/Using-hyprctl/#output

但我不确定 hyprland 启动的时候是不是能 headless ,不过看 hyprctl instances 似乎是可以这么用的,你可以试一下。

实在有需要可以考虑 sway 啊,我用这个方案已经很久了,也不用欺骗头。

我用 sway 的方式是

WLR_BACKENDS=headless WLR_LIBINPUT_NO_DEVICES=1 sway

然后

swaymsg create_output

最后

wayvnc --output=HEADLESS-1

你可以试试看能不能把类似方法用在 hyprland 上面。
201 天前
回复了 digd 创建的主题 分享发现 GPD 新曝光的双屏变形本
不只是 GPD ,所有依赖差异化竞争的电子、硬件厂,受出货量限制研发成本难以均摊,即便设计层面不翻车,品控方面能过关的都很罕见。

就这个设计来说,我觉得以 GPD 现在的能力,屏轴、排线会是品控灾难。我能理解做 13 寸的技术理由,但我不理解做这个产品的商业决策。
你的疑问是对的。一旦离开 UEFI/grub 之后,之前 ventoy 虚拟出来的光驱设备就不存在了。

然后魔改 grub2 也是对的,只是如果你去看修改的代码 https://github.com/ventoy/Ventoy/tree/master/GRUB2/MOD_SRC/grub-2.04 会发现这些改动都是表面的,跟启动机制没有关系。

所以 ventoy 并不是用 grub2 loopback 加载 iso 完成引导的。

“除非系统内有对应的驱动程序去专门去访问这段内存及模拟驱动器”这句话不完全对,Win/Linux 内核都有 loopback 加载机制,也就是你说的“驱动”。真正的问题,去哪里加载。这里需要的是逻辑意义上的地址,不管它背后是内存还是硬盘文件。

如果你大致浏览一下 ventoy 的代码,肯定会注意到 vtoyjump 这个与众不同的名字,其他目录要么是一些基础功能,要么是 ventoy 的功能。另外 ventoy 是可以输出 log 的,你会在日志中看到 vtoyjump 相关功能的调用。

以下是我根据之前的印象总结的,我估计原理应该没问题,细节可能有出入。

如果是引导 Linux ,会首先引导 ventoy.cpio ,完成虚拟光驱初始化之后再链式引导 initrd 。

如果是引导 Windows ,会用 vtoyjump 替代 Windows 的引导文件(大概名字是 winpeXXX 的一个文件),然后原版在内存中保留一份副本。(这个“替代”是 overlay 的意思,并非修改原始文件,相关的 injection 功能也被 ventoy 做成了插件)此时还处于 grub 阶段,以你描述的 loopback 方式引导 boot.wim ,完成内核初始化。此时 grub 阶段虚拟的光驱设备就访问不到了。

原本下一步是引导 winpe 进入安装程序,这时被 vtoyjump 替代,vtoyjump 利用 NT 内核 API 加载原始 iso 成为 loopback 设备。 之后 vtoyjump 再引导 winpe 那个程序,就进入正常安装过程了。
203 天前
回复了 swordsmile 创建的主题 Linux archlinux hyprland swaylock-effects wayvnc 自动退出
你这个组合我没有用过,估计需要排查一下是谁崩了。从描述来看 wayvnc 晚于 hyprland ,是不是与 swaylock-effects 有关不清楚,看一下各自的 log 吧。

不启动 hyprland 这个应该做不到,就像 x11 一定要有 x server 一样。我估计你说的应该是 headless 模式? wayvnc 应该支持,hyprland 不清楚。如果 hyprland 不支持 headless 模式的话,可以用一个 hdmi 欺骗头让 hyprland 输出到这个 hdmi 上。
204 天前
回复了 Bo0 创建的主题 程序员 电脑抓包 APP 的请求失败
这个话题搜 ssl pinning 会有很多信息。

如果抓包的时候 App 看不到相关数据了,这种一般只是简单拒绝走代理,或者拒绝了中间人证书,导致连不上了。如果 App 看到的数据正常,说明有 fallback 机制,多数都是非平台默认的 ssl 实现。

不管哪种方式,常规应对策略基本上是 frida hook 掉类似证书加载、证书验证和代理之类的调用。有比较通用的脚本,支持常见的 ssl 库。

但这样做的前提是应用本身没有混淆,调用的方法名没变过,或者有能力推断出混淆过的名字和地址。混淆一般是通过加壳完成的,除了少数基于 vmp 的方案,大多数都有比较通用的应对思路。

脱壳之后一般还会需要绕过 root/签名检测,拦截客户端异常汇报等等,比起脱壳来说要简单。


非常规的应对策略是类似 eCapture 这种非入侵式的方案,可以绕开证书校验。配合虚拟机效果会更好。
206 天前
回复了 noisywolf 创建的主题 程序员 Linux 启动自己的 GUI 应用,不进桌面
取决于用途和场景,大致有几个思路,你可以按照关键词去搜对应的做法。

如果以安装的包来划分,整个显示框架大概有:Framebuffer 层( DRM/KMS ),Display Server 层( X.org/Wayland ),窗口管理器,桌面附加组件四层。FB 层肯定都有也不需要关心,Display Server 层必须要有一个,根据你的应用来选。按照你的描述,桌面( KDE/Gnome )这些都可以不要,窗口管理器可以有也可以没有。

这里以 X11 作为显示后端为例:

在某个 tty 启动 X session 。用到的命令就是 `startx`,后面可以直接跟你的 x11 应用,也可以跟 `xterm` 之类的 vte 终端,之后可以在终端里面按需启动 x11 应用。

这里对于 tty 的分配,以及对应到显示器需要你手动配置。如果你是 ssh 连接到远程服务器,需要 ssh x11 tunneling 。

如果需要窗口管理器,可以 `startx` 先启动窗口管理器,然后再启动对应的应用。
207 天前
回复了 XdpCs 创建的主题 Go 编程语言 如何更好的打印日志
以打印日志的方式来 debug ,在开发阶段和生产阶段是有区别的。正好前两天我在 https://v2ex.com/t/1038327 这个帖子里有个回复,可以参考一下。

以我的经验来看,日志 debug 这个话题表面上看是个技术问题,然而我更倾向于把它们定义为工程问题。把问题聚焦在代码怎么写上面很难形成有效的思路,想明白动机和目标之间的联系,才能得出如何正确实现的方法。



我就针对生产环境日志打印做个补充,这里就拿一般 go 服务器后端的场景来说明。

- 日志采集的方式

一般要么写本地文件,然后有 agent 负责汇总到日志服务器。要么直接根据配置向日志数据库来写。也就是说一般生产环境里日志排错主要靠数据库查询。

- 日志生成方式

上面帖子里有提到,生产环境的日志是“共享”的,也就是说日志主数据库很可能是全链路的信息都在,而某个 go 模块的信息只是一部分。这个主日志里面,多数时间理论上只包含业务信息,无法用于 debug 。

所以在我接触的项目里,会构建 release/debug 两个版本,线上部署 release 版本。当出现故障需要排错的时候,切一部分流量进入 debug 版本,debug 版本单独输出详细日志。这个日志包含程序运行信息,stack trace 等等。

- 日志记录的内容

生产环境排错主要是回答两个问题:首先是判断是不是该模块的错误,其次才是为什么出错。利用日志作为 debug 手段属于某种意义上的“异步”,感到困难的主要原因是,这个异步行为很难在本地开发工作流中复现。

如果能方便且顺利地在本地复现,困难可能就解决了一半。为了达到复现的目的,需要 debug 级日志中一定要包含进入本模块的输入,以及离开本模块的输出。



回到代码的层面,要做的就是把上面的思路具体化。

- 构建测试用例

当拿到输入和输出的时候,就可以在本地开发流程构建测试用例了。考虑到 go 实现的功能模块会涉及其他模块的调用,那就需要以 mock 的方式,直接引入其他模块的输入输出结果。这里就能看出来,debug 级别的日志,一定要包含和其他模块交互的输入输出。

这里顺便一提,mock 实现本地测试应该是开发流程中就要做的。

- 日志可读性

如果 go 实现的模块比较复杂,要记录各种输入输出就需要一定的组织规范,这就是你提到的问题。如果直接回答如何做,可能很难帮助你解决问题。

由于异常分析要在请求层面上,所以要求整个系统必须有统一的 traceID 之类的标志,不然就会可能拿着不匹配的输入与输出做调试。多数时候这个 traceID 是以中间件控制,或者在各个模块之间显式传递,无论哪种实现方式,都一定要有。

大的层面上,一定要记录 stack trace 即调用栈的回溯信息。它的作用是帮助你快速定位出问题的子模块,我个人比较倾向使用的方式类似于 error wrapping 的方式,可以参考 https://go.dev/blog/go1.13-errors 这篇文章。

这样对于错误,可以输出类似于 `调用方法 A ,入参 XXX : 调用方法 B ,入参 XXX : ...` 这样形式的字符串,冒号用于分隔。怎么记录不重要,标准一致即可。如果有需要,可以本地额外做一个更加 verbose 的 debug 版本,利用反射机制输出调用代码的行数等等。(这个事情主要是 API 接口相关的,模块内部流程的底层错误建议 wrap ,到达模块边界只返回定义好的 sentinel 错误。)

对于非错误的情况,一般在每个调用的入口和出口,根据调用的状态来输出不同等级的信息,比如某个方法调用的入口,输出 log.Info("XXX"),如果是走的正常分支,就在出口处再记录 log.Info("XXX"),如果走的是异常分支,可以在出口处记录 log.Warning("XXX")。这样最终会形成类似

```
INFO: 调用 A 开始
INFO: > 调用 B 开始
INFO: >> 调用 C 开始
INFO: << 调用 C 分支 C1
WARN: < 调用 B 分支 B2
INFO: 调用 A 结束
```

这个样子的日志记录,可以肉眼观察也可以利用文本工具来筛选。

这个日志的主要作用是帮助你确定异常发生时,对应的逻辑流程是什么,免去了你动态调试跟踪的麻烦。即便后续需要动态调试,也可以很准确地在目标位置下断点。

- 其他细节

我暂时能想起来一部分,如果以后想起来再补充。

日志层面一定要使用结构化的方式来记录,这样无论是写入数据库,还是查询筛选都会方便很多。

代码层面不要忽略任何错误,所有的错误都要有相应的处理(忽略也是一种处理,但一定要有对忽略错误这个行为的日志记录)。

模块内部对于所有外部输入都做 mock ,以提高测试覆盖率,方便后期 debug 。

这个事情的终极目标就是,出问题的时候,日志数据库筛一下就知道业务层面上是否有异常。有的话,立即切 debug 版本就能复现输入输出。拿到输入输出直接扔给本地 mock 版本,跑一下流程就知道是自己模块的问题,还是涉及其他交互模块的问题。如果确定是自己的问题,看 verbose 日志快速确定是哪个方法哪一行的错误。

写日志本质上就是以上述思路为目标做准备。
207 天前
回复了 qinconquer 创建的主题 程序员 app 软件中的热门榜单怎么做的呢
榜单如果只有 10~20 个这样,更新频率不高的话,建议人工。

程序只负责筛选出一定量的备选。
208 天前
回复了 nullcloud 创建的主题 Go 编程语言 用 go 做音频采集播放
Linux 平台搞音频处理,上限高下限也低。用什么语言是其次的,而且很大概率没有太多选择,关键是 Linux 的音频系统过于混乱和复杂,开发更多是做适配而不是写功能代码。



我有一点语言无关的 Linux 音频开发经验,估计能帮你少走很多弯路。写功能代码不难,难的是处理工程层面的细节。

- 最好能有输入、输出设备的 Datasheet/TRM 文档,像做嵌入式开发那样。音频必然涉及 ADC/DAC 这个天然的软硬件边界,很多时候只能去适配而不是一般意义上的写代码控制。

- 统一度量衡。音频(信号)处理是一门科学,所以要用科学的方法来对待。这个层面有几个需要着重关注的点:时钟、精度和单位换算。

最底层硬件和系统的交互,可能是中断,也可能是某种 IO ,还可以是 DMA 。这些交互有天然的工作频率和延迟,了解这个限制并在软件层面适配,可以避免设计层面产生的 xruns/desync 之类的问题。

精度核心还是受限于硬件,软件层面通常体现在采样率、中断频率 和 buffer 三个指标(一般两个指标可以确定第三个)。总的思路是中间处理的精度要高于硬件支持的精度(采样定理的结论),然后全程交换都使用相同的数据格式来避免 mix 操作的额外转换。

时钟和精度是相互制约的,所要要根据使用场景(对于实时性的要求)来确定参数。比如 44.1kHz 采样率,buffer 是 1024 采样,硬件每 256 采样触发一次中断,那么延迟就是 1024/44.1k 大约 24ms ,应用程序需要以 256/44.1kHz 约 6ms 的频率稳定输出。由于 Linux 常规内核是非实时系统,在一些对延迟要求高的场景里,很难保证下限,要考虑实时内核,应用程序也要尽可能减小 buffer 。(舞台演出歌手耳返一般要 15ms 以内,ktv 大概不能超过 30ms ,留给处理系统的时间并不多。)另一个思路是使用 buffer rewinding 之类的数据结构和算法,可以使用无限大 buffer 但是需要匹配硬件层面的时钟信号。

单位转换还是与软硬件边界有关。音量有 dB/dBFS/SPL/dBV 等等单位,核心思想是全程使用统一的单位与绝对零点参考。有点类似于图像系统中的光学、数码变焦,统一参考点可以使信号更好地位于硬件支持的范围内。超出硬件支持范围的部分要靠软件处理。



继续说点与 Go 相关的。Linux 音频系统大概可以分三层:内核、底层库和用户层。

内核层没什么需要深究的,以前还有 OSS 系统,现在 ALSA 完全替代了。内核加载驱动后,ALSA 负责将硬件抽象为具有特定功能的设备/子设备,然后以 sysfs 的形式暴露给用户空间。如果有很特殊的需要,可以用 Go 写 ALSA 的控制模块,但这个交互是绕不开 binding 的,除非完全用 unsafe syscall 实现。其实只有写底层库才会用到,正常只是使用音频设备是用不到的。

底层库可以大致分为三类:第一类是用来支持用户层的胶水封装,这一类不会用到。第二类是游戏引擎等等常用的抽象后端,比如 libSDL/OpenAL 这些,多数时候也不会用到。还有一类是 GStreamer/LADSPA 这一类,这是很可能用到的,既可以做一般性的降噪消回声,也可以做定制类的变声调音等等,重点在于信号层面的处理。但还是那个问题,GStreamer 这种要用 Go 来调用只能通过 binding ,有个 go-gst 是专门做这个的。估计原文提到的 livekit 也是这个层面的。

用户层一般叫做 Sound Server ,封装了底层 API 并提供功能抽象,多数都是模块化的,一般的音频处理工具也是在这一层。目前主流的几个是 JACK/PulseAudio/PipeWire ,应用层面 PulseAudio 可能占比更大一些,趋势是往 PipeWire 过渡。

开发层面,JACK 的主要场景是支持 MIDI 混声和合成器之类的专业设备,设备支持的话尽量还是用 JACK 比较好。有 Go 相关的 binding 但我没有使用过。

PulseAudio 能实现几乎绝大多数的功能,包括对音频的离线、实时处理也能在这一层做。比较好的地方在于它是多数发行版的默认选项,再就是它有 DBus 接口,可以避免 binding ,自己写个 DBus 封装也不是很麻烦的事情,只要做用到的部分就行。( DBus 其实不太适合低延迟的场景) PulseAudio 有很多非常规的应用,比如支持很多网络协议,当然如果用 Go 的话这部分可能直接用 Go 来写了。

PipeWire 本身就是 GStreamer 开发者创立的,所以 PipeWire 的好处是不需要在单独考虑 GStreamer 的集成了。我在一两年前用的时候还不是很稳定,但现在应该好很多了。PipeWire 的主要不同是它定位自己是 media stream exchange 框架,而不是封装型框架。开发的时候,调用 PipeWire 层 API 可以某种程度上认为是透传到底层甚至内核,整个过程中采样率、精度、格式和 buffer 都可以很明确地定义和交互。另外就是它可以在 flatpak/AppImage 这样的沙盒环境里使用。不足是它只能通过 binding 供 Go 使用。



综上考虑的话,能用 C 最好还是用 C 。如果一定要用 Go 就很依赖 binding 库的维护和质量,再就是需要有相对较高的编码水平,能够手动管理对象和内存来避免 GC 的频繁介入。另外如果信号处理层面需要 SIMD 之类的优化,用 Go 也是不合适的。

这里说的都是实时、在线的音频处理,如果是离线为主的 DAW 场景,比如基于 sequence 的谱曲之类的,用 Go 没什么问题,只是由于缺乏 SDK 支持,需要写大量的功能和业务代码。
209 天前
回复了 fu82581983 创建的主题 Kotlin Kotlin 2.0.0 正式版发布了
@Jirajine #34

官方早就说过不想做 LSP 了,之前我试了一段时间那个第三方做的 LSP ,差得还是有点远。所以我说希望 LSP 跟 IDE 能跟上,达到八成水平,支持我用 neovim/VS Code + LSP 就很满意了。这个事情上有得选是第一步。
210 天前
回复了 busterian 创建的主题 Android app 反布局分析调试如何绕过
爆破思路的话,跳转无障碍应该是 startActivity(Intent) 的形式,把它拦截了。也有可能是带 Result 的版本,一起把 onResult 也拦截了。

正向思路的话,我不知道 dump activity 相关的 API 是什么,当然应用也不可能监听 API 的调用。只是这个 API 调用会有副作用,应用程序可以检测某个事件或者相关的状态变化来做出页面跳转的行为。

这就需要你逆向看下代码或者动态跟踪一下,目标程序可以利用的方式太多了。
210 天前
回复了 fu82581983 创建的主题 Kotlin Kotlin 2.0.0 正式版发布了
写 Kotlin 可比写 Java 爽太多了,等 LSP 和 IDE 跟上,体验还会更好。
210 天前
回复了 kuanat 创建的主题 Go 编程语言 分享一些 Go 在全栈开发中的经验
@Hamao #40

很高兴能够帮到你!
@easychen #45

我之前的思路局限在脚本和工作流都是人在写之上,如果把视野展开,让 AI 来做胶水类型的工作(这本来也是语言模型的强项),现在就具备可用性了。

我也有尝试用 AI 来辅助生成功能性脚本,目前遇到的主要问题是没有很好的验证机制,很多时候需要人参与代码审计。另外目前基于提示词的运作方式接近于声明式编程,生成代码的实现方式比较不可控,更增加了人工判断的成本。
@meshell #24

继续往下查吧,wh.srv 可能做了些什么操作。随便猜一下,可能是某个 context 有 30s 的设置,超时之后直接在 http 层面触发了 defer Close() 之类的操作,这个操作完成了 tcp 层面 FIN/ACK 的关闭,结果导致 ws 层面是没有 opClose 消息的。

我看了一下 gobwas/ws 的代码,UpgradeHTTP 这里就把 net.Conn 的 deadline 给清除了(设置了 time.Time 的零值)。(既然是无超时,理论上每次读 message 的时候 err := conn.SetReadDeadline(time.Time{}) 这个重置就没有必要了,不过与你的问题无关)
@meshell #17

你给的截图里,最后一次客户端 ACK 确认服务端 Pong 之后,服务端主动发送 FIN ,说明断开是服务端的行为。

这个断开没有 opClose ,说明不是你的程序、也不是 ws 库的行为。

因为你是本地测试,也不会涉及防火墙。

由于 Ping/Pong 的间隔是 2s ,有双向通信,说明不是 Idle 相关的超时。也就是说,并不是 KeepAlive 等机制触发的先断开底层,再断开上层。

整个协议层面,在 ws 之下,还有(大概率)标准库 net/http ,再下层就是系统的 tcp socket 了。

我记忆中标准库 DefaultTransport 有个 30s 超时,查了一下 https://go.dev/src/net/http/transport.go 确实有,但是应该与你的问题无关。

正好你说你用的不是 http.client ,可以贴一下最小可复现的完整代码。因为之前的代码看不到 conn 的来源,可能是有哪个地方设置了超时参数。
我没有用过这个库,随便猜测一下。

理论上 ws 这种应用层协议,没有主动关闭行为,是不会在自己层面关闭连接的。底层的 tcp 在没有 keepalive 介入的情况下,连接建立后能够无限保持。ws 库在收到关闭信号之后,会向更底层传递这个信号,于是 http 到 tcp 都会关闭相应 socket 。

上面的意思是,这个行为不是 ws 库和你的程序主动行为造成的。

我看到你说有定时发送 ping ,那么另一端是否有回应 pong 呢?如果没有回应的话会出现一种情况,接收方会保持正常,而发送方连续 30s 只有发送而没有接收,触发了更底层协议的某个断开机制。

正好 golang net/http 默认 transport 超时就是 30s 。如果上面的库是基于标准库实现的话,可能就是 http 层先断开了。
之前的话题让我给带跑偏了,前面解释了 accept interfaces ,这里回归到 return structs 上面总结一下。

这句话的应用场景应该是 API 兼容性方面的,即返回结构体的代码写法可以避免很多不兼容的改动。

我之前在 Python 包管理的一个帖子里 https://hk.v2ex.com/t/1007645 简单提到过,像 Go 这样设计先于实现的语言,都会将包管理作为工具链的一部分。但这里的大前提是广大开发者合作,所有开源项目的包都尽量支持 semantic versioning 的版本号原则。包的提供者通过版本号主动声明 API 兼容性,包管理可以以很低的成本(非 NP 算法)解决依赖计算问题。

另一方面,Go 在 OO 抽象层面选择了组合机制而不是继承,从客观事实上也鼓励了包的复用。作为 Go 的开发者需要一个思维转变,就是任何一个包都可能依赖别的包,也可能被别的包依赖。后者这个情况就需要开发者清楚了解,什么情况下会造成 API 无法向后兼容。

在之前的讨论里已经明确过,Go 的接口是由使用方定义的,当这个使用方 X 的包变成其他包的依赖之后,X 就很难对这个导出接口做改动了,因为给一个接口增加新的方法一定是个非兼容的改动。

所以对于一般的应用场景来说,既然接口是调用方来定义的,那么这个定义只对调用方有意义,它完全可以是非导出的形式。这样 X 对于接口的改动都不会影响到下游的使用者。

这句话隐含的意思是 return structs (not interfaces),针对的是从传统基于继承的语言转过来,习惯使用工厂方法而言的。

在 Java 中需要在多个实现中选择一个实例化的时候, 受到接口必须和实现在一个包里的限制,使用工厂方法实际上是暴露接口,隐藏对象(结构体)。在 Go 当中没有这个限制,实际是鼓励暴露结构体,隐藏接口。(当然技术上说一个非导出的接口只是形式上不可见,下游依旧可以根据源码隐式实现,这里不展开说了。)



Java 部分就不举例了,这里用 Go 模仿工厂方法模式来展示这样做的缺陷:

```go
type Storage interface {
____Get()
}
func NewStore(provider string) Storage {
____switch provider {
____case "A": ...
____case "B": ...
____default: ...
____}
}
```

项目使用过程中发现,还需要批量下载接口,于是想修改接口为以下形式:

```go
type Storage interface {
____Get()
____GetBatch()
}
```

无论在 Go 还是 Java 中,这个非兼容改动会导致大量的修改工作。

回到 Go idiomatic 的实现方式上:

```go
type storage interface {
____Get()
____GetBatch()
}
type MyStore struct {}
func NewStore(s storage) *MyStore {}
```

下游只依赖 MyStore ,上有对于 storage 的改动是 API 兼容的。

对于接口改动,需要对 A/B 的实现进行封装,改动也比工厂方法模式简单。比如可以独立另一个接口:

```go
type storageExt interface {
____GetBatch()
}
```

也可以用在结构体中嵌入( embedding )一个接口,其他部分封装一下:

```go
type MyStore interface {
____storage
}
```

这里有个技术层面的大前提,扩展结构体在绝大多数时候都是向后兼容的,而扩展接口永远都不是向后兼容的。所以暴露一个未来可能扩展的结构体,远比暴露一个接口更合理。关于这一点可以看我在另一个帖子 https://v2ex.com/t/1007845 当中的回复,中间提到两个讲座就是对这个问题的解释。

由于 V2EX 回复里面插代码太难读了,我这里就不举例展开了,顺着这个场景想象一下大概就知道增加功能这个需求所需要的工作量。就这个扩展接口的场景,Java 无论如何都要 X 主导这个修改,而 Go 里面 X 有需要就 X 来改; Y 如果有需要,把 X 的包引入进来,Y 也可以做这个修改。还是那句话,Go 的接口模式实现了工程层面(不仅仅是 API 层面)的解耦。


做个简单总结:

Java 工厂方法模式是为了解决只有 Java 才有的问题而形成的一般设计方法,而 Go 天然是不存在这个问题的。所以在 Go 中使用接口的原则和 Java 中是完全不一样的。

站在上游的角度,主动暴露接口一般是两个目的:一是规范使用,比如标准库把 Error 定义为接口;二是为文档服务,因为非导出接口是不会体现在 godoc 里面的。

站在下游的角度,只有在第一种情况才会主动使用上游接口,比如所有人都用 slog 的日志接口;使用上游接口等于主动为自己增加一个硬编码的依赖,正确的做法是使用上游暴露的结构体,然后封装并实现自己的接口。

顺便一提,由于太多下游错误使用上游接口的情况存在,很多上游开发者会在导出接口中包含一个非导出方法,这样下游就无法实现这个接口,上游就可以主动控制下游的使用方式,避免后期改动影响太多用户。

Accept interfaces, return structs 虽然只有四个字,但它代表的是思维模型的转变,想要说清楚实在是太困难了。这句话的核心思想我觉得 Rob Pike 的总结更恰当:Don't design with interfaces, discover them.

用我的话来总结就是,不要沿用 Java 面向接口编程的思路,先设计再实现,而是先实现,当发现有重复实现的需要时,再用接口来重构。在 Java 里面,没有设计到的功能重构起来代价非常大,所以变相要求预先做大量设计,而 Go 里面重构代价非常小,用到了再改。这是组合优于继承的体现。
1  2  3  4  5  6  7  8  9  10 ... 13  
关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5415 人在线   最高记录 6679   ·     Select Language
创意工作者们的社区
World is powered by solitude
VERSION: 3.9.8.5 · 37ms · UTC 08:59 · PVG 16:59 · LAX 00:59 · JFK 03:59
Developed with CodeLauncher
♥ Do have faith in what you're doing.