فصل پنجم - توابع

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

blogs tailwind section

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

اعلان و فراخوانی توابع

مبانی توابع Go برای هر کسی که در زبان‌های دیگر با توابع درجه یک، مثل C، Python، Ruby، یا JavaScript برنامه‌نویسی کرده باشد، آشنا است. (Go همچنین متدها دارد که در فصل ۷ پوشش خواهم داد.) درست مثل ساختارهای کنترل، Go تفسیر خودش را به ویژگی‌های تابع اضافه می‌کند. برخی بهبود هستند، و برخی دیگر آزمایش‌هایی هستند که باید از آنها اجتناب کرد. هر دو را در این فصل پوشش خواهم داد.

شما قبلاً توابع در حال اعلان و استفاده شدن را دیده‌اید. هر برنامه Go از یک تابع main شروع می‌شود، و شما تابع fmt.Println را برای چاپ کردن روی صفحه فراخوانی کرده‌اید. از آنجایی که تابع main پارامتر دریافت نمی‌کند یا مقدار برنمی‌گرداند، بیایید ببینیم وقتی یک تابع این کار را انجام می‌دهد چگونه به نظر می‌رسد:

func div(num int, denom int) int {
    if denom == 0 {
        return 0
    }
    return num / denom
}

بیایید به همه چیزهای جدید در این نمونه کد نگاه کنیم. یک اعلان تابع چهار بخش دارد: کلیدواژه func، نام تابع، پارامترهای ورودی، و نوع بازگشتی. پارامترهای ورودی در پرانتز فهرست می‌شوند، با کاما از هم جدا می‌شوند، با نام پارامتر ابتدا و نوع دوم. Go یک زبان نوع‌دار است، بنابراین باید نوع پارامترها را مشخص کنید. نوع بازگشتی بین پرانتز بسته پارامتر ورودی و آکولاد باز برای بدنه تابع نوشته می‌شود.

درست مثل زبان‌های دیگر، Go یک کلیدواژه return برای برگرداندن مقادیر از یک تابع دارد. اگر یک تابع مقداری برمی‌گرداند، باید یک return ارائه دهید. اگر یک تابع هیچ چیز برنمی‌گرداند، یک دستور return در انتهای تابع لازم نیست. کلیدواژه return در تابعی که هیچ چیز برنمی‌گرداند فقط در صورتی لازم است که قبل از آخرین خط از تابع خارج شوید.

تابع main هیچ پارامتر ورودی یا مقدار بازگشتی ندارد. وقتی یک تابع هیچ پارامتر ورودی ندارد، از پرانتز خالی (()) استفاده کنید. وقتی یک تابع هیچ چیز برنمی‌گرداند، هیچ چیز بین پرانتز بسته پارامتر ورودی و آکولاد باز برای بدنه تابع ننویسید:

func main() {
    result := div(5, 2)
    fmt.Println(result)
}

کد این برنامه ساده در دایرکتوری sample_code/simple_div در مخزن فصل ۵ است.

فراخوانی یک تابع باید برای توسعه‌دهندگان باتجربه آشنا باشد. در سمت راست :=، شما تابع div را با مقادیر ۵ و ۲ فراخوانی می‌کنید. در سمت چپ، مقدار برگشتی را به متغیر result اختصاص می‌دهید.

نکته: وقتی دو یا چند پارامتر ورودی متوالی از همان نوع دارید، می‌توانید نوع را یک بار برای همه آنها مشخص کنید مثل این:

func div(num, denom int) int {

شبیه‌سازی پارامترهای نامی و اختیاری

قبل از رسیدن به ویژگی‌های منحصر به فرد تابعی که Go دارد، دو ویژگی را که Go ندارد ذکر خواهم کرد: پارامترهای ورودی نامی و اختیاری. به جز یک استثنا که در بخش بعدی پوشش خواهم داد، باید همه پارامترهای یک تابع را ارائه دهید. اگر می‌خواهید پارامترهای نامی و اختیاری را تقلید کنید، یک struct تعریف کنید که فیلدهایی داشته باشد که با پارامترهای مطلوب مطابقت داشته باشد، و struct را به تابع خود ارسال کنید. مثال ۵-۱ قطعه کدی را نشان می‌دهد که این الگو را نمایش می‌دهد.

مثال ۵-۱. استفاده از struct برای شبیه‌سازی پارامترهای نامی

type MyFuncOpts struct {
    FirstName string
    LastName  string
    Age       int
}

func MyFunc(opts MyFuncOpts) error {
    // کاری اینجا انجام دهید
}

func main() {
    MyFunc(MyFuncOpts{
        LastName: "Patel",
        Age:      50,
    })
    MyFunc(MyFuncOpts{
        FirstName: "Joe",
        LastName:  "Smith",
    })
}

کد این برنامه در دایرکتوری sample_code/named_optional_parameters در مخزن فصل ۵ است.

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

پارامترهای ورودی متغیر و برش‌ها

شما از fmt.Println برای چاپ نتایج روی صفحه استفاده کرده‌اید و احتمالاً متوجه شده‌اید که این تابع هر تعداد پارامتر ورودی را می‌پذیرد. چگونه این کار را انجام می‌دهد؟ مانند بسیاری از زبان‌ها، Go از پارامترهای متغیر (variadic parameters) پشتیبانی می‌کند. پارامتر متغیر باید آخرین (یا تنها) پارامتر در لیست پارامترهای ورودی باشد. شما آن را با سه نقطه (...) قبل از نوع مشخص می‌کنید. متغیری که در داخل تابع ایجاد می‌شود، یک برش (slice) از نوع مشخص شده است. شما از آن دقیقاً مانند هر برش دیگری استفاده می‌کنید. بیایید ببینیم چگونه کار می‌کنند با نوشتن برنامه‌ای که یک عدد پایه را به تعداد متغیری از پارامترها اضافه می‌کند و نتیجه را به عنوان یک برش از int برمی‌گرداند. می‌توانید این برنامه را در The Go Playground یا در دایرکتوری sample_code/variadic در مخزن فصل ۵ اجرا کنید. ابتدا، تابع متغیر را بنویسید:

func addTo(base int, vals ...int) []int {
    out := make([]int, 0, len(vals))
    for _, v := range vals {
        out = append(out, base+v)
    }
    return out
}

و اکنون آن را به چند روش فراخوانی کنید:

func main() {
    fmt.Println(addTo(3))
    fmt.Println(addTo(3, 2))
    fmt.Println(addTo(3, 2, 4, 6, 8))
    a := []int{4, 3}

    fmt.Println(addTo(3, a...))
    fmt.Println(addTo(3, []int{1, 2, 3, 4, 5}...))
}

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

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

[]
[5]
[5 7 9 11]
[7 6]
[4 5 6 7 8]

مقادیر بازگشتی چندگانه

اولین تفاوتی که بین Go و سایر زبان‌ها خواهید دید این است که Go امکان مقادیر بازگشتی چندگانه را فراهم می‌کند. بیایید یک ویژگی کوچک به برنامه تقسیم قبلی اضافه کنیم. قرار است هم خارج قسمت و هم باقیمانده را از تابع برگردانید. در اینجا تابع به‌روزرسانی شده است:

func divAndRemainder(num, denom int) (int, int, error) {
    if denom == 0 {
        return 0, 0, errors.New("cannot divide by zero")
    }
    return num / denom, num % denom, nil
}

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

چیز دیگری هست که هنوز ندیده‌اید: ایجاد و برگرداندن یک خطا. اگر می‌خواهید درباره خطاها بیشتر بدانید، به فصل ۹ بروید. در حال حاضر، فقط باید بدانید که از پشتیبانی مقادیر بازگشتی چندگانه Go برای برگرداندن خطا استفاده می‌کنید اگر چیزی در تابع اشتباه پیش برود. اگر تابع با موفقیت تکمیل شود، nil را برای مقدار خطا برمی‌گردانید. بر اساس قرارداد، خطا همیشه آخرین (یا تنها) مقداری است که از تابع برگردانده می‌شود.

فراخوانی تابع به‌روزرسانی شده به این شکل است:

func main() {
    result, remainder, err := divAndRemainder(5, 2)
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    fmt.Println(result, remainder)
}

کد این برنامه در دایرکتوری sample_code/updated_div در مخزن فصل ۵ قرار دارد.

من در بخش "var در مقابل :=" در صفحه ۲۸ درباره اختصاص چندین مقدار به طور همزمان صحبت کردم. در اینجا از آن ویژگی برای اختصاص نتایج فراخوانی تابع به سه متغیر استفاده می‌کنید. در سمت راست :=، تابع divAndRemainder را با مقادیر ۵ و ۲ فراخوانی می‌کنید. در سمت چپ، مقادیر برگشتی را به متغیرهای result، remainder و err اختصاص می‌دهید. با مقایسه err با nil بررسی می‌کنید که آیا خطایی وجود داشته یا نه.

مقادیر بازگشتی چندگانه، مقادیر چندگانه هستند

اگر با Python آشنا هستید، ممکن است فکر کنید که مقادیر بازگشتی چندگانه مانند توابع Python هستند که یک tuple برمی‌گردانند که به صورت اختیاری تجزیه می‌شود اگر مقادیر tuple به چندین متغیر اختصاص داده شوند. مثال ۵-۲ نمونه کدی را که در مفسر Python اجرا شده نشان می‌دهد.

مثال ۵-۲. مقادیر بازگشتی چندگانه در Python، tuple های تجزیه شده هستند

>>> def div_and_remainder(n,d):
...   if d == 0:
...     raise Exception("cannot divide by zero")
...   return n / d, n % d
>>> v = div_and_remainder(5,2)
>>> v
(2.5, 1)
>>> result, remainder = div_and_remainder(5,2)
>>> result
2.5
>>> remainder
1

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

نادیده گرفتن مقادیر برگشتی

اگر تابعی را فراخوانی کنید و نخواهید از همه مقادیر برگشتی استفاده کنید چه؟ همان‌طور که در بخش "متغیرهای استفاده نشده" در صفحه ۳۲ پوشش داده شد، Go متغیرهای استفاده نشده را اجازه نمی‌دهد. اگر تابعی چندین مقدار برمی‌گرداند، اما شما نیازی به خواندن یک یا چند مقدار ندارید، مقادیر استفاده نشده را به نام _ اختصاص دهید. به عنوان مثال، اگر قرار نبود remainder را بخوانید، اختصاص را به این شکل می‌نوشتید: result, _, err := divAndRemainder(5, 2).

جالب اینکه، Go به شما اجازه می‌دهد که به صورت ضمنی همه مقادیر بازگشتی یک تابع را نادیده بگیرید. می‌توانید divAndRemainder(5,2) بنویسید و مقادیر برگشتی حذف می‌شوند. در واقع از اولین مثال‌ها این کار را انجام داده‌اید: fmt.Println دو مقدار برمی‌گرداند، اما معمول است که آن‌ها را نادیده بگیریم. در تقریباً همه موارد دیگر، باید صراحتاً مشخص کنید که مقادیر بازگشتی را نادیده می‌گیرید با استفاده از underscore ها.

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

مقادیر بازگشتی نام‌دار

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

func divAndRemainder(num, denom int) (result int, remainder int, err error) {
    if denom == 0 {
        err = errors.New("cannot divide by zero")
        return result, remainder, err
    }
    result, remainder = num/denom, num%denom
    return result, remainder, err
}

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

اگر می‌خواهید فقط برخی از مقادیر بازگشتی را نام‌گذاری کنید، می‌توانید این کار را با استفاده از _ به عنوان نام برای هر مقدار بازگشتی که می‌خواهید بدون نام باقی بماند انجام دهید.

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

func main() {
    x, y, z := divAndRemainder(5, 2)
    fmt.Println(x, y, z)
}

در حالی که مقادیر بازگشتی نام‌دار گاهی می‌توانند کد شما را واضح‌تر کنند، برخی موارد بالقوه مشکل‌ساز دارند. اول مسئله سایه‌انداختن (shadowing) است. درست مانند هر متغیر دیگر، می‌توانید روی مقدار بازگشتی نام‌دار سایه بیندازید. مطمئن شوید که به مقدار بازگشتی اختصاص می‌دهید و نه به سایه آن.

مشکل دیگر مقادیر بازگشتی نام‌دار این است که مجبور نیستید آن‌ها را برگردانید. بیایید نگاهی به تنوع دیگری از divAndRemainder بیندازیم. می‌توانید آن را در The Go Playground یا در تابع divAndRemainderConfusing در main.go در دایرکتوری sample_code/named_div در مخزن فصل ۵ اجرا کنید:

func divAndRemainder(num, denom int) (result int, remainder int, err error) {
    // assign some values
    result, remainder = 20, 30
    if denom == 0 {
        return 0, 0, errors.New("cannot divide by zero")
    }
    return num / denom, num % denom, nil
}

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

2 1

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

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