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

代码生成的协议类,里面自带了一个未实现的 process 方法

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 实现类似的操作,实践的效果如下

代码生成了 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 都可能有问题,然后Processimport 了 logic ,而 logic 自然要 import echo ,非常经典的 import cycle 。
这是第一步遇到的问题,我打算先用 interface 解决看看,为什么不用 func 替换,我觉得好丑啊!各位 Go 大神有没有什么建议?我这种思路,符合 Go 的设计哲学吗?

在我看来只需要定义
func Process(msg *Echo)就够了,再来个 ProcessEcho 意义何在

我是不喜欢反射,反射会让代码变得不可预测

因为不要去 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 ,我想省去这个文件

参考一下这个 github.com/brahma-adshonor/gohook

我试着理解你的意图:你是不是想定义一个 func Process() , 这个 Process 的参数可以是 Echo 类型的量,也是跟 Echo 类似的还有几百上千个的其他结构体的变量,你不想为这些结构体里一个一个定义 Process 函数?

你既然只关心 Process ,那直接泛型不就完事了。
很常见的设计模式,go 处理 wiredata 简直舒服的不要不要的,泛型前写这种协议代码还脏一点 interface 乱飞。现在直接给你类型检查都安排的明明白白 。
多练,多看。

业务只需实现 ProcessMsg ,然后直接业务代码自己 Register 就行了。

不是一个
Echo 协议类生成的时候,会伴随着在业务项目中生成一个

func Process 协议名(协议){}

这样其实当协议收取的时候,框架就应该知道对应的处理函数是什么,信息完全是足够的,但需要一个方法让协议内部的 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 函数就行了
go.dev/play/p/IaPb1GktEsS

至于是不是符合 Go 哲学的问题,我看不出这样做的意义。正常使用接口就可以了。

奇怪,咋这么多人把自己菜说是被 Java 毒害。。。。

这样和我的需求反过来了...变成了协议的 package 里写业务逻辑,业务的 package 生成后不动了....

业务逻辑总是要写在一个地方的,不是写包里就是写外边,你不就是要让业务那边不管框架怎么处理只专注业务本身并且不需要重复写函数定义吗。
如果这样还不行那恕我实在没法理解你的业务逻辑到底想写在哪。

自从用了 wire 之后,我现在写 go 代码都是一股 java 味

可以试试 wire ,也许能解决你的问题

兜兜转转还是用了上面说的生成个 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 中写实际业务,协议和业务分开
痛苦

你可以实现一下 protobuf 的插件方法,具体可以参考这个做法:

github.com/micro/micro/blob/v3.19.0/cmd/protoc-gen-micro/main.go

生成的文件:
github.com/micro/services/blob/master/helloworld/proto/helloworld.pb.micro.go

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

至于你的问题:循环引用

通常来说,pb 文件不引用工程里面的任何依赖,pb 文件属于最底层的设施,如果需要引用其他文件,建议定义出 interface, 然后在 pb 里面引用该 interface ,再在上层注入具体的实现类。