Go项目在组织启动代码上的一次尝试


最开始的项目目录

$ tree
.
├── ./api/
├── ./service/
├── ./dao/
├── ./model/
├── main.go
func main(){
	flag.Parse()
	artemis := NewArtemis(flag.String(`config`))	// artemis 是我们的框架,类似beego
	log.init()
	service.init()
	dao.init()
	worker.init()
	api.Route(artemis.Router)
	artemis.Run()
}

写个测试吧

packege dao

func TestGetNote(t *testing.T){
	db := NewNoteDB()	// panic: app not init
	db.Get(1)
}

改:

  • copy启动代码
  • 把config路径改一下
  • 去除不需要的部分,比如在 dao 里去掉 service.init()
func TestGetNote(t *testing.T){
	artemis := NewArtemis(`../../config`)
	log.init()
	ddns.init()
	service.init()
	dao.init()
	// test code 
}

// 也可以放在 TestMain 里统一处理
func TestMain(m *testing.M){
  // init()
  m.Run()
}

简单粗暴的解决办法,调试也够用了。

如果 package 比较少,也没那么多脚本要用,已经能满足需要了

但当时我觉得还是不完美,我的项目又比较复杂,然后继续尝试优化:

先抽出个函数放重复代码

package boot
func Boot(){
	// init()
}
func BootTest(){
	SetRootPath()		// 在子目录跑ut时,要把 wd 对齐到项目目录
	Boot()
}

package main
func main(){
	boot.Boot()
	artemis.Run()
}

package dao
func test(){
	boot.BootTest()
	// test code
}
$ go build
import cycle not allowed
# dao 和 boot 互相依赖

改成外部测试

package dao_test

import "dao"

func test(){
	db := dao.NewDB()
}

缺点:

  • ugly
  • 不能测私有函数
  • 每次改动几乎重新编译整个项目

最后一点很重要,测试代码时会频繁修改,而每次修改几乎都要重新编译整个项目。 我的项目在忽略缓存的情况下,编译需要30s 调个bug,一下午就过去了。。

回头一看整个过程,总结就是

  

民科

于是,放下一切,重新开始

我想要什么?

  • 不用太多重复代码,好维护
  • 需要时再加载,不需要在测试时搞一堆没用的东西

看看成熟的框架是如何处理的,以我熟悉的 Laravel 为例,很多地方都采用了这样的思路:先注册,后使用

class FooServiceProvider {
	function register($app) {
		$app->singleton("noteDB", function(){
			$noteDB = new NoteDB();
		})
		
		$app->sinleton(IRouter::class, new Router())
	}
	
	function boot() {
		make(IRouter::class)->load();
	}
}

突然发现,Go的init就是天然的注册机制

  • 所有的init都一定会在main之前执行
  • 依赖包的init一定先于本包的init,连优先级都帮我做好了

于是大概轮廓就出来了:

  • 在每个 init 里定义自己需要启动什么,代码里依赖了哪个包,就会调用对应的init,不依赖的不会启动
  • 运行cmd(webserver/script/test)时,再逐个执行init时注册的任务
package dao
func init(){
	boot.Register(func(){
		// ...
	})
}
// web server
func main(){
	boot.Boot()
	defer boot.Shutdown()

	api.Route(artemis.App)
	artemis.Run()
}

// script
func main(){
	boot.Boot()
	// ...
}

// test
func TestMain(m *testing.M){
	boot.Register(factories.Init)

	boottest.Boot()
	defer boottest.Shutdown()

	m.Run()
	// ...
}

到此,基本上达成了写脚本、写单测时不用操心框架启动的问题。

不过限于框架限制,依然有一些不爽的地方没有解决。

  • Go很多人摒弃了面向接口编程,导致在写UT时无从mock。后面想办法靠工具对所有的类走一次DI容器
  • 测试的问题,限于框架限制,不能全局开始事务
Avatar
huiren
Code Artisan

问渠那得清如许,为有源头活水来

相关

下一页
上一页
comments powered by Disqus