فصل چهارم - بلوک‌ها، سایه‌زنی (Shadowing) و ساختارهای کنترلی

حال که متغیرها، ثابت‌ها و انواع داده‌های داخلی را پوشش داده‌ام، شما آماده‌اید تا به منطق برنامه‌نویسی و سازماندهی نگاه کنید. من با توضیح بلوک‌ها و نحوه کنترل آن‌ها بر در دسترس بودن یک شناسه شروع می‌کنم. سپس ساختارهای کنترل Go را ارائه خواهم داد: if، for و switch. در نهایت، درباره goto و تنها موقعیتی که باید از آن استفاده کنید صحبت خواهم کرد.

blogs tailwind section

حال که متغیرها، ثابت‌ها و انواع داده‌های داخلی را پوشش داده‌ام، شما آماده‌اید تا به منطق برنامه‌نویسی و سازماندهی نگاه کنید. من با توضیح بلوک‌ها و نحوه کنترل آن‌ها بر در دسترس بودن یک شناسه شروع می‌کنم. سپس ساختارهای کنترل Go را ارائه خواهم داد: if، for و switch. در نهایت، درباره goto و تنها موقعیتی که باید از آن استفاده کنید صحبت خواهم کرد.

بلوک‌ها

Go به شما اجازه می‌دهد متغیرها را در مکان‌های زیادی اعلان کنید. می‌توانید آن‌ها را خارج از توابع، به عنوان پارامترهای توابع، و به عنوان متغیرهای محلی درون توابع اعلان کنید.

تا کنون، شما فقط تابع main را نوشته‌اید، اما در فصل بعد توابعی با پارامتر خواهید نوشت.

هر مکانی که یک اعلان در آن رخ می‌دهد، بلوک نامیده می‌شود. متغیرها، ثابت‌ها، انواع داده‌ها و توابعی که خارج از هر تابعی اعلان می‌شوند، در بلوک بسته قرار می‌گیرند. شما از دستورات import در برنامه‌هایتان برای دسترسی به توابع چاپ و ریاضی استفاده کرده‌اید (و در فصل ۱۰ به تفصیل درباره آن‌ها صحبت خواهم کرد). آن‌ها نام‌هایی برای بسته‌های دیگر تعریف می‌کنند که برای فایلی که حاوی دستور import است معتبر هستند. این نام‌ها در بلوک فایل قرار دارند. تمام متغیرهای تعریف شده در سطح بالای یک تابع (شامل پارامترهای یک تابع) در یک بلوک هستند. در درون یک تابع، هر مجموعه از آکولادها ({}) بلوک دیگری را تعریف می‌کند، و به زودی خواهید دید که ساختارهای کنترل در Go بلوک‌های خاص خود را تعریف می‌کنند.

می‌توانید به یک شناسه تعریف شده در هر بلوک خارجی از درون هر بلوک داخلی دسترسی پیدا کنید. این سوال را مطرح می‌کند: چه اتفاقی می‌افتد وقتی اعلانی با همان نام یک شناسه در یک بلوک حاوی دارید؟ اگر این کار را انجام دهید، شناسه ایجاد شده در بلوک خارجی را سایه‌زنی می‌کنید.

سایه‌زنی متغیرها

قبل از اینکه توضیح دهم سایه‌زنی چیست، بیایید نگاهی به کدی بیندازیم (مثال ۴-۱ را ببینید). می‌توانید آن را در Go Playground یا در دایرکتوری sample_code/shadow_variables در مخزن فصل ۴ اجرا کنید.

مثال ۴-۱. سایه‌زنی متغیرها

func main() {
    x := 10
    if x > 5 {
        fmt.Println(x)
        x := 5
        fmt.Println(x)
    }
    fmt.Println(x)
}

قبل از اجرای این کد، سعی کنید حدس بزنید که چه چیزی چاپ خواهد شد:

• ۱۰ در خط اول، ۵ در خط دوم، ۱۰ در خط سوم • ۱۰ در خط اول، ۵ در خط دوم، ۵ در خط سوم • هیچ چیز چاپ نمی‌شود؛ کد کامپایل نمی‌شود

اینجا چه اتفاقی می‌افتد:

10
5
10

یک متغیر سایه‌زن، متغیری است که همان نام متغیری در یک بلوک حاوی را دارد. تا زمانی که متغیر سایه‌زن وجود دارد، نمی‌توانید به متغیر سایه‌زده شده دسترسی پیدا کنید.

در این مورد، تقریباً مطمئناً نمی‌خواستید یک x کاملاً جدید در داخل دستور if ایجاد کنید. در عوض، احتمالاً می‌خواستید ۵ را به x اعلان شده در سطح بالای بلوک تابع اختصاص دهید. در اولین fmt.Println در داخل دستور if، می‌توانید به x اعلان شده در سطح بالای تابع دسترسی پیدا کنید. اما در خط بعدی، با اعلان متغیر جدیدی با همان نام در داخل بلوکی که توسط بدنه دستور if ایجاد شده، x را سایه‌زنی می‌کنید. در دومین fmt.Println، وقتی به متغیر به نام x دسترسی پیدا می‌کنید، متغیر سایه‌زن را دریافت می‌کنید که مقدار ۵ دارد. آکولاد بسته برای بدنه دستور if، بلوکی که x سایه‌زن در آن وجود دارد را پایان می‌دهد، و در سومین fmt.Println، وقتی به متغیر به نام x دسترسی پیدا می‌کنید، متغیر اعلان شده در سطح بالای تابع را دریافت می‌کنید که مقدار ۱۰ دارد. توجه کنید که این x ناپدید نشد یا مجدداً اختصاص داده نشد؛ فقط راهی برای دسترسی به آن وجود نداشت زمانی که در بلوک داخلی سایه‌زنی شد.

در فصل قبل ذکر کردم که در برخی موقعیت‌ها از استفاده از := اجتناب می‌کنم زیرا می‌تواند مشخص نباشد که کدام متغیرها استفاده می‌شوند. این بدان دلیل است که هنگام استفاده از := بسیار آسان است که به طور تصادفی یک متغیر را سایه‌زنی کنید. به یاد داشته باشید، می‌توانید از := برای ایجاد و اختصاص به چندین متغیر به طور همزمان استفاده کنید. همچنین، همه متغیرهای سمت چپ نیازی نیست جدید باشند تا := قانونی باشد. می‌توانید از := استفاده کنید تا زمانی که حداقل یک متغیر جدید در سمت چپ وجود داشته باشد. بیایید به برنامه دیگری نگاه کنیم (مثال ۴-۲ را ببینید)، که می‌توانید آن را در Go Playground یا در دایرکتوری sample_code/shadow_multiple_assignment در مخزن فصل ۴ پیدا کنید.

مثال ۴-۲. سایه‌زنی با اختصاص چندگانه

func main() {
    x := 10
    if x > 5 {
        x, y := 5, 20
        fmt.Println(x, y)
    }
    fmt.Println(x)
}

اجرای این کد نتیجه زیر را می‌دهد:

5 20
10

اگرچه تعریف موجودی از x در یک بلوک خارجی وجود داشت، x هنوز در داخل دستور if سایه‌زنی شد. این بدان دلیل است که := فقط متغیرهایی را که در بلوک فعلی اعلان شده‌اند مجدداً استفاده می‌کند. هنگام استفاده از :=، مطمئن شوید که هیچ متغیری از یک محدوده خارجی در سمت چپ ندارید مگر اینکه قصد سایه‌زنی آن‌ها را داشته باشید.

همچنین باید مراقب باشید که import یک بسته را سایه‌زنی نکنید. در فصل ۱۰ بیشتر درباره وارد کردن بسته‌ها صحبت خواهم کرد، اما شما بسته fmt را برای چاپ نتایج برنامه‌هایمان وارد کرده‌اید. بیایید ببینیم چه اتفاقی می‌افتد وقتی متغیری به نام fmt در داخل تابع main تعریف می‌کنید، همانطور که در مثال ۴-۳ نشان داده شده است. می‌توانید سعی کنید آن را در Go Playground یا در دایرکتوری sample_code/shadow_package_names در مخزن فصل ۴ اجرا کنید.

مثال ۴-۳. سایه‌زنی نام‌های بسته

func main() {
    x := 10
    fmt.Println(x)
    fmt := "oops"
    fmt.Println(fmt)
}

وقتی سعی می‌کنید این کد را اجرا کنید، خطایی دریافت می‌کنید:

fmt.Println undefined (type string has no field or method Println)

توجه کنید که مشکل این نیست که متغیرتان را fmt نام‌گذاری کرده‌اید؛ مشکل این است که سعی کردید به چیزی دسترسی پیدا کنید که متغیر محلی fmt آن را نداشت. زمانی که متغیر محلی fmt اعلان می‌شود، بسته به نام fmt در بلوک فایل را سایه‌زنی می‌کند و استفاده از بسته fmt را برای بقیه تابع main غیرممکن می‌سازد.

بلوک جهان

یک بلوک دیگر کمی عجیب است: بلوک جهان. به یاد داشته باشید، Go زبان کوچکی با فقط ۲۵ کلمه کلیدی است. جالب اینجاست که انواع داده‌های داخلی (مانند int و string)، ثابت‌ها (مانند true و false)، و توابع (مانند make یا close) در آن فهرست گنجانده نشده‌اند. nil نیز همینطور.

پس، آن‌ها کجا هستند؟

به جای اینکه آن‌ها را کلمات کلیدی کند، Go آن‌ها را شناسه‌های از پیش اعلان شده در نظر می‌گیرد و در بلوک جهان تعریف می‌کند، که بلوکی است که تمام بلوک‌های دیگر را در بر می‌گیرد.

از آنجا که این نام‌ها در بلوک جهان اعلان شده‌اند، می‌توانند در محدوده‌های دیگر سایه‌زنی شوند. می‌توانید این اتفاق را با اجرای کد در مثال ۴-۴ در Go Playground یا در دایرکتوری sample_code/shadow_true در مخزن فصل ۴ ببینید.

مثال ۴-۴. سایه‌زنی true

fmt.Println(true)
true := 10
fmt.Println(true)

وقتی آن را اجرا می‌کنید، خواهید دید:

true
10

باید بسیار مراقب باشید که هرگز هیچ شناسه‌ای در بلوک جهان را مجدداً تعریف نکنید. اگر تصادفاً این کار را انجام دهید، رفتار عجیبی خواهید داشت. اگر خوش‌شانس باشید، شکست‌های کامپایل خواهید داشت. اگر خوش‌شانس نباشید، ردیابی منبع مشکلاتتان دشوارتر خواهد بود.

از آنجا که سایه‌زنی در چند مورد مفید است (فصل‌های بعدی آن‌ها را نشان خواهند داد)، go vet آن را به عنوان خطای احتمالی گزارش نمی‌کند. در بخش "استفاده از اسکنرهای کیفیت کد" در صفحه ۲۶۷، درباره ابزارهای شخص ثالث که می‌توانند سایه‌زنی تصادفی در کدتان را شناسایی کنند، یاد خواهید گرفت.

if

دستور if در Go بسیار شبیه به دستور if در اکثر زبان‌های برنامه‌نویسی است. چون که این ساختار بسیار آشنا است، من در نمونه کدهای قبلی از آن استفاده کرده‌ام بدون اینکه نگران باشم که گیج‌کننده خواهد بود. مثال ۴-۵ نمونه کاملتری را نشان می‌دهد.

مثال ۴-۵. if و else

n := rand.Intn(10)
if n == 0 {
    fmt.Println("That's too low")
} else if n > 5 {
    fmt.Println("That's too big:", n)
} else {
    fmt.Println("That's a good number:", n)
}

قابل‌مشاهدترین تفاوت بین دستورات if در Go و سایر زبان‌ها این است که شما پرانتز دور شرط نمی‌گذارید. اما Go ویژگی دیگری به دستورات if اضافه می‌کند که به شما کمک می‌کند متغیرهایتان را بهتر مدیریت کنید.

همان‌طور که در بخش "سایه‌گذاری متغیرها" در صفحه ۶۸ بحث کردم، هر متغیری که درون آکولادهای یک دستور if یا else تعریف شود، تنها درون همان بلوک وجود دارد. این چیز غیرعادی نیست؛ در اکثر زبان‌ها چنین است. آنچه که Go اضافه می‌کند، توانایی تعریف متغیرهایی است که محدوده آن‌ها شامل شرط و هم بلوک‌های if و else می‌شود. نگاهی به مثال ۴-۶ بیندازید که مثال قبلی ما را برای استفاده از این محدوده بازنویسی می‌کند.

مثال ۴-۶. محدودسازی یک متغیر به دستور if

if n := rand.Intn(10); n == 0 {
    fmt.Println("That's too low")
} else if n > 5 {
    fmt.Println("That's too big:", n)
} else {
    fmt.Println("That's a good number:", n)
}

داشتن این محدوده خاص مفید است. این امکان را به شما می‌دهد که متغیرهایی بسازید که تنها در جایی که نیاز هست در دسترس باشند. وقتی که سری دستورات if/else به پایان می‌رسد، n تعریف نشده می‌شود. می‌توانید این را با تلاش برای اجرای کد مثال ۴-۷ در Go Playground یا در دایرکتوری sample_code/if_bad_scope در مخزن فصل ۴ تست کنید.

مثال ۴-۷. خارج از محدوده...

if n := rand.Intn(10); n == 0 {
    fmt.Println("That's too low")
} else if n > 5 {
    fmt.Println("That's too big:", n)
} else {
    fmt.Println("That's a good number:", n)
}
fmt.Println(n)

تلاش برای اجرای این کد خطای کامپایل تولید می‌کند:

undefined: n

از نظر فنی، می‌توانید هر دستور ساده‌ای را قبل از مقایسه در یک دستور if قرار دهید—از جمله فراخوانی تابعی که مقداری برنمی‌گرداند یا تخصیص مقدار جدید به متغیر موجود. اما این کار را نکنید. از این ویژگی تنها برای تعریف متغیرهای جدیدی استفاده کنید که محدوده آن‌ها به دستورات if/else محدود باشد؛ هر چیز دیگری گیج‌کننده خواهد بود.

همچنین آگاه باشید که درست مثل هر بلوک دیگر، متغیری که به‌عنوان بخشی از دستور if تعریف شود، متغیرهایی با همان نام که در بلوک‌های شامل تعریف شده‌اند را سایه‌گذاری خواهد کرد.

for، چهار روش

همان‌طور که در سایر زبان‌های خانواده C، Go از دستور for برای حلقه استفاده می‌کند. آنچه که Go را از سایر زبان‌ها متفاوت می‌کند این است که for تنها کلیدواژه حلقه‌ای در این زبان است. Go این کار را با استفاده از کلیدواژه for در چهار قالب انجام می‌دهد:

• یک for کامل، به سبک C • یک for تنها با شرط • یک for بی‌نهایت • for-range

دستور for کامل

اولین سبک حلقه for، اعلان کامل for است که ممکن است از C، Java، یا JavaScript با آن آشنا باشید، همان‌طور که در مثال ۴-۸ نشان داده شده است.

مثال ۴-۸. یک دستور for کامل

for i := 0; i < 10; i++ {
    fmt.Println(i)
}

ممکن است تعجب نکنید که این برنامه اعداد ۰ تا ۹ را شامل چاپ می‌کند.

درست مثل دستور if، دستور for پرانتز دور قسمت‌هایش استفاده نمی‌کند. در غیر این صورت، باید آشنا به نظر برسد. دستور if سه قسمت دارد که با نقطه‌ویرگول از هم جدا شده‌اند. قسمت اول مقداردهی اولیه است که یک یا چند متغیر را قبل از شروع حلقه تنظیم می‌کند. باید دو جزئیات مهم در مورد بخش مقداردهی اولیه به خاطر بسپارید. اول، باید از := برای مقداردهی اولیه متغیرها استفاده کنید؛ var اینجا قانونی نیست. دوم، درست مثل اعلان متغیرها در دستورات if، می‌توانید اینجا متغیری را سایه‌گذاری کنید.

قسمت دوم مقایسه است. این باید عبارتی باشد که به bool ارزیابی شود. بلافاصله قبل از هر تکرار حلقه بررسی می‌شود. اگر عبارت به true ارزیابی شود، بدنه حلقه اجرا می‌شود.

قسمت آخر یک دستور for استاندارد، افزایش است. معمولاً چیزی مثل i++ را اینجا می‌بینید، اما هر تخصیصی معتبر است. بلافاصله بعد از هر تکرار حلقه، قبل از ارزیابی شرط اجرا می‌شود.

Go به شما اجازه می‌دهد یک یا چند تا از سه قسمت دستور for را حذف کنید. معمولاً، یا مقداردهی اولیه را حذف می‌کنید اگر بر اساس مقداری محاسبه شده قبل از حلقه باشد:

i := 0
for ; i < 10; i++ {
    fmt.Println(i)
}

یا افزایش را حذف می‌کنید چون قانون افزایش پیچیده‌تری درون حلقه دارید:

for i := 0; i < 10; {
    fmt.Println(i)
    if i % 2 == 0 {
        i++
    } else {
        i+=2
    }
}

دستور for تنها با شرط

وقتی که هم مقداردهی اولیه و هم افزایش را در دستور for حذف می‌کنید، نقطه‌ویرگول‌ها را شامل نکنید. (اگر این کار را بکنید، go fmt آن‌ها را حذف خواهد کرد.) این یک دستور for باقی می‌گذارد که مثل دستور while که در C، Java، JavaScript، Python، Ruby و بسیاری از زبان‌های دیگر یافت می‌شود، عمل می‌کند. شبیه مثال ۴-۹ است.

مثال ۴-۹. یک دستور for تنها با شرط

i := 1
for i < 100 {
    fmt.Println(i)
    i = i * 2
}

دستور for بی‌نهایت

فرمت سوم دستور for شرط را نیز حذف می‌کند. Go نسخه‌ای از حلقه for دارد که تا ابد تکرار می‌شود. اگر شما در دهه 1980 برنامه‌نویسی یاد گرفته‌اید، احتمالاً اولین برنامه شما یک حلقه بی‌نهایت در BASIC بوده که HELLO را تا ابد روی صفحه چاپ می‌کرده:

10 PRINT "HELLO"
20 GOTO 10

مثال 4-10 نسخه Go این برنامه را نشان می‌دهد. می‌توانید آن را به صورت محلی اجرا کنید یا در The Go Playground یا در دایرکتوری sample_code/infinite_for در مخزن فصل 4 امتحان کنید.

مثال 4-10. نوستالژی حلقه بی‌نهایت

package main

import "fmt"

func main() {
    for {
        fmt.Println("Hello")
    }
}

اجرای این برنامه همان خروجی را به شما می‌دهد که صفحه‌های میلیون‌ها Commodore 64 و Apple ][ را پر می‌کرده:

Hello
Hello
Hello
Hello
Hello
Hello
Hello
...

وقتی از قدم زدن در خاطرات خسته شدید، Ctrl-C را فشار دهید.

اگر مثال 4-10 را در The Go Playground اجرا کنید، متوجه خواهید شد که پس از چند ثانیه اجرا را متوقف می‌کند. به عنوان یک منبع مشترک، playground اجازه نمی‌دهد هر برنامه‌ای برای مدت طولانی اجرا شود.

break و continue

چگونه می‌توانید از یک حلقه for بی‌نهایت خارج شوید بدون استفاده از صفحه‌کلید یا خاموش کردن کامپیوتر؟ این وظیفه دستور break است. این دستور فوراً از حلقه خارج می‌شود، درست مانند دستور break در زبان‌های دیگر. البته، می‌توانید break را با هر دستور for استفاده کنید، نه فقط دستور for بی‌نهایت.

Go معادلی برای کلمه کلیدی do در Java، C، و JavaScript ندارد. اگر می‌خواهید حداقل یک بار تکرار کنید، تمیزترین راه استفاده از یک حلقه for بی‌نهایت است که با یک دستور if پایان می‌یابد. اگر کدی در Java دارید که از حلقه do/while استفاده می‌کند:

do {
    // things to do in the loop
} while (CONDITION);

نسخه Go شبیه این است:

for {
    // things to do in the loop
    if !CONDITION {
        break
    }
}

توجه کنید که شرط دارای یک ! در ابتدا برای نفی کردن شرط از کد Java است. کد Go مشخص می‌کند چگونه از حلقه خارج شود، در حالی که کد Java مشخص می‌کند چگونه در حلقه باقی بماند.

Go همچنین شامل کلمه کلیدی continue است که بقیه بدنه حلقه for را رد می‌کند و مستقیماً به تکرار بعدی می‌رود. از نظر فنی، شما به دستور continue نیاز ندارید. می‌توانید کدی مانند مثال 4-11 بنویسید.

مثال 4-11. کد گیج‌کننده

for i := 1; i <= 100; i++ {
    if i%3 == 0 {
        if i%5 == 0 {
            fmt.Println("FizzBuzz")
        } else {
            fmt.Println("Fizz")
        }
    } else if i%5 == 0 {
        fmt.Println("Buzz")
    } else {
        fmt.Println(i)
    }
}

اما این کد اصطلاحی نیست. Go بدنه‌های کوتاه دستور if را تشویق می‌کند، تا حد امکان به سمت چپ تراز شده. کد تو در تو دنبال کردنش دشوارتر است. استفاده از دستور continue درک آنچه در حال اتفاق است را آسان‌تر می‌کند. مثال 4-12 کد مثال قبلی را نشان می‌دهد که برای استفاده از continue بازنویسی شده است.

مثال 4-12. استفاده از continue برای واضح‌تر کردن کد

for i := 1; i <= 100; i++ {
    if i%3 == 0 && i%5 == 0 {
        fmt.Println("FizzBuzz")
        continue
    }
    if i%3 == 0 {
        fmt.Println("Fizz")
        continue
    }
    if i%5 == 0 {
        fmt.Println("Buzz")
        continue
    }
    fmt.Println(i)
}

همان‌طور که می‌بینید، جایگزین کردن زنجیره‌های دستورات if/else با یک سری دستورات if که از continue استفاده می‌کنند، شرایط را در یک خط قرار می‌دهد. این چیدمان شرایط شما را بهبود می‌بخشد، که به معنای آن است که کد شما خواندن و درک آن آسان‌تر است.

دستور for-range

چهارمین فرمت دستور for برای تکرار روی عناصر در برخی از انواع داخلی Go است. این دستور for-range loop نامیده می‌شود و شبیه به تکرارکننده‌هایی است که در زبان‌های برنامه‌نویسی دیگر یافت می‌شوند. این بخش نشان می‌دهد که چگونه از حلقه for-range با رشته‌ها، آرایه‌ها، slice ها و map ها استفاده کنید. زمانی که channel ها را در فصل ۱۲ پوشش می‌دهم، در مورد نحوه استفاده از آنها با حلقه‌های for-range صحبت خواهم کرد.

شما می‌توانید از حلقه for-range فقط برای تکرار روی انواع ترکیبی داخلی و انواع تعریف‌شده توسط کاربر که بر اساس آنها هستند، استفاده کنید.

ابتدا، بیایید نگاهی به استفاده از حلقه for-range با یک slice بیندازیم. می‌توانید کد موجود در مثال ۴-۱۳ را در Go Playground یا در تابع forRangeKeyValue در main.go در دایرکتوری sample_code/for_range در مخزن فصل ۴ امتحان کنید.

مثال ۴-۱۳. حلقه for-range

evenVals := []int{2, 4, 6, 8, 10, 12}
for i, v := range evenVals {
    fmt.Println(i, v)
}

اجرای این کد خروجی زیر را تولید می‌کند:

0 2
1 4
2 6
3 8
4 10
5 12

آنچه که حلقه for-range را جالب می‌کند این است که شما دو متغیر حلقه دریافت می‌کنید. متغیر اول موقعیت در ساختار داده‌ای است که تکرار می‌شود، در حالی که دومی مقدار آن موقعیت است. نام‌های اصطلاحی برای این دو متغیر حلقه بستگی به آنچه که روی آن حلقه زده می‌شود دارد. هنگام حلقه زدن روی آرایه، slice یا رشته، معمولاً از i برای index استفاده می‌شود. هنگام تکرار در یک map، به جای آن از k (برای key) استفاده می‌شود.

متغیر دوم اغلب v برای value نامیده می‌شود، اما گاهی اوقات نامی بر اساس نوع مقادیری که تکرار می‌شوند به آن داده می‌شود. البته، می‌توانید هر نامی که دوست دارید به متغیرها بدهید. اگر بدنه حلقه فقط شامل چند دستور باشد، نام‌های متغیر تک حرفی به خوبی کار می‌کنند. برای حلقه‌های طولانی‌تر (یا تو در تو)، بهتر است از نام‌های توصیفی‌تر استفاده کنید.

اگر نیازی به استفاده از متغیر اول در حلقه for-range خود ندارید چه؟ به یاد داشته باشید، Go شما را ملزم می‌کند که به تمام متغیرهای اعلام‌شده دسترسی داشته باشید، و این قانون برای آنهایی که به عنوان بخشی از حلقه for اعلام می‌شوند نیز اعمال می‌شود. اگر نیازی به دسترسی به key ندارید، از خط زیر (_) به عنوان نام متغیر استفاده کنید. این به Go می‌گوید که مقدار را نادیده بگیرد.

بیایید کد تکرار slice را بازنویسی کنیم تا موقعیت را چاپ نکند. می‌توانید کد موجود در مثال ۴-۱۴ را در Go Playground یا در تابع forRangeIgnoreKey در main.go در دایرکتوری sample_code/for_range در مخزن فصل ۴ اجرا کنید.

مثال ۴-۱۴. نادیده گرفتن شاخص slice در حلقه for-range

evenVals := []int{2, 4, 6, 8, 10, 12}
for _, v := range evenVals {
    fmt.Println(v)
}

اجرای این کد خروجی زیر را تولید می‌کند:

2
4
6
8
10
12

هر زمان که در موقعیتی هستید که مقداری بازگردانده می‌شود، اما می‌خواهید آن را نادیده بگیرید، از خط زیر برای مخفی کردن مقدار استفاده کنید. الگوی خط زیر را دوباره زمانی که در فصل ۵ در مورد توابع و در فصل ۱۰ در مورد بسته‌ها صحبت می‌کنم، خواهید دید.

اگر key را می‌خواهید اما value را نمی‌خواهید چه؟ در این موقعیت، Go به شما اجازه می‌دهد که فقط متغیر دوم را حذف کنید. این کد Go معتبری است:

uniqueNames := map[string]bool{"Fred": true, "Raul": true, "Wilma": true}
for k := range uniqueNames {
    fmt.Println(k)
}

رایج‌ترین دلیل برای تکرار روی key زمانی است که map به عنوان یک مجموعه استفاده می‌شود. در آن موقعیت‌ها، value مهم نیست. با این حال، می‌توانید value را هنگام تکرار روی آرایه‌ها یا slice ها نیز حذف کنید. این نادر است، زیرا دلیل معمول برای تکرار روی ساختار داده خطی دسترسی به داده‌ها است. اگر خود را در حال استفاده از این فرمت برای آرایه یا slice می‌بینید، احتمال زیادی وجود دارد که ساختار داده اشتباهی را انتخاب کرده‌اید و باید بازسازی را در نظر بگیرید.

زمانی که در فصل ۱۲ به channel ها نگاه می‌کنید، موقعیتی را خواهید دید که حلقه for-range هر بار که حلقه تکرار می‌شود فقط یک مقدار بازمی‌گرداند.

تکرار روی map ها

چیز جالبی در مورد نحوه تکرار حلقه for-range روی map وجود دارد. می‌توانید کد موجود در مثال ۴-۱۵ را در Go Playground یا در دایرکتوری sample_code/iterate_map در مخزن فصل ۴ اجرا کنید.

مثال ۴-۱۵. ترتیب تکرار Map متغیر است

m := map[string]int{
    "a": 1,
    "c": 3,
    "b": 2,
}

for i := 0; i < 3; i++ {
    fmt.Println("Loop", i)
    for k, v := range m {
        fmt.Println(k, v)
    }
}

وقتی این برنامه را build و اجرا می‌کنید، خروجی متغیر است. در اینجا یک احتمال:

Loop 0
c 3
b 2
a 1
Loop 1
a 1
c 3
b 2
Loop 2
b 2
a 1
c 3

ترتیب key ها و value ها متغیر است؛ برخی اجراها ممکن است یکسان باشند. این در واقع یک ویژگی امنیتی است. در نسخه‌های قبلی Go، ترتیب تکرار برای key ها در map معمولاً (اما نه همیشه) یکسان بود اگر همان آیتم‌ها را در map قرار می‌دادید. این دو مشکل ایجاد می‌کرد:

• مردم کدی می‌نوشتند که فرض می‌کرد ترتیب ثابت است، و این کد در زمان‌های عجیبی خراب می‌شد.

• اگر map ها همیشه آیتم‌ها را به دقیقاً همان مقادیر hash کنند، و شما بدانید که سرور داده‌های کاربر را در map ذخیره می‌کند، می‌توانید سرور را با حمله‌ای به نام Hash DoS کند کند، با ارسال داده‌های خاص طراحی‌شده با key هایی که همگی به همان bucket hash می‌شوند.

برای جلوگیری از هر دوی این مشکلات، تیم Go دو تغییر در پیاده‌سازی map ایجاد کرد. ابتدا، آنها الگوریتم hash برای map ها را تغییر دادند تا شامل عددی تصادفی باشد که هر بار که متغیر map ایجاد می‌شود تولید می‌شود. سپس، آنها ترتیب تکرار for-range روی map را کمی هر بار که map حلقه زده می‌شود متغیر کردند. این دو تغییر پیاده‌سازی حمله Hash DoS را بسیار سخت‌تر می‌کند.

این قانون یک استثنا دارد. برای آسان‌تر کردن اشکال‌زدایی و ثبت log های map ها، توابع قالب‌بندی (مانند fmt.Println) همیشه map ها را با key هایشان به ترتیب صعودی مرتب‌شده خروجی می‌دهند.

تکرار روی رشته‌ها

همان‌طور که قبلاً اشاره کردم، می‌توانید از یک رشته نیز با حلقه for-range استفاده کنید. بیایید نگاهی بیندازیم. می‌توانید کد مثال 4-16 را روی کامپیوتر خود یا در Go Playground یا در دایرکتوری sample_code/iterate_string در مخزن فصل 4 اجرا کنید.

مثال 4-16. تکرار روی رشته‌ها

samples := []string{"hello", "apple_π!"}
for _, sample := range samples {
    for i, r := range sample {
        fmt.Println(i, r, string(r))
    }
    fmt.Println()
}

خروجی هنگامی که کد روی کلمه "hello" تکرار می‌شود هیچ تعجبی ندارد:

0 104 h
1 101 e
2 108 l
3 108 l
4 111 o

در ستون اول اندیس قرار دارد؛ در دوم، مقدار عددی حرف؛ و در سوم مقدار عددی حرف که به رشته تبدیل شده است.

نگاه کردن به نتیجه برای "apple_π!" جالب‌تر است:

0 97 a
1 112 p
2 112 p
3 108 l
4 101 e
5 95 _
6 960 π
8 33 !

دو چیز در مورد این خروجی توجه کنید. اول، توجه کنید که ستون اول عدد 7 را رد می‌کند. دوم، مقدار در موقعیت 6 برابر 960 است. این خیلی بزرگ‌تر از چیزی است که می‌تواند در یک بایت جا بگیرد. اما در فصل 3، دیدید که رشته‌ها از بایت‌ها ساخته شده‌اند. چه اتفاقی می‌افتد؟

آنچه که می‌بینید رفتار خاصی از تکرار روی یک رشته با حلقه for-range است. این روی rune ها تکرار می‌کند، نه بایت‌ها. هر وقت که یک حلقه for-range با یک rune چندبایتی در رشته برخورد می‌کند، نمایش UTF-8 را به یک عدد 32-بیتی واحد تبدیل می‌کند و آن را به مقدار اختصاص می‌دهد. offset به اندازه تعداد بایت‌های موجود در rune افزایش می‌یابد. اگر حلقه for-range با بایتی برخورد کند که نمایانگر یک مقدار UTF-8 معتبر نباشد، به جای آن کاراکتر جایگزین یونیکد (مقدار هگزادسیمال 0xfffd) برگردانده می‌شود.

از حلقه for-range برای دسترسی به rune های یک رشته به ترتیب استفاده کنید. متغیر اول تعداد بایت‌ها از ابتدای رشته را نگه می‌دارد، اما نوع متغیر دوم rune است.

مقدار for-range یک کپی است

باید آگاه باشید که هر بار که حلقه for-range روی نوع مرکب شما تکرار می‌کند، مقدار را از نوع مرکب به متغیر مقدار کپی می‌کند. تغییر دادن متغیر مقدار، مقدار موجود در نوع مرکب را تغییر نمی‌دهد. مثال 4-17 برنامه سریعی را برای نشان دادن این موضوع ارائه می‌دهد. می‌توانید آن را در Go Playground یا در تابع forRangeIsACopy در main.go در دایرکتوری sample_code/for_range در مخزن فصل 4 امتحان کنید.

مثال 4-17. تغییر دادن مقدار، منبع را تغییر نمی‌دهد

evenVals := []int{2, 4, 6, 8, 10, 12}
for _, v := range evenVals {
    v *= 2
}
fmt.Println(evenVals)

اجرای این کد خروجی زیر را می‌دهد:

[2 4 6 8 10 12]

در نسخه‌های Go قبل از 1.22، متغیر مقدار یک بار ایجاد می‌شد و در هر تکرار از طریق حلقه for دوباره استفاده می‌شد. از Go 1.22، رفتار پیش‌فرض این است که یک متغیر اندیس و مقدار جدید در هر تکرار از طریق حلقه for ایجاد شود. این تغییر ممکن است مهم به نظر نرسد، اما از یک باگ رایج جلوگیری می‌کند. وقتی در "Goroutines, for Loops, and Varying Variables" در صفحه 298 در مورد goroutine ها و حلقه‌های for-range صحبت می‌کنم، خواهید دید که قبل از Go 1.22، اگر goroutine هایی را در یک حلقه for-range راه‌اندازی می‌کردید، باید در نحوه انتقال اندیس و مقدار به goroutine ها دقت می‌کردید، وگرنه نتایج به طرز تعجب‌آوری اشتباه می‌گرفتید.

چون این یک تغییر backward-breaking است (حتی اگر تغییری باشد که یک باگ رایج را حذف می‌کند)، می‌توانید با مشخص کردن نسخه Go در دستورالعمل go در فایل go.mod برای ماژول خود، کنترل کنید که آیا این رفتار فعال شود یا نه. در "Using go.mod" در صفحه 224 با جزئیات بیشتر در مورد این موضوع صحبت می‌کنم.

درست مثل سه فرم دیگر دستور for، می‌توانید از break و continue با حلقه for-range استفاده کنید.

برچسب‌گذاری دستورات for شما

به طور پیش‌فرض، کلیدواژه‌های break و continue به حلقه for ای اعمال می‌شوند که مستقیماً شامل آن‌ها است. اگر حلقه‌های for تو در تو داشته باشید و بخواهید از یک iterator حلقه بیرونی خارج شوید یا آن را رد کنید چه؟ بیایید نگاهی به مثالی بیندازیم. قرار است برنامه تکرار رشته قبلی را طوری تغییر دهید که به محض برخورد با حرف "l" تکرار از طریق رشته را متوقف کند. می‌توانید کد مثال 4-18 را در Go Playground یا در دایرکتوری sample_code/for_label در مخزن فصل 4 اجرا کنید.

مثال 4-18. برچسب‌ها

func main() {
    samples := []string{"hello", "apple_π!"}
outer:
    for _, sample := range samples {
        for i, r := range sample {
            fmt.Println(i, r, string(r))
            if r == 'l' {
                continue outer
            }
        }
        fmt.Println()
    }
}

توجه کنید که برچسب outer توسط go fmt به همان سطح تابع احاطه‌کننده تورفتگی داده شده است. برچسب‌ها همیشه به همان سطح براکت‌های بلوک تورفتگی داده می‌شوند. این کار آن‌ها را آسان‌تر قابل توجه می‌کند. اجرای برنامه خروجی زیر را می‌دهد:

0 104 h
1 101 e
2 108 l
0 97 a
1 112 p
2 112 p
3 108 l

حلقه‌های for تو در تو با برچسب نادر هستند. آن‌ها بیشتر برای پیاده‌سازی الگوریتم‌هایی شبیه به pseudocode زیر استفاده می‌شوند:

outer:
    for _, outerVal := range outerValues {
        for _, innerVal := range outerVal {
            // process innerVal
            if invalidSituation(innerVal) {
                continue outer
            }
        }
        // اینجا کدی داریم که فقط وقتی همه مقادیر
        // innerVal با موفقیت پردازش شدند اجرا می‌شود
    }

انتخاب دستور for مناسب

حالا که همه اشکال دستور for را پوشش دادم، ممکن است تعجب کنید که چه موقع از کدام فرمت استفاده کنید. بیشتر اوقات، قرار است از فرمت for-range استفاده کنید. حلقه for-range بهترین روش برای طی کردن یک رشته است، چرا که به درستی rune ها را به جای بایت‌ها به شما برمی‌گرداند. همچنین دیده‌اید که حلقه for-range برای تکرار از طریق slice ها و map ها خوب کار می‌کند، و در فصل 12 خواهید دید که channel ها نیز به طور طبیعی با for-range کار می‌کنند.

هنگام تکرار روی تمام محتویات یک نمونه از یکی از انواع مرکب built-in، حلقه for-range را ترجیح دهید. این کار مقدار زیادی از کد boilerplate که هنگام استفاده از آرایه، slice، یا map با یکی از سبک‌های حلقه for دیگر نیاز است را اجتناب می‌کند.

چه موقع باید از حلقه for کامل استفاده کنید؟ بهترین جای آن زمانی است که از اولین عنصر به آخرین عنصر در یک نوع مرکب تکرار نمی‌کنید. در حالی که می‌توانید از ترکیبی از if، continue، و break درون یک حلقه for-range استفاده کنید، یک حلقه for استандارد روش واضح‌تری برای نشان دادن شروع و پایان تکرار شما است. این دو قطعه کد را مقایسه کنید، هر دو روی عناصر دوم تا دوم از آخر در یک آرایه تکرار می‌کنند. ابتدا حلقه for-range:

evenVals := []int{2, 4, 6, 8, 10}
for i, v := range evenVals {
    if i == 0 {
        continue
    }
    if i == len(evenVals)-1 {
        break
    }
    fmt.Println(i, v)
}

و اینجا همان کد، با یک حلقه for استندارد:

evenVals := []int{2, 4, 6, 8, 10}
for i := 1; i < len(evenVals)-1; i++ {
    fmt.Println(i, evenVals[i])
}

کد حلقه for استاندارد هم کوتاه‌تر و هم آسان‌تر برای فهمیدن است.

این الگو برای رد کردن ابتدای یک رشته کار نمی‌کند. به یاد داشته باشید، یک حلقه for استاندارد کاراکترهای چندبایتی را به درستی اداره نمی‌کند. اگر می‌خواهید برخی از rune های یک رشته را رد کنید، باید از یک حلقه for-range استفاده کنید تا rune ها را برای شما به درستی پردازش کند.

دو فرمت باقی‌مانده دستور for کمتر استفاده می‌شوند. حلقه for فقط با شرط، مثل حلقه while که جایگزین آن می‌شود، زمانی مفید است که بر اساس یک مقدار محاسبه‌شده حلقه می‌زنید.

حلقه for بی‌نهایت در برخی موقعیت‌ها مفید است. بدنه حلقه for همیشه باید شامل یک break یا return باشد چرا که به ندرت می‌خواهید تا ابد حلقه بزنید. برنامه‌های دنیای واقعی باید تکرار را محدود کنند و وقتی عملیات نمی‌توانند تکمیل شوند با احترام شکست بخورند. همان‌طور که قبلاً نشان داده شد، یک حلقه for بی‌نهایت می‌تواند با یک دستور if ترکیب شود تا دستور do که در زبان‌های دیگр موجود است را شبیه‌سازی کند. حلقه for بی‌نهایت همچنین برای پیاده‌سازی برخی نسخه‌های الگوی iterator استفاده می‌شود، که وقتی کتابخانه استاندارد را در "io and Friends" در صفحه 319 بررسی می‌کنم به آن نگاه خواهید کرد.

switch

همانند بسیاری از زبان‌های مشتق شده از C، Go نیز دستور switch دارد. اکثر توسعه‌دهندگان در آن زبان‌ها از دستورات switch اجتناب می‌کنند به دلیل محدودیت‌هایشان روی مقادیری که می‌توان بر اساس آن‌ها تغییر مسیر داد و رفتار پیش‌فرض fall-through. اما Go متفاوت است. این زبان دستورات switch را مفید می‌سازد.

برای آن دسته از خوانندگانی که با Go آشنایی بیشتری دارند، من در این فصل دستورات expression switch را پوشش خواهم داد. در مورد دستورات type switch وقتی در فصل ۷ در مورد interface ها صحبت می‌کنم، بحث خواهم کرد.

در نگاه اول، دستورات switch در Go چندان متفاوت از نحوه ظاهر شدنشان در C/C++، Java، یا JavaScript به نظر نمی‌رسند، اما چند تا تعجب وجود دارد. بیایید نگاهی به یک نمونه دستور switch بیندازیم. می‌توانید کد موجود در مثال ۴-۱۹ را در The Go Playground یا در تابع basicSwitch در main.go در پوشه sample_code/switch در مخزن فصل ۴ اجرا کنید.

مثال ۴-۱۹. دستور switch

words := []string{"a", "cow", "smile", "gopher",
    "octopus", "anthropologist"}

for _, word := range words {
    switch size := len(word); size {
    case 1, 2, 3, 4:
        fmt.Println(word, "is a short word!")
    case 5:
        wordLen := len(word)
        fmt.Println(word, "is exactly the right length:", wordLen)
    case 6, 7, 8, 9:
    default:
        fmt.Println(word, "is a long word!")
    }
}

وقتی این کد را اجرا می‌کنید، خروجی زیر را دریافت می‌کنید:

a is a short word!
cow is a short word!
smile is exactly the right length: 5
anthropologist is a long word!

من ویژگی‌های دستور switch را بررسی خواهم کرد تا خروجی را توضیح دهم. همانطور که در مورد دستورات if صادق است، شما پرانتز دور مقداری که در switch مقایسه می‌شود، نمی‌گذارید. همچنین همانند دستور if، می‌توانید متغیری تعریف کنید که محدوده آن به تمام شاخه‌های دستور switch اختصاص یابد. در این مورد، شما متغیر size را به تمام case های دستور switch محدود می‌کنید.

تمام بندهای case (و بند اختیاری default) درون مجموعه‌ای از آکولاد قرار گرفته‌اند. اما باید توجه کنید که آکولاد دور محتویات بندهای case نمی‌گذارید. می‌توانید چندین خط درون یک بند case (یا default) داشته باشید، و همه آن‌ها بخشی از همان بلوک در نظر گرفته می‌شوند.

درون case 5:، شما wordLen، یک متغیر جدید تعریف می‌کنید. از آنجایی که این یک بلوک جدید است، می‌توانید متغیرهای جدیدی درون آن تعریف کنید. درست مانند هر بلوک دیگری، هر متغیری که درون بلوک یک بند case تعریف شود تنها درون همان بلوک قابل مشاهده است.

اگر عادت دارید در انتهای هر case در دستورات switch خود دستور break بگذارید، خوشحال خواهید شد که متوجه شوید آن‌ها حذف شده‌اند. به طور پیش‌فرض، case ها در دستورات switch در Go fall through نمی‌کنند. این بیشتر با رفتار در Ruby یا (اگر برنامه‌نویس قدیمی هستید) Pascal هماهنگ است.

این سوال را مطرح می‌کند: اگر case ها fall through نمی‌کنند، چه کار می‌کنید اگر مقادیر متعددی وجود داشته باشند که باید دقیقاً همان منطق را فعال کنند؟ در Go، شما تطبیق‌های متعدد را با کاما از هم جدا می‌کنید، همانطور که هنگام تطبیق ۱، ۲، ۳، و ۴ یا ۶، ۷، ۸، و ۹ انجام می‌دهید. به همین دلیل است که برای هر دوی a و cow خروجی یکسانی دریافت می‌کنید.

که به سوال بعدی منجر می‌شود: اگر fall-through ندارید، و case خالی دارید (همانطور که در برنامه نمونه وقتی طول آرگومان شما ۶، ۷، ۸، یا ۹ کاراکتر است داریم)، چه اتفاقی می‌افتد؟ در Go، case خالی به معنای این است که هیچ اتفاقی نمی‌افتد.

به همین دلیل است که وقتی از octopus یا gopher به عنوان پارامتر استفاده می‌کنید، هیچ خروجی از برنامه‌تان نمی‌بینید.

برای کامل بودن، Go کلیدواژه fallthrough را شامل می‌شود، که به یک case اجازه می‌دهد به case بعدی ادامه دهد. لطفاً قبل از پیاده‌سازی الگوریتمی که از آن استفاده می‌کند، دوبار فکر کنید. اگر خودتان را نیازمند استفاده از fallthrough می‌بینید، سعی کنید منطق خود را بازسازی کنید تا وابستگی‌های بین case ها را حذف کنید.

در برنامه نمونه، شما بر اساس مقدار یک عدد صحیح switch می‌کنید، اما تنها این کار نیست که می‌توانید انجام دهید. می‌توانید بر اساس هر نوعی که با == قابل مقایسه است switch کنید، که شامل تمام انواع داخلی به جز slice ها، map ها، channel ها، توابع، و struct هایی که حاوی فیلدهایی از این انواع هستند، می‌شود.

اگرچه نیازی نیست در انتهای هر بند case دستور break بگذارید، می‌توانید از آن‌ها استفاده کنید وقتی می‌خواهید زودتر از یک case خارج شوید. با این حال، نیاز به دستور break ممکن است نشان دهد که شما کار پیچیده‌ای انجام می‌دهید. بازسازی کد خود را برای حذف آن در نظر بگیرید.

یک مکان دیگر وجود دارد که ممکن است خودتان را در حال استفاده از دستور break در یک case در دستور switch بیابید. اگر دستور switch درون حلقه for دارید، و می‌خواهید از حلقه for خارج شوید، برچسبی روی دستور for بگذارید و نام برچسب را روی break قرار دهید. اگر از برچسب استفاده نکنید، Go فرض می‌کند که می‌خواهید از case خارج شوید. بیایید نگاهی به مثال سریعی بیندازیم که در آن می‌خواهیم از حلقه for خارج شویم به محض رسیدن به ۷. می‌توانید کد موجود در مثال ۴-۲۰ را در The Go Playground یا در تابع missingLabel در main.go در پوشه sample_code/switch در مخزن فصل ۴ اجرا کنید.

مثال ۴-۲۰. مورد برچسب گمشده

func main() {
    for i := 0; i < 10; i++ {
        switch i {
        case 0, 2, 4, 6:
            fmt.Println(i, "is even")
        case 3:
            fmt.Println(i, "is divisible by 3 but not 2")
        case 7:
            fmt.Println("exit the loop!")
            break
        default:
            fmt.Println(i, "is boring")
        }
    }
}

اجرای این کد خروجی زیر را تولید می‌کند:

0 is even
1 is boring
2 is even
3 is divisible by 3 but not 2
4 is even
5 is boring
6 is even
exit the loop!
8 is boring
9 is boring

این چیزی نیست که در نظر گرفته شده است. هدف این بود که از حلقه for خارج شویم وقتی به ۷ می‌رسد، اما break به عنوان خروج از case تفسیر شد. برای حل این مسئله، نیاز دارید برچسبی معرفی کنید، درست مانند زمانی که از حلقه for تودرتو خارج می‌شدید. ابتدا، دستور for را برچسب‌گذاری می‌کنید:

loop:
    for i := 0; i < 10; i++ {

سپس از برچسب روی break خود استفاده می‌کنید:

break loop

می‌توانید این تغییرات را در The Go Playground یا در تابع labeledBreak در main.go در پوشه sample_code/switch در مخزن فصل ۴ مشاهده کنید. وقتی دوباره اجرا می‌کنید، خروجی مورد انتظار را دریافت می‌کنید:

0 is even
1 is boring
2 is even
3 is divisible by 3 but not 2
4 is even
5 is boring
6 is even
exit the loop!

سوئیچ‌های خالی

می‌توانید از دستورات switch به روشی دیگر و قدرتمندتر استفاده کنید. درست همانطور که Go به شما اجازه می‌دهد بخش‌هایی از اعلان دستور for را حذف کنید، می‌توانید دستور switch بنویسید که مقداری را که با آن مقایسه می‌کنید مشخص نکند. این را سوئیچ خالی (blank switch) می‌نامند. یک switch معمولی فقط به شما اجازه می‌دهد یک مقدار را برای برابری بررسی کنید. یک سوئیچ خالی به شما اجازه می‌دهد از هر مقایسه بولی برای هر case استفاده کنید. می‌توانید کد موجود در مثال ۴-۲۱ را در The Go Playground یا در تابع basicBlankSwitch در main.go در دایرکتوری sample_code/blank_switch در مخزن فصل ۴ امتحان کنید.

مثال ۴-۲۱. سوئیچ خالی

words := []string{"hi", "salutations", "hello"}
for _, word := range words {
    switch wordLen := len(word); {
    case wordLen < 5:
        fmt.Println(word, "is a short word!")
    case wordLen > 10:
        fmt.Println(word, "is a long word!")
    default:
        fmt.Println(word, "is exactly the right length.")
    }
}

وقتی این برنامه را اجرا می‌کنید، خروجی زیر را دریافت می‌کنید:

hi is a short word!
salutations is a long word!
hello is exactly the right length.

درست مانند یک دستور switch معمولی، می‌توانید به صورت اختیاری یک اعلان متغیر کوتاه را به عنوان بخشی از سوئیچ خالی خود شامل کنید. اما برخلاف یک switch معمولی، می‌توانید تست‌های منطقی برای case های خود بنویسید. سوئیچ‌های خالی بسیار جالب هستند، اما آن‌ها را زیاده‌روی نکنید. اگر متوجه شدید که سوئیچ خالی نوشته‌اید که تمام case های آن مقایسه‌های برابری با همان متغیر هستند:

switch {
case a == 2:
    fmt.Println("a is 2")
case a == 3:
    fmt.Println("a is 3")
case a == 4:
    fmt.Println("a is 4")
default:
    fmt.Println("a is ", a)
}

باید آن را با یک دستور switch عبارتی جایگزین کنید:

switch a {
case 2:
    fmt.Println("a is 2")
case 3:
    fmt.Println("a is 3")
case 4:
    fmt.Println("a is 4")
default:
    fmt.Println("a is ", a)
}

انتخاب بین if و switch

از نظر عملکرد، تفاوت زیادی بین یک سری دستورات if/else و یک دستور سوئیچ خالی وجود ندارد. هر دو اجازه یک سری مقایسه را می‌دهند. پس، چه زمانی باید از switch استفاده کنید و چه زمانی باید از مجموعه‌ای از دستورات if یا if/else استفاده کنید؟ یک دستور switch، حتی یک سوئیچ خالی، نشان می‌دهد که رابطه‌ای بین مقادیر یا مقایسه‌ها در هر case وجود دارد. برای نشان دادن تفاوت در وضوح، برنامه FizzBuzz از مثال ۴-۱۱ را با استفاده از یک سوئیچ خالی بازنویسی کنید، همانطور که در مثال ۴-۲۲ نشان داده شده است. همچنین می‌توانید این کد را در دایرکتوری sample_code/simplest_fizzbuzz در مخزن فصل ۴ پیدا کنید.

مثال ۴-۲۲. بازنویسی یک سری دستورات if با یک سوئیچ خالی

for i := 1; i <= 100; i++ {
    switch {
    case i%3 == 0 && i%5 == 0:
        fmt.Println("FizzBuzz")
    case i%3 == 0:
        fmt.Println("Fizz")
    case i%5 == 0:
        fmt.Println("Buzz")
    default:
        fmt.Println(i)
    }
}

اکثر افراد موافق خواهند بود که این خوانا‌ترین نسخه است. دیگر نیازی به دستورات continue نیست و رفتار پیش‌فرض با case پیش‌فرض صریح شده است.

البته، هیچ چیز در Go شما را از انجام همه نوع مقایسه‌های غیرمرتبط در هر case در یک سوئیچ خالی منع نمی‌کند. با این حال، این روش idiomatic نیست. اگر خود را در موقعیتی یافتید که می‌خواهید این کار را انجام دهید، از یک سری دستورات if/else استفاده کنید (یا شاید بازسازی کد خود را در نظر بگیرید).

دستورات سوئیچ خالی را بر زنجیره‌های if/else ترجیح دهید وقتی که چندین case مرتبط دارید. استفاده از switch مقایسه‌ها را قابل مشاهده‌تر می‌کند و تأکید می‌کند که آن‌ها مجموعه‌ای از نگرانی‌های مرتبط هستند.

goto—بله، goto

Go یک دستور کنترل چهارم دارد، اما احتمالاً هرگز از آن استفاده نخواهید کرد. از زمانی که Edsger Dijkstra در سال ۱۹۶۸ "Go To Statement Considered Harmful" نوشت، دستور goto گوسفند سیاه خانواده برنامه‌نویسی بوده است. دلایل خوبی برای این موضوع وجود دارد.

به طور سنتی، goto خطرناک بود زیرا می‌توانست تقریباً به هر جایی در یک برنامه بپرد؛ می‌توانستید به داخل یا خارج از یک حلقه بپرید، تعاریف متغیر را رد کنید، یا به وسط مجموعه‌ای از دستورات در یک دستور if بپرید. این باعث می‌شد درک اینکه یک برنامه استفاده‌کننده از goto چه کاری انجام می‌دهد دشوار باشد.

اکثر زبان‌های مدرن goto را شامل نمی‌شوند. با این حال Go یک دستور goto دارد. شما همچنان باید تا آنجا که می‌توانید از استفاده از آن خودداری کنید، اما کاربردهایی دارد، و محدودیت‌هایی که Go بر آن اعمال می‌کند آن را با برنامه‌نویسی ساختاریافته سازگارتر می‌کند.

در Go، یک دستور goto خط برچسب‌گذاری شده‌ای از کد را مشخص می‌کند و اجرا به آن می‌پرد. با این حال، نمی‌توانید به هر جایی بپرید. Go پرش‌هایی را که تعاریف متغیرها را رد می‌کنند و پرش‌هایی را که به یک بلوک داخلی یا موازی می‌روند منع می‌کند.

برنامه موجود در مثال ۴-۲۳ دو دستور goto غیرقانونی را نشان می‌دهد. می‌توانید سعی کنید آن را در The Go Playground یا در دایرکتوری sample_code/broken_goto در مخزن فصل ۴ اجرا کنید.

مثال ۴-۲۳. goto در Go قوانین دارد

func main() {
    a := 10
    goto skip
    b := 20
skip:
    c := 30
    fmt.Println(a, b, c)
    if c > a {
        goto inner
    }
    if a < b {
    inner:
        fmt.Println("a is less than b")
    }
}

تلاش برای اجرای این برنامه خطاهای زیر را تولید می‌کند:

goto skip jumps over declaration of b at ./main.go:8:4
goto inner jumps into block starting at ./main.go:15:11

پس برای چه باید از goto استفاده کنید؟ عمدتاً نباید. دستورات break و continue برچسب‌گذاری شده به شما اجازه می‌دهند از حلقه‌های عمیقاً تودرتو خارج شوید یا تکرار را رد کنید. برنامه موجود در مثال ۴-۲۴ یک goto قانونی دارد و یکی از معدود موارد استفاده معتبر را نشان می‌دهد. همچنین می‌توانید این کد را در دایرکتوری sample_code/good_goto در مخزن فصل ۴ پیدا کنید.

مثال ۴-۲۴. دلیلی برای استفاده از goto

func main() {
    a := rand.Intn(10)
    for a < 100 {
        if a%5 == 0 {
            goto done
        }
        a = a*2 + 1
    }
    fmt.Println("do something when the loop completes normally")
done:
    fmt.Println("do complicated stuff no matter why we left the loop")
    fmt.Println(a)
}

این مثال ساختگی است، اما نشان می‌دهد که چگونه goto می‌تواند برنامه را واضح‌تر کند. در این مورد ساده، منطقی وجود دارد که نمی‌خواهید در وسط تابع اجرا شود، اما می‌خواهید در انتهای تابع اجرا شود. راه‌هایی برای انجام این کار بدون goto وجود دارد. می‌توانید یک پرچم بولی تنظیم کنید یا کد پیچیده را بعد از حلقه for تکرار کنید به جای داشتن goto، اما هر دو رویکرد اشکال دارند. پر کردن کد خود با پرچم‌های بولی برای کنترل جریان منطق مسلماً همان عملکرد دستور goto است، فقط پرمخاطب‌تر. تکرار کد پیچیده مشکل‌آفرین است زیرا نگهداری کد شما را دشوارتر می‌کند. این موقعیت‌ها نادر هستند، اما اگر نمی‌توانید راهی برای بازسازی منطق خود پیدا کنید، استفاده از goto مانند این در واقع کد شما را بهبود می‌بخشد.

اگر می‌خواهید مثال دنیای واقعی ببینید، می‌توانید نگاهی به متد floatBits در فایل atof.go در پکیج strconv در کتابخانه استاندارد بیندازید. خیلی طولانی است که به طور کامل شامل شود، اما متد با این کد پایان می‌یابد:

overflow:
    // ±Inf
    mant = 0
    exp = 1<<flt.expbits - 1 + flt.bias
    overflow = true

out:
    // Assemble bits.
    bits := mant & (uint64(1)<<flt.mantbits - 1)
    bits |= uint64((exp-flt.bias)&(1<<flt.expbits-1)) << flt.mantbits
    if d.neg {
        bits |= 1 << flt.mantbits << flt.expbits
    }
    return bits, overflow

قبل از این خطوط، چندین بررسی شرط وجود دارد. برخی نیاز دارند کد بعد از برچسب overflow اجرا شود، در حالی که شرایط دیگر نیاز دارند آن کد را رد کنند و مستقیماً به out بروند. بسته به شرط، دستورات goto وجود دارند که به overflow یا out می‌پرند. احتمالاً می‌توانید راهی برای اجتناب از دستورات goto پیدا کنید، اما همه آن‌ها کد را دشوارتر برای درک می‌کنند.

باید خیلی سخت تلاش کنید تا از استفاده از goto خودداری کنید. اما در موقعیت‌های نادری که کد شما را خواناتر می‌کند، گزینه‌ای است.

تمرین‌ها

حالا وقت آن رسیده که همه چیزهایی که در مورد ساختارهای کنترل و بلوک‌ها در Go آموخته‌اید را به کار ببرید. می‌توانید پاسخ این تمرین‌ها را در مخزن فصل ۴ پیدا کنید.

  1. یک حلقه for بنویسید که ۱۰۰ عدد تصادفی بین ۰ و ۱۰۰ را در یک slice از نوع int قرار دهد.
  2. روی slice‌ای که در تمرین ۱ ایجاد کردید حلقه بزنید. برای هر مقدار در slice، قوانین زیر را اعمال کنید:
    a. اگر مقدار بر ۲ بخش‌پذیر باشد، "Two!" را چاپ کنید
    b. اگر مقدار بر ۳ بخش‌پذیر باشد، "Three!" را چاپ کنید
    c. اگر مقدار بر ۲ و ۳ بخش‌پذیر باشد، "Six!" را چاپ کنید. چیز دیگری چاپ نکنید.
    d. در غیر این صورت، "Never mind" را چاپ کنید.
  3. برنامه جدیدی شروع کنید. در main، یک متغیر int به نام total اعلان کنید. یک حلقه for بنویسید که از متغیری به نام i برای تکرار از ۰ (شامل) تا ۱۰ (غیرشامل) استفاده کند. بدنه حلقه for باید به شکل زیر باشد:
total := total + i
fmt.Println(total)

بعد از حلقه for، مقدار total را چاپ کنید. چه چیزی چاپ می‌شود؟ باگ احتمالی در این کد چیست؟

جمع‌بندی

این فصل موضوعات مهم زیادی را برای نوشتن Go معنادار پوشش داد. شما در مورد بلوک‌ها، سایه‌گذاری، و ساختارهای کنترل، و نحوه استفاده صحیح از آنها آموختید. در این نقطه، شما قادر به نوشتن برنامه‌های ساده Go هستید که در تابع main جای می‌گیرند. وقت آن رسیده که به برنامه‌های بزرگ‌تر بروید و از توابع برای سازماندهی کد خود استفاده کنید.