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

楼上有个链接,也是提到了原文那个说法 accept interfaces, return structs 含义是很模糊的。

现在 reddit 上有个帖子,里面提到了这句话最原始的出处:
https://medium.com/@cep21/preemptive-interface-anti-pattern-in-go-54c18ac0668a

我上面的回答其实是个简化的版本,并没有非常正面回答 accept interfaces, return structs 的意义,因为这句话根本体现不出来接口对于 Go 的意义(况且很多场合并不适用)。

上面的解释是回归到本质,即它真正想解决的问题什么。我对这个问题的解释是,这样的写法不仅在代码层面把功能进行了解耦,也在工程层面对人的责任边界完成了切分。

就像你所说的,标准库里的接口是个指导作用,如果没有标准库的影响,下层写成任何形式都是有可能的。现在的写法是在当前语言表达能力下,最 idiomatic 那一个。
@gowk #23

就事论事,我不是很认可这位作者的说法。即使我和他得到了相同的结论,也不代表我们有一样的推理过程。
@whitedroa #31

我尝试用注释里带文件名的方式来区分是谁写的某个文件,看起来还是不够清晰表达意图。

现在假设我是一个包的作者,我只关心赶快完成我的功能。这时候 A.go 和 main.go 都是我自己写的,我的 use 方法入参就用我的 *A ,简单粗暴。

现在另一个人看到我写的包,他觉得你竟然把这么复杂的 A 业务给抽象出来了,那他就借你写的 A.go 里面的 get/put/... 方法一用吧,这样他就只需要写个 B 支持,就能同时支持 A/B 了。这里的重点是,他其实只想用我写的 get/put 方法,对其他的不感兴趣。

考虑到对他来说,他的 main 里面不希望为 A/B 写不同的调用方法,于是就写成了 func New(s Storage) *MyStorage 的形式,需要调用 get/put 就在 *MyStorage 上面调用。这个 type Storage interface 里面只包含 get/put 两个方法即可。

这样他的 B.go 就可以简单封装一下 sdk 满足接口就可以了。

这个事情还可以继续下去,第三个人看见了第二个人的代码,还可以添加 C 支持。甚至当他需要 get/put 之外第三个 delete 方法的时候,可以用到 embedding 机制:

```go
type C struct {
a A
}
func (c C) delete() { ... }
```

调用的时候接受一个三个方法 get/put/delete 的接口即可。

全程下游都不需要上游配合。
@yusheng88 #28

你有没有考虑过,为什么上游要提供接口呢?这是下游的事情。上游 preemptively 接口化是个违反工程实践的行为。

你的思路还停留在“Java 可以用 XXX 的方式来做同样的事情”,不好意思,就本文讨论的话题,Java 真做不到。
@sagaxu #19

你提到“我们”的时候,还是没有跳出固有的思维。

在 Go 的设计思想里,包或者说库的作者,应当( should )假定自己的包有一天可能成为别人的依赖。然而你并不需要假想别人要以何种方式使用你的包,你只管写你的实现完成功能就可以了。别人可能仅仅因为一个结构定义,或者一个导出方法就引用你的包作为依赖。

如果我自己要同时完成某个功能的 A/B/C 三个实现,我当然会提前把接口写好。但是如果我只有 A 的需求,那我完全可以不写接口,写成接口的形式是为了某一天我要添加 B/C 支持、或者我知道这个包可能被别人拿去用而做的预先设计。

无论如何,受限于 Java 的类型检查机制,如果最初没有写成接口,任何人除了包的作者都没有很简单的办法复用一个没有接口的包,因为把接口抽象出来这个操作只能由包的所有者完成。虽然你觉得原作者对于 A 功能的实现写得很好,但想拿过来用,才不得不选择复制粘贴,否则就要求助原包的作者将代码改成接口的形式。复制粘贴的问题是一旦上游更新,你就会面临是不是要手动跟随更新的问题。提 PR 是考虑到,减轻原作者的工作量,提高原作者接口化的意愿,避免你自己跟随更新的麻烦。

这两个都是在尽量减少对原包作者的依赖,但是在人的层面解耦不够彻底。
Reply

上面解释得可能还不是很清晰,我加一点代码来说明吧,还是以对象存储支持两个后端为例。由于回复不支持 markdown ,所以手动排版了一下。



1.
第一个非接口的版本:

```java
// A.java 实现功能的部分
class A {
____void get();
____void put();
}

// main.java 调用的部分
class main {
____void use(a A);
}
```

这个版本如果别人引用去,是很难添加 B 服务商支持的。除非是复制粘贴拿来用,但如果是复杂的项目,要么只能长期手动维护,要么向上游提 PR 。

所以包作者往往会以 preemptive 的形式,改成接口的版本,方便下游使用:

```java
// A.java
interface Storage {
____void get();
____void put();
}
class A implements Storage { ... }

// main.java
class main {
____void use(s Storage);
}
```

这样下游的人可以自己去适配另一个类 B 来实现 Storage 接口了。

```java
// B.java
import A.Storage;
class B implements Storage { ... }
```

这样做的问题是,如果 Storage 接口方法非常多(正常云服务 sdk 少说都有二三十个方法),那么 B 也要适配同样数量的方法。实际上下游适配 B 可能仅仅需要 get/put 两个方法而已,这对于开发和测试都是非常不利的。



2.
再来看看 Go 的初始非接口版本:

```go
// package A
type A struct { ... }
func (a *A) Get() {}
func (a *A) Put() {}

// package main
func Use(a *A)
```

然后上游作者就什么都不用管了。下游用户看到,想要增加 B 支持:

```go
// package B
type B struct { ... }
func (b *B) Get() {}
func (b *B) Put() {}

// package main
type Storage interface {
____Get()
____Put()
}
type MyStorage struct {}
func NewStorage(s Storage) *MyStorage { ... }
// 没有写成 func Use(s Storage) 是为了体现后半句 return structs ,这个不重要
```

其他不用管了。假如上游发布了更新,只要 Get/Put 接口签名不变(即上有发布的新版 API 向后兼容),那就可以直接升级使用。整个过程里,上游下游的工作是完全独立的。

即便考虑现实中 Storage 有几十个接口,下游用户也只需要实现他所用到的少量方法即可。



二者区别其实是在于:谁来定义接口。Java 的机制决定了只能是 producer 说了算,而 Go 则是 consumer 主导。这样看 preemptive 这个词的意义就很清晰了:还不知道会被怎么用的情况下就把接口定好了,当然是“抢占”了,实际上 Go 这里根本没必要预先定义,等用到的时候再写就是了,用多少写多少。
我刚好在写一模一样主题的文章,完成后会发上来。起因可以看我最近回复,当时我评价某个项目的代码“不能正确使用接口解耦”。

如果你在学习 Go 之前没有太多编程经验,这句话对你来说可能非常自然,自然到令人疑惑,因为你不知道反过来做是什么样子的。如果你的思维模型受 Java 的影响很深,那么理解这句话才能理解 Go 在解耦方面带来的巨大进步。(不抬杠,这里以 Go 和 Java 做对比纯粹因为最方便理解)

要说清楚这个问题需要比较大的篇幅,我这里简单概括一下。

1.
Go/Rust 这类现代语言都放弃了“继承”,这是设计思想的巨大进步,Java 这种旧时代的语言在设计的时候是没有意识到“组合优于继承”的。

理解组合优于继承对于中国人来说非常简单,汉语只需要几千个字就能描述整个宇宙,除此之外的其他语言的,那句台词怎么说的,不是针对谁,在座的诸位……

2.
基于组合的理念之上的 OO 抽象,才产生了 Accept Interfaces, return structs 这个 Go idiomatic 的范式。

我比较认同 Rob Pike 对这句话的评价,它不够准确,也不够精确。如果让我来表述,我会分成两句话:

- The bigger the interface, the weaker the abstraction. 这一句是纯引用作为铺垫,意在表达接口越小越好。

- Don't implement Interfaces preemptively. Preemptive 这个词一般翻译成“抢占式”,这里取其衍生含义,提前或者预先。Java 实现接口的代码范式就是 preemptive 的。

3.
Go 的隐式接口实现和 Java 显式接口实现,根本区别在于 Go 能够以接口为边界,从工程层面将开发工作解耦。

举个例子,你开发了一个库,功能是对象存储中间件,它支持以 A 厂商云存储作为后端,实现了 get/put 的读写方法。

如果有另外一个人需要增加对 B 厂商的支持:

- 用 Go 的话,他只需要引用你的包,然后定义一个包含 get/put 方法的接口,同时将 B 厂商的 sdk 做封装即可。调用的时候直接以接口为参数,而不需要关注具体实现接口的对象。

-用 Java 来实现的话,他要么把你的包以源码的形式复制一遍加入到项目里,要么就要向你提 PR ,来增加对 B 厂商的支持。这是因为 class B implements StorageInterface 只能写在你的包里。

这里就看出 Go 的先进之处了,你要做什么和原包的作者没有关系,原包的作者也不需要关心其他人是怎么用他的包的。而 Java 世界里,要么把上游的人拉进来,要么自己成为上游。代码上好像解耦了,但又没完全解耦,工程上是非常低效率的事情。

为了解决这个麻烦,Java 就有了 Preemptive 实现接口的惯例。考虑到需要增加适配,写类的时候有事没事先写个接口出来,不管用得到用不到。

但这样做的问题是,接口会变得巨大无比。一般的对象存储服务,少说也有二三十个方法。一个有二三十个接口的方法是什么概念?当你需要 mock 一下写测试的时候就有得受了,事实上你可能只会用到 get/put 两个接口而已。

然后 Java 提了一个叫 SOLID 的原则,其中 I 代表接口隔离,意思是要把接口拆分。问题是作为库的作者,你只有一次拆分机会,而包的使用者却有数不清的排列组合。


PS
补充几句题外话,我在 Go 语言主题里的回复经常会被人说是踩 Java 捧 Go ,但我依旧坚持有理有据地论证,而不是停留在嘴巴或者屁股上。

很容易看出来,如果设计思想落后了,想要模仿先进的东西是非常困难的。我不止一次重复过,考虑到 Java/Python 这些语言诞生的时间,从设计层面上评价它们落后是不公平的,毕竟这些语言作为先驱踩了坑,才会有后来现代语言的设计指导思想。

另外需要指出的是,Go 基于 duck typing 的隐士式接口的范式是少数不能通过语法糖等方式在 Java 中实现的机制。在一个静态类型语言上实现 weakly typed 的特性( duck typing ),Go 应该算是第一个。
2024-05-13 16:40:53 +08:00
回复了 nmap 创建的主题 程序员 求一个文件 N 点同步的方案
TLDR

不知道你考虑过冲突解析的问题?同一时间两个节点上的同一文件产生了不同的变化,要以哪个副本为准?如果存在这种情况,那你要解决的实际上是分布式系统的同步问题,方案会非常复杂。比如一般的对象存储,都是传一个副本,然后节点之间完成副本同步。

如果不存在这种情况,可以降级成非分布式问题,方案也简单很多。比如你只是需要多个备份,同一时间只会在一个节点上操作。这个情况比较简单的做法是选一个节点作为权威副本,其他的节点都以这个节点为准,向权威副本推送更新的时候要先合并权威副本。类似于使用 github 之类的平台进行协作的模式。rsync 加上简单脚本就能实现。


----手动分割----

这个问题没有普适的答案,本质上它是受分布式系统 CAP 理论限制的。需要确认需求,进而在 CAP 三者当中选二,然后才能确认方案。由于绝大多数情况下,P 是不能放弃的,所以要么只能 AP 要么只能 CP 。

AP 方案放弃 C ,结果就是某一个时刻,各个节点之间的副本有的是最新版,有的是旧版。

CP 方案放弃 A ,在任意同步行为完成之前,不能进行其他操作。

实际应用里最先考虑的是降级,把分布式降级为星型,就是上面说的权威副本节点。如果无法降级,那就需要使用基于 paxos/raft 这类共识算法的同步机制。
2024-05-11 13:31:34 +08:00
回复了 solywsh 创建的主题 Linux 有没能按照 ip 进行流量统计的
这个需求也算是 V2EX 上的常客了,我详细说一下思路和方向。

Linux 网络栈是在内核里实现的,流量统计就是 packet 计数(或者计量)这个计数有几个方式:

1. 内核态 pf 用户态 iptables

最简单的方式就是 iptables 对 chain 计数,只要把想统计的流量走一下自定义的 chain (这个 chain 可以什么都不做),就可以利用 chain 自身计数达到目的。

像 openwrt 之类的路由器统计内网流量基本都是这个方式,用 arp 配合 MAC 地址来区分客户端。

这个方式的优点是轻量,缺点是要预先设定规则。你的这个场景,如果几个朋友的 IP 有比较明显的区分度,可以根据 IP 段每个人走一个规则 chain 来统计。但是对于大量无规律来源 IP 就很难写这个规则,或者写出来几万条不实际。

2. conntrack

想要动态处理 ip 规则,就需要做基于状态的统计。conntrack 有自己维护一套映射表,这样就无需预先知道来源 ip 就可以按需统计。优点是灵活性比较高,但是性能影响也会比上一个方法高一些。

这两个方法的实现可以参考 https://openwrt.org/docs/guide-user/services/network_monitoring/bwmon 这个帖子 Available tools 章节,可以参考拿来用。

第一个方案其实很好写,第二个要想自己写可能需要比较多的背景知识。

3. 抓包自己算

就是计量的方式,不是很推荐的做法,为了做流量统计结果把所有数据包都过滤一遍,有点杀鸡用牛刀的意思了。
2024-05-11 12:57:23 +08:00
回复了 pegasusz 创建的主题 程序员 戴尔内存泄漏漏洞?还是恶意攻击?
做个无责任推测,如果很多人同时发生类似的事情,可能是这个程序有个访问特定服务器的定时任务,然后服务器挂了导致程序无限高频重试,进而放大了内存泄漏、cpu 占用等问题。
2024-05-09 17:05:07 +08:00
回复了 kuanat 创建的主题 Go 编程语言 分享一些 Go 在全栈开发中的经验
@AbrahamKindle #38

我不太确定你所说的性能指的是哪个方面,我就随便说一下。

原文里谈到 Go 不适合音频视频处理指的是生态问题。比如图片领域有 imagemagick 音频视频领域有 ffmpeg ,通常没有重新造轮子的。如果要用到 Go 项目里就需要通过 FFI 来调用,这就需要通过 CGO 的方式。如果没有人开发相应的 binding ,那就要通过命令行直接调用了。这两个都不是很理想的方式。

音视频处理领域谈性能,一般应该是实时领域吧,指标是延迟和 jitter 这种。粗略来说,不仅仅是 Go ,凡是带 GC 的语言都不适合干这个事情,因为没有完全控制 GC 介入行为的方法,GC 介入会短暂中断程序的运行,这对实时应用来说很难接受。当然这个事情不绝对,通过一些手动管理对象、内存或者 zero copy 流式处理的方式说不定也能满足性能需求,就是回到 C 那种思路上面。

另外音频视频处理涉及的范围很广,以我有限的经验来说:

- 涉及底层硬件的尽量还是用 C ,写 binding 一点都不好玩
- 调用个摄像头做 cv 识别这种是可以的,因为你对接的不是硬件而是 v4l2 之类的抽象,opencv 的 golang binding 也够用
- 走 opengl 写个 shader 什么的也还好,但是基本没法对接到一般游戏开发工作流里面

所以说还是尽量用对应领域的工具吧。
2024-05-07 18:38:59 +08:00
回复了 dimtutac 创建的主题 Go 编程语言 在看某个 golang 的教程,不太理解为啥 Init Logger 要加锁
抱着解答问题的心态点进来,看见楼上的评论就笑了。名著你可以揣测一下作者的写作思路,地摊文学就算了……

代码层面的问题 @bv 在楼上已经说得比较清楚了,我补充一点我的理解。

这种代码水平在我之前的面试标准里面,按照初级/中级/高级三档水平,只能划分到初级那一档。我只通过看链接里那个文件就做出了判断,而且对此我非常有把握。

划分到初级的主要理由是:无法准确使用接口 Interface 来实现功能解耦。这个能力在我之前负责面试的时候是中级技能里的。相比之下,代码层面反倒是小问题了。

如果看不懂的话,我可以抽时间单独就 Go 通过接口来解耦专门做个解释。有隔壁那个代码氛围的帖子在,其实不想过多评价。但我还是要说,这段代码的作者的 Go 水平真就是初学者,出来卖课就是误人子弟了。
2024-05-07 16:41:03 +08:00
回复了 zoeyx 创建的主题 程序员 前端 Coder 如何学习 Golang?
语法层面过一遍官方的 Tour 差不多够了。

练手的话我比较推荐尝试写个爬虫服务,不是让你真去爬什么,而是这个过程用到的东西学习曲线比较平滑。

解析页面元素这个过程,大概能熟悉强类型语言处理字符串的模式,了解结构体的应用。后期还会接触到反射等机制的应用。

之后是一般的网络编程,发送接收请求。这个过程可以熟悉标准库的风格和惯例,网络库算是 Go 比较精髓的部分了。

再之后是多线程处理,了解 chan/goroutine 的使用,以及常见的并发模型。领会一下用通信的方式来共享内存的核心思维。

最后把改造成服务,学习一下路由处理、中间件等等服务端常见的应用。之后有可能会慢慢接触到模板、泛型的应用。

前期不用考虑处理太复杂的情形,用标准库把功能实现出来就行。整个过程里可以慢慢熟悉后端工程化的实践。
2024-05-07 13:23:09 +08:00
回复了 saranz 创建的主题 V2EX 好奇,至今还在 V2EX 活跃的 ID 是那些。
一般差 20 岁就是一代人,V2EX 从 10 算已经 15 年多,当年的种子用户可能都已经三十多岁了。

我估计有一批人应该和我一样,只是老账号各种原因不用了但是人还在。
2024-05-07 13:11:39 +08:00
回复了 nobject 创建的主题 Go 编程语言 golang 日志记录
我不确定我的做法是不是 idiomatic 只是简单描述一下我的思路。毕竟 slog 进标准库也没多久,看 api 也是受 zap 影响很大。

大的逻辑方面我比较倾向于全局只有一个唯一 logger ,中间调用过程只传递。另外我习惯把业务层面的日志和功能实现层面的日志分开,然后两者用不同的方式来处理。

业务层面的日志我倾向于用格式化的字符串:一方面它是给运维用的,需要一眼看明白在哪个环节出了问题,但并不需要知道出问题的细节;另一方面这类日志要进日志服务器做后续统计等功能,需要方便处理。

对应的就是你的描述里 controller->service->repo 这个部分,我会用 errors 传递字符串,每个环节用 errorf 来 wrap 上一个环节的错误,起到简单的 stack trace 的功能。这里一般不建议用 runtime 来获取完整的 stack trace ,主要是性能开销的问题。

包或者实现层面的日志,一般是给开发者方便调试用的,比如看到底是什么入参触发了什么样的边界条件导致出错。这个信息在开发和测试环节比较有用,实际线上是相对冗余的信息。

日志记录这个信息我用的是自定义的 context ,因为标准库里的 context 实际上只有下文没有上文。可以看这个帖子 https://s.ex.noerr.eu.org/t/1012453 我的回复。

用 context 的话可以自定义数据结构,能够比较好处理那些复杂的结构体,也就是你说的第二条里的麻烦。

对于 context 信息的记录就没有必要走业务层面的 logger 了。对于不太敏感又注重性能的业务,我的处理是加一个开关,当线上出现的问题无法复现的时候,开启开关再记录完整的 runtime stack trace 信息。对于相对敏感的业务,会单独用一个 logger 导出到日志服务器单独的表中,使用 traceId 对业务层面的日志和实现层面的日志做个关联。业务层面的日志要保留很久,但功能层面的日志一般三个月就可以清理了。
2024-05-07 11:54:31 +08:00
回复了 afxcn 创建的主题 Go 编程语言 使用 go 遇到的一个奇怪问题,求教
对于 rand.NewSource 存在竞态导致 panic 这一点应该没有异议,而 panic 没有中断而是正常被调用,说明上层应该是存在 recover 逻辑的。结合 OP #23 指出本地正常而测试服务器异常,推测本地测试的生成逻辑,而服务器完成的是全部调用逻辑。

@R18 #16 指出 byte 空值转换为 string 之后是空字符串,说明在可见代码的部分,只可能产生空字符串。 @AceGo 和 @hopingtop 判断出是上层调用将空字符串变成了全 0 字符串。

我这里做一点补充,一般要保证字符串定长都会做 padding ,用字符串 0 做填充是最常见的。
2024-05-06 19:07:30 +08:00
回复了 Livid 创建的主题 V2EX 站点状态 20240505 - 邀请码系统
我觉得金币机制的数值设计可能需要调整一下,至于如何调整我没什么思路,这个调整的影响并不是单一的。

我这个号是特意注册的,主要是回复解答问题,算是内容质量相对较高的了吧(自认为)。简单统计了一下,不到一年时间:

- 长回复(超过 5 消耗)大概 40 次,总消耗 650 左右
- 收到感谢大约 250 次,获取 2600 左右

目前余额 8500 ,其中来自于高质量内容获取(感谢)净值只有 2000 ,其他均为自动签到或者活跃度获取的。

相对来说,目前的数值设计对于活跃度的鼓励远高于内容产出。
2024-05-04 13:30:59 +08:00
回复了 1daydayde 创建的主题 奇思妙想 关于打包箱子引发的思考
可以参考这个装箱问题 wiki 上面 Bin_packing_problem

放到现实世界里,搬家这个场景一般都是一个固定大箱子,往里面放各种小箱子的问题。可能还会有更多约束,比如重量,一箱子书换谁都搬不动。所以人靠直觉一般能很快找到一个可行但不一定最优的方案,换作程序里可能就是经验或者启发式算法。

另外大箱子也是可以变形的,比如 50cm 长 30cm 宽,切割一下胶带一封就可以变成 40x40 或者 60x20 来用。
2024-05-03 23:52:25 +08:00
回复了 unclemcz 创建的主题 Ubuntu snap 已经在污染 apt
如果你在用 Ubuntu ,同时你又不认可 Canonical 的做法,我建议你考虑一下 Debian ,它是 Ubuntu 的上游发行版。

Debian 的全称是 Debian GNU/Linux ,如果不清楚什么是 GNU 可以看官方 FAQ https://www.debian.org/doc/manuals/debian-faq/basic-defs.en.html#whatisdebian

至于原因,我这里引用一下 Richard Stallman 于 1985 年写的 GNU Manifesto 中的一段话 https://www.gnu.org/gnu/manifesto.html

- Why All Computer Users Will Benefit
Users will no longer be at the mercy of one programmer or company which owns the sources and is in sole position to make changes.
1 ... 2  3  4  5  6  7  8  9  10  11 ... 17  
关于   ·   帮助文档   ·   自助推广系统   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   917 人在线   最高记录 6679   ·     Select Language
创意工作者们的社区
World is powered by solitude
VERSION: 3.9.8.5 · 33ms · UTC 20:21 · PVG 04:21 · LAX 13:21 · JFK 16:21
Developed with CodeLauncher
♥ Do have faith in what you're doing.