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

go 程序结构的探讨

  •  
  •   yujianwjj · 1 天前 · 897 次点击

    一个项目就是几个模块的组合,假设有 3 个模块,目前我看到以下几种架构:

    type InterfaceA interface {
    	MethodA1()
    	MethodA2()
    }
    type structA struct {
    	a int
    }
    func (a *structA)MedhtodA1() {}
    func (a *structA)MedhtodA2() {}
    
    func (a *structA)run(context) {
       for {
          // MethodA1() 和 MethodA2() 的一些调用
       }
    }
    
    func NewStructA() structA {
       a := structA{}
       go a.run()
       return a
    }
    
    // 类似的还有 InterfaceB/structB ,特点都是在 New 方法里面开了一个 goroutine ,运行 run 方法。
    
    type InterfaceC interface {
    	MethodC1()
    	MethodC2()
    	MethodC3()
    }
    // 这里 c 的运行需要 a
    type structC struct {
    	a struct
    	c string
    }
    func (c *structC)MedhtodC1() {}
    func (c *structC)MedhtodC2() {}
    func (c *structC)MedhtodC3() {}
    
    
    func (a *structA)run(context) {
       for {
          // MethodC1()、MethodC2()、MethodC3()及 MethodA()的一些调用
       }
    }
    
    func NewStructC() InterfaceC {
       a := structA{}
       c := structC{ a }
       go c.run()
       return c
    }
    
    type manager struct {
    	ib InterfaceB
    	ic InterfaceC
    }
    
    func NewManager() *manager{
    	ia := NewStructA()
    	ib := NewStructB()
    	ic := NewStructC(ia)
    	return &manager{
    	   ib
    	   ic
    	}
    }
    
    func (m *manager)run(ch) {
    	for {
    		select {
    			case <- ch:
    				return
    		}
    	}
    }
    
    func main() {
    	m := NewManager()
    	m.run()
    }
    

    这种架构的话,很清晰的表明了程序模块,比如直接从项目的目录结构就可以看到这个程序的几个模块(一个模块对应一个目录),每个模块的接口功能表达也很清晰。但是他的问题是,在阅读代码的时候,隐藏了程序里面的几个模块的执行和交互逻辑。比如我从 main 看到 manager 的时候,再看到 manager.run 方法就结束了,完全不知道程序内部的模块是怎么运行。

    // 另外一种架构
    type Module interface {
    	Run()
    }
    type StructA struct {
    }
    func (a *StructA) Run() {
    	a.methodA1()
    	a.methodA2()
    }
    
    type StructB struct {
    }
    func (a *StructB) Run() {
    	b.methodB1()
    	b.methodB2()
    }
    
    type StructC struct {
    	a StructA
    }
    func (a *StructC) Run() {
    	a.Run()
    	c.MethodC1()
    	c.MethodC2()
    }
    
    type manager struct {
    	b StructB
    	c StructC
    }
    
    func NewManager() *manager{
    	a := NewStructA()
    	b := NewStructB()
    	c := NewStructC(a)
    	return &manager{
    		b, c
    	}
    }
    
    func (m *manager)run(ch) {
    	go m.b.Run()
    	go m.c.Run()
    	for {
    		select {
    			case <- ch:
    				return
    		}
    	}
    }
    
    func main() {
    	m := NewManager()
    	m.run()
    }
    

    这样的话,阅读代码的时候,可以快速的知道程序的执行逻辑。但是吧,每个模块对外只有一个 Run 方法,模块本身的含义又没了。

    这里,估计有人会提出下面的方案。

    type Stage interface {
    	Run()
    }
    
    type InterfaceA interface {
    	Stage
    	MethodA1()
    	MethodA2()
    }
    
    type InterfaceB interface {
    	Stage
    	MethodB1()
    	MethodB2()
    }
    
    type InterfaceC interface {
    	Stage
    	MethodC1()
    	MethodC2()
    	MethodC3()
    }
    

    这个也是可行的,但是吧,看着别扭,因为这个 Run() 其实跟接口自身的含义没啥关系。

    想问问大家,怎么设计程序的模块架构的。

    5 条回复    2024-11-02 00:07:26 +08:00
    GooMS
        1
    GooMS  
       22 小时 45 分钟前 via Android
    不明白第一个问题,你的外层 run 起来后监听信号不就结束了吗 看代码到服务里去看
    yujianwjj
        2
    yujianwjj  
    OP
       18 小时 4 分钟前 via Android
    run 是小写,外层调用不了
    neotheone2333
        3
    neotheone2333  
       14 小时 49 分钟前
    OP 的意思是,struct 的属性类型指定 interface 还是 struct 的问题?

    如果我理解的没错的话,我的习惯是:
    1. 属性类型用 interface
    2. NewXXX 方法接受参数类型用 interface ,返回值用 struct
    3. 看 interface 的实现用 IDE 的 go to implementation 功能,具体哪走的一个实现 debug 打断点看
    reatang
        4
    reatang  
       10 小时 16 分钟前
    interface 主要用在:
    1 、可替换实现的地方
    2 、会产生包依赖冲突的地方

    其他情况下,不建议使用 interface
    CLMan
        5
    CLMan  
       2 小时 14 分钟前
    模式 1 ,定义了一堆 interface ,模块执行逻辑在 New 里面,间接在 run 里面。
    模式 2 ,定义了一个通用的 Module 接口,模块实现 Module 接口,模块执行逻辑在 Run ,由 Manager 在 run 里面调用。
    模式 3 ,定义了一个通用接口 Stage ,以及定义了一堆 interface ,模块实现 Stage 接口以及自身的接口,模块执行逻辑在 Run ,由 Manager 在 run 里面调用。

    所以你这 3 种模式,无非就是抽取了一堆接口,有点 Java 了,代码的可读性没有任何区别。

    首先,模式 1 的将模块执行触发丢在 New 里面,通常是不建议的,因为多模块系统是由模块组装得到的,最后再走启动的流程,模块提前执行是不合适的。

    其次,除非真有必要,Go 是很少上来就抽取一堆 interface ,建议先写下原型,根据实际需要再抽取接口。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1214 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 18:21 · PVG 02:21 · LAX 11:21 · JFK 14:21
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.