توسعه دهنده Back-End
آرایه و Slice در گولنگ
آرایه
آرایه مجموعه ای از عناصر است که به یک نوع تعلق دارند. به عنوان مثال، مجموعه اعداد صحیح 5، 8 ، 9 ، 79 و 76 یک آرایه را تشکیل می دهد. تریکب کردن مقادیر از انواع مختلف به عنوان مثال، یک آرایه که هم رشته دارد و هم عدد صحیح، در Go مجاز نیست.
اندازه آرایه کاملا مشخص بوده به طوری که این مقدار در زمان کامپایل باید ثابت باشد.
تعریف آرایه
یک آرایه به نوع [n] T تعلق دارد. n تعداد عناصر موجود در یک آرایه را نشان می دهد و T نوع هر عنصر را نشان می دهد. تعداد عناصر n نیز قسمتی از نوع آن است (در ادامه با جزئیات بیشتری در این مورد بحث خواهیم کرد)
روش های مختلفی برای اعلام آرایه ها وجود دارد. باهم تک تک آنها را بررسی می کنیم.
func main() {
var a [3]int //int array with length 3
fmt.Println(a)
}
عبارت var a [3] int یک آرایه صحیح را به طول ۳ تعریف می کند. به همه عناصر یک آرایه به طور خودکار مقدار صفر نوع آرایه اختصاص می یابد. در این حالت a یک آرایه صحیح است و از این رو به تمام عناصر a، مقدار صفر int که 0 است اختصاص می یابد. خروجی اجرای دستور پرینت برنامه فوق چنین خواهید بود:
[0 0 0]
ایندکس آرایه با ۰ شروع می شود و ۱- آخرین ایندکس آن می باشد. اگر بخواهیم به آرایه مثال بالا مقدار اساین کنیم، چنین عمل می کنیم:
var a [3]int //int array with length 3
a[0] = 12 // array index starts at 0
a[1] = 78
a[2] = 50
fmt.Println(a)
خروجی قطعه کد فوق:
[12 78 50]
می تواتیم برای تعریف آرایه ها از نوع میانبر آن به صورتshort hand declaration (یعنی روش کوتاه) عمل کنیم.بدین صورت که می بینید:
a := [3]int{12, 78, 50} // short hand declaration to create array
نیازی نیست که در هنگام تعریف آرایه به شکل بالا همه عناصر یک آرایه مقدار داده شود.
package main
import (
"fmt"
)
func main() {
a := [3]int{12}
fmt.Println(a)
}
در برنامه فوق در خط شماره ۸ a: = [3] int {12} آرایه ای به طول ۳ را اعلام می کند اما فقط با مقدار ۱۲ پر می شود. ۲ خانه باقی مانده به طور خودکار 0 تعیین می شود. خروجی قطعه کد فوق:
[12 0 0]
نکتهای که باید بهش اشاره کرد اینه که مقدار دادن به خونهای از آرایه که وجود نداره باعث ایجاد خطا می شود.
نکته: جهت راحتی کار میشه به جای عدد ازسه نقطه ... استفاده کرد با این قابلیت در زمان کامپایل سایز آرایه توسط مقادیر Initialize شده حساب میشود:
a := [...]int{2, 9, 5} //... makes the compiler determine the length
اندازه آرایه بخشی از نوع آن است. از این رو [5] int و [25] int انواع متمایزی هستند. به همین دلیل، اندازه آرایه ها قابل تغییر نیست. نگران این محدودیت نباشید زیرا slice ها به کمک ما می آیند.
آرایه ها value type هستند
آرایه ها در Go انواع مقداری هستند(value types) و انواع مرجعی (reference types) نیستند. این به این معنی است که وقتی آنها به یک متغیر جدید اختصاص می یابند، یک کپی از آرایه اصلی به متغیر جدید اختصاص می یابد. اگر تغییراتی در متغیر جدید ایجاد شود، در آرایه اصلی منعکس نمی شود.
package main
import "fmt"
func main() {
a := [...]string{"TEH", "MHD", "AWZ", "IFN", "SYZ"}
b := a // a copy of a is assigned to b
b[0] = "ADU"
fmt.Println("a is ", a)
fmt.Println("b is ", b)
}
در برنامه فوق در خط شماره ۷، یک نسخه از a به b اختصاص داده شده است. در خط شماره ۸ اولین عنصر b به ADU تغییر کرده ولی این در آرایه اصلی (همان a) منعکس نشده است.
a is [TEH MHD AWZ IFN SYZ]
b is [ADU MHD AWZ IFN SYZ]
به همین علت وقتی آرایه ها به عنوان پارامتر به توابع منتقل می شوند، آنها از نظر مقدار(passed by value) منتقل می شوند و آرایه اصلی بدون تغییر می ماند.
package main
import "fmt"
func changeLocal(num [5]int) {
num[0] = 55
fmt.Println("inside function ", num)
}
func main() {
num := [...]int{5, 6, 7, 8, 8}
fmt.Println("before passing to function ", num)
changeLocal(num) //num is passed by value
fmt.Println("after passing to function ", num)
}
در برنامه فوق در خط شماره ۱۳ آرایه num در واقع به صورت pass by value به تابع changeLocal منتقل می شود وبه همین مقادیر اصلی آن بعد صدا زدن آرایه هیچ تغییری نخواهد کرد.
before passing to function [5 6 7 8 8]
inside function [55 6 7 8 8]
after passing to function [5 6 7 8 8]
طول یک آرایه
طول آرایه با پاس دادن آرایه به عنوان پارامتر به تابع len پیدا می شود.
package main
import "fmt"
func main() {
a := [...]float64{67.7, 89.8, 21, 78}
fmt.Println("length of a is",len(a))
}
خروجی برنامه فوق بدین صورت می باشد:
length of a is 4
تکرار (iterate) روی عناصر آرایه ها
همانطور که انتظار می رود میتوان از حلقه for می توان برای تکرار عناصر آرایه استفاده کرد.
package main
import "fmt"
func main() {
a := [...]float64{67.7, 89.8, 21, 78}
for i := 0; i < len(a); i++ { //looping from 0 to the length of the array
fmt.Printf("%d th element of a is %.2f\n", i, a[i])
}
}
برنامه فوق از یک حلقه for برای تکرار عناصر آرایه از شاخص 0 تا طول آرایه منهای یک استفاده می کند. خروجی کد فوق بدین صورت می باشد:
0 th element of a is 67.70
1 th element of a is 89.80
2 th element of a is 21.00
3 th element of a is 78.00
گولنگ با استفاده از range روشی بهتر و مختصر برای تکرار در یک آرایه ارائه می دهد. range هم ایندکس و هم مقدار آن ایندکس را برمی گرداند. بیایید کد بالا را با استفاده از range بازنویسی کنیم و همچنین مجموع تمام عناصر آرایه را کنیم
package main
import "fmt"
func main() {
a := [...]float64{67.7, 89.8, 21, 78}
sum := float64(0)
for i, v := range a {//range returns both the index and value
fmt.Printf("%d the element of a is %.2f\n", i, v)
sum += v
}
fmt.Println("\nsum of all elements of a",sum)
}
خط شماره کد بالا for i, v := range a
حالت range حلقه for است. هم ایندکس و هم مقدار آن شاخص را برمی گرداند. مقادیر را چاپ می کنیم و همچنین مجموع تمام عناصر آرایه a را محاسبه می کنیم. خروجی برنامه:
0 the element of a is 67.70
1 the element of a is 89.80
2 the element of a is 21.00
3 the element of a is 78.00
sum of all elements of a 256.5
درصورتی که فقط مقدار می خواهید و می خواهید ایندکس را نادیده بگیرید، می توانید این کار را با جایگزینی متغیر ایندکس با شناسه _ خالی انجام دهید.
for _, v := range a { //ignores index
}
حلقه for فوق ایندکس را نادیده می گیرد. به همین ترتیب مقدار نیز قابل چشم پوشی است. (خاطرتان حتما هست که در گولنگ اگر از متغیری که تعریف کردید استفاده نکنید با خطا مواجه می شوید. پس این نادیده گرفتن متغیرها کاملا طبیعی می باشد)
آرایه های چند بعدی
آرایه هایی که ما تاکنون ایجاد کرده ایم همه تک بعدی هستند. ایجاد آرایه های چند بعدی امکان پذیر است.
package main
import (
"fmt"
)
func printarray(a [3][2]string) {
for _, v1 := range a {
for _, v2 := range v1 {
fmt.Printf("%s ", v2)
}
fmt.Printf("\n")
}
}
func main() {
a := [3][2]string{
{"lion", "tiger"},
{"cat", "dog"},
{"pigeon", "peacock"}, //this comma is necessary. The compiler will complain if you omit this comma
}
printarray(a)
var b [3][2]string
b[0][0] = "apple"
b[0][1] = "samsung"
b[1][0] = "microsoft"
b[1][1] = "google"
b[2][0] = "AT&T"
b[2][1] = "T-Mobile"
fmt.Printf("\n")
printarray(b)
}
در برنامه فوق در خط شماره ۱۷ آرایه رشته ای دو بعدی a با استفاده از سینتکس short hand (که بالاتر توضیح دادیم)اعلام شده است.(comma در انتهای خط شماره 20 لازم است)
آرایه دو بعدی دیگر به نام b در سطر شماره ۲۳ تعریف می شود و string ها برای هر ایندکس یکی یکی به آن اضافه می شوند. این روش دیگری برای مقداردهی اولیه آرایه دو بعدی است است. فانکشن printarray
در خط شماره ۷ برای چاپ محتویات آرایه های دوبعدی از دو حلقه range استفاده می کند. برنامه فوق چاپ خواهد کرد:
lion tiger
cat dog
pigeon peacock
apple samsung
microsoft google
AT&T T-Mobile
به نظرم در مورد آرایه ها تا اینجا کافیه! موافقید !؟ اگرچه آرایه ها به اندازه کافی انعطاف پذیر به نظر می رسند اما با محدودیت طول ثابت ارائه می شوند. افزایش طول یک آرایه امکان پذیر نیست. این جایی است که slice ها وارد بازی می شوند. در واقع در Go اسلایس ها خیلی رایج تر از آرایه ها هستند. در ادامه آنها را توضیح می دهیم.
اسلایس (slice) در گولنگ
یک slice در واقع یک wrapper راحت، انعطاف پذیر و قدرتمند برای آرایه است. slice ها به خودی خود هیچ داده ای ندارند. آنها فقط ارجاعات به آرایه های موجود هستند.
ایجاد یک slice
یک slice با عناصر نوع T با [] T نشان داده می شود
package main
import (
"fmt"
)
func main() {
a := [5]int{76, 77, 78, 79, 80}
var b []int = a[1:4] //creates a slice from a[1] to a[3]
fmt.Println(b)
}
سینتکس a[start:end]
اسلایسی از آرایه a ایجاد می کند که از ایندکس start
می شود تا ایندکس end-1
. بنابراین در خط شماره 9 از برنامه فوق [4: 1] نمایش آرایه a را با شروع از ایندکس های 1 تا 3 ایجاد می کند. از این رو اسلایس b دارای مقادیر [ 79 78 77 ] است.
به یک روش دیگر هم برای ایجاد یک slice نگاه کنیم.
package main
import (
"fmt"
)
func main() {
c := []int{6, 7, 8} //creates and array and returns a slice reference
fmt.Println(c)
}
در برنامه فوق در خط شماره ۹، c: = [] int {6، 7، 8} آرایه ای با ۳ عدد صحیح ایجاد می کند و یک reference اسلایس را که در c ذخیره شده است برمی گرداند.
تغییر یک slice
یک اسلایس به خودی خود هیچ داده ای ندارد بلکه فقط فقط رفرنسی نمایشی به آرایه زمینه ای است. هرگونه تغییری که در slice ایجاد شود، در آرایه زیرین منعکس خواهد شد.
package main
import (
"fmt"
)
func main() {
darr := [...]int{57, 89, 90, 82, 100, 78, 67, 69, 59}
dslice := darr[2:5]
fmt.Println("array before",darr)
for i := range dslice {
dslice[i]++
}
fmt.Println("array after",darr)
}
در خط شماره ۹ برنامه فوق، از شاخص های 2 ، 3 ، 4 آرایه dslice ایجاد می کنیم. حلقه for مقدار این ایندکس ها را یکی افزایش می دهد. وقتی آرایه را بعد از حلقه for چاپ می کنیم، می توانیم ببینیم که تغییرات slice در آرایه منعکس شده است. خروجی برنامه:
array before [57 89 90 82 100 78 67 69 59]
array after [57 89 91 83 101 78 67 69 59]
می بینید که با توجه به تغییرات اسلایس مقادیر آرایه اصلی هم تغییر پیدا کرده اند. مقادیر ایندکس های2، 3 و 4 هر کدام یکی بیشتر شده اند. (91, 83, 101)
هنگامی که تعدادی از اسلایس ها آرایه اصلی یکسانی دارند، تغییراتی که هر یک ایجاد می کند در آرایه منعکس می شود.
package main
import (
"fmt"
)
func main() {
numa := [3]int{78, 79 ,80}
nums1 := numa[:] //creates a slice which contains all elements of the array
nums2 := numa[:]
fmt.Println("array before change 1",numa)
nums1[0] = 100
fmt.Println("array after modification to slice nums1", numa)
nums2[1] = 101
fmt.Println("array after modification to slice nums2", numa)
}
در خط شماره ۹ در numa [:] مقادیر شروع و پایان وجود ندارد. مقادیر پیش فرض برای شروع و پایان به ترتیب 0 و len (numa) است. هر دو برش nums1 و nums2 یک آرایه مشترک دارند. خروجی برنامه:
array before change 1 [78 79 80]
array after modification to slice nums1 [100 79 80]
array after modification to slice nums2 [100 101 80]
از خروجی واضح است که وقتی اسلایس ها آرایه یکسانی دارند. تغییرات ایجاد شده در اسلایس در آرایه منعکس می شود.
طول و ظرفیت یک اسلایس
طول(length) یک slice تعداد عناصر موجود در آن است و ظرفیت(capacity) slice تعداد عناصر موجود در آرایه زیرین است که از نمایه ای که برش از آن ایجاد می شود شروع می شود.
package main
import (
"fmt"
)
func main() {
fruitarray := [...]string{"apple", "orange", "grape", "mango", "water melon", "pine apple", "chikoo"}
fruitslice := fruitarray[1:3]
fmt.Printf("length of slice %d capacity %d", len(fruitslice), cap(fruitslice)) //length of fruitslice is 2 and capacity is 6
}
در برنامه فوق ، fruitslice
از شاخص 1 و 2 fruitarray
ایجاد می شود. از این رو طول fruitslice برابر
۲ است. طول آرایه fruitarray برابر
۷ برابر است. برنج میوه از شاخص 1 آرایه ایجاد شده است. از این رو ظرفیت میوه میوه تعداد عناصر موجود در مجموعه میوه ای است که از شاخص 1 یعنی نارنجی شروع می شود و این مقدار 6 است. از این رو ظرفیت میوه میوه 6 است. برنامه طول قطعه 2 ظرفیت 6 را چاپ می کند. یک قطعه را می توانید مجدداً به ظرفیت خود برش دهید. هر چیزی فراتر از آن باعث می شود برنامه خطای زمان اجرا ایجاد کند.
ایجاد اسلایس ها با استفاده از فانکشن make
از سینتکس func make ([] T ، len ، cap) [] T می توان با پاس دادن نوع، طول و ظرفیت یک slice ایجاد کرد. پارامتر ظرفیت(cap) اختیاری است و به طور پیش فرض نسبت به طول(len) انجام می شود. تابع make یک آرایه ایجاد می کند و یک slice که به آرایه اشاره می کند را برمی گرداند.
package main
import (
"fmt"
)
func main() {
i := make([]int, 5, 5)
fmt.Println(i)
}
هنگام ایجاد یک slice، به طور پیش فرض مقادیر zero یا صفر (برای مثال برای نوع int مقدار 0) می شوند.
در برنامه فوق خروجی [0 0 0 0 0] تولید خواهد شد.
اضافه کردن مقدار جدید به یک slice
ممکن است یک سوال برای شما پیش بیاید. اگر slice ها توسط آرایه ها پشتیبانی می شوند و آرایه ها نیز دارای طول ثابت هستند، پس چگونه یک slice دارای طول پویا است؟ خوب آنچه در اصل اتفاق می افتد این است، وقتی عناصر جدیدی به slice اضافه می شوند، یک آرایه جدید ایجاد می شود. عناصر آرایه موجود در این آرایه جدید کپی شده و رفرنس slice جدیدی برای این آرایه جدید بازگردانده می شود. ظرفیت slice جدید اکنون دو برابر slice قدیمی است. خیلی باحاله نه ؟ :) برنامه زیر همه چیز را روشن می کند.
package main
import (
"fmt"
)
func main() {
cars := []string{"Ferrari", "Honda", "Ford"}
fmt.Println("cars:", cars, "has old length", len(cars), "and capacity", cap(cars)) //capacity of cars is 3
cars = append(cars, "Toyota")
fmt.Println("cars:", cars, "has new length", len(cars), "and capacity", cap(cars)) //capacity of cars is doubled to 6
}
در برنامه فوق، ظرفیت اسلایس cars در ابتدا ۳ است. ما در خط شماره ۱۰ عنصر جدید را به cars اضافه می کنیم و slice برگردانده شده توسط append(cars, "Toyota")
دوباره به cars اختصاص دهید. اکنون ظرفیت cars دو برابر شده و ۶ می شود.
cars: [Ferrari Honda Ford] has old length 3 and capacity 3
cars: [Ferrari Honda Ford Toyota] has new length 4 and capacity 6
پاس دادن یک اسلایس به یک فانکشن به عنوان آرگومان
قبلا در مورد struct ها در این پست صحبت کردیم. اسلایس ها رو می توانیم مثل یک struct به صورت زیر در نظر بگیریم.
type slice struct {
Length int
Capacity int
ZerothElement *byte
}
یک ساختار slice شامل length، capacity و pointer به عنصر صفر آرایه است. هنگامی که یک برش به یک تابع منتقل می شود، با اینکه با مقدار (pass by value) پاس داده می شود ولی نشانگر به همان آرایه زیرین ارجاع می کند(اشاره می کند). از این رو وقتی یک اسلایس به عنوان پارامتر به یک تابع پاس داده می می شود، تغییرات ایجاد شده در داخل تابع نیز در خارج از تابع قابل مشاهده است. بیایید برنامه ای برای بررسی این موضوع بنویسیم.
package main
import (
"fmt"
)
func subtactOne(numbers []int) {
for i := range numbers {
numbers[i] -= 2
}
}
func main() {
nos := []int{8, 7, 6}
fmt.Println("slice before function call", nos)
subtactOne(nos) //function modifies the slice
fmt.Println("slice after function call", nos) //modifications are visible outside
}
فراخوانی فانکشن در خط شماره ۱۷ برنامه فوق هر یک از عناصر اسلایس را 2 تا کم میکند. وقتی قطعه بعد از فراخوانی فانکشن چاپ می شود، این تغییرات قابل مشاهده هستند.
slice before function call [8 7 6]
slice after function call [6 5 4]
برش های چند بعدی
مشابه آرایه ها، اسلایس ها می توانند ابعاد مختلفی داشته باشند.
package main
import (
"fmt"
)
func main() {
pls := [][]string {
{"C", "C++"},
{"JavaScript"},
{"Go", "Rust"},
}
for _, v1 := range pls {
for _, v2 := range v1 {
fmt.Printf("%s ", v2)
}
fmt.Printf("\n")
}
}
خروجی برنامه فوق:
C C++
JavaScript
Go Rust
بهینه سازی حافظه
اسلایس ها اشاره ای به آرایه اصلی دارند. تا زمانی که اسلایس در حافظه باشد، آرایه را نمی توان garbage collect کرد. وقتی صحبت از مدیریت حافظه می شود، این مورد ممکن است نگران کننده باشد. بیایید فرض کنیم که ما یک آرایه بسیار بزرگ داریم و ما علاقه مند هستیم که فقط قسمت کوچکی از آن را پردازش کنیم. برای این کار ما یک اسلایس از آن آرایه ایجاد می کنیم و پردازش را روی اسلایس انجام می دهیم. نکته مهمی که در اینجا باید به آن اشاره شود این است که آرایه همچنان در حافظه باقی می ماند زیرا این اسلایس به آن اشاره می کند.
یکی از راه های حل این مشکل استفاده از تابع copy به صورت func copy (dst، src [] T) int برای ایحاد کپی از آن اسلایس است. به این ترتیب می توانیم از برش جدید استفاده کنیم و آرایه اصلی می تواند از حافظه حذف شود.
جمله پایانی: در صورتی که مطلب را دوست داشتید لایک ❤️ کنید یا نظرتان را از طریق کامنت برای من بنویسید.
مطلبی دیگر از این انتشارات
متدها در زبان GoLang
مطلبی دیگر از این انتشارات
شروع برنامه نویسی با go گولنگ
مطلبی دیگر از این انتشارات
پیاده سازی یک سرویس قابل تست در Golang - قسمت ۱