本文完全出于学习的目的,如有异议,请联系删除。
之前XL事件流出的优秀代码太多了,这次选择的是一个好像与具体业务无关的模块(admin-ep-saga)来进行学习。
首先看看目录结构:
这么多先看哪一个呢?在不知道具体每个包是干什么的情况下,只好一个一个的看了。
先看看api
下有些什么:
不得不说这个目录的结构相当规范呀,虽然我没有点开具体文件,但是仅仅从目录名和文件名就能猜出这个目录下是干什么的:
应该是使用了grpc框架和protobuf协议定义的接口。这个暂时先放一边,我需要先找到程序入口 ,这样才能一步一步的学习优秀代码是如何编写的。
接下来打开cmd
:
这个目录下有三个文件:
BUILD
看着应该是用来做构建用的,这不是我这次学习的重点,先跳过。
saga-admin-test.toml
我打开看了一眼,是一个配置文件,从名字可以看出应该是测试用的配置项,后面还会碰到。
main.go
如果不出意外,这个应该就是程序入口 了,运气还不错,第二个目录就找到了入口。
接下来详细的看看main.go做了些什么事情:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 func main () { flag.Parse() if err := conf.Init(); err != nil { log.Error("conf.Init() error(%v)" , err) panic (err) } log.Init(conf.Conf.Log) defer log.Close() log.Info("saga-admin start" ) s := service.New() http.Init(s) grpcsvr, err := grpc.New(nil , s.Wechat()) if err != nil { panic (err) } c := make (chan os.Signal, 1 ) signal.Notify(c, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT) for { si := <-c log.Info("saga-admin get a signal %s" , si.String()) switch si { case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT: grpcsvr.Shutdown(context.Background()) log.Info("saga-admin exit" ) s.Close() time.Sleep(_durationForClosingServer * time.Second) return case syscall.SIGHUP: default : return } } }
粗略的看了一下代码后,带着疑问,一行一行的来分析,首先第一行:
作为Golang小白,我知道这个应该是使用在flag.StringVar
这样的代码后面,定义需要获取的命令行参数。
但是flag.Parse()
作为第一行代码前面并没有flag.StringVar
类似这样的代码呀,然后我想到了Golang中init
函数的作用。于是我开始找main.go
中导入的其他包中有没有定义init函数,果不其然,在saga/conf/conf.go
中我找到了:
1 2 3 4 5 6 func init () { flag.StringVar(&confPath, "conf" , "" , "config path" ) reload = make (chan bool , 10 ) }
回到main.go
来看下面几行代码:
1 2 3 4 if err := conf.Init(); err != nil { log.Error("conf.Init() error(%v)" , err) panic (err) }
忽略错误判断以及日志打印,我们可以看到这几行中最关键的代码就是conf.Init()
,这个代码做了些什么事情呢?接下来进入saga/conf/conf.go
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 func Init () (err error) { if confPath == "" { return configCenter() } if _, err = toml.DecodeFile(confPath, &Conf); err != nil { log.Error("toml.DecodeFile(%s) err(%+v)" , confPath, err) return } Conf = parseTeamInfo(Conf) return }
首先看:
1 2 3 if confPath == "" { return configCenter() }
从函数名可以看出,当没有手动指定配置文件路径是,走配置中心解析配置。configCenter
我们先放一放,我们接着往下看:
1 2 3 4 if _, err = toml.DecodeFile(confPath, &Conf); err != nil { log.Error("toml.DecodeFile(%s) err(%+v)" , confPath, err) return }
跟之前一样,我们忽略错误和日志处理,可以看到这几行关键代码是toml.DecodeFile(confPath, &Conf)
,
toml
这个看着是不是很眼熟,之前在cmd
包下我们看到过一个这个格式的文件saga-admin-test.toml
,这是一个由GitHub联合创始人Tom Preston-Werner 搞出的极简配置文件格式。各个语言都有相关实现,BZ这里使用的是github.com/BurntSushi/toml
这个库。
总而言之,这几行代码无非就是解析配置文件。
继续往下看:
1 Conf = parseTeamInfo(Conf)
单独用了一个方法来解析TeamInfo
说明toml标准的DecodeFile解析不了,我们来看看这个方法做了什么事情:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 func parseTeamInfo (c *Config) *Config { DeLabel := strings.Fields(c.Property.Department.Label) DeValue := strings.Fields(c.Property.Department.Value) for i := 0 ; i < len (DeLabel); i++ { info := &model.PairKey{ Label: DeLabel[i], Value: DeValue[i], } c.Property.DeInfo = append (c.Property.DeInfo, info) } buLabel := strings.Fields(c.Property.Business.Label) buValue := strings.Fields(c.Property.Business.Value) for i := 0 ; i < len (buLabel); i++ { info := &model.PairKey{ Label: buLabel[i], Value: buValue[i], } c.Property.BuInfo = append (c.Property.BuInfo, info) } return c }
话说,上面的Business
和Department
处理过程一样,为啥不将这个过程提取成一个函数呢?来自小白的疑问。
还记得我们之前跳过一个函数configCenter()
吗?接下来我们一起来看看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 func configCenter () (err error) { if client, err = conf.New(); err != nil { panic (err) } if err = load(); err != nil { return } client.WatchAll() go func () { for range client.Event() { log.Info("config reload" ) if load() != nil { log.Error("config reload error (%v)" , err) } else { reload <- true } } }() return }
忽略其它代码,可以看到上面实现配置中心配置加载的应该是load函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 func load () (err error) { var ( s string ok bool tmpConf *Config ) if s, ok = client.Value(_configKey); !ok { err = errors.Errorf("load config center error [%s]" , _configKey) return } if _, err = toml.Decode(s, &tmpConf); err != nil { err = errors.Wrapf(err, "could not decode config err(%+v)" , err) return } Conf = parseTeamInfo(tmpConf) return }
到这里我们差不多刚刚看完main.go
中conf.Init()
的调用,接下来回到main.go
,继续往下看:
我们跳过Log的初始化,直接看:
1 2 3 4 s := service.New() http.Init(s)
跳转到New
函数中,我们看看做了些什么:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 func New () (s *Service) { var ( err error ) s = &Service{ dao: dao.New(), cron: cron.New(), } if err = s.cron.AddFunc(conf.Conf.Property.SyncProject.CheckCron, s.collectprojectproc); err != nil { panic (err) } if err = s.cron.AddFunc(conf.Conf.Property.Git.CheckCron, s.alertProjectPipelineProc); err != nil { panic (err) } if err = s.cron.AddFunc(conf.Conf.Property.SyncData.CheckCron, s.syncdataproc); err != nil { panic (err) } if err = s.cron.AddFunc(conf.Conf.Property.SyncData.CheckCronAll, s.syncalldataproc); err != nil { panic (err) } if err = s.cron.AddFunc(conf.Conf.Property.SyncData.CheckCronWeek, s.syncweekdataproc); err != nil { panic (err) } s.cron.Start() s.gitlab = gitlab.New(conf.Conf.Property.Gitlab.API, conf.Conf.Property.Gitlab.Token) s.git = gitlab.New(conf.Conf.Property.Git.API, conf.Conf.Property.Git.Token) s.wechat = wechat.New(s.dao) return }
上面代码大部分都是在做定时任务的创建,cron
使用的是"github.com/robfig/cron"
这个库,我们挑一个看看:
1 2 3 if err = s.cron.AddFunc(conf.Conf.Property.SyncProject.CheckCron, s.collectprojectproc); err != nil { panic (err) }
conf.Conf.Property.SyncProject.CheckCron
是配置中的cron表达式,在saga-admin-test.toml
中看到是
* */15 * * * ?
也就是说这个任务每15分钟执行一次。
s.collectprojectproc
是要执行的任务,接下来看看这个任务做了什么事情:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 func (s *Service) collectprojectproc () { var err error if err = s.CollectProject(context.TODO()); err != nil { log.Error("s.CollectProject err (%+v)" , err) } } func (s *Service) CollectProject (c context.Context) (err error) { var ( projects []*gitlab.Project total = 0 page = 1 ) log.Info("Collect Project start" ) for page <= 1000 { if projects, err = s.gitlab.ListProjects(page); err != nil { return } num := len (projects) if num <= 0 { break } total = total + num for _, p := range projects { if err = s.insertDB(p); err != nil { return } } page = page + 1 } log.Info("Collect Project end, find %d projects" , total) return }
在service.New()
中其他的cron也差不多是做着类似的事情,由于太多,就不在这里一一展开。刚刚说到
s.gitlab.ListProjects(page);
我有一个疑问,是什么呢?我们看这里:
1 2 3 4 5 6 7 8 s.cron.Start() s.gitlab = gitlab.New(conf.Conf.Property.Gitlab.API, conf.Conf.Property.Gitlab.Token) s.git = gitlab.New(conf.Conf.Property.Git.API, conf.Conf.Property.Git.Token) s.wechat = wechat.New(s.dao)
可以发现cron的start是在gitlab、git、wechat实例化之前,而cron相关的任务中又依赖了这些client,那有没有这么一种可能:这个程序启动的时候正好碰上cron触发,而gitlab,wechat这些client还没有实例化,所以有没有可能出现panic?当然了,这个可能性很小。
让我们再次回到main.go中:
进入Init
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func Init (s *service.Service) { srv = s authSvc = permit.New2(nil ) engine := bm.DefaultServer(conf.Conf.BM) engine.Ping(ping) initRouter(engine) if err := engine.Start(); err != nil { log.Error("engine.Start error(%v)" , err) panic (err) } }
由于这个http服务是依赖的BZ公共的组件,就不继续深入了,我怕出不来了。我们看看initRouter
中定义的Router,由于太长,我只选择开始一段:
1 2 3 4 5 6 7 8 9 version := e.Group("/ep/admin/saga/v1" , authSvc.Permit2("" )) { project := version.Group("/projects" ) { project.GET("/favorite" , favoriteProjects) project.POST("/favorite/edit" , editFavorite) project.GET("/common" , queryCommonProjects) } ...
这就是很常见的url-mapping了,这里的favoriteProjects
、editFavorite
、queryCommonProjects
都是定义在当前http
包下的函数,我们选择favoriteProjects
看下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func favoriteProjects (ctx *bm.Context) { var ( req = &model.Pagination{} err error userName string ) if err = ctx.Bind(req); err != nil { ctx.JSON(nil , err) return } if userName, err = getUsername(ctx); err != nil { ctx.JSON(nil , err) return } ctx.JSON(srv.FavoriteProjects(ctx, req, userName)) }
上面的流程大概是这样的,首先在http
包中将参数等一些信息进行解析,最后调用到了service
中的方法。这个就很像Java中流行的写法:从Controller
到Service
,符合MVC
分层的思想。
最后还有一个grpc,我大概看了下,应该是用来企业微信发消息的。
总结:
这应该是用来做gitlab ci告警之类的project,可以看到这个project层次很分明,代码看过去一目了然。
最后,之前还有一个疑问,就是reload
这个变量,在初始化时是有长度的:
1 reload = make (chan bool , 10 )
长度为10,在每次配置文件修改后,goroute watch到event之后会往这个channel写入一个true
,但是看完整个代码之后,并没有看到有地方从这个channel取出数据(也有可能是我漏看了),也就是说当修改10次之后,这个地方:
就会阻塞,从而导致这个goroute无响应?
码字不易,且转且珍惜。