V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
assiadamo
V2EX  ›  Go 编程语言

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

  •  
  •   assiadamo · 10 天前 · 6050 次点击

    以前接触过的一个 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 的设计哲学吗?

    68 条回复    2024-12-07 20:24:06 +08:00
    NessajCN
        1
    NessajCN  
       10 天前 via Android   ❤️ 1
    在我看来只需要定义
    func Process(msg *Echo)就够了,再来个 ProcessEcho 意义何在
    yolee599
        2
    yolee599  
       10 天前 via Android   ❤️ 2
    我是不喜欢反射,反射会让代码变得不可预测
    assiadamo
        3
    assiadamo  
    OP
       10 天前 via iPhone
    @NessajCN 因为不要去 proto 包下生成的协议类中写业务逻辑,想把两边的编码隔离开
    cookii
        4
    cookii  
       10 天前 via Android
    @yolee599 同不喜欢反射,滥用反射的场景太多了。
    zjsxwc
        5
    zjsxwc  
       10 天前 via Android   ❤️ 1
    把 echo.go 改名成 echo.go.proto ,把 echo_override.go 改名成 echo.go 呗
    NessajCN
        6
    NessajCN  
       10 天前 via Android
    @assiadamo 那就直接在 logic 里定义 Process 啊,proto 那里那个不用了呗
    assiadamo
        7
    assiadamo  
    OP
       10 天前
    @zjsxwc 有点天才,可以一试
    assiadamo
        8
    assiadamo  
    OP
       10 天前
    @NessajCN 框架想做到不接触业务层,直接调用协议的`msg.Process()`就能执行业务逻辑,否则还要手动将业务层的各种协议处理函数注册一下
    NessajCN
        9
    NessajCN  
       10 天前 via Android
    @assiadamo 你一边说不想在框架里写业务,一边又说直接调用框架里的方法执行业务逻辑,你到底想说啥?
    to2false
        10
    to2false  
       10 天前
    proto 生成的是 interface

    自己实现这个 interface 不就好了
    assiadamo
        11
    assiadamo  
    OP
       10 天前
    @NessajCN 这就是想要做到的魔法
    assiadamo
        12
    assiadamo  
    OP
       10 天前
    @to2false Go 的 interface 不能放成员变量就很可惜,比如协议内数据定义,Decode/Encode 相关的代码都不想出现在业务层,要想用 interface 就要再搞个 BaseEcho 之类的组合,还是觉得有点丑
    NessajCN
        13
    NessajCN  
       10 天前
    @assiadamo 所以你的意思让框架猜你的业务逻辑?
    assiadamo
        14
    assiadamo  
    OP
       10 天前
    @NessajCN Java 有注解和字节码替换,可以批量的处理一系列相同定义的东西,但 Go 如果没有相关的魔法,就要手动的注册函数,比如经典的 HandleFunc(path, func(){}),这样在玩具中,或者接口比较少的项目中可以手动,但如果有几百上千种协议定义呢?
    当然可以借用代码生成,比如再生成一个 handle.go ,我想省去这个文件
    zerozerone
        15
    zerozerone  
       10 天前
    NessajCN
        16
    NessajCN  
       10 天前
    @assiadamo 我试着理解你的意图:你是不是想定义一个 func Process() , 这个 Process 的参数可以是 Echo 类型的量,也是跟 Echo 类似的还有几百上千个的其他结构体的变量,你不想为这些结构体里一个一个定义 Process 函数?
    povsister
        17
    povsister  
       10 天前
    你既然只关心 Process ,那直接泛型不就完事了。
    很常见的设计模式,go 处理 wiredata 简直舒服的不要不要的,泛型前写这种协议代码还脏一点 interface 乱飞。现在直接给你类型检查都安排的明明白白 。
    多练,多看。

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

    assiadamo
        18
    assiadamo  
    OP
       10 天前
    @NessajCN 不是一个
    Echo 协议类生成的时候,会伴随着在业务项目中生成一个
    ```
    func Process 协议名(协议){}
    ```
    这样其实当协议收取的时候,框架就应该知道对应的处理函数是什么,信息完全是足够的,但需要一个方法让协议内部的 Process 方法和业务对应协议 Prosess 方法联动起来,我想这一步让框架自己处理,而不是手动的去配置联动关系
    povsister
        19
    povsister  
       10 天前
    补充楼上,你顺带也可以把 Constraint 扩展一下加个 Name 方法,这样直接 struct 定义业务消息处理和业务消息类型,框架负责序列化反序列化。
    更多一点,通过提供不同入参的 register 方法,或者变长参数提供 register option ,还可以实现业务可选定义 encode decode ,或者直接将整个 codec 层可插拔可协商,非常简洁明了。
    NessajCN
        20
    NessajCN  
       10 天前
    @assiadamo
    那我基本确定了,你可能没太理解 interface 的本质
    Go 里的 interface 就是为你说的这种应用场景准备的
    你不需要给每个协议都生成一个 Process ,
    只需要定义个 interface ,再定义单独一个 Process ,用这个 interface 当参数类型
    然后在框架里给每个协议定义好 interface 需要用到的方法就行

    这样就是业务完全不需要管框架里怎么实现 Process , 直接定义一个协议结构体变量,然后 Process 它即可
    pkoukk
        21
    pkoukk  
       10 天前
    我曾经也这么想过,后来彻底理解了组合优于继承之后,就再也没动过这个念头
    真的,抛弃继承吧,仔细想想,你只是为了要其中的几个函数而已,组合个 interface 就可以了,没必要要求依赖那个 struct
    assiadamo
        22
    assiadamo  
    OP
       10 天前
    @povsister 我理解 register 需要开发者自己做,就兴趣缺缺
    assiadamo
        23
    assiadamo  
    OP
       10 天前
    @povsister 我预想的使用方式是开发者定义好协议,这个协议可能带 package 信息,然后 go generate ,所有的模板都生成好了,开发者只需要打开一个生成的 go 文件写业务代码就行
    assiadamo
        24
    assiadamo  
    OP
       10 天前
    @NessajCN 我理解一下,如果有代码例子就更好了
    povsister
        25
    povsister  
       10 天前 via iPhone   ❤️ 2
    @assiadamo
    #23 你这才叫毒瘤… 改 go generate 代码真不是碳基生物能想的活,不要滥用 generate 和开发脚手架。
    securityCoding
        26
    securityCoding  
       10 天前
    注入一个 process interface 就好了...
    assiadamo
        27
    assiadamo  
    OP
       10 天前
    @povsister 因为以前用过这样的框架,觉得用起来非常爽,现在也算是体验到了框架开发者的心情
    kuanat
        28
    kuanat  
       10 天前
    我有两个想法:

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

    - 运行时方案,理论上这个需求和 hot reloading 应该差不多,对于 JIT 来说是比较好实现的,对于 Go 应该比较难。像 C 没有 runtime 是可以做到的,如果 Go 要实现类似的功能我估计需要魔改 runtime 才行。
    NessajCN
        29
    NessajCN  
       10 天前
    @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
        30
    kuanat  
       10 天前
    至于是不是符合 Go 哲学的问题,我看不出这样做的意义。正常使用接口就可以了。
    HiShan
        31
    HiShan  
       10 天前   ❤️ 5
    奇怪,咋这么多人把自己菜说是被 Java 毒害。。。。
    assiadamo
        32
    assiadamo  
    OP
       10 天前
    @NessajCN 这样和我的需求反过来了...变成了协议的 package 里写业务逻辑,业务的 package 生成后不动了....
    NessajCN
        33
    NessajCN  
       10 天前
    @assiadamo 业务逻辑总是要写在一个地方的,不是写包里就是写外边,你不就是要让业务那边不管框架怎么处理只专注业务本身并且不需要重复写函数定义吗。
    如果这样还不行那恕我实在没法理解你的业务逻辑到底想写在哪。
    Jemini
        34
    Jemini  
       10 天前
    自从用了 wire 之后,我现在写 go 代码都是一股 java 味
    Jemini
        35
    Jemini  
       10 天前
    @Jemini 可以试试 wire ,也许能解决你的问题
    assiadamo
        36
    assiadamo  
    OP
       10 天前
    @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
        37
    mrjnamei  
       10 天前
    你可以实现一下 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
        38
    NessajCN  
       10 天前 via Android
    @assiadamo 所以你还是在外面写逻辑不是在框架里写,那你在外面写了外面调就是了为啥一定要传回去?
    leonshaw
        39
    leonshaw  
       10 天前 via Android
    你应该就是想把注册函数调用包装成注解语法糖
    assiadamo
        40
    assiadamo  
    OP
       10 天前 via iPhone   ❤️ 1
    @NessajCN 业务逻辑写在框架外面,这里的框架是通信框架,业务逻辑调用的入口肯定是框架吧。
    典型的长链接服务器处理流程:
    1. 绑定端口等待链接
    2. 从链接获取数据,解析成协议
    3. 从协议号获取对应的业务逻辑处理函数,传入协议体
    4. 若需要返回结果,也要包装成协议,编码成字节属于,通过链接写回

    我认为 go 的设计哲学突出了一个简单,让基于网络层的服务器程序都非常容易实现,所以当然能一把梭全写在一起。但 java 的设计逻辑很不一样,看中抽象复用等很软工的东西,我受毒害很深。
    说到软工,分层设计是很有用的思路,上述步骤中 12 应该都是通信层做的事情,协议作为通信层和业务层的桥梁,虽然位置和业务层在一起,但不应该有任何编码行为,比如 protobuf 生成的协议类,注释就有 DO NOT EDIT IT 。
    问题也在这里,protobuf 不能自解释,一段数据来了不知道他是什么协议,需要再包一层加上协议号或其他数据,再结合 go 自己的一些特性,比如参数是接口的函数,不接受接口的实现类做入参,ChatGPT 说 Go 不支持协变,我都不知道有这种词,让单纯的写业务逻辑变的艰难,我见过一些框架,直接传入业务层 byte 数组,在业务层做协议编解码,我忍不了这个,所以才折腾这一出。
    assiadamo
        41
    assiadamo  
    OP
       10 天前 via iPhone
    @leonshaw 对的,如果有注解就不会太纠结
    jeesk
        42
    jeesk  
       10 天前 via Android
    @assiadamo 还有一点就是 request 和 response 有规范,java 也有 servlet api ,但是有些根本不鸟 servlet , 导致有各种框架
    NessajCN
        43
    NessajCN  
       10 天前
    @assiadamo 你要不写段完整点的代码,发 playground 或 gist 都行,我看看你到底要做成啥样
    至少从你的文字里我还是没法理解你在框架外面写的逻辑为啥还要传回给框架
    soul11201
        44
    soul11201  
       10 天前 via Android
    @NessajCN #29 OP 的诉求确实没看明白想干嘛。看原文应该是做关注点分离:看上去想把 rpc 协议生成代码,跟基于生成的代码写的业务逻辑做分离。先前用 thrift 的时候,thrift 解决思路感觉还不错,记得大概思路:
    1. 基于协议生成 interface ,并且组合 thrift 基础设施层的 interface
    2. 实现 interface 对应的业务逻辑 struct
    3. 注册类实例,配合 thrift 留置的中间件能力实现 aop

    这套整体解决方案还算可以,即不太复杂,还做到了关注点分离
    Z1on
        45
    Z1on  
       10 天前 via Android
    看看 kratos 框架用的那套代码生成是不是能解决你的问题
    xuanbg
        46
    xuanbg  
       10 天前
    @HiShan 人菜确实该怪 Java 。。。早让他写 C 艹写个 10 年,不只需要 3 年,你看他还菜不菜。
    soul11201
        47
    soul11201  
       10 天前 via Android   ❤️ 1
    还是针对原帖,没细看楼主的评论内容。

    贴中 Java 注解实现的注解非常好,不是什么毒害,Go 里面还不太好用优雅的方式实现类似的能力。

    其中一个原因是 Go 是单向依赖,如果依赖图最终 main 到达不了,就会排除编译。
    Java springboot 添加完扫描目录就会被编译进去了,且循环依赖也不是什么事情。
    assiadamo
        48
    assiadamo  
    OP
       10 天前
    @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()
    }

    这是可执行的代码示例
    做了很多工作就是为了干掉那两句注册
    mcfog
        49
    mcfog  
       10 天前
    大型 XY 问题现场
    soul11201
        50
    soul11201  
       10 天前 via Android   ❤️ 2
    @assiadamo 从最后一个评论,我想起来了 Java 确实有一个很深的毒害就是过度 DRY ,Go 里面是鼓励 copy 一部分代码减少依赖,或者说减少整体复杂度的。
    assiadamo
        51
    assiadamo  
    OP
       10 天前
    @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()
    }
    xuanbg
        52
    xuanbg  
       10 天前
    Java 生态确实有一种很不好的思想,就是什么都要搞个框架出来,然后把什么业务都往这个框子里面装。明明不好装也要硬装,譬如 Spring Security ,简直就是业界毒瘤。
    soul11201
        53
    soul11201  
       10 天前 via Android
    @mcfog 也不算是 XY 作者应该就是想把,框架层,协议层,业务层彻底解耦和,不想写一点冗余代码。Go 里面确实有点难搞。其中一个很重要的原因是 Go 对依赖的管理跟 Java 完全不是路子。
    assiadamo
        54
    assiadamo  
    OP
       10 天前
    @xuanbg 互联网行业叫框架,我更喜欢把我做的这套叫引擎...
    我想了想图啥呀这么折腾,很多服务器也是静态代码做引擎,业务逻辑和协议处理用脚本语言,比如 go+lua ,cpp+python ,语法糖多还不折腾还好热更,go 服务器热更要用的 plugin 也是很耐人寻味
    povsister
        55
    povsister  
       10 天前 via iPhone
    搞那么多就是为了省掉注册两行?你早说呗

    开发脚手架 new 个消息桩子出来,然后生成一下 init ,导入包路径即注册,这不是很简单的
    mcfog
        56
    mcfog  
       10 天前
    @soul11201 可是 golang 的 duck typing 风格的 interface ,就单纯按官方风格正确使用,就已经是非常(超过其他主流语言)的解耦了

    在我看来,这里描述不清楚问题也讨论不明白,是因为出发点已经错了,又希望按正常的思路解决一个错误 or 不存在的问题。所以我说是 XY 问题
    Danswerme
        57
    Danswerme  
       10 天前
    作为一个只会前端的人看你们讨论这个和看魔法一样,请教下想了解这些是不是从 Java 开始学比较合适?
    assiadamo
        58
    assiadamo  
    OP
       10 天前 via iPhone
    @Danswerme 我看前端也是看魔法的,比如 vue 为啥改了 data 里的数据,显示也跟着变了
    NessajCN
        59
    NessajCN  
       10 天前 via Android
    @assiadamo
    我精简了一下你的代码,你看看哪里不符合你的需求? 我就直接用了你的业务逻辑
    https://go.dev/play/p/zCDaPEkuPL3
    daju233
        60
    daju233  
       10 天前
    @Danswerme 同问,简直像看天书
    Danswerme
        61
    Danswerme  
       9 天前
    @assiadamo Vue 这个还是比较好理解的,Vue 2 和 3 的实现原理不太一样,前者是用了 Object.defineProperty ,后者用的是 Proxy ,原理基本都是拦截对 data 的访问,并记录依赖了这个数据的函数,data 数据中发生变化时重新执行依赖中记录的函数就可以更新视图啦。
    yoyolichen
        62
    yoyolichen  
       9 天前
    反射,字节码替换 process ,这操作好骚啊,为什么不用设计模式,比如策略?没细看评论
    Keuin
        63
    Keuin  
       9 天前
    首先 go 用 codegen 这个思路没问题,可以看一下 grpc 是怎么做的,利用了 go implements interface implicitly 这个特性,codegen 依赖 service 的 interface ,具体 service 实现留着让人在外部包完成,最后启动的时候组装,也就是形成了单向依赖:
    - main 包依赖 protocol 包、service 包
    - protocol 和 service 之间不互相依赖( service 隐式实现了 protocol 定义的一个 interface ,但是不需要导入它)
    ns09005264
        64
    ns09005264  
       9 天前   ❤️ 1
    看起来似乎你是想给 Msg 的实现类动态指定 process 函数,在 Go 里函数可以作为变量来使用,所以你可以在 Msg 的实现类 Echo 里添加一个字段 Processor ,类型是接口或函数,然后 Echo 的 process 方法内直接固定调用(e *Echo).processor(),具体的 Processor 实现在 NewEcho 注入即可。
    和#59 的方式类似,但是 Map[TypeID]Processor 不直观,不如 NewEcho(processor)明确,易于阅读。

    还有你 Java 里的那种写法我觉得是旧时代的遗毒,Java 也可以像 Go 那样,将函数作为变量,一切都简单明了,易于追踪。
    assiadamo
        65
    assiadamo  
    OP
       9 天前 via iPhone
    @yoyolichen 策略模式就是类型和他的处理函数的 map ,是用上的,Java 和 Go 的区别在于,Java 可以依赖注解在运行时构建,而 Go 只能手动注册或代码生成,总之要在编译器前准备好,运行时虽然可能可以做到,但性能不一定好
    assiadamo
        66
    assiadamo  
    OP
       9 天前 via iPhone
    @Keuin 我理解了,虽然仍然要在 main 中组装,但基于 interface 的写法更符合 go 的设计一点,我会试试看
    james122333
        67
    james122333  
       9 天前 via Android
    确实用 type embedding 组合+interface 就好
    毕竟这种东西用常用静态语言实作起来都很难看 註解一样丑的不行 用 shell 类的就会很飘逸
    james122333
        68
    james122333  
       9 天前 via Android
    @yolee599

    这我说过 想要动态化 一反射 二代码生成
    都含不可确定性 代码生成少一些不可确定性
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   957 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 22:57 · PVG 06:57 · LAX 14:57 · JFK 17:57
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.