Golang - GRPC function wrapper in shared library

Posted by Jimmy's Blog on March 4, 2021

前言

筆者最近因為必須要串接一個 GRPC 的 service 要做使用,發現公司許多同仁也跟我一樣需要這個 service,所以每個人都得在自己的 module 裡面都要寫一段 code 去 connect 這個 GRPC service。許多人寫相同的 code 去 connection 相同的 service 其實並不好,不但效率不高而且倘若這個 service 的 proto 改變了,那其實每個人都得要修改。

因此,這邊便想跟讀者 share 一下,筆者如何將一個 GRPC service 包裝起來,統稱叫做 shared library,提供外部的其他 module 可以簡單快速地使用,各自 module 不但不用寫相同的 code,而且維護上也更為方便,只需更改此 shared library 就可以了。

GRPC

以下是筆者想要包裝起來的 GRPC service 範例,因有公司的 usage 所以筆者不會列出所有細節,而是列出相似範例來做說明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
syntax = "proto3";

service systemMessage {

    rpc getInfoMsg (getRequest) returns (getResponse) {}

    rpc getWarningMsg (getRequest) returns (getResponse) {}

    rpc addInfoMsg (addRequest) returns (addResponse) {}

    rpc addWarningMsg (addRequest) returns (addResponse) {}

    ...

}

...

大概說明一下這個 service 流程以及作用。這個 GRPC service 是一個 message center,各自 module 可以將想要送的 message (一般的 information 或是 warning 類別的) 送到此 GRPC service,有另外一端的 consumer 會取得所有 messages 並呈現到 UI。

PS. 有關 Request 及 Response 牽扯到實作部分,筆者就不列實際的 struct 出來了,讀者可以自行補齊,像是 Request 裡面就會有 title、message 等等的欄位。

Wrap the GRPC service

接著,我們就可以開始來包裝此 service。整體的 code 就如以下:

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
package sysmsg

import (
	"context"
	"crypto/sha256"
	"fmt"
	"log"
	"os"
	"os/signal"
	spec "pb/systemmessage"
	"sync"
	"syscall"
	"time"

	"google.golang.org/grpc"
)

//SystemMsg define
type SystemMsg struct {
	Conn         *grpc.ClientConn
	Client       spec.SystemMessageClient
	Module       string
	Mutex        sync.Mutex
}

//connecToSystemWarnAgent is to connect SystemMessage agent
func connecToSystemWarnAgent(server string) (*SystemMsg, error) {
	conn, err := grpc.Dial(
		server,
		grpc.WithInsecure(),
	)
	if err != nil {
		return &SystemMsg{
			Conn:   nil,
			Client: nil,
		}, err
	}
	return &SystemMsg{
		Conn:         conn,
		Client:       spec.NewSystemWarningClient(conn),
	}, nil
}

//NewSystemMessageSender is to generate a sender for system messages, input for your own module name,
//default server will be "systemmessage:9876" by using docker network to connect systemmessage. If you are host mode,
//you can assign gateway IP instead. Like: NewSystemMessageSender("your_module_name","gateway_ip:9876")
func NewSystemMessageSender(module string, server ...string) *SystemMsg {
	var c = make(chan os.Signal, 1)
	sysServer := "systemwarning:9876"
	if len(server) != 0 {
		sysServer = server[0]
	}
	systemCli, err := connecToSystemWarnAgent(sysServer)
	if err != nil {
		log.Printf("Can not connect to systemwarning module, err: %v\n", err)
	}
	systemCli.Module = module
	signal.Notify(c, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
	go func() {
		for range c {
			log.Printf("Close systemwarning connection instance\n")
			systemCli.Close()
		}
	}()
	return systemCli
}

//Close systemwarning connection
func (s *SystemMsg) Close() {
	if s.Conn != nil {
		s.Conn.Close()
	}
}

//Info is to send messages to systemmessage, assign title and message in parameter field
func (s *SystemMsg) Info(title string, message string) {
	s.Mutex.Lock()
	defer s.Mutex.Unlock()
	if s.Conn == nil {
		return
	}
	if s.Client != nil {
		ctx, cancel := context.WithCancel(context.Background())
		defer cancel()
		_, err := s.Client.PostSysInfo(ctx, &spec.PostSysInfoRequest{
			Module:  s.Module,
			Title:   title,
			Message: message,
		})
		//if send message failed, print the error msg on console
		if err != nil {
			log.Printf("Send system message failed, err: %s\n", err)
		}
	}
}

這邊就一部分一部分來說明。

generate grpc proto

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import (
	"context"
	"crypto/sha256"
	"fmt"
	"log"
	"os"
	"os/signal"
	spec "pb/systemmessage"
	"sync"
	"syscall"
	"time"

	"google.golang.org/grpc"
)

這邊的 spec 就是剛剛上面 systemmessage 的 pb.go 檔,因此這邊要做 Import。還不太了解 GRPC 怎麼 build 出 pb.go 檔的人可以來學習一下官網

define wrapper and how to use it

接著,我們來定義一下 wrapper。SystemMsg 會有需要儲存 GRPC connection 以及 client 的物件。筆者這邊多訂一個 Module 欄位,方便後面 Implementation 的 function,就不用每次都要填 Module 名稱,代表這個物件送 Message 就都是使用這個 Module 名稱去帶到 Request 裡面。Mutex 其實很重要,當你有許多地方會使用到這個 wrapper 的時候就要用 Mutex 去做 Lock 的動作,這個概念套用到很多實作都是一樣喔。

1
2
3
4
5
6
7
//SystemMsg define
type SystemMsg struct {
	Conn         *grpc.ClientConn
	Client       spec.SystemMessageClient
	Module       string
	Mutex        sync.Mutex
}

再來就是連接的部分,把 connection 及 client 產生出來並且放進 wrapper 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//connecToSystemWarnAgent is to connect SystemMessage agent
func connecToSystemWarnAgent(server string) (*SystemMsg, error) {
	conn, err := grpc.Dial(
		server,
		grpc.WithInsecure(),
	)
	if err != nil {
		return &SystemMsg{
			Conn:   nil,
			Client: nil,
		}, err
	}
	return &SystemMsg{
		Conn:         conn,
		Client:       spec.NewSystemMessageClient(conn),
	}, nil
}

generate the wrapper

接著,Implement 產生 wrapper 的 function。這個 function 就是使用者會直接用到的,包含使用者 module 名稱,message center server 的位置在哪裡。因為筆者公司是用 docker 包起來的系統架構,所以 comment 是在說明有關如何 connect message center 的事項。底下的 Close 則是用關閉 connection。讀者們在撰寫一般的 GRPC 的時候也需注意這部分唷,沒有 close 的話常常會造成系統 FD Leak 導致 crash。

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
//NewSystemMessageSender is to generate a sender for system messages, input for your own module name,
//default server will be "systemmessage:9876" by using docker network to connect systemmessage. If you are host mode,
//you can assign gateway IP instead. Like: NewSystemMessageSender("your_module_name","gateway_ip:9876")
func NewSystemMessageSender(module string, server ...string) *SystemMsg {
	var c = make(chan os.Signal, 1)
	sysServer := "systemwarning:9876"
	if len(server) != 0 {
		sysServer = server[0]
	}
	systemCli, err := connecToSystemWarnAgent(sysServer)
	if err != nil {
		log.Printf("Can not connect to systemwarning module, err: %v\n", err)
	}
	systemCli.Module = module
	signal.Notify(c, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
	go func() {
		for range c {
			log.Printf("Close systemwarning connection instance\n")
			systemCli.Close()
		}
	}()
	return systemCli
}

//Close systemwarning connection
func (s *SystemMsg) Close() {
	if s.Conn != nil {
		s.Conn.Close()
	}
}

send the message

最後,就可以撰寫送 message 的 function。前面有提到先用 Mutex Lock 這個 section 防止造成 race condition。接著利用 client 將 message 送出去囉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//Info is to send messages to systemmessage, assign title and message in parameter field
func (s *SystemMsg) Info(title string, message string) {
	s.Mutex.Lock()
	defer s.Mutex.Unlock()
	if s.Conn == nil {
		return
	}
	if s.Client != nil {
		ctx, cancel := context.WithCancel(context.Background())
		defer cancel()
		_, err := s.Client.PostSysInfo(ctx, &spec.PostSysInfoRequest{
			Module:  s.Module,
			Title:   title,
			Message: message,
		})
		//if send message failed, print the error msg on console
		if err != nil {
			log.Printf("Send system message failed, err: %s\n", err)
		}
	}
}

How to use it

到這邊為止就大功告成啦! 接著如何來使用這個 shared library呢 ? 筆者這邊是使用 go module 來管理套件。所以要使用 shared library,要在 go.mod 裡面指定到你的 shared library,假設你將上面的 code 放在一個 folder sharedlib,並且把它放在跟你的 project 同一層:

1
2
ls
sharedlib/ your_module/

go.mod

1
2
3
4
5
6
7
8
9
10
module your_module

go 1.12

require (
	...
    sharedlib
)

replace sharedlib => ../sharedlib

接著下

1
go mod vendor

便可以把你剛剛撰寫的 sharedlib 在你 module 裡使用囉! 以剛剛我們上面的 message center 為例子,就可以在自己的 module 裡使用像是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import (
	...

	"sharedlib/sysmsg"

	...
)

//SysMsgSender is message sender for module testing
var SysMsgSender = sysmsg.NewSystemMessageSender("testing")
...

SysMsgSender.Info("test sender", fmt.Sprintf("Testing message will be sent!"))

...

在你的 module 中,你可以只產生一個 SysMsgSender,重複使用它即可。若是想使用多個 wrapper 在不同的 package 中去送 message 當然也是可以的。

總結

這樣一來,每個人都只需要使用這一個 shared library,就可以達到連接這個 GRPC service 並且不需要寫重複相關的 code,不但更簡潔一點而且維護上只需要 maintain 此 shared library 就可以了! 此種做法可以套用到任何 GRPC service 上面,像是很常見的 Log center 也都是可以運用這個機制。但是,(人生就是很多但是XD),很多同仁使用的時候有遇到一些問題,後來發現是筆者的 testing 做得不夠啊! Testing 很重要,之前的前輩也是一直耳提面命一定都要寫 testing。因此,筆者後面一篇就會提到,該如何針對這個機制去做 Unit testing !

Contact me

有任何疑問或是建議歡迎聯絡 Jimmy!

Gmail: jimmyw86878@gmail.com