در میان ویژگیهای قابل توجه Go نسخه 1.8، سیستم جدید پلاگینهای Go قرار دارد. این ویژگی به برنامهنویسان اجازه میدهد تا برنامههای ماژولار بدون ارتباط زنجیرهای با هم، با استفاده از بستههایی که به صورت کتابخانههای اشیا شیءگرا کامپایل شدهاند، ساخته شوند و بهصورت پویا در زمان اجرا بارگذاری و بهصورت پیوسته به آنها پیوند داده شوند.
این تغییر بزرگی است! چرا که توسعهدهندگان نرمافزارهای سیستمی بزرگ در Go، بدون شک، نیاز به ماژولارسازی کد خود دارند. ما برای دستیابی به ماژولارسازی کد، از طراحیهای متعدد out-of-process، مانند OS exec calls، سوکت و RPC/gRPC (و غیره) استفاده کردهایم. این روشها ممکن است به خوبی کار کنند، اما در بسیاری از زمینهها بهعنوان پایانهای برای پردازش با قبل نبود یک سیستم پلاگینای ناتیو در Go بکار رفتهاند.
در این بخش از کتاب، من بررسی میکنم که ایجاد نرمافزار ماژولار با استفاده از سیستم پلاگینهای Go (plugin) چه تبعاتی دارد.
از نسخه 1.8، plugin فقط در لینوکس کار میکند. با توجه به سطح علاقمندی به این ویژگی، این احتمالا در نسخههای آینده تغییر خواهد کرد.
4.22.1 طراحی ماژولار با Go #
ایجاد برنامههای ماژولار با پلاگینهای Go، نیاز به همان شیوه کار قابل اعتماد نرمافزاری دارد که به بستههای Go رایج اعمال میشود. با اینحال، پلاگینها نگرانیهای طراحی جدید را با توجه به اتصالشان به دیگر اجزای برنامه، بیشتر کردهاند.
- در هنگام ساخت سیستم نرمافزاری قابل پلاگینگذاری، بسیار مهم است که قابلیتهای واضحی برای اجزای سیستم تعریف شوند. سیستم باید رابطهای ساده و شفافی برای یکپارچهسازی پلاگین فراهم کند. از سوی دیگر، توسعهدهندگان پلاگین باید به عنوان یک جعبه سیاه برای سیستم در نظر گرفته شوند و علاوه بر قراردادهای ارائهشده، هیچ فرضیاتی را انجام ندهند.
- پلاگین باید به عنوان یک اجزای مستقل در نظر گرفته شود که از دیگر اجزا جدا شده است. این باعث میشود که پلاگینها بتوانند دورهی life cycle توسعه و استقرار خود را بدون وابستگی به مصرفکنندگانشان دنبال کنند.
- کد پلاگین باید طراحی شود تا تمرکز خود را فقط بر روی یکی از مسائل عملکردی داشته باشد و نه بیشتر از آن.
- از آنجایی که پلاگینها اجزای مستقلای هستند که در زمان اجرا بارگیری میشوند، مهم این است که از مستندات خوبی برخوردار باشند. به عنوان مثال، نام توابع و متغیرهای پیاده سازی باید به طور واضح مشخص شوند تا خطاهای جستجوی symbol ها را جلوگیری کنند.
- پلاگینهای Go میتوانند تابعهای بسته و متغیرهایی از هر نوع را بهصورت خروجی دهند. میتوانید پلاگینتان را طراحی کنید تا قابلیتهای خود را بهصورت یک مجموعهی تابعهای آزاد گروهبندی کند. سردرگمی، این است که شما باید بهصورت جداگانه هر symbol تابع را جستجو و به آن متصل شوید. راهکار بهتر این است که از انواع interface استفاده کنید. ایجاد یک interface برای صادرکردن قابلیتها، یک سطح تعاملی یکنواخت و مختصر با نشانگرهای عملیاتی واضح فراهم میکند. جستجو و متصل کردن بهنمادی که به یک رابط حل میشود، دسترسی به کل مجموعه شیوه های تابعی برای قابلیتها را فراهم میکند، نه فقط یکی از آنها.
4.22.2 کتابخانه plugin #
کتابخانه plugin، یک کتابخانه خیلی ساده و آسان است و فقط یک تابع Open و یک متد Lookup دارد که به شما برای بازکردن فایل so. و استفاده ازinterface های پیاده سازی شده کمک می کند.
1type Plugin
2 func Open(path string) (*Plugin, error)
3 func (p *Plugin) Lookup(symName string) (Symbol, error)
4
5type Symbol
کتابخانه plugin، یک پکیج اصلی Go با توابع و متغیرهای صادرشده است که با استفاده از دستور زیر برای کامپایل ساخته شده است:
1$ go build -buildmode=plugin
وقتی که یک plugin برای اولین بار باز میشود، تابع init تمام بستههایی که هنوز قسمت برنامه نیستند فراخوانی میشوند. تابع اصلی اجرا نمیشود. یک plugin تنها یکبار مقداردهی اولیه میشود و نمیتواند بسته شود.
4.22.3 پیاده سازی قدم به قدم یک برنامه ماژولار با plugin #
فرض کنید قصد یک پروژه بنویسیم که hello world را به زبان های مختلف در خروجی terminal چاپ کنیم.
4.22.3.1 نمونه ساختار پروژه #
در ابتدا نیاز داریم یک پروژه با ساختار زیر پیاده سازی کنیم:
1├── [ 22] go.mod
2├── [ 779] main.go
3└── [ 224] plugin
4 ├── [ 240] en
5 │ └── [ 152] en.go
6 └── [ 240] fa
7 ├── [ 155] fa.go
4.22.3.2 نوشتن پلاگین #
در ابتدا یک دایرکتوری plugin ایجاد کنید سپس براساس زبان مورد نظر خود یک یا چند sub directory ایجاد کنید.
حال برای زبان فارسی و انگلیسی از کد زیر استفاده کنید:
English
1package main
2
3import "fmt"
4
5type hello string
6
7func (h hello) Hello() {
8fmt.Println("Hello 🌎")
9}
10
11// Hello exported as symbol named
12var Hello hello
Persian
1package main
2
3import "fmt"
4
5type hello string
6
7func (h hello) Hello() {
8fmt.Println("سلام 🌎")
9}
10
11// Hello exported as symbol named
12var Hello hello
در کد فوق ما به ازای هر زبان یک فایل go ایجاد کردیم که با پکیج main شروع می شود و داخلش یک type مشخص قرار دادیم و متد Hello را پیاده سازی کردیم. سپس یک متغییر با نام Hello تعریف کردیم تا به عنوان symbol برای پلاگین در دسترس باشد.
اگر package شما نام دیگری غیر از main باشد با خطا مواجه خواهید شد به دلیل build شدن ماژول هستش.
1-buildmode=plugin requires exactly one main package
4.22.3.3 بیلد پلاگین ها #
برای بیلد گرفتن پلاگین ها باید از دستورات زیر استفاده کنید:
1go build -buildmode=plugin -o plugin/en/en.so plugin/en/en.go
2
3 go build -buildmode=plugin -o plugin/fa/fa.so plugin/fa/fa.go
زمانیکه بیلد میگیرید فایل پلاگین ها در محل plugin/en
یا plugin/fa
با پسوند so.
قرار میگیرد.
حال با استفاده از ابزار file در لینوکس می توانید اطلاعات ماژول بیلد شده را ببینید که به عنوان dynamic shared object شناخته می شود:
1$ file plugin/en/en.so
2plugin/en/en.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=d23f35974563f658267b158466cbb551a97fb049, with debug_info, not stripped
ساختار پروژه پس از بیلد پلاگین ها
1├── [ 22] go.mod
2├── [ 779] main.go
3└── [ 224] plugin
4 ├── [ 240] en
5 │ ├── [ 152] en.go
6 │ └── [ 3.5M] en.so
7 └── [ 240] fa
8 ├── [ 155] fa.go
9 └── [ 3.5M] fa.so
4.22.3.4 استفاده از پلاگین ها #
داخل روت پروژه یک فایل main.go ایجاد کنید و کد زیر را قرار دهید.
توجه کنید نیاز دارید فایل های پلاگین را در هر محلی هست load کنید.
1package main
2
3import (
4 "fmt"
5 "os"
6 "plugin"
7)
8
9type Greeter interface {
10 Hello()
11}
12
13func main() {
14 // determine plugin to load
15 lang := "english"
16 if len(os.Args) == 2 {
17 lang = os.Args[1]
18 }
19 var mod string
20 switch lang {
21 case "english":
22 mod = "./plugin/en/en.so"
23 case "persian":
24 mod = "./plugin/fa/fa.so"
25 default:
26 fmt.Println("don't support your language")
27 os.Exit(1)
28 }
29
30 // load module
31 plug, err := plugin.Open(mod)
32 if err != nil {
33 fmt.Println(err)
34 os.Exit(1)
35 }
36
37 // lookup for symbol
38 symbol, err := plug.Lookup("Hello")
39 if err != nil {
40 fmt.Println(err)
41 os.Exit(1)
42 }
43
44 // assert symbol with interface
45 p, ok := symbol.(Greeter)
46 if !ok {
47 fmt.Println("unexpected type from module symbol")
48 os.Exit(1)
49 }
50
51 // call interface method
52 p.Hello()
53
54}
- در ابتدا یک interface به همراه متد مشابه داخل پلاگین قرار دادیم.
- سپس داخل main یک زبان پیش فرض را داخل متغیر lang تعیین کردیم سپس از طریق os.Args زبان از طریق os.Stdin گرفتیم و داخل lang قرار دادیم پس از آن با استفاده از switch چک کردیم براساس زبان یک پلاگین یا ماژول مشخص را داخل متغیر mod مسیر دهی کنیم.
- سپس تابع Open کتابخانه plugin را فراخوانی کردیم و مسیر پلاگین را قرار دادیم.
- حال پس از باز شدن پلاگین متد Lookup را برای پیدا کردن symbol فراخوانی کردیم که ما نام symbol را Hello گذاشتیم.
- پس از اینکه symbol بدون خطا load شد ما symbol را با اینترفیس Greeter گرفتیم Assert کردیم تا بتوانیم از متدهای پیاده سازی شده استفاده کنیم.
4.22.4 پروژه هایی که از plugin استفاده کرده اند #
در زیر ما لیستی از پروژه های فعالی که از پلاگین استفاده کرده اند را قرار دادیم تا بتوانید برای پیاده سازی پروژه های ماژولار ایده بگیرید:
- https://github.com/hashicorp/go-plugin
- https://github.com/luraproject/lura
- https://github.com/smartcontractkit/chainlink-starknet
- https://github.com/ava-labs/blobvm
- https://github.com/easysoft/zentaoatf
4.22.5 کلام آخر #
ماژولارنویسی یکی از مهم ترین عناوین توسعه و طراحی نرم افزار بوده که شما با اینکار می توانید پلاگین های reusable بنویسید و در هر پروژه ای بسته به نیازتان استفاده کنید. شرکت های بزرگی نظیر hashicorp برای اکثر پروژهایش نظیر terraform یا consul از این قابلیت استفاده کرده است.