6.1 ژنریک مقدماتی

6.1 ژنریک مقدماتی

6.1.1 مقدمه #

این آموزش اصول اولیه generics در Go را معرفی می کند. با generics، می توانید functions یا types را که برای کار با هر یک از set of types ارائه شده با فراخوانی کد نوشته شده اند، اعلام و استفاده کنید. همینطور این قسمت فقط یک چشم انداز در استفاده از generic است و موارد بیشتر مثل Struct Types و Field Access به زودی به این کتاب اضافه خواهد شد. همینطور قسمت بیشتر این سند بر اساس مستندات go در این لینک نوشته شده است.

6.1.2 تاریخچه و کاربرد #

در Go 1.18، این زبان ویژگی جدیدی به نام انواع عمومی (که معمولاً با به صورت اختصار ، generic شناخته می‌شود) معرفی کرد که برای مدتی در لیست انتظارات توسعه‌دهندگان Go قرار داشت. در برنامه نویسی،generic type در واقع type ای است که می تواند همراه با چندین type دیگر استفاده شود. معمولاً در Go، اگر می‌خواهید بتوانید از دو مختلف برای یک متغیر استفاده کنید، باید از یک interface خاص مانند io.Reader یا interface{} استفاده کنید که امکان استفاده از هر value را فراهم می‌کند. استفاده از interface{} می‌تواند کار با آن typeها را جالب‌تر کند، زیرا برای تعامل با آن متغیرها باید بین چندین type دیگر translate کنید. استفاده از generic typeها به شما این امکان را می دهد که مستقیماً با typeها خود تعامل داشته باشید که منجر به کدهای تمیزتر و خواناتر می شود.

در این آموزش، شما دو تابع ساده non-generic را اعلام می‌کنید، سپس همان منطق را در یک تابع single generic ثبت می‌کنید. از طریق بخش های زیر ادامه می دهیم: 1- یک پوشه برای کد خود ایجاد کنید. 2- توابع non-generic را اضافه کنید. 3- یک تابع generic برای مدیریت چندین type اضافه کنید. 4- هنگام فراخوانی تابع generic، آرگومان های type را حذف کنید. 5- یک محدودیت type را اعلام کنید. نکته: برای سایر آموزش ها به این لینک مراجعه کنید. نکته: اگر ترجیح می دهید، می توانید از Go playground in “Go dev branch” mode برای ویرایش و اجرای برنامه خود استفاده کنید.

6.1.3 پیش نیازها #

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

6.1.4 ساخت فولدر پروژه #

برای شروع ، یک پوشه برای کدی که می نویسید ایجاد کنید.

1- یک command prompt را باز کنید و به مسیر دلخواه خود تغییر دهید.

در لینوکس یا مک:On Linux or Mac:

$ cd

در ویندوز:

1C:\> cd %HOMEPATH%

در ادامه آموزش علامت $ را به عنوان درخواست نشان می دهد. دستوراتی که استفاده می کنید در ویندوز نیز کار می کنند.

2- از terminal یک دایرکتوری برای کد خود به نام generics ایجاد کنید.

$ mkdir generics
$ cd generics

3- یک ماژول برای نگهداری کد خود ایجاد کنید. دستور go mod init را اجرا کنید و مسیر ماژول کد جدید خود را به آن بدهید.

1$ go mod init example/generics
2go: creating new go.mod: module example/generics

نکته: برای کد production، یک مسیر ماژول را مشخص می‌کنید که بیشتر به نیازهای شما اختصاص دارد. برای اطلاعات بیشتر، حتما به مدیریت وابستگی ها مراجعه کنید.

در مرحله بعد، چند کد ساده برای کار با maps اضافه می کنید.

6.1.5 اضافه کردن توابع non-generic #

در این مرحله، دو تابع اضافه می‌کنید که هر کدام مقادیر یک map را با هم جمع کرده و مقدار کل را برمی‌گرداند. شما به جای یکی، دو تابع را declaration می کنید زیرا با دو نوع مختلف map کار می کنید: یکی که مقادیر int64 را ذخیره می کند و دیگری که مقادیر float64 را ذخیره می‌کند.

نوشتن کد:

1- با استفاده از ویرایشگر متن خود، فایلی به نام main.go در دایرکتوری generic ایجاد کنید. شما کد Go خود را در این فایل می نویسید.

2- در main.go، در بالای فایل، package declaration زیر را قرار دهید.

package main

یک برنامه مستقل (برخلاف یک library) همیشه در package main است.

3- در زیر package declaration، دو اعلان تابع زیر را بچسبانید.

 1// SumInts adds together the values of m.
 2func SumInts(m map[string]int64) int64 {
 3    var s int64
 4    for _, v := range m {
 5        s += v
 6    }
 7    return s
 8}
 9
10// SumFloats adds together the values of m.
11func SumFloats(m map[string]float64) float64 {
12    var s float64
13    for _, v := range m {
14        s += v
15    }
16    return s
17}

در این کد:

دو تابع را برای جمع کردن مقادیر یک map و برگرداندن جمع اعلام کنید.

  • تابع SumFloats یک map از نوع string را به مقادیر float64 می گیرد.
  • تابع SumInts یک map از نوع string را به مقادیر int64 می گیرد.

4- در بالای main.go، در زیر اعلان package، تابع اصلی زیر را برای مقداردهی اولیه دو map قرار دهید و هنگام فراخوانی توابعی که در مرحله قبل اعلام کردید، از آنها به عنوان آرگومان استفاده کنید.

 1func main() {
 2    // Initialize a map for the integer values
 3    ints := map[string]int64{
 4        "first":  34,
 5        "second": 12,
 6    }
 7
 8    // Initialize a map for the float values
 9    floats := map[string]float64{
10        "first":  35.98,
11        "second": 26.99,
12    }
13
14    fmt.Printf("Non-Generic Sums: %v and %v\n",
15        SumInts(ints),
16        SumFloats(floats))
17}

در این کد:

  • یک map از مقادیر float64 و یک map از مقادیر int64 را راه اندازی کنید که هر کدام دارای دو ورودی است.
  • برای یافتن مجموع مقادیر هر map، دو تابعی را که قبلاً اعلام کردید، فراخوانی کنید.
  • نتیجه را چاپ کنید.

5- در نزدیکی و بالای main.go، درست در زیر package declaration، همیشه بسته‌ای را که برای پشتیبانی از کدی که نوشتید، وارد کنید.

اولین خطوط کد باید به شکل زیر باشد:

package main

import "fmt"

6- فایل main.go ذخیره کنید.

اجرای کد

از خط فرمان در دایرکتوری حاوی main.go، کد را اجرا کنید.

$ go run .
Non-Generic Sums: 46 and 62.97

با ژنریک، می توانید به جای دو تابع، یک تابع را در اینجا بنویسید. در مرحله بعد، یک تابع کلی برای نقشه‌های حاوی مقادیر صحیح یا شناور اضافه می‌کنید.

6.1.6 اضافه کردن تابع ژنریک برای استفاده از تایپ‌های مختلف #

با generics، می توانید به جای دو تابع، یک تابع را در اینجا بنویسید. در مرحله بعد، یک تابع کلی برای mapهای حاوی مقادیر صحیح یا شناور اضافه می‌کنید.

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

برای این منظور تابعی می نویسید که علاوه بر پارامترهای تابع معمولی، پارامترهای type را نیز اعلام می کند. این پارامترهای type، تابع را generic می‌کنند و آن را قادر می‌سازند با آرگومان‌هایی از typeهای مختلف کار کند درنهایت شما تابع را با آرگومان های type و آرگومان های تابع معمولی(ordinary) فراخوانی خواهید کرد.

هر پارامتر type دارای یک type constraint (محدودیت نوع) است که به عنوان meta-type برای پارامتر type اثر می کند. هر type constraint، در واقع type arguments مجاز را مشخص می کند که کد فراخوانی می تواند برای type arguments مربوطه استفاده کند.

در حالی که محدودیت یک type parameter معمولاً مجموعه‌ای از types را نشان می‌دهد، در زمان کامپایل، پارامتر نهایی به صورت single type است - typeی که به عنوان type argument توسط کد فراخوان ارائه می‌شود. اگر type argument مورد استفاده type توسط محدودیت type parameter مجاز نباشد، کد کامپایل نخواهد شد.

به خاطر داشته باشید که یک type argument باید از تمام عملیاتی که کد generic روی آن انجام می‌دهد پشتیبانی کند. به عنوان مثال، اگر کد تابع شما سعی کند عملیات string ای (مانند indexing) را روی یک type parameter که محدودیت آن شامل انواع عددی است انجام دهد، کد کامپایل نمی شود.

در کدی که می خواهید بنویسید، از محدودیتی استفاده خواهید کرد که به type عدد صحیح یا شناور اجازه می دهد.

نوشتن کد: 1- در زیر دو تابعی که قبلا اضافه کردید، generic function زیر را قرار دهید.

1// SumIntsOrFloats sums the values of map m. It supports both int64 and float64
2// as types for map values.
3func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
4    var s V
5    for _, v := range m {
6        s += v
7    }
8    return s
9}

در این کد:

  • یک تابع SumIntsOrFloats با دو پارامتر نوع (داخل پرانتز)، K و V، و یک آرگومان که ازtype parameters است و m از نوع map[K]V استفاده می‌کند، تعریف شده. تابع مقداری از نوع V را برمی گرداند.
  • برای پارامتر نوع K محدودیت type parameter را جهت مقایسه مشخص شده و به طور خاص برای مواردی مانند این در نظر گرفته شده است، محدودیت قابل مقایسه در Go از قبل اعلام شده است. این اجازه می دهد تا هر نوع که مقادیر آن ممکن است به عنوان عملوند از عملگرهای مقایسه == و != استفاده شود همینطور در زبان Go مستلزم این است که map keys قابل مقایسه باشند. بنابراین اعلام K به عنوان پارامتر قابل مقایسه ضروری است تا بتوانید از K به عنوان کلید در map variable استفاده کنید. همچنین تضمین می کند که کد فراخوانی از یک type مجاز برای map keys استفاده می کند.
  • برای پارامتر نوع V محدودیتی را مشخص کنید که ترکیبی از دو type است: int64 و float64. با استفاده از علامت « | » اتحادی از این دو نوع type را مشخص می کند، به این معنی که این محدودیت اجازه می دهد تا هر type اجرا شود. کامپایلر به عنوان آرگومان در کد فراخوانی شده به هر کدام از typeها را اجازه فعالیت می‌دهد.
  • مشخص کنید که آرگومان m از نوع map[K]V باشد، که در آن K و V تایپ‌هایی هستند که از قبل برای type parameters مشخص شده‌اند. توجه داشته باشید که می دانیم map[K]V یک نوع map معتبر است زیرا K یک نوع type مقایسه است. اگر K را قابل مقایسه اعلام نکرده بودیم، کامپایلر ارجاع به map[K]V را رد می کرد.

2- در main.go، در کنار کدی که از قبل نوشتید، کد زیر را اضافه کنید:

1fmt.Printf("Generic Sums: %v and %v\n",
2    SumIntsOrFloats[string, int64](ints),
3    SumIntsOrFloats[string, float64](floats))

در این کد موارد زیر رو داریم:

  • با فراخوانی کردن generic function که تعریف کردید تمام maps هایی که ایجاد کردید را pass کنید.
  • همیشه type arguments را مشخص کنید - type names موجود درsquare brackets - در مورد types مختلفی که باید type parameters را در عملکردی که فراخوانی میشود جایگزین کنید. همانطور که در بخش بعدی مشاهده خواهید کرد، اغلب می توانید type arguments را در function call حذف کنید. Go اغلب می تواند آنها را از کد شما ویش بینی کند.
  • مقدار sums returned رو پرینت کنید.

اجرای کد:

1$ go run .
2Non-Generic Sums: 46 and 62.97
3Generic Sums: 46 and 62.97

برای اجرای کد باید در هر فراخوانی، کامپایلر type parameters را با typeهای مشخص شده در آن فراخوانی جایگزین می‌کند.

در فراخوانی generic function که نوشته‌اید، type arguments را مشخص کرده‌اید که به کامپایلر می‌گویند از چه typeهایی به جای type parameterهای تابع استفاده کند. همانطور که در بخش بعدی خواهید دید، در بسیاری از موارد می توانید این نوع آرگومان ها را حذف کنید زیرا کامپایلر می تواند آنها را پیش‌بینی کند.

6.1.7 حذف تایپ آرگومان تابع در زمان استفاده از تابع ژنریک #

در این بخش، یک نسخه تغییر یافته از فراخوانی تابع generic را اضافه می‌کنید و یک تغییر کوچک برای ساده کردن کد فراخوانی ایجاد می‌کنید. شما type arguments را که در این مورد مورد نیاز نیستند، حذف خواهید کرد.

وقتی کامپایلر Go می تواند typeهای را که می خواهید استفاده کنید پیش‌بینی کند، می توانید type argumentsها را در فراخوانی کد حذف کنید. کامپایلر type arguments را از typeهای function argument پیش بینی می کند.

توجه داشته باشید که این همیشه امکان پذیر نیست. به عنوان مثال، اگر شما نیاز به فراخوانی یک تابع generic دارید که هیچ آرگومان ندارد، باید type argumentها را در فراخوانی تابع قرار دهید.

نوشتن کد:

در main.go، در کنار کدی که از قبل نوشتید، کد زیر را اضافه کنید:

1fmt.Printf("Generic Sums, type parameters inferred: %v and %v\n",
2    SumIntsOrFloats(ints),
3    SumIntsOrFloats(floats))

در مرحله بعد، تابع را با گرفتن اشتراک اعداد صحیح و شناور در یک محدودیت type که می توانید مجدداً استفاده کنید، مانند کدهای دیگر، ساده‌تر می کنید.

Declare a type constraint #

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

شما یک type constraint را به عنوان یک interface اعلام می کنید. این محدودیت اعمال شده به هر type اجرای interface منحصر به فردی را اجازه می دهد. به عنوان مثال ، اگر type constraint interface را با سه متد تعریف کنید، از آن با یک type arguments در یک generic function استفاده کنید ، type arguments استفاده شده برای call the function باید دارای تمام این متد ها باشند.

همینطور Constraint interfaces نیز می توانند به typeهای خاصی مراجعه کنند ، همانطور که در این بخش مشاهده خواهید کرد.

نوشتن کد: 1- درست بالای main، بلافاصله بعد از دستورهای import، کد زیر را برای اعلام یک type constraint قرار دهید.

1type Number interface {
2    int64 | float64
3}

در این کد:

  • باید شماره interface type را برای استفاده به عنوان یک type constraint اعلام کنید.
  • اشتراک int64 و float64 را در داخل interface اعلام کنید.

در اصل، شما اشتراک را از function declaration به یک type constraint جدید منتقل می‌کنید. به این ترتیب، هنگامی که می خواهید یک پارامتر نوع را به int64 یا float64 محدود کنید، می توانید از این Number به صورت  type constraint به جای نوشتن int64 | float64 استفاده کنید.

2 - در زیر توابعی که از قبل دارید، تابع SumNumbers که از نوع generic در زیر را جای‌گذاری کنید.

1// SumNumbers sums the values of map m. It supports both integers
2// and floats as map values.
3func SumNumbers[K comparable, V Number](m map[K]V) V {
4    var s V
5    for _, v := range m {
6        s += v
7    }
8    return s
9}

در این کد :

یک تابع generic را با همان منطق تابع generic که قبلاً اعلام کرده بودید، اما با interface type جدید به جای union به عنوان type constraint، اعلام کنید. مانند قبل، از پارامترهای type برای انواع حالت های آرگومان و بازگشت استفاده می کنید.

3 - در main.go زیر کدی که از قبل دارید، کد زیر را قرار دهید.

1fmt.Printf("Generic Sums with Constraint: %v and %v\n",
2    SumNumbers(ints),
3    SumNumbers(floats))

در این کد:

همیشه SumNumbers را با هر map فراخوانی کنید و sum را از مقادیر هر یک از اجزا چاپ کنید. مانند بخش قبل، در فراخوانی generic function، مقدار type آرگومان (type names in square brackets) را حذف می کنید. کامپایلر Go می تواند آرگومان نوع را از آرگومان های دیگر تشخیص دهد.

اجرای کد: از خط فرمان در دایرکتوری حاوی main.go، کد را اجرا کنید.

$ go run .
Non-Generic Sums: 46 and 62.97
Generic Sums: 46 and 62.97
Generic Sums, type parameters inferred: 46 and 62.97
Generic Sums with Constraint: 46 and 62.97

در نتیجه:

به خوبی انجام شد! بلاخره شما کار با generic ها در Go یاد گرفتید.

موضوعات پیشنهادی بعدی:

  • استفاده از Go Tour یک معرفی گام به گام عالی برای اصول Go است.
  • بهترین شیوه های مفید Go را که در Effective Go و How to write Go code توضیح داده شده است، خواهید یافت.

کد نهایی: می توانید این برنامه را درGo playground اجرا کنید. درGo playground به سادگی روی دکمه Run کلیک کنید.

 1package main
 2
 3import "fmt"
 4
 5type Number interface {
 6    int64 | float64
 7}
 8
 9func main() {
10    // Initialize a map for the integer values
11    ints := map[string]int64{
12        "first": 34,
13        "second": 12,
14    }
15
16    // Initialize a map for the float values
17    floats := map[string]float64{
18        "first": 35.98,
19        "second": 26.99,
20    }
21
22    fmt.Printf("Non-Generic Sums: %v and %v\n",
23        SumInts(ints),
24        SumFloats(floats))
25
26    fmt.Printf("Generic Sums: %v and %v\n",
27        SumIntsOrFloats[string, int64](ints),
28        SumIntsOrFloats[string, float64](floats))
29
30    fmt.Printf("Generic Sums, type parameters inferred: %v and %v\n",
31        SumIntsOrFloats(ints),
32        SumIntsOrFloats(floats))
33
34    fmt.Printf("Generic Sums with Constraint: %v and %v\n",
35        SumNumbers(ints),
36        SumNumbers(floats))
37}
38
39// SumInts adds together the values of m.
40func SumInts(m map[string]int64) int64 {
41    var s int64
42    for _, v := range m {
43        s += v
44    }
45    return s
46}
47
48// SumFloats adds together the values of m.
49func SumFloats(m map[string]float64) float64 {
50    var s float64
51    for _, v := range m {
52        s += v
53    }
54    return s
55}
56
57// SumIntsOrFloats sums the values of map m. It supports both floats and integers
58// as map values.
59func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
60    var s V
61    for _, v := range m {
62        s += v
63    }
64    return s
65}
66
67// SumNumbers sums the values of map m. Its supports both integers
68// and floats as map values.
69func SumNumbers[K comparable, V Number](m map[K]V) V {
70    var s V
71    for _, v := range m {
72        s += v
73    }
74    return s
75}