V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2EX  ›  kuanat  ›  全部回复第 1 页 / 共 9 页
回复总数  177
1  2  3  4  5  6  7  8  9  
15 小时 54 分钟前
回复了 Bo0 创建的主题 程序员 电脑抓包 APP 的请求失败
这个话题搜 ssl pinning 会有很多信息。

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

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

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

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


非常规的应对策略是类似 eCapture 这种非入侵式的方案,可以绕开证书校验。配合虚拟机效果会更好。
2 天前
回复了 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` 先启动窗口管理器,然后再启动对应的应用。
3 天前
回复了 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 日志快速确定是哪个方法哪一行的错误。

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

程序只负责筛选出一定量的备选。
4 天前
回复了 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 支持,需要写大量的功能和业务代码。
6 天前
回复了 fu82581983 创建的主题 Kotlin Kotlin 2.0.0 正式版发布了
@Jirajine #34

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

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

这就需要你逆向看下代码或者动态跟踪一下,目标程序可以利用的方式太多了。
6 天前
回复了 fu82581983 创建的主题 Kotlin Kotlin 2.0.0 正式版发布了
写 Kotlin 可比写 Java 爽太多了,等 LSP 和 IDE 跟上,体验还会更好。
7 天前
回复了 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 里面重构代价非常小,用到了再改。这是组合优于继承的体现。
这种通信场景一般没有路由的说法吧,都是用协议这个词。

如楼上说得都挺好了。我有个建议,你可以看看用 unix domain socket 做 IPC 通信一般是怎么做的。ws 就是把本地变远程,protobuf 就是 socket 的具体实现(协议/路由)。

在 web 编程里是匹配路由然后把请求交给对应的 handler ,在 socket 编程里硬要说路由或者协议的话就是某个字节代表特定的类型,然后每个类型有一个专门的 handler 来响应。
9 天前
回复了 wzhings 创建的主题 信息安全 [求助] 如何有效地保存用户名和密码?
Pass, the standard unix password manager.
https://www.passwordstore.org/

git+gpg ,把楼主想做的自动化了。
一开始看帖子没看明白是做什么用的,看了手册才反应过来这是做了一套类似 IPC 的规范,外加一个注册中心。

有一点我觉得 OP 做得非常好,schema 自动生成。但是就如 #5 @w568w 所说的,这套规范化的做法是和写脚本这个行为的初衷天然互斥的,一个追求复用,一个追求敏捷。而且楼主在文章里也说,为了这个工具重写了大量脚本,着实有一种为了这碟醋包了一顿饺子的意味。

正好借这个帖子我想探讨一下 IPC 和工作流的话题,不知楼主是否了解过 dmenu ?



鉴于很多人可能从来没有听说过 dmenu ,我就简单介绍一下。它原本是为 dwm 窗口管理器设计的动态菜单( dynamic menu )应用,常常用作 Linux 环境的启动器。

和其他所谓效率工具最大的不同在于,dmenu 的哲学是它仅仅只负责从 stdin 读取输入生成菜单,然后向 stdout 输出用户的选择。整个过程的交互全部是纯文本。如果有必要上一个 dmenu 的输出也可以作为下一个 dmenu 的输入。

我个人认为 dmenu 设计思想的优秀之处在于:将写脚本和写工作流恰当地分离开,写脚本依然是那个能跑就行的随手工作,当需要将脚本集成进工作流的时候,适配所需的代价非常小,关键是并不需要刻意改动脚本。



单纯这样说可能依旧看不出 dmenu 设计思路的巧妙之处,我举几个例子说明。

比如我希望命令行将 wifi 切换至手机热点。需要准备两个脚本,一个是调用 nmcli 扫描当前可用网络,然后字符串处理一下生成 wifi 列表;另一个是从命令行读入一个字符串,调用 nmcli 连接该字符串所代表的 wifi 网络。

这个过程里 dmenu 的作用是,调用第一个脚本得到输出结果,以列表的形式供用户选择,并将用户的选择输出,传递给第二个脚本使用。写成命令行就是 `script1 | dmenu | script2`,通过管道的形式传递。

从这个例子可以看出,两个脚本本身就是实现功能用的,原本怎么写就怎么写,不需要预先考虑去适配某种输出。即便是别人写的脚本,只要它是文本输出,完全可以套一层 wrapper 使用 awk/sed 等工具转换成需要的输出。脚本不支持命令行输入,也可以使用 xargs 等方式将 stdin 转换为命令行参数。

至于工作流,无非就是扩展之前的命令行 `script1 | dmenu | script2 | xargs ... script3 | dmenu | script4` 这样,在有需要用户输入或者选择的地方加入 dmenu 即可。更进一步的话,将脚本都放在 ~/.local/bin 然后设计一个“入口”工作流,让用户首先选择所需要调用的脚本,那就变成了万能入口的效率工具了。



我不记得是什么时候接触到 dmenu 了,15 年应该是有了,现在我早已不用 dmenu ,但是依旧在用基于 dmenu 思路的替代品。写这个回复的时候突然想起了很早之前比较流行的一篇文章《开发人员为何应该使用 Mac OS X 兼 OS X 小史》,链接在 https://blog.youxu.info/2010/02/28/why-mac-os-x-for-programmers/

从时代的发展来看,即便是 macOS 这么理想化的 IPC 设计(图形化界面基于 Mach 的 Service 服务概念),依旧是推广不开的。抛开技术层面统一 runtime 不靠谱这一点,更重要的应该是人性,想要别人主动去适配实在过于困难。

万能入口、启动器这种工具的生态其实挺有意思的:Linux 用户似乎从来不关心,估计是都有自己手搓的方案; Windows/macOS 平台上的实现几乎看不到基于 dmenu 思想的实现,虽然可以解释为这些工具面向的都是没有编程能力的群体,那适配脚本的工作就显得更加难以实现了。
@shinelamla #74

回复比较晚……单就引文那个代码来看,我觉得没有必要写接口,因为还没用到。直接写成 func (c *Conn) ListPosts() []*Post { ... } 就行,Conn 可以 embed 一个 grpc.ClientConn 这样。

等我写完文章吧,这个话题确实不太容易说清楚。
@xywanghb #72

我也是到了 70 楼的回复才意识到关键所在,你说的就是我想表达的。

Java 的接口和 Go 的接口只是有一样的名字,实际上作用完全不一样,根本不能拿来类比的。Java 的接口是用来解决多重继承问题的,而 Go 天然基于组合而非继承,接口的能力和责任范围都更大。

Java 的思维模型里,抽象(动词)设计这个行为越早越好,而且机制上鼓励你尽可能考虑易用性和扩展性,原因是后期做调整很麻烦。这让我想起了上学的时候,万物皆对象,想把整个宇宙都用对象和类描述出来。这个思路导致了 Java 在工程方面是有过度设计和复杂化倾向的,现实里 java 团队往往也比较大。

Go 的思维模型里,越简单越好,不需要考虑额外的东西。责任划分非常清晰,抽象这个行为局限在非常小的业务层面。

这中间的区别我认为可以上升到哲学层面,就是我开头提到的汉语和其他语言的区别。汉语是建立在组合的哲学上的,把全宇宙所有具象、抽象的概念都解构归纳成最基础的元素,大概只有几千个汉字。任何人学会这几千个字,就可以尝试自行描述整个世界。

换到其他语言,简单举例几个,化学、医学和植物学,每个都有自己无限衍生的词汇表,在一个领域的词汇积累是无法平移到另一个领域的(多继承失败)。

从这个意义上说,我认为以 Go/Rust 等等现代语言就是先进生产力的代表,减轻了开发者的心智负担,也就解放了生产力。
@aababc #64

没办法确定“实现”了接口。

在 Java 这种 strongly typed 语言中,这个判定过程发生在编译时,implements 就是告诉编译器做这个验证工作的。在 Go 这种 weakly typed 语言中,这个判定被推迟到运行时,如果没能真正实现,调用的那一刻会产生运行时错误。

于是 Java 的思维模型就是要先说清楚,即库和包的作者主动声明并接口化。而 Go 的思维模型是用到的时候再说,即调用方来定义到底需要什么接口(我定义的我自己当然知道谁实现了谁没实现)。

我前面举的例子可能不是特别恰当,但是由于 Go 的接口声明在调用方,而实现在上游的包和库,这个隔离或者独立已经是非常大的进步了。从各种开源项目看,引用上游依赖几乎是毫无副作用的事情。
1  2  3  4  5  6  7  8  9  
关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1096 人在线   最高记录 6679   ·     Select Language
创意工作者们的社区
World is powered by solitude
VERSION: 3.9.8.5 · 75ms · UTC 19:01 · PVG 03:01 · LAX 12:01 · JFK 15:01
Developed with CodeLauncher
♥ Do have faith in what you're doing.