以前接触过的一个 Java 项目,实现了一种在我看来很新的做法:
public class Echo extend Msg {
String msg;
public void decode(){}
public void encode(){}
public void process() throws Exception {
throw new UnsupportedOperationException();
}
}
@MsgProcess
public static boolean process(Echo echo) {
return true;
}
@MsgProcess
的 Metchod 和他的参数,然后用 javaassist 的字节码操作,将协议类Echo
的process
方法给替换掉!这样框架层调用协议的msg.process()
就可以直接执行业务逻辑!Java 写了 10 年,一说起框架,自然想到的就是各种设计模式抽象继承与反射之类,当写 Go 的时候,也受到影响,我现在想用 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 (msg *Echo) Process() {
panic("implement me")
}
package logic
import proto
func ProcessEcho(msg *proto.Echo) {}
Echo
的process
的方法体替换为ProcessEcho
func (msg *Echo) Process() {
logic.ProcessEcho(msg)
}
但重新生成的 Echo 类,有一些问题,首先生成出来的文件,我将其保存为echo_override.go
放在另一个 package ,相关的 import 都可能有问题,然后Process
import 了 logic ,而 logic 自然要 import echo ,非常经典的 import cycle 。
这是第一步遇到的问题,我打算先用 interface 解决看看,为什么不用 func 替换,我觉得好丑啊!各位 Go 大神有没有什么建议?我这种思路,符合 Go 的设计哲学吗?
1
NessajCN 13 天前 via Android 1
在我看来只需要定义
func Process(msg *Echo)就够了,再来个 ProcessEcho 意义何在 |
2
yolee599 13 天前 via Android 2
我是不喜欢反射,反射会让代码变得不可预测
|
5
zjsxwc 13 天前 via Android 1
把 echo.go 改名成 echo.go.proto ,把 echo_override.go 改名成 echo.go 呗
|
10
to2false 13 天前
proto 生成的是 interface
自己实现这个 interface 不就好了 |
12
assiadamo OP @to2false Go 的 interface 不能放成员变量就很可惜,比如协议内数据定义,Decode/Encode 相关的代码都不想出现在业务层,要想用 interface 就要再搞个 BaseEcho 之类的组合,还是觉得有点丑
|
14
assiadamo OP @NessajCN Java 有注解和字节码替换,可以批量的处理一系列相同定义的东西,但 Go 如果没有相关的魔法,就要手动的注册函数,比如经典的 HandleFunc(path, func(){}),这样在玩具中,或者接口比较少的项目中可以手动,但如果有几百上千种协议定义呢?
当然可以借用代码生成,比如再生成一个 handle.go ,我想省去这个文件 |
15
zerozerone 13 天前
|
16
NessajCN 13 天前
@assiadamo 我试着理解你的意图:你是不是想定义一个 func Process() , 这个 Process 的参数可以是 Echo 类型的量,也是跟 Echo 类似的还有几百上千个的其他结构体的变量,你不想为这些结构体里一个一个定义 Process 函数?
|
17
povsister 13 天前
|
18
assiadamo OP @NessajCN 不是一个
Echo 协议类生成的时候,会伴随着在业务项目中生成一个 ``` func Process 协议名(协议){} ``` 这样其实当协议收取的时候,框架就应该知道对应的处理函数是什么,信息完全是足够的,但需要一个方法让协议内部的 Process 方法和业务对应协议 Prosess 方法联动起来,我想这一步让框架自己处理,而不是手动的去配置联动关系 |
19
povsister 13 天前
补充楼上,你顺带也可以把 Constraint 扩展一下加个 Name 方法,这样直接 struct 定义业务消息处理和业务消息类型,框架负责序列化反序列化。
更多一点,通过提供不同入参的 register 方法,或者变长参数提供 register option ,还可以实现业务可选定义 encode decode ,或者直接将整个 codec 层可插拔可协商,非常简洁明了。 |
20
NessajCN 13 天前
@assiadamo
那我基本确定了,你可能没太理解 interface 的本质 Go 里的 interface 就是为你说的这种应用场景准备的 你不需要给每个协议都生成一个 Process , 只需要定义个 interface ,再定义单独一个 Process ,用这个 interface 当参数类型 然后在框架里给每个协议定义好 interface 需要用到的方法就行 这样就是业务完全不需要管框架里怎么实现 Process , 直接定义一个协议结构体变量,然后 Process 它即可 |
21
pkoukk 13 天前
我曾经也这么想过,后来彻底理解了组合优于继承之后,就再也没动过这个念头
真的,抛弃继承吧,仔细想想,你只是为了要其中的几个函数而已,组合个 interface 就可以了,没必要要求依赖那个 struct |
23
assiadamo OP @povsister 我预想的使用方式是开发者定义好协议,这个协议可能带 package 信息,然后 go generate ,所有的模板都生成好了,开发者只需要打开一个生成的 go 文件写业务代码就行
|
25
povsister 13 天前 via iPhone 2
@assiadamo
#23 你这才叫毒瘤… 改 go generate 代码真不是碳基生物能想的活,不要滥用 generate 和开发脚手架。 |
26
securityCoding 13 天前
注入一个 process interface 就好了...
|
28
kuanat 13 天前
我有两个想法:
- 编译时方案,可以交给外部 preprocessor 当作模板来处理,后续代码生成之后再用 Go 编译,当然这个外部工具也可以用 go 写。目前来看基本上都要用特定的模板写法,而不是 Go 代码。 - 运行时方案,理论上这个需求和 hot reloading 应该差不多,对于 JIT 来说是比较好实现的,对于 Go 应该比较难。像 C 没有 runtime 是可以做到的,如果 Go 要实现类似的功能我估计需要魔改 runtime 才行。 |
29
NessajCN 13 天前
@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 |
30
kuanat 13 天前
至于是不是符合 Go 哲学的问题,我看不出这样做的意义。正常使用接口就可以了。
|
31
HiShan 13 天前 5
奇怪,咋这么多人把自己菜说是被 Java 毒害。。。。
|
33
NessajCN 13 天前
@assiadamo 业务逻辑总是要写在一个地方的,不是写包里就是写外边,你不就是要让业务那边不管框架怎么处理只专注业务本身并且不需要重复写函数定义吗。
如果这样还不行那恕我实在没法理解你的业务逻辑到底想写在哪。 |
34
Jemini 13 天前
自从用了 wire 之后,我现在写 go 代码都是一股 java 味
|
36
assiadamo OP @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 中写实际业务,协议和业务分开 痛苦 |
37
mrjnamei 13 天前
你可以实现一下 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 ,再在上层注入具体的实现类。 |
39
leonshaw 13 天前 via Android
你应该就是想把注册函数调用包装成注解语法糖
|
40
assiadamo OP @NessajCN 业务逻辑写在框架外面,这里的框架是通信框架,业务逻辑调用的入口肯定是框架吧。
典型的长链接服务器处理流程: 1. 绑定端口等待链接 2. 从链接获取数据,解析成协议 3. 从协议号获取对应的业务逻辑处理函数,传入协议体 4. 若需要返回结果,也要包装成协议,编码成字节属于,通过链接写回 我认为 go 的设计哲学突出了一个简单,让基于网络层的服务器程序都非常容易实现,所以当然能一把梭全写在一起。但 java 的设计逻辑很不一样,看中抽象复用等很软工的东西,我受毒害很深。 说到软工,分层设计是很有用的思路,上述步骤中 12 应该都是通信层做的事情,协议作为通信层和业务层的桥梁,虽然位置和业务层在一起,但不应该有任何编码行为,比如 protobuf 生成的协议类,注释就有 DO NOT EDIT IT 。 问题也在这里,protobuf 不能自解释,一段数据来了不知道他是什么协议,需要再包一层加上协议号或其他数据,再结合 go 自己的一些特性,比如参数是接口的函数,不接受接口的实现类做入参,ChatGPT 说 Go 不支持协变,我都不知道有这种词,让单纯的写业务逻辑变的艰难,我见过一些框架,直接传入业务层 byte 数组,在业务层做协议编解码,我忍不了这个,所以才折腾这一出。 |
42
jeesk 13 天前 via Android
@assiadamo 还有一点就是 request 和 response 有规范,java 也有 servlet api ,但是有些根本不鸟 servlet , 导致有各种框架
|
43
NessajCN 13 天前
@assiadamo 你要不写段完整点的代码,发 playground 或 gist 都行,我看看你到底要做成啥样
至少从你的文字里我还是没法理解你在框架外面写的逻辑为啥还要传回给框架 |
44
soul11201 13 天前 via Android
@NessajCN #29 OP 的诉求确实没看明白想干嘛。看原文应该是做关注点分离:看上去想把 rpc 协议生成代码,跟基于生成的代码写的业务逻辑做分离。先前用 thrift 的时候,thrift 解决思路感觉还不错,记得大概思路:
1. 基于协议生成 interface ,并且组合 thrift 基础设施层的 interface 2. 实现 interface 对应的业务逻辑 struct 3. 注册类实例,配合 thrift 留置的中间件能力实现 aop 这套整体解决方案还算可以,即不太复杂,还做到了关注点分离 |
45
Z1on 13 天前 via Android
看看 kratos 框架用的那套代码生成是不是能解决你的问题
|
47
soul11201 13 天前 via Android 1
还是针对原帖,没细看楼主的评论内容。
贴中 Java 注解实现的注解非常好,不是什么毒害,Go 里面还不太好用优雅的方式实现类似的能力。 其中一个原因是 Go 是单向依赖,如果依赖图最终 main 到达不了,就会排除编译。 Java springboot 添加完扫描目录就会被编译进去了,且循环依赖也不是什么事情。 |
48
assiadamo OP @NessajCN
// 注册协议处理函数 type MsgProcessorFunc[T Msg] func(msg T) error var MsgProcessor = map[int32]MsgProcessorFunc[Msg]{} // 注册协议创建函数 var MsgCreator = map[int32]func() Msg{} // ========= 生成的协议类 =========== type Echo struct { // TypeId 应该隐藏在 MsgBase 中 此处简略 TypeId int32 Msg string } func NewEcho() *Echo { return &Echo{ TypeId: 1, } } func (echo *Echo) Process() error { return MsgProcessor[echo.TypeId](echo) } // ========= 业务层 =========== func ProcessEcho(echo *Echo) error { fmt.Println(echo.Msg) return nil } // 调用例子 func main() { // 想干掉的手动注册 如果不行只能用代码生成 MsgCreator[1] = func() Msg { return NewEcho() } MsgProcessor[1] = func(msg Msg) error { return ProcessEcho(msg.(*Echo)) } // 模拟协议发送 msg := &Echo{TypeId: 1, Msg: "test"} // 省掉了编解码和 socket 操作 msg.Process() } 这是可执行的代码示例 做了很多工作就是为了干掉那两句注册 |
49
mcfog 13 天前
大型 XY 问题现场
|
50
soul11201 13 天前 via Android 2
@assiadamo 从最后一个评论,我想起来了 Java 确实有一个很深的毒害就是过度 DRY ,Go 里面是鼓励 copy 一部分代码减少依赖,或者说减少整体复杂度的。
|
51
assiadamo OP @NessajCN
少贴了 这是全部 // ========= 通信框架层 =========== type Msg interface { Process() error } // 注册协议处理函数 type MsgProcessorFunc[T Msg] func(msg T) error var MsgProcessor = map[int32]MsgProcessorFunc[Msg]{} // 注册协议创建函数 var MsgCreator = map[int32]func() Msg{} // ========= 生成的协议类 =========== type Echo struct { // TypeId 应该隐藏在 MsgBase 中 此处简略 TypeId int32 Msg string } func NewEcho() *Echo { return &Echo{ TypeId: 1, } } func (echo *Echo) Process() error { return MsgProcessor[echo.TypeId](echo) } // ========= 业务层 =========== func ProcessEcho(echo *Echo) error { fmt.Println(echo.Msg) return nil } // 调用例子 func main() { // 想干掉的手动注册 如果不行只能用代码生成 MsgCreator[1] = func() Msg { return NewEcho() } MsgProcessor[1] = func(msg Msg) error { return ProcessEcho(msg.(*Echo)) } // 模拟协议发送 msg := &Echo{TypeId: 1, Msg: "test"} // 省掉了编解码和 socket 操作 msg.Process() } |
52
xuanbg 13 天前
Java 生态确实有一种很不好的思想,就是什么都要搞个框架出来,然后把什么业务都往这个框子里面装。明明不好装也要硬装,譬如 Spring Security ,简直就是业界毒瘤。
|
53
soul11201 13 天前 via Android
@mcfog 也不算是 XY 作者应该就是想把,框架层,协议层,业务层彻底解耦和,不想写一点冗余代码。Go 里面确实有点难搞。其中一个很重要的原因是 Go 对依赖的管理跟 Java 完全不是路子。
|
54
assiadamo OP @xuanbg 互联网行业叫框架,我更喜欢把我做的这套叫引擎...
我想了想图啥呀这么折腾,很多服务器也是静态代码做引擎,业务逻辑和协议处理用脚本语言,比如 go+lua ,cpp+python ,语法糖多还不折腾还好热更,go 服务器热更要用的 plugin 也是很耐人寻味 |
55
povsister 13 天前 via iPhone
搞那么多就是为了省掉注册两行?你早说呗
开发脚手架 new 个消息桩子出来,然后生成一下 init ,导入包路径即注册,这不是很简单的 |
56
mcfog 13 天前
@soul11201 可是 golang 的 duck typing 风格的 interface ,就单纯按官方风格正确使用,就已经是非常(超过其他主流语言)的解耦了
在我看来,这里描述不清楚问题也讨论不明白,是因为出发点已经错了,又希望按正常的思路解决一个错误 or 不存在的问题。所以我说是 XY 问题 |
57
Danswerme 13 天前
作为一个只会前端的人看你们讨论这个和看魔法一样,请教下想了解这些是不是从 Java 开始学比较合适?
|
59
NessajCN 13 天前 via Android
|
61
Danswerme 12 天前
@assiadamo Vue 这个还是比较好理解的,Vue 2 和 3 的实现原理不太一样,前者是用了 Object.defineProperty ,后者用的是 Proxy ,原理基本都是拦截对 data 的访问,并记录依赖了这个数据的函数,data 数据中发生变化时重新执行依赖中记录的函数就可以更新视图啦。
|
62
yoyolichen 12 天前
反射,字节码替换 process ,这操作好骚啊,为什么不用设计模式,比如策略?没细看评论
|
63
Keuin 12 天前
首先 go 用 codegen 这个思路没问题,可以看一下 grpc 是怎么做的,利用了 go implements interface implicitly 这个特性,codegen 依赖 service 的 interface ,具体 service 实现留着让人在外部包完成,最后启动的时候组装,也就是形成了单向依赖:
- main 包依赖 protocol 包、service 包 - protocol 和 service 之间不互相依赖( service 隐式实现了 protocol 定义的一个 interface ,但是不需要导入它) |
64
ns09005264 12 天前 1
看起来似乎你是想给 Msg 的实现类动态指定 process 函数,在 Go 里函数可以作为变量来使用,所以你可以在 Msg 的实现类 Echo 里添加一个字段 Processor ,类型是接口或函数,然后 Echo 的 process 方法内直接固定调用(e *Echo).processor(),具体的 Processor 实现在 NewEcho 注入即可。
和#59 的方式类似,但是 Map[TypeID]Processor 不直观,不如 NewEcho(processor)明确,易于阅读。 还有你 Java 里的那种写法我觉得是旧时代的遗毒,Java 也可以像 Go 那样,将函数作为变量,一切都简单明了,易于追踪。 |
65
assiadamo OP @yoyolichen 策略模式就是类型和他的处理函数的 map ,是用上的,Java 和 Go 的区别在于,Java 可以依赖注解在运行时构建,而 Go 只能手动注册或代码生成,总之要在编译器前准备好,运行时虽然可能可以做到,但性能不一定好
|
67
james122333 12 天前 via Android
确实用 type embedding 组合+interface 就好
毕竟这种东西用常用静态语言实作起来都很难看 註解一样丑的不行 用 shell 类的就会很飘逸 |
68
james122333 12 天前 via Android
|