software engineer
پیشتاز مانند Delve در خطایابی برنامه به زبان Go
بخشی از یادگیری هر زبان برنامه نویسی، آشنایی و کاربرد ابزارهایی یه که استفاده از اون زبان رو تسهیل میکنه. یکی از مهمترین ابزارها Debugger ها هستن که در زمان بروز باگ در برنامه (چه در زمان توسعه چه در محیط production) یا در زمان فهم چگونگی عملکرد قابلیت های زبان (یادگیری و آموزش) تا حد بسیار زیادی عامل هایی کمک کننده هستن. در واقع بهترین نکته در انتخاب یک debugger برای یک زبان برنامه نویسی یک عبارت ساده است:
Must But Not Least Provide Fully Featured Language Support Easily.
در مورد زبان برنامه نویسی Go به دلیل قابلیت امکان اجرای concurrent توسط goroutine ها، این مورد کمی متفاوت شاید کمی هم پیچیده تر باشه و قاعدتا استفاده از ابزارهای دیباگ general تمامی قابلیت های زبان برنامه نویسی Go رو تحت پوشش قرار نده. در مورد Go انتخاب از میان debugger ها محدود هستن و به چند انتخاب بیشتر ختم نمیشن که مهترین اونها Go-debug و Delve هستن. نکته مهم و شاید ضعف در مورد Go-debug این هستش که باید در کد تغییر ایجاد کرد تا امکان دیباگ فراهم بشه (برای دیباگ باید کد نوشت!) که این مورد، امکان دیباگ ساده (Easily) رو از کاربر میگیره (البته اگر این نکته رو در نظر نگیریم که این پروژه deprecate شده و این خودش یه ضعف بزرگ محسوب میشه).
در این مقاله قصد دارم تا به شیوه استفاده از Delve به عنوان ابزار پیشتاز در debugging کدهای زبان Go و روش کاربرد اون از طریق محیط خط فرمان (terminal، shell و امثال اونها) بپردازم. Delve به عنوان مهمترین ابزار دیباگ برای زبان Go شناخته میشه که تمامی قابلیت های این زبان از جمله goroutine ها رو تحت پوشش قرار میده. دو نکته مهم در مورد این ابزار باید ازش یاد کنیم. قابلیتی به عنوان multiple start scenario، به این مفهوم هست که این قابلیت وجود داره که هم در زمان توسعه و هم در هنگامی که برنامه به صورت فایل اجرایی سیستم عامل در حال اجراست امکان دیباگ فراهمه. نکته دوم اینکه IDE های موجود تا حدی delve رو پشیتبانی میکنن [شاید دلیل اینکه ترجیح میدم در از delve توی خط فرمان استفاده کنم همین partially support شدنش در IDE هاست. چون قابلیت و انعطاف بیشتری رو فراهم میکنه].
شروع کار با Delve
برای شروع کار باید delve در سیستم عامل نصب شده باشه. نصب delve کار پیچیده ای نیست. برای این کار میتونین از راهنمای موجود در github این دیباگر در لینک زیر استفاده کنین.
نصب بسیار راحته (البته به جز macOS که پیش نیازهایی رو داره) که با استفاده از دستور get به عنوان یکی از ابزارهای دانلود package ها و dependency ها در Go این عمل به راحتی انجام میشه.
go get github.com/go-delve/delve/cmd/dlv
بعد از اجرای این دستور و پس از خاتمه اون، delve بر روی سیستم عامل شما نصب شده و آماده استفاده است. برای اطمینان از اینکه در سیستم عامل نصب شده دستور زیر رو میتونین توی terminal اجرا کنین.
dlv version
درصورتی که به درستی همه چی پیشرفته باشه خروجی مثل تصویر زیر رو دریافت می کنین:
نکته مهمی که باید در نظر گرفت این هست که اگر از Visual Studio Code استفاده می کنین و از پلاگین Go استفاده میکنین delve همراه با نصب این پلاگین نصب میشه و احتیاجی به نصب مستقیم اون نیست. فقط جهت اطمینان ورژن delve رو با دستور بالا یکبار کنترل کنین تا از نصب اون اطمینان پیدا کنین.
مدل اجرایی delve به صورت halt and catch هستش. به این مفهوم در ابتدا باید پراسسش رو اجرا کنیم تا شروع به listen کردن به دستورات روی پورت خاصی بکنه. برای نمایش اولیه شیوه کار با delve در gif زیر نحوه تعاملش رو براتون نمایش دادم.
برای شروع، برای اینکه کارکردش کمی واضحتر بشه دستور زیر رو توی مسیر پروژه تون اجرا کنین:
dlv debug <path to the main file in your project>
با زدن این دستور Delve آماده catch کردن دستورات شما میشه. در مورد دستورات جلوتر به طور کاملتر بحث خواهم کرد ولی الان اگر دستور رو اجرا کرده باشین می بینین که delve آماده شنیدین دستورات شما میشه. میتونین با زدن دستور help لیست دستوراتش رو ببینین (در تصویر مشخص شده). برای قرار دادن breakpoint در خطی از برنامه که در مسیر دستور debug مشخص کردین میتونین از ترکیب اسم فایل و خط مورد نظرتون استفاده کنین. مثلا برای اینکه در فایل main.go در خط پنجم breakpoint قرار بدین به راحتی میتونین از دستور زیر استفاده کنین:
(dlv) break main.go:5
با زدن این دستور اطلاعاتی از قرار دادن breakpoint در خط مورد نظرتون نمایش داده میشه. بعد میتونین با دستور continue شروع به اجرای برنامه برای دیباگ کنین. با این کار دیباگ آغاز میشه و در خط پنجم در خروجی قرار میگیره (توی تصویر بالا نمایش داده شده). نکته زیبای این قسمت اینه که همیشه چند خط بالا و پایین تر از breakpoint رو به شما در خروجی نشون میده که دید کلی از تکه کد در حال دیباگ داشته باشین. میتونین با دستور step گام به گام جلو برین و مثلا با دستور locals مقادیر متغییر هاتون رو در هر لحظه مشاهده کنین (در تصویر بالا در هر مرحله locals رو اجرا کردم و مقادیر دو متغییر رو در هر لحظه نمایش دادم. نکته ای که باید مد نظر قرار بگیره وجود مقدار garbage قبل از مقداردهی اولیه هر متغییره که تا قبل از قرار گرفتن مقدار مشخص شده، مقدار garbageدر خودش داره. نکته دیگه هم اینکه تا زمان رسیدن به دستور در y:= 20 این متغییر در حافظه قرار نگرفته). در آخر هم میتونین بعد از انجام دیباگ با دستور quit از delve خارج بشین (همیشه برای خروج از quit استفاده کنین. عملکردی مثل defer رو در اختیارتون قرار میده). این مقدمه ای از شیوه تعامل با delve بود که برای اینکه بتونیم بیشتر داخل جزییاتش بشیم. روالی که بیان کردم روالیه که به صورت متناوب شما در هربار استفاده از delve تکرار و تکرار میکنین.
دسته بندی دستورات Delve
دستورات delve به شش دسته اساسی تقسیم میشن:
هر دستوری که در delve بکار گرفته بشه به یکی از این شش دسته تعلق داره. کار رو با دسته ساده تر یعنی دسته دستورات اجرای عملیات دیباگ شروع کنیم.
دسته اول: دستورات اجرای دیباگ:
این دسته ساده ترین دستورات برای اجرای عملیات دیباگ رو برعهده دارن و مهمترین دستورات اون اینها هستن:
دستورات اجرایی دستورات ساده ای هستن که توی روال روزمره برای دیباگ کد در Go ازشون استفاده می کنیم. با مفاهیم این دستورها اگر با زبان برنامه نویسی دیگه ای کار کرده باشین (که فرض من بر اینه) و از ابزارهایی برای اون زبان استفاده کرده باشین حتما آشنا هستین. دستور continue عملکردی شبیه trigger داره که با شروع فرآیند دیباگ عموما اولین دستور بعد از تعیین خط های breakpoint هستش. برای نمایش این دستورات و شیوه عملیاتیشیون یه نمونه برنامه ساده از اینترنت پیدا کردم و برای اجرا و استفاده از ترکیب این دستورها در زیر نمایش دادم.
همانطور که مشخصه از دستور break برای قرار دادن چند breakpoint توی کد استفاده کردم و با استفاده از دستور continue، step و stepout کد رو پیمایش کردم. نکته ای که قابل اهمیته اینه که برای راحتی در اجرای دستورات و تایپ اونها از abbreviation دستورات هم میشه استفاده کرد. مثلا برای اجرای دستور continue میشه به راحتی از c استفاده کرد. یا برای step از s و برای stepout از so.
دسته دوم : Breakpoint ها
این دسته از دستورات برای مدیریت breakpoint ها مورد استفاده قرار می گیرن.
تا حدودی با مفاهیم این دستورات آشنا هستین. مثلا برای قرار دادن یک breakpoint در کد تا حالا چندین بار از دستورbreak (یا مختصر اون b) در دیباگ استفاده کردیم. برای اینکه لیت تمامی breakpoint ها رو نمایش بدیم میتونیم از دستور bp یا همون breakpoints استفاده کنیم. دو دستور بعدی برای حذف breakpoint از یک خط خاص از برنامه مورد استفاده قرار می گیرن. با این تفاوت که برای حذف یک breakpoint به وسیله دستور clear باید شناسه اون breakpoint رو که در زمان قراردادنش در خروجی نمایش داده شده بوده رو به عنوان پارامتر ورودی به این دستور بدیم.
دستور condition برای توقف در اجرای یک breakpoint در شرایطی خاص هست. همونطور که حدث زدید باید breakpoint قبلش مشخص شده باشه تا بشه از این دستور استفاده کرد. فرمت دستور اینطوریه
condition <breakpoint number> <boolean condition>
مثلا برای اینکه در خط نهم از تصویر سورس کد بالا بخوایم در صورتی که i==3 بود breakpoint اجرا بشه از دستور زیر استفاده میکنیم. باز هم تاکید میکنم که باید قبلش با دستور break توی خط ۹ breakpoint ایجاد کرده باشین. (ممکنه شماره breakpoint شما این شماره ۱ نباشه. برای این مورد به خروجی دستور break زمان اجراتون دقت کنین تا شماره breakpointدرست رو استفاده کنین)
(dlv) condition 1 i==3
دستور trace مانند دستور break هستش با این تفاوت که این دستور اجرا برنامه در زمان دیباگ رو halt نمی کنه و تنها در صورتی که به tracepoint مورد نظر رسید میتونه hint خاصی بده. به عنوان نمونه اگر بخوایم در خط ۹ از نمونه کد بالا یه tracepoint ست کنیم و به اون اسم خاصی بدیم که در زمان عبور کد از اون نمایش داده بشه میتونیم از دستور زیر استفاده کنیم:
(dlv) trace sampleHint main.go:9
با زدن این دستور و رسیدن اجرای دیباگر به اون در خروجی hint ایی با عنوان sampleHint در خروجی نمایش داده میشه.
دستور on برای زمانی هست که میخوایم با رسیدن به اجرای یک breakpoint خاص دستور خاصی اجرا بشه. مثلا پیامی در خروجی چاپ بشه. فرمت دستور به این صورته
(dlv) on <breakpoint number> <command>
(dlv) on 1 print "This is a sample event"
برای مشخص شدن بیشتر نمونه کد اجرایی رو برای این مجموعه از دستورات به صورت نمونه دیباگ کردم و توی تصویر نتیجه اون رو قرار دادم:
دسته سوم: دستورات نمایش متغییرها و حافظه
این دسته از دستورات در ارتباط با نمایش وضعیت حافظه و مقادیر متغییرها در زمان توقف دیباگ هستن که در تصویر زیر خلاصه ای از مهمترین های اونها رو اوردم:
دستورات این دسته دستورات ساده ای هستن. فقط ذکر چند نکته میتونه مهم باشه. دستور display باید یک عبارت مثل نام یک متغییر رو به عنوان ورودی بگیره تا در هر زمان اجرا halt بشه اون مقدار رو نشون در اون لحظه محاسبه و نشون بده. فرمت استفاده از این دستور هم در زیر بیان کردم:
(dlv) display -a <expression>
(dlv) display -a temp
مثلا دستور بالا با قطع اجرا در هر گام مقدار متغییر temp رو نشون میده. نکته دیگه در مورد دستور vars هست که مقادیر متغییر ها در سطح package رو نشون میدن. این دستور میتونه بایک regex استفاده بشه تا متغییر های در سطح package با اون الگو رو نشون بده. دستور set هم باید به محدودیتش توجه بشه که تنها اعداد و اشاره گرها قابل تغییر هستن. برای واضح تر شدن این دسته از دستورات تصویر زیر رو قرار دادم:
دسته چهارم: goroutine و thread
مهمترین تفاوتی که میتونیم بین delve و سایر دیباگرها بیان کنیم، همین فهم و درک goroutine های زبان Go هست. این دسته چند دستور بیشتر نداره ولی بسیار کارآمد.
عملکرد این دستورها مشخصه. برای اینکه متوجه بشیم goroutine جاری کدوم goroutine هست میتونیم از دستور goroutine (یا خلاصه اون gr) استفاده کنیم. در صورتی که خواستیم از بین یک goroutine به یه goroutine دیگه سوییچ کنیم و اون رو در حال اجرا قرار بدیم میتونیم از همین دستور با پارامتر شماره goroutine ایی که قصد سوییچ کردن بهش رو داریم، استفاده کنیم.
(dlv) goroutine <number of goroutine to switch>
یه کاربرد دیگه ای که میتونیم از دستور goroutine داشته باشیم این هستش که میتونیم همزمان با سوییچ کردن بین goroutine ها دستور خاصی رو هم اجرا کنیم.
(dlv) goroutine 20 set x=12
این دستور به goroutine شماره ۲۰ سوییچ میکنه و در صورتی که متغییر x در اسکوپش وجود داشته باشه مقدار اون رو به ۱۲ تغییر میده.
همین موارد در مورد دو دستور بعدی، البته با محدودیت به مراتب بیشتر، هم صدق میکنه با این تفاوت که دستور thread فقط برای switch بین thread ها با شماره thread مورد نظر مورد استفاده قرار میگیره. برای بهتر مشخص شدن کارایی این دستورها تصویر زیر رو قرار دادم:
دسته پنجم: دستورات stack و frame
این دسته از دستورات دارای دو دستور پراهمیت تر هستند که در طول دیباگ برنامه ها بیشتر مورد استفاده قرار می گیرن. این دو دستور deferred و دستور stack هستن. دستور stack که همونطور که از اسمش هم پیداست برای نمایش stack در زمان توقف اجرای برنامه در دیباگ مورد استفاده قرار می گیره. دستور deferred هم برای اینکه در n امین دستور defer در تابع عبارتی رو نمایش بده بکار میره. مثلا در صورتی که کدی داشته باشیم که دریک تابع اون چندین دستور deferاستفاده شده باشه، برای اینکه در defer شماره n عبارتی نمایش داده بشه میتونیم از دستور زیر استفاده کنیم:
(dlv) deferred 2 print x
نکته ای که باید در مورد عبارت قابل نمایش در deferred مد نظر داشته باشیم این هستش که دستورات محدود هستن و به سه دستور locals، args و print ختم میشن.
در مورد دستور stack هم گفتن این نکته میتونه کمک کننده باشه که stack -full نمایش کاملی از فریم استک رو به شما نشون خواهد داد.
این دسته دستورات دیگه ای مثل frame و up و down هم داره که دستورات سطح پایین تری توی دیباگ هستن و ازشون کمتر استفاده میشه. به همین دلیل ترجیح دادم تا در آپدیت های بعدی این مقاله در موردشون بنویسم.
دسته ششم: سایر دستورات
این دسته از دستورات اگرچه کم اهمیت تر جلوه داده شدن، ولی قدرت بالایی رو در اختیار توسعه دهنده قرار میدن. این دستورات رو با هم مرور میکنیم:
دستور source
دستور source از کاربردی ترین دستورات delve به شمار میاد. برای اینکه عملیات دیباگ به صورت مشخص و با اجرای دیباگ آغاز بشه و از دستورات تکراری در دیباگ یک کد به صورت مداوم جلوگیری بشه، میتونیم از این دستور استفاده کنیم. به این صورت که تمامی دستوراتی رو که قراره در حین اجرای دیباگ قصد ورودش در console رو داریم رو در یک فایل قرار میدیم و با شروع اجرای دیباگ و زدن این دستور به همراه آدرس اون فایل، دستورات درون فایل به صورت متناوب شروع به اجرا میکنه.
در این دسته از دستورات مجموعه دستوراتی با اولویت پایین تر هم وجود دارن که فقط به صورت خلاصه کاربردشون رو ذکر میکنم.
استفاده از Flag هاو سایر دستورات
تاحالا تمامی عملیاتی که در delve در موردشون صحبت کردیم، از دستور debug استفاده میکردیم. توی این بخش قصدم اینه یکم در مورد بقیه command ها هم صحبت کنم. در کنار دستورات اجرایی در هنگام دیباگ در delve، خود دستور دارای یه سری فلگ کاربردی هست که در بسیاری از مواقع به شما تو کاربرد delve کمک میکنه. در این بخش قصدم اینه که به مهمترین فلگ ها اشاره کنم و در مورد کاربردهاشون مثلاهایی واقعی تری بیان کنم. اگر خود دستور dlv رو بدون هیچ پارامتری در محیط terminal تون وارد کنید خروجی مثل تصویر زیر می بینین که دو بخش دستورات و فلگ ها رو منحصرا مدنظر قرار دادم:
حالا در مورد مهمترین دستورات و فلگ ها صحبت کنیم:
دستور connect و فلگ های headless و listen
در مواقعی قصد دارین که در یه محیط indirect مثلا sandbox یا در یه container کد رو دیباگ کنین. مثلا برنامه در یک docker container در قالب یه pod در Kubernetes در حال اجراست و شما قصد دارین متناسب با دیتای production یا staging، و در همون لحظه امکان دیباگ کد رو داشته باشین. در این حالت باید این امکان فراهم شده باشه که شما یه debug server با یک port مشخص در container تون ایجاد و port اش رو expose کرده باشین. این کار توسط دستور debugو به وسیله فلگ های headless و listen قابل انجامه به اینصورت که شما در Dockerfile میتونین در هر زمان که نیاز داشتین از این عبارت برای اجرای کد همزمان در debug mode استفاده کنین:
dlv debug --headless --listen=:6666 main.go
اینکار باعث میشه که برنامه در مد دیباگ یه debug server به وجود بیاره که به پورت ۶۶۶۶ در حال listen کردنه (روش های دیگه ای وجود داره که مناسب تر هستن. اینجا فقط برای توضیح مکانیزم، این روش رو استفاده کردم. در مورد پورت ۶۶۶۶ اگر در k8s باشیم سرویسی که این پورت رو راوت میکنه شرایطی باید داشته باشه که از اون هم صرف نظر کردم. اونجا معمول برای این دست کارها اینه که محدوده پورت ها بالای ۳۰۰۰۰ باشن). بعد برای اتصال به سرویس دیباگ روی اون پورت از دستور connect میشه استفاده کرد.
dlv connect 127.0.0.1:6666
که IP قابل کانفیگه. برای نمونه تصویر زیر رو برای شبیه سازی headless ببینین:
مابقی فلگ ها کاربری ساده ای دارن که با نگاه کردن به خروجی دستور dlv میشه به راحتی کشفشون کرد. مثلا کاربرد log که اطلاعات بیشتری رو حین دیباگ در اختیار قرار میده یا init که کاربردی شبیه دستور source داره رو میشه از مستنداتش به راحتی به دست آورد. که این مورد رو به خودتون واگذار میکنم.
سایر دستورات dlv
علاوه بر debug که دستور پرکاربرد برای delve هستش دو دستور مهم دیگه هم وجود داره که در برخی زمان ها کمک کننده هستن. اولین دستور dlv exec هستش که کمک میکنه تا کد کامپایل شده رو بتونیم دیباگ کنیم. نکته مهم اینه که کامپایلر زبان Go بعد کامپایل کد optimized شده تولیر میکنه که این مورد استفاده از این دستور رو خیلی سخت و غیرقابل فهم میکنه. برای اینکه بشه از این مشکل عبور کرد باید در زمان کامپایل کدها فلگ زیر رو استفادع کنین تا قابلیت کامپایل کد اجرایی رو داشته باشین:
go build -gcflags="-N -l" <path to the main file in main package>
بعد از این شیوه استفاده در کامپایل و build کد به راحتی میتونین بادستور زیر فایل اجرایی رو دیباگ کنین.
dlv exec <path to executable file>
دستور بعدی که خیلی شبیه exec کار میکنه دستور attach هستش که در صورتی که مثل execبا gcflags که بالا توضیح دادم رو استفاده کرده باشین میتونین با PID به فایل اجرایی کد Go متصل بشین و دیباگ انجام بدین.
dlv attach PID
خاتمه
خیلی دوست داشتم مطلبی در مورد gdlv هم بنویسم ولی احساس میکنم ممکنه از حوصله خارج بشه. به همین دلیل این ابزار visual برای delve رو فقط معرفی میکنم که بتونین ازش استفاده کنین. کار باهاش بسیار ساده است (البته من خودم استفاده نمیکنم :)). اگر مفاهیمی که توضیح دادم رو آشنا شده باشین، به راحتی میتونین ازش استفاده کنین.
خوب فکر کنم تونسته باشم یکم از توانایی های delve رو به دوستان علاقه مند نشون داده باشم. ممنون میشم که نظراتتون و اصلاحاتتون رو برام ایمیل کنین یا زیر همین مقاله بهم اطلاع بدین. پیشاپیش از شما متشکر خواهم بود :)
مطلبی دیگر از این انتشارات
الگوهای concurrency (بخش ۲): cancellation و context
مطلبی دیگر از این انتشارات
آناتومی channelها در زبان برنامه نویسی Go
مطلبی دیگر از این انتشارات
الگوهای concurrency (بخش ۱): for-select