go-micro提供了分佈式系統開發的核心需求,包括RPC和事件驅動的通訊機制。關於go-micro的詳細內容請參考git上的go-micro項目,這篇文章主要來說go-micro的組件register的源碼剖析。git
go-micro的結構圖以下(來源git倉庫)。github
能夠看到go-micro底層分爲6個組件,分別是broker、Codec、Register、Selector、Transport。 Registry是go-micro的註冊模塊,它提供可插拔的服務註冊與發現功能,它目前的實現的方式有Consul,etcd,內存和k8s。咱們以consul爲例子,來看一下go-micro是如何完成整個註冊實現的。bash
./consul agent -dev -client 0.0.0.0 -ui
來啓用consulMICRO_REGISTRY=consul
環境變量,而後跑通demo。服務註冊在go-micro服務端實現,找到service demo裏的service.Run()
,它是最外層service啓動的入口。Run
的實如今service.go裏。進入到Run方法裏裏找到(s *service) Start()
。服務器
func (s *service) Start() error {
for _, fn := range s.opts.BeforeStart {
if err := fn(); err != nil {
return err
}
}
if err := s.opts.Server.Start(); err != nil {
return err
}
for _, fn := range s.opts.AfterStart {
if err := fn(); err != nil {
return err
}
}
return nil
}
複製代碼
這個方法內部按照順序執行,進行了服務器啓動前事件處理,服務啓動,和服務結束事件處理的三個流程。核心代碼在s.opts.Server.Start()
。跟蹤代碼進入這個start函數,進入到(s *rpcServer) Start()
裏面,這裏就是go-micro裏service的核心代碼。代碼行數比較多,咱們直接關注重點也就是register的功能。找到Register
註冊部分的的代碼。app
func (s *rpcServer) Start() error {
...
// use RegisterCheck func before register
if err = s.opts.RegisterCheck(s.opts.Context); err != nil {
log.Logf("Server %s-%s register check error: %s", config.Name, config.Id, err)
} else {
// announce self to the world
if err = s.Register(); err != nil {
log.Log("Server %s-%s register error: %s", config.Name, config.Id, err)
}
}
...
}
複製代碼
這裏,首先檢查了register環境的上下文。若是沒有問題則進行註冊操做。進入到s.Register()
裏面,(s *rpcServer) Register()
這個函數就是註冊功能的核心代碼部分。不看前面的預處理,只看咱們關注的核心部分,找到下面的代碼行:分佈式
func (s *rpcServer) Register() error {
...
if err := config.Registry.Register(service, rOpts...); err != nil {
return err
}
...
}
複製代碼
不難猜出,這裏就是註冊的功能實現了,可是go-micro是怎麼知道該使用哪一個註冊器呢。ide
先看一下registry包的結構。函數
參考包的目錄結構,咱們能夠知道註冊器支持4種類型的註冊操做,分別是consul、gossip、mdns、memory。 咱們設置了MICRO_REGISTRY=consul
的環境變量,來告訴go-micro使用consul方式註冊。那麼,config.Registry
究竟是在哪設置成consulRegister的呢。工具
答案是,在service.Init()
服務初始化的裏面進行了設置。回到service demo裏學習
// Init will parse the command line flags.
service.Init()
複製代碼
跟蹤進入Init函數
func (s *service) Init(opts ...Option) {
// process options
for _, o := range opts {
o(&s.opts)
}
s.once.Do(func() {
// Initialise the command flags, overriding new service
_ = s.opts.Cmd.Init(
cmd.Broker(&s.opts.Broker),
cmd.Registry(&s.opts.Registry),
cmd.Transport(&s.opts.Transport),
cmd.Client(&s.opts.Client),
cmd.Server(&s.opts.Server),
)
})
}
複製代碼
Init裏面,for循環執行了一系列預處理的函數。而後使用了sync.Once裏的once操做,保證裏面的函數只執行一次。重點關注Cmd.Init
這個方法,它這裏接收的參數使用的比較繞。
func (c *cmd) Init(opts ...Option) error {
for _, o := range opts {
o(&c.opts)
}
c.app.Name = c.opts.Name
c.app.Version = c.opts.Version
c.app.HideVersion = len(c.opts.Version) == 0
c.app.Usage = c.opts.Description
c.app.RunAndExitOnError()
return nil
}
複製代碼
首先cmd.Init
的參數接受type Option func(o *Options)
類型的方法,而後依次執行,而後再進行變量賦值和函數處理。
它接受的方法以broker爲例。
func Broker(b *broker.Broker) Option {
return func(o *Options) {
o.Broker = b
}
}
複製代碼
Broker方法有點繞,要和前面兩個Init一塊兒看。Broker方法接受了一個Broker
類型的指針。它返回了一個Option
方法,Option
方法接受一個Options
類型的指針。而後把o *Options
的Broker設置成外層函數參數傳進來的Broker。
進入到cmd.Init
裏for循環依次執行了同Broker相似的方法,o(&c.opts)
也就是把c.opts對應的組件進行賦值。所賦的值就是service.Init
裏的s.opts.Broker
、s.opts.Registry
等組件。
因此它一系列的操做就是s.opts.Cmd.opts去複用service.opts上的組件。這裏能夠學習一下它這種寫法,在對象不可見的狀況下傳遞對象的字段。(這種寫法其餘的好處,請大佬必定訴我)
咱們繼續找,進入c.app.RunAndExitOnError()
的這個方法裏,而後進入到a.Run()
。
func (a *App) Run(arguments []string) (err error) {
...
if a.Before != nil {
err = a.Before(context)
if err != nil {
fmt.Fprintf(a.Writer, "%v\n\n", err)
ShowAppHelp(context)
return err
}
}
...
}
複製代碼
直接告訴你設置Register的位置是在a.Before這裏。這個方法沒有本身定義,也是複用了cmd.Before方法。cmd.go的newCmd(opts ...Option)
方法裏你會找到cmd.app.Before = cmd.Before
,就是在這裏設置的。最終賦值的地方就是在這個cmd.Before
裏。
func (c *cmd) Before(ctx *cli.Context) error {
...
if name := ctx.String("registry"); len(name) > 0 && (*c.opts.Registry).String() != name {
r, ok := c.opts.Registries[name]
if !ok {
return fmt.Errorf("Registry %s not found", name)
}
*c.opts.Registry = r()
serverOpts = append(serverOpts, server.Registry(*c.opts.Registry))
clientOpts = append(clientOpts, client.Registry(*c.opts.Registry))
...
}
...
}
複製代碼
那麼到這裏咱們就知道了,它會去從環境變量裏拿registry的值,咱們設置的是consul,對應的就是consulRegistry。而後*c.opts.Registry = r()
這裏最終設置了這個註冊器。
費勁千辛萬苦終於知道從哪裏拿的Registry。接下來回到上面的Register
函數調用那裏。咱們去找consulRegistry的Register
函數
func (c *consulRegistry) Register(s *registry.Service, opts ...registry.RegisterOption) error {
...
if err := c.Client.Agent().ServiceRegister(asr); err != nil {
return err
}
...
}
複製代碼
這個函數比較長,它主要作的處理是和consul服務進行通訊。在Agent().ServiceRegister(asr)
裏面,它發起了一個PUT請求,具體的請求內容能夠本身去實際看一下發包。
func (a *Agent) ServiceRegister(service *AgentServiceRegistration) error {
r := a.c.newRequest("PUT", "/v1/agent/service/register")
r.obj = service
_, resp, err := requireOK(a.c.doRequest(r))
if err != nil {
return err
}
resp.Body.Close()
return nil
}
複製代碼
到這裏,go-micro就講服務註冊到了consul上。最終consul會鏈接你的服務端和客戶端,讓它們之間進行通訊。
到此咱們從源碼角度,瞭解了go-micro的服務註冊流程,選擇註冊器,而後進行服務的註冊,其餘的註冊器的邏輯也是同樣的就再也不復述。
下面是我記錄的代碼流程圖
經過go-micro的代碼review,咱們瞭解了它內部的細節實現。一般當咱們想要review源碼的時候,首先要從總體把握項目的結構,這個能夠經過文檔或者是包名去知道。瞭解了它的項目結構以後,咱們能夠着重關注某一個感興趣的子組件,而後深刻去閱讀源代碼。閱讀代碼的同時帶着本身的疑問,而後去尋找答案,同時學習一些代碼的寫法,最終把學到的東西記錄下來,我想這大概就是學習和閱讀源碼的意義。