پکیج های زبان برنامه نویسی Go – از صفر تا صد


اگر با زبانهایی مانند جاوا یا Node.js آشنایی دارید، در این صورت احتمالاً با مفهوم پکیجها کاملاً آشنا هستید. پکیج چیزی به جز یک دایرکتوری با تعدادی فایلهای کد نیست که هر کدام متغیرها (قابلیتهای) مختلفی را از یک نقطه مرجع واحد عرضه میکنند. در ادامه به توضیح مفاهیم مرتبط با پکیج های Go میپردازیم.
پکیج چیست؟
تصور کنید بیش از هزار تابع دارید که در زمان کار روی هر پروژهای به طور مداوم به آنها نیاز پیدا میکنید. برخی از این تابعها رفتار مشترکی دارند. برای نمونه تابع toUpperCase و toLowerCase حالت حروف یک رشته را تغییر میدهند، بنابراین آنها را در یک فایل منفرد مثلاً با نام case.go مینویسید. تابعهای دیگری نیز وجود دارند که عملیات دیگری روی نوع داده String اجرا میکنند از این رو آنها را نیز در فایل جداگانهای مینویسید.
از آنجا که فایلهای مختلفی دارید که با نوع داده String کار میکنند، باید یک دایرکتوری به نام string بسازید و همه فایلهای مرتبط با string را درون آن قرار دهید. در نهایت همه این دایرکتوریها را در یک دایرکتوری والد قرار دهید که پکیج شما را تشکیل میدهند. بدین ترتیب کل ساختار پکیج به صورت زیر درمیآید:
package-name ├── string | ├── case.go | ├── trim.go | └── misc.go └── number ├── arithmetics.go └── primes.go
در ادامه به طور کامل روش ایمپورت کردن تابعها و متغیرها از پکیج و این که چطور همه چیز با هم درمیآمیزد تا یک پکیج تشکیل یابد توضیح خواهیم داد. اما در حال حاضر، تصور کنید پکیج شما به صورت یک دایرکتوری شامل فایلهای go. است.
هر برنامه Go باید بخشی از یک پکیج باشد. یک برنامه منفرد اجرایی Go باید اعلان package main را داشته باشد. اگر برنامه بخشی از پکیج main باشد، در این صورت go install یک فایل باینری میسازد که در زمان اجرا تابع main برنامه را فراخوانی میکند. اگر برنامه بخشی از چیزی به جز main باشد در این صورت با اجرای دستور go install یک فایل package archive ایجاد میشود. اگر از این توضیحات سردرگم شدهاید جای نگرانی نیست، چون در ادامه همه آنها را به تفصیل توضیح خواهیم داد.
ساخت پکیج اجرایی
در ادامه یک پکیج اجرایی ایجاد میکنیم. چنان که میدانیم برای ایجاد یک فایل اجرایی باینری برنامه ما باید بخشی از پکیج main باشد و یک تابع main داشته باشد که نقطه ورودی اجرای برنامه است.
نام یک پکیج همان نام دایرکتوری شامل آن است که در دایرکتوری src قرار دارد. در حالت فوق، app نام پکیج است زیرا app دایرکتوری فرزند دایرکتوری src است. چرا که دستور go install app در زیردایرکتوری app درون src مسیر GOPATH به دنبال فایل میگردد. سپس پکیج را کامپایل میکند و فایل اجرایی باینری app را درون دایرکتوری bin کامپایل میکند. این فایل باید از ترمینال قابل اجرا باشد، زیرا دایرکتوری bin در PATH قرار دارد.
اعلان پکیج باید نخستین خط در کد پکیج باشد و در مثال فوق به صورت package main دیده میشود و میتواند از نام پکیج متفاوت باشد. از این رو ممکن است پکیجهایی را مشاهده کنید که نام پکیج (نام دایرکتوری) از اعلان پکیج متفاوت باشد. زمانی که یک پکیج را ایمپورت میکنید، اعلان پکیج برای ساخت متغیر ارجاع پکیج استفاده میشود. در ادامه این موضوع را بیشتر توضیح خواهیم داد.
دستور <go install <package به دنبال هر فایلی میگردد که اعلان پکیج main را درون دایرکتوری package مفروض داشته باشد. اگر فایل را پیدا کند، در این صورت Go میفهمد که یک برنامه اجرایی است و باید فایل باینری آن را ایجاد کند. یک پکیج میتواند فایلهای زیادی داشته باشد، اما فقط یک فایل دارای تابع main است زیرا آن فایل نقطه ورودی اجرای برنامه خواهد بود.
اگر پکیجی شامل فایلی با اعلان پکیج main نباشد، در این صورت Go یک فایل آرشیو پکیج (با پسوند a.) درون دایرکتوری pkg میسازد.
از آنجا که app یک پکیج اجرایی نیست، یک فایل app.a درون دایرکتوری pkg ایجاد کرده است. ما نمیتوانیم این فایل را اجرا کنیم زیرا یک فایل باینری نیست.
رسم نامگذاری پکیج
جامعه Go پیشنهاد میکند که از نامهای ساده و سرراست برای پکیجها استفاده کنید. برای نمونه strutils برای تابعهای string utility و یا http برای تابعهای مرتبط با درخواستهای HTTP مناسب هستند. نامگذاری پکیجها به صورت under_scores ،hy-phens یا mixedCaps توصیه نمیشوند.
ایجاد یک پکیج
چنان که پیشتر توضیح دادیم، دو نوع پکیج وجود دارند. یک پکیج اجرایی و یک پکیج کاربردی (utility). پکیج اجرایی اپلیکیشن اصلی شما است چون آن را اجرا خواهید کرد. پکیج کاربردی به تنهایی نمیتواند اجرا شود، بلکه کارکردهای یک پکیج اجرایی را از طریق ارائه تابعهای کاربری و دیگر موارد مهم بهبود میبخشد.
چنان که میدانیم پکیج چیزی به جز یک دایرکتوری نیست. بنابراین در ادامه ابتدا یک دایرکتوری به نام greet درون دایرکتوری src میسازیم و چند فایل در آن ایجاد میکنیم. این بار یک اعلان package greet در ابتدای فایل مینویسیم تا بیان کنیم که این یک پکیج کاربردی است.
اکسپورت اعضا
یک پکیج کاربردی برای ارائه برخی متغیرهای به پکیجی که آن را ایمپورت کند طراحی شده است. این وضعیت شبیه به ساختار export در جاوا اسکریپت است. Go در صورتی یک متغیر را اکسپورت میکند که نام متغیر با حروف بزرگ آغاز شده باشد. همه متغیرهای دیگر که با حروف کوچک آغاز میشوند در پکیج به صورت خصوصی تعریف شدهاند.
از اینجا به بعد در این مقاله ما قصد داریم از کلمه variable برای توصیف اکسپورت یک عضو استفاده کنیم، اما توجه داشته باشید که اکسپورت اعضا میتوانند از هر نوع مانند constant ،map ،function ،struct ،array ،slice و غیره باشند.
در ادامه یک متغیر را از فایل day.go اکسپورت میکنیم.
در برنامه فوق متغیر Morning از پکیج اکسپورت میشود، اما متغیر morning اکسپورت نمیشود زیرا با حرف کوچک آغاز شده است.
ایمپورت کردن یک پکیج
اکنون به یک پکیج اجرایی نیاز داریم که پکیج greet ما را مصرف کند. در ادامه یک دایرکتوری app درون دایرکتوری src میسازیم و فایل entry.go را با اعلان پکیج main و تابع main ایجاد میکنیم. توجه داشته باشید که در این حالت پکیجهای Go یک سیستم نامگذاری فایل مدخل مانند index.js در Node ندارند. برای هر پکیج اجرایی یک فایل با تابع main به عنوان فایل مدخل برای اجرا استفاده میشود.
برای ایمپورت کردن یک پکیج از ساختار import به همراه نام پکیج استفاده میکنیم.
برخلاف دیگر زبانهای برنامهنویسی نام یک پکیج باید مسیر زیرمجموعه مانند some-dir/greet باشد تا Go به طور خودکار مسیر را به پکیج greet بیابد. این موضوع را در بخش پکیجهای تودرتو در ادامه بیشتر توضیح خواهیم داد.
Go ابتدا در دایرکتوریهای درون دایرکتوری GOROOT/src جستجو میکند و اگر پکیج را پیدا نکند در این صورت به دنبال GOPATH/src میگردد. از آنجا که پکیج fmt بخشی از کتابخانه استاندارد Go است که در GOROOT/src قرار دارد، از آنجا ایمپورت میشود. از آنجا که Go نمیتواند پکیج greet را درون GOROOT بیابد، درون GOPATH/src را میگردد و آنجا آن را مییابد.
برنامه فوق یک خطای کامپایل صادر میکند، چون متغیر morning از پکیج greet قابل مشاهده نیست. چنان که مشاهده میکنید ما از نمادگذاری نقطهای برای دسترسی به اعضای اکسپورت شده یک پکیج استفاده کردهایم. زمانی که یک پکیج را ایمپورت میکنید، Go یک متغیر سراسری با استفاده از اعلان پکیج ایجاد میکند. در حالت فوق، greet یک متغیر سراسری است که از سوی Go ایجاد شده است، زیرا ما از اعلان package greet در برنامههای که در پکیج greet گنجانده شدهاند استفاده کردهایم.
ما میتوانیم پکیجهای fmt و greet را با هم با استفاده از ساختار گروهبندی (پرانتزها) ایمپورت کنیم. این بار برنامه ما به درستی کامپایل میشود زیرا متغیر Morning از خارج پکیج قابل دسترسی است.
پکیجهای تو در تو
ما میتوانیم یک پکیج را درون پکیج دیگر به صوت تودرتو تعریف کنیم. از آنجا که در Go هر پکیج صرفاً یک دایرکتوری است این کار مانند ایجاد یک دایرکتوری فرعی درون یک پکیج از قبل موجود است. تنها کاری که باید انجام دهیم ارائه یک مسیر نسبی از پکیجهای تو در تو است.
کامپایل کردن پکیجها
چنان که پیشتر توضیح دادیم، دستور go run یک برنامه را کامپایل کرده و اجرا میکند. ما میدانیم که دستور go install پکیجها را کامپایل میکند و فایلهای اجرایی باینری یا فایلهای آرشیو پکیج را میسازد. این کار به منظور اجتناب از کامپایل تکراری پکیجها اجرا میشود. دستور go install یک پکیج را از قبل کامپایل میکند و Go به فایلهای a. اشاره میکند.
به طور کلی زمانی که یک پکیج شخص ثالث را نصب میکنید Go پکیج را کامپایل میکند و فایل آرشیو پکیج را میسازد. اگر پکیج را به صوت محلی نوشته باشید، در این صورت IDE ممکن است آرشیو پکیج را به محض این که فایل را در پکیج ذخیره کردید یا هنگامی که پکیج تغییر یافت ذخیره کند. در صورتی که افزونه Go را روی VSCode نصب کرده باشید، پکیج را زمانی کامپایل میکند که آن را ذخیره کنید.
مقداردهی اولیه پکیج
زمانی که یک برنامه Go را اجرا میکنید، کامپایلر Go از ترتیب اجرایی خاصی برای پکیجها، فایلها در پکیج و اعلان متغیر در پکیج پیروی میکند.
دامنه پکیج
دامنه به منطقه خاصی از بلوک کد گفته میشود که یک متغیر تعریف شده در آن قابل دسترسی است. دامنه یک پکیج منطقهای درون آن پکیج است که یک متغیر از درون پکیج قابل دسترسی باشد. این منطقه بالاترین سطح بلوک هر فایلی در پکیج است.
نگاهی به دستور go run بیندازید. این بار به جای اجرای یک فایل، یک الگوی سراسری داریم که شامل همه فایلهای درون پکیج app میشود که باید اجرا شوند. Go به قدر کافی هوشمند است تا یک نقطه ورودی برای اپلیکیشن به صورت entry.go تشخیص دهد، زیرا تابع main را دارد. ما میتوانیم از یک دستور مانند زیر نیز استفاده کنیم (ترتیب نام فایل اهمیتی ندارد):
go run src/app/version.go src/app/entry.go
دستورهای go install یا go build به یک نام پکیج نیاز دارند که شامل همه فایلهای درون یک پکیج است و از این رو لازم نیست آنها را به صورت فوق مورد اشاره قرار دهیم.
اگر به موضوع اصلی خود بازگردیم، میتوانیم از متغیر version که در فایل version.go اعلان شده است در هر جای پکیج استفاده کنیم گرچه اکسپورت نشده است، زیرا در دامنه پکیج اعلان شده است. اگر متغیر باشد در یک تابع اعلان شده باشد، در دامنه پکیج نخواهد بود و برنامه فوق در زمان کامپایل با خطا مواجه میشود.
شما مجاز به اعلان مجدد متغیر سراسری با همان نام در همان پکیج نیستید. از این رو زمانی که متغیر version اعلان میشود دیگر نمیتوان آن را در دامنه پکیج مجدداً اعلان کرد. اما میتوان آن را در جای دیگر مجدداً اعلان کرد.
مقداردهی اولیه متغیر
زمانی که یک متغیر مانند a به متغیر دیگری مانند b وابسته باشد، متغیر b باید پیش از آن تعریف شده باشد، در غیر این صورت برنامه کامپایل نمیشود. Go از این قاعده درون تابعها استفاده میکند.
اما زمانی که این متغیرها در دامنه پکیج استفاده شوند، میتوان آنها را در چرخههای مقداردهی اولیه اعلان کرد. به مثال ساده زیر توجه کنید.
در مثال فوق، ابتدا c اعلان شده است، زیرا مقدار آن قبلاً اعلان شده است. در چرخه بعدی مقداردهی b اعلان میشود، زیرا به c وابسته است و مقدار c قبلاً اعلان شده است. در چرخه نهایی مقداردهی a اعلان شده است و مقدار b به آن انتساب یافته است. Go میتواند چرخههای مقداردهی پیچیدهای مانند وضعیت زیر را مدیریت کند.
در مثال فوق، ابتدا c اعلان شده است و سپس b اعلان میشود، زیرا مقدار آن وابسته به c است و در نهایت a اعلان میشود، چون مقدار آن به b بستگی دارد. شما باید از هر حلقه مقداردهی مانند زیر که مقداردهی اولیه وارد حلقه بازگشتی میشود اجتناب کنید:
مثال دیگری از دامنه پکیج مانند زیر حالتی است که تابع f در یک فایل جداگانهای باشد که به متغیر c از فایل اصلی ارجاع داده باشد.
تابع init
تابع init نیز مانند تابع main از سوی Go زمانی که پکیج مقداردهی اولیه میشود فراخوانی خواهد شد. این تابع هیچ آرگومانی نمیگیرد و هیچ مقداری بازنمیگرداند. تابع init به صورت صریح از سوی Go اعلان شده است، چون نمیتوانید از جای دیگری به آن ارجاع دهید و یا آن را به صورت ()init فراخوانی کنید. شما میتوانید چندین تابع init در یک فایل یا پکیج داشته باشید. ترتیب اجرای تابع init در یک فایل بر اساس ترتیب نمایش آنها خواهد بود.
شما میتوانید تابع init را در هر جای پکیج خود داشته باشید. این تابعهای init به ترتیب الفبایی فراخوانی میشوند.
پس از آن که همه تابعهای init اجرا شدند، تابع main فراخوانی میشود. زیرا وظیفه اصلی تابع init مقداردهی اولیه متغیرهای سراسری است که در چارچوب سراسری قابل مقداردهی نیستند. برای نمونه یک آرایه را مقداردهی اولیه میکند.
از آنجا که ساختار for در دامنه پکیج Go معتبر نیست، میتوانیم آرایه integers با اندازه 10 را با استفاده از حلقه for درون تابع init مقداردهی اولیه کنیم.
اسامی مستعار پکیج
زمانی که یک پکیج را ایمپورت میکنید، Go یک متغیر با استفاده از اعلانهای پکیج ایجاد میکنید. اگر چندین پکیج را با نام یکسان ایمپورت کنید، منجر به بروز تداخل میشود.
از این رو از «اسامی مستعار پکیج» (package alias) استفاده میکنیم. بین کلیدواژه inport و نام پکیج یک متغیر میآوریم که آن را به یک متغیر جدید برای ارجاع به پکیج تبدیل میکند.
در مثال فوق، پکیج greet/greet از سوی متغیر child مورد ارجاع قرار میگیرد. شاید متوجه شده باشید که ما پکیج greet را با کاراکتر زیرخط به صورت یک نام مستعار اعلان کردیم. زیرخط، یک کاراکتر خاص در Go است که به عنوان کانتینر null عمل میکند. از آنجا که ما در حال ایمپورت کردن پکیج greet هستیم، اما از آن استفاده نمیکنیم، کامپایلر Go از این موضوع شکایت میکند. برای اجتناب از این وضعیت، ما یک ارجاع به آن پکیج با استفاده از _ حفظ میکنیم و بدین ترتیب کامپایلر Go آن را نادیده میگیرد.
تعریف نام مستعار برای یک پکیج با یک زیرخط که به ظاهر هیچ کاری انجام نمیدهد، در برخی موارد زمانی که میخواهید یک پکیج را مقداردهی اولیه کنید؛ اما از آن استفاده نکنید، کاملاً مفید خواهد بود.
ترتیب اجرای برنامه
تا به اینجا، همه مسائلی که مرتبط با پکیجها بود را توضیح دادیم. اینک نوبت آن رسیده است که درک خود در مورد شیوه مقداردهی اولیه برنامهها در Go را جمعبندی کنیم.
go run *.go ├── Main package is executed ├── All imported packages are initialized | ├── All imported packages are initialized (recursive definition) | ├── All global variables are initialized | └── init functions are called in lexical file name order └── Main package is initialized ├── All global variables are initialized └── init functions are called in lexical file name order
در ادامه مثالی کوچک را ملاحظه میکنید:
نصب پکیجهای شخص ثالث
نصب پکیجهای شخص ثالث چیزی به جز کلون کردن کد ریموت درون یک دایرکتوری محلی به صورت <src/<package نیست. متأسفانه Go از نسخهبندی پکیجها پشتیبانی نمیکند و روشی نیز برای مدیریت پکیجها ارائه نکرده است.
از آنجا که Go یک رجیستری مرکزی رسمی برای پکیجها ندارد از شما میخواهد که نام میزبان و مسیر پکیج را مورد اشاره قرار دهید.
$ go get -u github.com/jinzhu/gorm
دستور فوق فایلهایی را از URL به نام http://github.com/jinzhu/gorm ایمپورت میکند و آنها را در دایرکتوری ذخیره میکند. چنان که در بخش پکیجهای تودرتو بررسی کردیم، میتوان پکیج gorm را مانند زیر ایمپورت کرد:
package main import "github.com/jinzhu/gorm" // use ==> gorm.SomeExportedMember
بنابراین اگر یک پکیج ساختید و خواستید افراد از آن استفاده کنند کافی است آن را روی گیتهاب منتشر کنید. اگر پکیج شما اجرایی است، افراد میتوانند از آن به عنوان یک ابزار خط فرمان استفاده کنند، در غیر این صورت باید آن را در برنامه خود ایمپورت کنند و از آن به عنوان یک ماژول کاربردی استفاده کنند. در این حالت تنها کاری که باید انجام دهید اجرای دستور زیر است:
$ go get github.com/your-username/repo-name
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای برنامهنویسی
- آموزش توسعه وب با زبان برنامه نویسی Go
- مجموعه آموزشهای دروس علوم و مهندسی کامپیوتر
- چرا باید زبان برنامه نویسی Go را بیاموزیم؟ — راهنمای جامع
- زبان برنامه نویسی Go — راهنمای شروع به کار
==