被 Java 毒害的脑子想在 Go 中实现一个操作,望打醒

6 天前
 assiadamo

以前接触过的一个 Java 项目,实现了一种在我看来很新的做法:

  1. 代码生成的协议类,里面自带了一个未实现的 process 方法
public class Echo extend Msg {
	String msg;
  	public void decode(){}
  	public void encode(){}
  	public void process() throws Exception {
  		throw new UnsupportedOperationException();
  	}
}
  1. 代码生成的协议处理类,格式是这样的
@MsgProcess
public static boolean process(Echo echo) {
	return true;
}
  1. 框架启动的时候,会反射获取到注解@MsgProcess的 Metchod 和他的参数,然后用 javaassist 的字节码操作,将协议类Echoprocess方法给替换掉!这样框架层调用协议的msg.process()就可以直接执行业务逻辑!

Java 写了 10 年,一说起框架,自然想到的就是各种设计模式抽象继承与反射之类,当写 Go 的时候,也受到影响,我现在想用 Go 实现类似的操作,实践的效果如下

  1. 代码生成了 Echo 协议类
package proto
type Echo struct {
	BaseMsg
	Msg string
}
func (msg *Echo) Decode(src *bytes.Buffer) error {}
func (msg *Echo) Encode(dst *bytes.Buffer) error {}
func (msg *Echo) Process() {
	panic("implement me")
}
  1. 代码生成了业务逻辑类
package logic
import proto
func ProcessEcho(msg *proto.Echo) {}
  1. 使用 ast/parser 将Echoprocess的方法体替换为ProcessEcho
func (msg *Echo) Process() {
	logic.ProcessEcho(msg)
}

但重新生成的 Echo 类,有一些问题,首先生成出来的文件,我将其保存为echo_override.go放在另一个 package ,相关的 import 都可能有问题,然后Processimport 了 logic ,而 logic 自然要 import echo ,非常经典的 import cycle 。

这是第一步遇到的问题,我打算先用 interface 解决看看,为什么不用 func 替换,我觉得好丑啊!各位 Go 大神有没有什么建议?我这种思路,符合 Go 的设计哲学吗?

5922 次点击
所在节点    Go 编程语言
68 条回复
pkoukk
5 天前
我曾经也这么想过,后来彻底理解了组合优于继承之后,就再也没动过这个念头
真的,抛弃继承吧,仔细想想,你只是为了要其中的几个函数而已,组合个 interface 就可以了,没必要要求依赖那个 struct
assiadamo
5 天前
@povsister 我理解 register 需要开发者自己做,就兴趣缺缺
assiadamo
5 天前
@povsister 我预想的使用方式是开发者定义好协议,这个协议可能带 package 信息,然后 go generate ,所有的模板都生成好了,开发者只需要打开一个生成的 go 文件写业务代码就行
assiadamo
5 天前
@NessajCN 我理解一下,如果有代码例子就更好了
povsister
5 天前
@assiadamo
#23 你这才叫毒瘤… 改 go generate 代码真不是碳基生物能想的活,不要滥用 generate 和开发脚手架。
securityCoding
5 天前
注入一个 process interface 就好了...
assiadamo
5 天前
@povsister 因为以前用过这样的框架,觉得用起来非常爽,现在也算是体验到了框架开发者的心情
kuanat
5 天前
我有两个想法:

- 编译时方案,可以交给外部 preprocessor 当作模板来处理,后续代码生成之后再用 Go 编译,当然这个外部工具也可以用 go 写。目前来看基本上都要用特定的模板写法,而不是 Go 代码。

- 运行时方案,理论上这个需求和 hot reloading 应该差不多,对于 JIT 来说是比较好实现的,对于 Go 应该比较难。像 C 没有 runtime 是可以做到的,如果 Go 要实现类似的功能我估计需要魔改 runtime 才行。
NessajCN
5 天前
@assiadamo
譬如我现在定义两个 struct, 或者按你的说法是协议
type EncStr struct {
Raw string
Encoded string
}

type DecStr struct {
Encrypted string
Decoded string
}

我要在业务里 Process 他俩,譬如打印出人能看到的信息,也就是在 EncStr 里的 Raw 或 DecStr 里的 Decoded
那我在业务里先定义一个 interface
type Protocol interface {
Print()
}
再定义一个
func Process(p Protocol) {
p.Print()
}
这时候业务里只有他俩就够了

回到前面定义协议的地方,加上下面的内容
func (e *EncStr)Print() {
fmt.Println(e.Raw)
}

func (d *DecStr) Print() {
fmt.Println(d.Decoded)
}

然后你在业务里调用 Process 函数就行了
https://go.dev/play/p/IaPb1GktEsS
kuanat
5 天前
至于是不是符合 Go 哲学的问题,我看不出这样做的意义。正常使用接口就可以了。
HiShan
5 天前
奇怪,咋这么多人把自己菜说是被 Java 毒害。。。。
assiadamo
5 天前
@NessajCN 这样和我的需求反过来了...变成了协议的 package 里写业务逻辑,业务的 package 生成后不动了....
NessajCN
5 天前
@assiadamo 业务逻辑总是要写在一个地方的,不是写包里就是写外边,你不就是要让业务那边不管框架怎么处理只专注业务本身并且不需要重复写函数定义吗。
如果这样还不行那恕我实在没法理解你的业务逻辑到底想写在哪。
Jemini
5 天前
自从用了 wire 之后,我现在写 go 代码都是一股 java 味
Jemini
5 天前
@Jemini 可以试试 wire ,也许能解决你的问题
assiadamo
5 天前
@NessajCN 兜兜转转还是用了上面说的生成个 handles.go 的方法,目前能跑通
```
package proto
type Echo struct {
BaseMsg
Msg string
}
func (msg *Echo) Decode(src *bytes.Buffer) error {}
func (msg *Echo) Encode(dst *bytes.Buffer) error {}
func (echo *Echo) Process() error {
return MsgProcessor[echo.GetHeader().TypeId](echo)
}
```
外部代码生成个放所有业务逻辑入口的 map
```
type MsgProcessorFunc[T Msg] func(msg T) error

var MsgProcessor = map[int32]MsgProcessorFunc[Msg]{}
MsgProcessor[1] = func(msg io.Msg) error { return echo.ProcessEcho(msg.(*proto.Echo)) }
```
在 echo.ProcessEcho 中写实际业务,协议和业务分开
痛苦
mrjnamei
5 天前
你可以实现一下 protobuf 的插件方法,具体可以参考这个做法:

[https://github.com/micro/micro/blob/v3.19.0/cmd/protoc-gen-micro/main.go]( https://github.com/micro/micro/blob/v3.19.0/cmd/protoc-gen-micro/main.go)

生成的文件:
[https://github.com/micro/services/blob/master/helloworld/proto/helloworld.pb.micro.go]( https://github.com/micro/services/blob/master/helloworld/proto/helloworld.pb.micro.go)

他的做法是 protoc 在编译 pb 的时候,通过插件处理,得到想要的文件,上一个插件的输出等于当前插件的输入、
然后在此插件你可以修改生成的 pb 源文件,或者衍生出你自己的 pb 文件,插入你自己想要的代码。

至于你的问题:循环引用

通常来说,pb 文件不引用工程里面的任何依赖,pb 文件属于最底层的设施,如果需要引用其他文件,建议定义出 interface, 然后在 pb 里面引用该 interface ,再在上层注入具体的实现类。
NessajCN
5 天前
@assiadamo 所以你还是在外面写逻辑不是在框架里写,那你在外面写了外面调就是了为啥一定要传回去?
leonshaw
5 天前
你应该就是想把注册函数调用包装成注解语法糖
assiadamo
5 天前
@NessajCN 业务逻辑写在框架外面,这里的框架是通信框架,业务逻辑调用的入口肯定是框架吧。
典型的长链接服务器处理流程:
1. 绑定端口等待链接
2. 从链接获取数据,解析成协议
3. 从协议号获取对应的业务逻辑处理函数,传入协议体
4. 若需要返回结果,也要包装成协议,编码成字节属于,通过链接写回

我认为 go 的设计哲学突出了一个简单,让基于网络层的服务器程序都非常容易实现,所以当然能一把梭全写在一起。但 java 的设计逻辑很不一样,看中抽象复用等很软工的东西,我受毒害很深。
说到软工,分层设计是很有用的思路,上述步骤中 12 应该都是通信层做的事情,协议作为通信层和业务层的桥梁,虽然位置和业务层在一起,但不应该有任何编码行为,比如 protobuf 生成的协议类,注释就有 DO NOT EDIT IT 。
问题也在这里,protobuf 不能自解释,一段数据来了不知道他是什么协议,需要再包一层加上协议号或其他数据,再结合 go 自己的一些特性,比如参数是接口的函数,不接受接口的实现类做入参,ChatGPT 说 Go 不支持协变,我都不知道有这种词,让单纯的写业务逻辑变的艰难,我见过一些框架,直接传入业务层 byte 数组,在业务层做协议编解码,我忍不了这个,所以才折腾这一出。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1095483

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX