Golang Go语言中 被 Java 毒害的脑子想在 Go 中实现一个操作 望打醒
以前接触过的一个 Java 项目,实现了一种在我看来很新的做法:
- 代码生成的协议类,里面自带了一个未实现的 process 方法
public class Echo extend Msg {
String msg;
public void decode(){}
public void encode(){}
public void process() throws Exception {
throw new UnsupportedOperationException();
}
}
- 代码生成的协议处理类,格式是这样的
[@MsgProcess](/user/MsgProcess)
public static boolean process(Echo echo) {
return true;
}
- 框架启动的时候,会反射获取到注解
[@MsgProcess](/user/MsgProcess)
的 Metchod 和他的参数,然后用 javaassist 的字节码操作,将协议类Echo
的process
方法给替换掉!这样框架层调用协议的msg.process()
就可以直接执行业务逻辑!
Java 写了 10 年,一说起框架,自然想到的就是各种设计模式抽象继承与反射之类,当写 Go 的时候,也受到影响,我现在想用 Go 实现类似的操作,实践的效果如下
- 代码生成了 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")
}
- 代码生成了业务逻辑类
package logic
import proto
func ProcessEcho(msg *proto.Echo) {}
- 使用 ast/parser 将
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 的设计哲学吗?
Golang Go语言中 被 Java 毒害的脑子想在 Go 中实现一个操作 望打醒
更多关于Golang Go语言中 被 Java 毒害的脑子想在 Go 中实现一个操作 望打醒的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
在我看来只需要定义
func Process(msg *Echo)就够了,再来个 ProcessEcho 意义何在
更多关于Golang Go语言中 被 Java 毒害的脑子想在 Go 中实现一个操作 望打醒的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
我是不喜欢反射,反射会让代码变得不可预测
因为不要去 proto 包下生成的协议类中写业务逻辑,想把两边的编码隔离开
同不喜欢反射,滥用反射的场景太多了。
把 echo.go 改名成 echo.go.proto ,把 echo_override.go 改名成 echo.go 呗
那就直接在 logic 里定义 Process 啊,proto 那里那个不用了呗
有点天才,可以一试
框架想做到不接触业务层,直接调用协议的msg.Process()
就能执行业务逻辑,否则还要手动将业务层的各种协议处理函数注册一下
你一边说不想在框架里写业务,一边又说直接调用框架里的方法执行业务逻辑,你到底想说啥?
proto 生成的是 interface
自己实现这个 interface 不就好了
这就是想要做到的魔法
Go 的 interface 不能放成员变量就很可惜,比如协议内数据定义,Decode/Encode 相关的代码都不想出现在业务层,要想用 interface 就要再搞个 BaseEcho 之类的组合,还是觉得有点丑
所以你的意思让框架猜你的业务逻辑?
Java 有注解和字节码替换,可以批量的处理一系列相同定义的东西,但 Go 如果没有相关的魔法,就要手动的注册函数,比如经典的 HandleFunc(path, func(){}),这样在玩具中,或者接口比较少的项目中可以手动,但如果有几百上千种协议定义呢?
当然可以借用代码生成,比如再生成一个 handle.go ,我想省去这个文件
我试着理解你的意图:你是不是想定义一个 func Process() , 这个 Process 的参数可以是 Echo 类型的量,也是跟 Echo 类似的还有几百上千个的其他结构体的变量,你不想为这些结构体里一个一个定义 Process 函数?
不是一个
Echo 协议类生成的时候,会伴随着在业务项目中生成一个<br>func Process 协议名(协议){}<br>
这样其实当协议收取的时候,框架就应该知道对应的处理函数是什么,信息完全是足够的,但需要一个方法让协议内部的 Process 方法和业务对应协议 Prosess 方法联动起来,我想这一步让框架自己处理,而不是手动的去配置联动关系
补充楼上,你顺带也可以把 Constraint 扩展一下加个 Name 方法,这样直接 struct 定义业务消息处理和业务消息类型,框架负责序列化反序列化。
更多一点,通过提供不同入参的 register 方法,或者变长参数提供 register option ,还可以实现业务可选定义 encode decode ,或者直接将整个 codec 层可插拔可协商,非常简洁明了。
那我基本确定了,你可能没太理解 interface 的本质
Go 里的 interface 就是为你说的这种应用场景准备的
你不需要给每个协议都生成一个 Process ,
只需要定义个 interface ,再定义单独一个 Process ,用这个 interface 当参数类型
然后在框架里给每个协议定义好 interface 需要用到的方法就行
这样就是业务完全不需要管框架里怎么实现 Process , 直接定义一个协议结构体变量,然后 Process 它即可
我曾经也这么想过,后来彻底理解了组合优于继承之后,就再也没动过这个念头
真的,抛弃继承吧,仔细想想,你只是为了要其中的几个函数而已,组合个 interface 就可以了,没必要要求依赖那个 struct
我理解 register 需要开发者自己做,就兴趣缺缺
我预想的使用方式是开发者定义好协议,这个协议可能带 package 信息,然后 go generate ,所有的模板都生成好了,开发者只需要打开一个生成的 go 文件写业务代码就行
我理解一下,如果有代码例子就更好了
#23 你这才叫毒瘤… 改 go generate 代码真不是碳基生物能想的活,不要滥用 generate 和开发脚手架。
注入一个 process interface 就好了…
因为以前用过这样的框架,觉得用起来非常爽,现在也算是体验到了框架开发者的心情
我有两个想法:
- 编译时方案,可以交给外部 preprocessor 当作模板来处理,后续代码生成之后再用 Go 编译,当然这个外部工具也可以用 go 写。目前来看基本上都要用特定的模板写法,而不是 Go 代码。
- 运行时方案,理论上这个需求和 hot reloading 应该差不多,对于 JIT 来说是比较好实现的,对于 Go 应该比较难。像 C 没有 runtime 是可以做到的,如果 Go 要实现类似的功能我估计需要魔改 runtime 才行。
譬如我现在定义两个 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
奇怪,咋这么多人把自己菜说是被 Java 毒害。。。。
这样和我的需求反过来了…变成了协议的 package 里写业务逻辑,业务的 package 生成后不动了…
业务逻辑总是要写在一个地方的,不是写包里就是写外边,你不就是要让业务那边不管框架怎么处理只专注业务本身并且不需要重复写函数定义吗。
如果这样还不行那恕我实在没法理解你的业务逻辑到底想写在哪。
自从用了 wire 之后,我现在写 go 代码都是一股 java 味
可以试试 wire ,也许能解决你的问题
兜兜转转还是用了上面说的生成个 handles.go 的方法,目前能跑通<br>package proto<br>type Echo struct {<br> BaseMsg<br> Msg string<br>}<br>func (msg *Echo) Decode(src *bytes.Buffer) error {}<br>func (msg *Echo) Encode(dst *bytes.Buffer) error {}<br>func (echo *Echo) Process() error {<br> return MsgProcessor[echo.GetHeader().TypeId](echo)<br>}<br>
外部代码生成个放所有业务逻辑入口的 map<br>type MsgProcessorFunc[T Msg] func(msg T) error<br><br>var MsgProcessor = map[int32]MsgProcessorFunc[Msg]{}<br>MsgProcessor[1] = func(msg io.Msg) error { return echo.ProcessEcho(msg.(*proto.Echo)) }<br>
在 echo.ProcessEcho 中写实际业务,协议和业务分开
痛苦
你可以实现一下 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 ,再在上层注入具体的实现类。
所以你还是在外面写逻辑不是在框架里写,那你在外面写了外面调就是了为啥一定要传回去?
你应该就是想把注册函数调用包装成注解语法糖
业务逻辑写在框架外面,这里的框架是通信框架,业务逻辑调用的入口肯定是框架吧。
典型的长链接服务器处理流程:
1. 绑定端口等待链接
2. 从链接获取数据,解析成协议
3. 从协议号获取对应的业务逻辑处理函数,传入协议体
4. 若需要返回结果,也要包装成协议,编码成字节属于,通过链接写回
我认为 go 的设计哲学突出了一个简单,让基于网络层的服务器程序都非常容易实现,所以当然能一把梭全写在一起。但 java 的设计逻辑很不一样,看中抽象复用等很软工的东西,我受毒害很深。
说到软工,分层设计是很有用的思路,上述步骤中 12 应该都是通信层做的事情,协议作为通信层和业务层的桥梁,虽然位置和业务层在一起,但不应该有任何编码行为,比如 protobuf 生成的协议类,注释就有 DO NOT EDIT IT 。
问题也在这里,protobuf 不能自解释,一段数据来了不知道他是什么协议,需要再包一层加上协议号或其他数据,再结合 go 自己的一些特性,比如参数是接口的函数,不接受接口的实现类做入参,ChatGPT 说 Go 不支持协变,我都不知道有这种词,让单纯的写业务逻辑变的艰难,我见过一些框架,直接传入业务层 byte 数组,在业务层做协议编解码,我忍不了这个,所以才折腾这一出。
对的,如果有注解就不会太纠结
你要不写段完整点的代码,发 playground 或 gist 都行,我看看你到底要做成啥样
至少从你的文字里我还是没法理解你在框架外面写的逻辑为啥还要传回给框架
看看 kratos 框架用的那套代码生成是不是能解决你的问题
人菜确实该怪 Java 。。。早让他写 C 艹写个 10 年,不只需要 3 年,你看他还菜不菜。
还是针对原帖,没细看楼主的评论内容。
贴中 Java 注解实现的注解非常好,不是什么毒害,Go 里面还不太好用优雅的方式实现类似的能力。
其中一个原因是 Go 是单向依赖,如果依赖图最终 main 到达不了,就会排除编译。
Java springboot 添加完扫描目录就会被编译进去了,且循环依赖也不是什么事情。
// 注册协议处理函数
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 MsgProcessorecho.TypeId
}
// ========= 业务层 ===========
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()
}
这是可执行的代码示例
做了很多工作就是为了干掉那两句注册
从最后一个评论,我想起来了 Java 确实有一个很深的毒害就是过度 DRY ,Go 里面是鼓励 copy 一部分代码减少依赖,或者说减少整体复杂度的。
少贴了 这是全部
// ========= 通信框架层 ===========
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 MsgProcessorecho.TypeId
}
// ========= 业务层 ===========
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()
}
也不算是 XY 作者应该就是想把,框架层,协议层,业务层彻底解耦和,不想写一点冗余代码。Go 里面确实有点难搞。其中一个很重要的原因是 Go 对依赖的管理跟 Java 完全不是路子。
搞那么多就是为了省掉注册两行?你早说呗
开发脚手架 new 个消息桩子出来,然后生成一下 init ,导入包路径即注册,这不是很简单的
可是 golang 的 duck typing 风格的 interface ,就单纯按官方风格正确使用,就已经是非常(超过其他主流语言)的解耦了
在我看来,这里描述不清楚问题也讨论不明白,是因为出发点已经错了,又希望按正常的思路解决一个错误 or 不存在的问题。所以我说是 XY 问题
作为一个只会前端的人看你们讨论这个和看魔法一样,请教下想了解这些是不是从 Java 开始学比较合适?
我看前端也是看魔法的,比如 vue 为啥改了 data 里的数据,显示也跟着变了
我精简了一下你的代码,你看看哪里不符合你的需求? 我就直接用了你的业务逻辑
https://go.dev/play/p/zCDaPEkuPL3
同问,简直像看天书
Vue 这个还是比较好理解的,Vue 2 和 3 的实现原理不太一样,前者是用了 Object.defineProperty ,后者用的是 Proxy ,原理基本都是拦截对 data 的访问,并记录依赖了这个数据的函数,data 数据中发生变化时重新执行依赖中记录的函数就可以更新视图啦。
反射,字节码替换 process ,这操作好骚啊,为什么不用设计模式,比如策略?没细看评论
首先 go 用 codegen 这个思路没问题,可以看一下 grpc 是怎么做的,利用了 go implements interface implicitly 这个特性,codegen 依赖 service 的 interface ,具体 service 实现留着让人在外部包完成,最后启动的时候组装,也就是形成了单向依赖:
- main 包依赖 protocol 包、service 包
- protocol 和 service 之间不互相依赖( service 隐式实现了 protocol 定义的一个 interface ,但是不需要导入它)
看起来似乎你是想给 Msg 的实现类动态指定 process 函数,在 Go 里函数可以作为变量来使用,所以你可以在 Msg 的实现类 Echo 里添加一个字段 Processor ,类型是接口或函数,然后 Echo 的 process 方法内直接固定调用(e *Echo).processor(),具体的 Processor 实现在 NewEcho 注入即可。
和#59 的方式类似,但是 Map[TypeID]Processor 不直观,不如 NewEcho(processor)明确,易于阅读。
还有你 Java 里的那种写法我觉得是旧时代的遗毒,Java 也可以像 Go 那样,将函数作为变量,一切都简单明了,易于追踪。
策略模式就是类型和他的处理函数的 map ,是用上的,Java 和 Go 的区别在于,Java 可以依赖注解在运行时构建,而 Go 只能手动注册或代码生成,总之要在编译器前准备好,运行时虽然可能可以做到,但性能不一定好
我理解了,虽然仍然要在 main 中组装,但基于 interface 的写法更符合 go 的设计一点,我会试试看
这我说过 想要动态化 一反射 二代码生成
都含不可确定性 代码生成少一些不可确定性