Arrays και Go

Σε αυτό το άρθρο θα μιλήσουμε για arrays και θα δούμε πώς μπορεί κανείς να φτιάξει μία array, να γράψει στοιχεία και να διαβάσει στοιχεία από αυτήν, καθώς επίσης συχνά λάθη και άλλα πολλά.

Συλλογή από τιμές ίδιου τύπου

Οι arrays δεν είναι τίποτε άλλο παρά συλλογές από διάφορες τιμές που έχουν το ίδιο data type. Για παράδειγμα μπορούμε να έχουμε arrays με integers, με strings, αλλά και με οποιοδήποτε άλλο custom data structure έχετε φτιάξει. Μάλιστα μπορούμε να έχουμε an array of arrays και κάπου εκεί περνάμε σε καταστάσεις inception. Έχοντας μία array από στοιχεία του ίδιου τύπου, αυτόματα ξέρουμε πόσο χώρο καταλαμβάνουν μέσα στην μνήμη του υπολογιστή. Το πλεονέκτημα το να γνωρίζει το που βρίσκεται το καθε στοιχείο (έχοντας τον ίδιο data type σημαίνει ότι ισαπέχουν μεταξύ τους) και επίσης το γεγονός ότι τα στοιχεία είναι διαδοχικά, του επιτρέπει να κινείται με ασφάλεια και ταχύτητα

Θε λέγαμε λοιπόν ότι οι arrays είναι ένα αρκετά αποτελεσματό (efficient) data structure.

Δεν μεγαλώνουν - Δεν μικραίνουν

Το πόσα στοιχεία (τα λέμε elements) μπορεί να αποθηκεύσει μία array είναι κάτι που το δηλώνουμε στην αρχή, κατά την δημιουργία (declaration) αυτής. Κατόπιν, δεν μπορούμε να αλλάξουμε αυτό το νούμερο. Το μέγεθος της array παραμένει αμετάβλητο, το οποίο σημαίνει πως δεν μπορούμε να βάλουμε περισσότερα στοιχεία από αυτά που έχουμε δηλώσει εξ’αρχλης ότι χωράει. Για να ορίσουμε μία array χρειαζόμαστε μία μεταβλητή, όπου κατά την δήλωσή της θα βάλουμε μέσα σε square brackets ([]) τον αριθμό των στοιχείων που χωράει. Από δίπλα, γράφουμε το data type των στοιχείων αυτών.

Σημαντικό: Άπαξ και ορίσουμε μία array, ούτε το μέγεθος, ούτε το data type μπορεί να αλλάξει. Στην περίπτωση που χρειαστούμε περισσότερο χώρο, τότε πρέπει να φτιάξουμε μία καινούρια array (ορίζοντας καινούριο όνομα, τύπο και μέγεθος) και κατόπιν να αντιγράψουμε τα στοιχεία της παλιάς (μικρής) στην καινούρια (μεγάλη).

var myArray [4]string

// [4]: Αριθμός στοιχείων που χωράει
// string: είδος στοιχείων δέχεται

Ξεκινάμε να μετράμε από το μηδέν [0]

Το πρώτο στοιχείο δεν βρίσκεται στην πρώτη θεση αλλά στη μηδενική. Έτσι λοιπόν στον προγραμματισμό της Go λέμε ότι στην θέση [0] βρίσκεται το πρώτο στοιχείο της array, στην [1] θέση βρίσκεται το δεύτερο στοιχείο, στην [2] θέση βρίσκεται το τρίτο και στην 3 θέση βρίσκεται το τέταρτο και τελευταίο. Περίεργο, ε; Εφόσον η Go γράφτηκε βασισμένη στην C, η οποία γράφτηκε βασισμένη στην B, η οποία δεν έχω ιδέα που βασίστηκε (μήπως στην A; Υπάρχει Α;), κάποιος σοφός γέροντας αποφάσησε ότι θα ήταν καλό να ξεκινήσουμε να μετράμε από το μηδέν, και έτσι έγινε.

Βασικά, υπάρχουν διάφορες εξηγησεις αλλά δεν υπάρχει κάποιος που να τεκμηριώνει τον λόγο που έγινε αυτό. Ίσως γιατί ο κύριος που πήρε την απόφαση να μην ζει πλέον. Τέλος πάντων, οι φήμες λένε ότι στην C, η array δεν είναι άλλο από ένας pointer. Επειδή (όπως εξηγήσαμε νωρίτερα) όλα τα στοιχεία της ισαπέχουν, έτσι ο C Compiler υπολογίζει εύκολα την θέση κάθε στοιχείου. Για παράδειγμα o int32 είναι 4 bytes. Άρα το επόμενο στοιχείο θα βρίσκεται 4 bytes πιο μετά, και το μεθεπόμενο θα βρίσκεται 8 bytes πιο μετά.

var myArray [3]int
fmt.Println(&myArray[0])    // 0x40e020 --> (0*4 + 0x40e020)
fmt.Println(&myArray[1])    // 0x40e024 --> (1*4 + 0x40e020)
fmt.Println(&myArray[2])    // 0x40e028 --> (2*4 + 0x40e020)

Έτσι λοιπόν, για την πρώτη θέση, χρειαζόταν (λόγω μαθηματικών) να χρησιμοποιήσε το 0 και έτσι έμεινε στην ιστορία.

Παράδειγμα με strings

Αν θέλαμε να φτιάξουμε μία array με τις μουσικές νότες, τότε:

var notes [7]string // Φτιάξε μία array που θα χωράει 7 στοιχεία τύπου string
notes[0] = "ντο"    // Εκχώρησε μία τιμή στο πρώτο στοιχείο
notes[1] = "ρε"     // Εκχώρησε μία τιμή στο δεύτερο στοιχείο
notes[2] = "μι"     // Εκχώρησε μία τιμή στο τρίτο στοιχείο
notes[3] = "φα"
notes[4] = "σολ"
notes[5] = "λα"
notes[6] = "σι"     // Το τελευταίο στοιχείο βρίσκεται στην (μέγεθος - 1) εκτη θέση.

Αν μας ρωτούσε κάποιος να εμφανίσουμε τη δεύτερη νότα, θα κάναμε:

fmt.Println(notes[1])   // Θα εμφανίσει "ρε"

Παράδειγμα με integers

var akeraioi [5]int         // Φτιάξε μία array που θα χωράει 5 στοιχεία τύπου integer
akeraioi[0] = 6             // Εκχώρησε το πρώτο στοιχείο
akeraioi[1] = 3             // Εκχώρησε το δεύτερο στοιχείο
fmt.Println(akeraioi[0])    // Εμφάνισε το πρώτο στοιχείο, δηλαδή θα δείξει "6"
fmt.Println(akeraioi[1])    // Εμφάνισε το δευτερο στοιχείο, δηλαδή θα δείξει "3"

Σημείωση: Δεν χρειάζεται να γεμίσουμε την array με στοιχεία. Ενώ χωράει μέχρι 5 στοιχεία, εμείς επιλέξαμε να βάλουμε μόνο δύο (το 6 και το 3). Στην πραγματικότητα όμως, κατά τη δημιουργία της array (στην πρώτη πρώτη εντολή δηλαδή, βλ var akeraioi [5]int) έχουν ήδη μπει μηδενικά σε όλες τις θέσεις από τον compiler.

Παράδειγμα με struct

type Movie struct {
	title          string
	RottenTomatoes int
}

func main() {
    var tainia [3]Movie // Φτιάξε μία array που θα χωράει 3 στοιχεία τύπου Movie

    // Πρώτος τρόπος εκχώρησης
	tainia[0].title = "Lord of the Rings"
    tainia[0].RottenTomatoes = 10

    // Δεύτερος τρόπος εκχώρησης
    tainia[1] = Movie{"Matrix", 9}
    
    // Εμφάνισε το πρώτο και το δεύτερο στοιχείο
	fmt.Println(tainia[0])  // {Lord of the Rings 10}
    fmt.Println(tainia[1])  // {Matrix 9}
    
    // Εμφάνισε μόνο το "title" του πρώτου στοιχείου
    fmt.Println(tainia[0].title) // Lord of the Rings
}

Αυτόματη αρχικοποίηση με “Μηδενικες τιμές”

Όπως συμβαίνει και με τις μεταλητές, έτσι και με τις arrays. Όταν φτιάχνουμε μία array, όλες οι τιμές των στοιχείων της αντιστοιχούν (by default) στην αντίστοιχη μηδενική τιμή του τύπου των στοιχείων της ορίσαμε. Δηλαδή αν έχουμε μία array με 10 στοιχεία τύπου int, τότε κατά το declaration (δηλαδή: var myArray [10]int) ο compiler της Go, πάει και βάζει σε όλες τις θέσεις μηδενικά. Αυτό το κάνει αυτόματα, χωρίς να του το ζητήσετε εσείς. Δηλαδή κάνει:

var myArray [10]int
fmt.Println(myArray)    // [0 0 0 0 0 0 0 0 0 0]

/* Αυτό συμβαίνει γιατί ο compiler έχει κάνει
   στα κρυφά κάτι σαν το παρακάτω κώδικα */

myArray[0] = 0 // Πρώτο στοιχείο
myArray[1] = 0 // Δεύτερο στοιχείο
...
...
...
myArray[9] = 0 // Τελευταίο (το δέκατο) στοιχείο

Αντίστοιχα, αν είχαμε την ίδια array αλλά με τύπο string (δηλαδή var myArray [10]string) τότε ο compiler θα την γέμισε με κενά strings. Δηλαδή, αν το κάναμε χειροκίνητα θα ήταν καπως έτσι:

var myArray [10]string
fmt.Println(myArray)    // [         ]
myArray[0] = ""         // Πρώτο στοιχείο
myArray[1] = ""         // Δεύτερο στοιχείο
...
...
...
myArray[9] = ""         // Τελευταίο (το δέκατο) στοιχείο

Ο λόγος που το κάνει αυτό ο compiler είναι γιατί συνηθίζεται να κάνουμε πράγματα με τις arrays, όπου αν κάποιο στοιχείο τους δεν έχει τιμή τότε θα σκάσει το πρόγραμμα. Φανταστείτε ότι έχουμε μία array με όλους τους βαθμούς των μαθητών και θέλουμε να βγάλουμε τον μέσο όρο. Αν λείπει κάποιο στοιχείο, τότε κατά την διαίρεση θα κρασάρει το πρόγραμμα – κάτι που είναι πάρα πολύ κακό. Ο compiler μας προστεύει λοιπόν, συμπληρώνοντας μηδενικά (δηλαδή την αντίστοιχη μηδενική τιμή σε σχέση με το data type της array) έτσι ώστε να μην σκάσει το πρόγραμμα, προστατεύοντάς το.

Στην περίπτωση που αναρωτιέστε ποιες ειναι οι μηδενικές τιμές για τους γνωστούς τύπους δεδομένων, δείτε το παρακάτω:

// Φτιάχνω μεταβλητές (τις ίδιες τιμές θα πάρουν και τα στοιχεία της Array)
var i int
var f float64
var b bool
var s string
var p *int
fmt.Printf("%v %v %v %q %p\n", i, f, b, s, p) // 0 0 false "" 0x0

Αρχικοποίηση με δικές μου τιμές

Μέχρι τώρα φτιάχναμε μία array και στην συνέχεια, σαν δεύτερο βήμα, της βάζαμε τα στοιχεία που θέλαμε. Δηλαδή:

var myArray [5]int
myArray[0] = 10
myArray[1] = 20
myArray[2] = 30
myArray[3] = 40
myArray[4] = 50

Υπάρχει κι ένας πιο γρήγορος τρόπος όπου μας επιτρέπει να κάνουμε αυτά τα δύο βήματα σε ένα, δηλαδή να βάλουμε τις τιμές που θέλουμε κατά το φτιάξιμο της array. Αυτό γίνεται με μία σύνταξη όπου ονομάζεται array literal ή πιο γενικά composite literal και μοιάζει κάπως έτσι: [3]int{9,24,344}. Όπου:

Οπότε αντί να βάζουμε τις τιμές στην array μία-μία, μπορούμε με αυτόν τον τρόπο μπορούμε να τις βάλουμε όλες μαζί, κατά την αρχικοποίηση αυτής χρησιμοποιώντας την σύνταξη array literal.

// Φτιάξε μία integer array με 5 στοιχεία
// Αρχικοποίησε κάθε στοιχείο με μία τιμή
var myArray [5]int = [5]int{10,20,30,40,50}

Χρησιμοποιώντας array literals μπορούμε να χρησιμοποιήσουμε τον πιο γρήγορο τρόπο εκχώρησης τιμής, χρησιμοποιώντας το := όπως και στις μεταβλητές. Δηλαδή:

// Φτιάξε μία integer array με 5 στοιχεία
// Αρχικοποίησε κάθε στοιχείο με μία τιμή χρησιμοποιώντας τον ακόμα πιο γρήγορο τρόπο
myArray := [5]int{10,20,30,40,50}

TIP: Multiline string literal

Όταν θέλουμε να φτιάξουμε array τύπου string και παράλληλα να την αρχικοποιήσουμε με δικές μας τιμές, τότε ο κώδικας θα φαίνεται αρκετά μακρόστενος. Αυτό δεν είναι και πολύ καλό χαρακτηριστικό, καθώς αρκετά open source projects έχουν συγκεκριμένες οδηγίες γύρω από την σύνταξη του κώδικα και μπορεί να σας παραπονεθούν. Να σημειώσουμε εδώ ότι η Go έχει δικό της εργαλείο σύνταξης (go fmt) όπου δεν έχει πρόβλημα με το μήκος των χαρακτήρων σε αυτή την περίπτωση – ωστόσο ορισμένοι άλλοι συνάδελφοι μπορεί να έχουν.

myBestMovies := [5]string{"Lord of the Rings: The Fellowship of the Ring", "Godfather", "The Dark Knight", "Back to the Future", "Star Wars: Rogue One"}

Για να κάνουμε το παραπάνω κομμάτι κώδικα λιγότερο άχαρο, μία best practice είναι να σπάσετε τα elements της array ανά γραμμή, βάζοντας κόμμα , στο τέλος τους – ακόμα και στο τελευταίο στοιχείο! Δηλαδή:

myBestMovies := [5]string{  // Όλα τα παρακάτω είναι στοιχεία της ίδιας array myBestMovies
    "Lord of the Rings: The Fellowship of the Ring",
    "Godfather",
    "The Dark Knight",
    "Back to the Future",
    "Star Wars: Rogue One",     // Το κόμμα στο τέλος είναι απαραίτητο
}

Ο τεμπέλικος τρόπος

Μέχρι τώρα είδαμε τους παρακάτω τρόπους:

// Ο εκπαιδευτικός
var myArray [2]int
myArray[0] = 10
myArray[1] = 20

// Ο νουμπάδικος
var myArray [2]int
myArray = [2]int{10,20}

// Ο τέρμα ηλίθιος
var myArray [2]int = [2]int{10,20}

// Ο πιο σύνηθες (για λίγα στοιχεία)
myArray := [2]int{10,20}

// Ο πιο σύνηθες (για πολλά στοιχεία)
myArray := [10]int{
    1,
    2,
    3,
    4,
    5,
    6,
    7,
    8,
    9,
}

Στην περίπτωση όπου ξέρουμε τις τιμές για όλα τα στοιχεία της array που θέλουμε να αρχικοποποιήσουμε τότε, βάζοντας τις τιμές, η Go μπορεί να υπολογίσει το μέγεθος της array χωρίς να χρειάζεται να της το δηλώσουμε.

// Φτιάξε μία integer array
// Αρχικοποιήσε όλα τα στοιχεία με μία default τιμή
// Το μέγεθος της array βασίζεται στο πόσα στοιχεία αρχικοποιήσες
myArray := [...]int{10,20,30,40,50}

Αυτη η μέθοδος συνηθίζεται να χρησιμοποιείται με arrays όπου θέλουμε να αρχικοποιήσουμε πολλά στοιχεία και δίνουμε προετεραιότητα στο readibility του κώδικα, όπου χρησιμοποιούμε multiline εκχώρηση.

planets := [...]string{ // μην ξεχάσεις τις 3 τελείες [...]
    "Mercury",
    "Venus",
    "Earth",
    "Mars",
    "Jupiter",
    "Saturn",
    "Uranus",
    "Neptune",  // Μην ξεχάσεις το κόμμα στο τέλος
}

ΠΡΟΣΟΧΗ: Στη περίπτωση που δεν βάλετε τα [...] τότε η Go αντί να φτιάξει μία array, θα φτιάξει μία slice! Τρελή γκάφα, καθώς είναι διαφορετικά πράγματα, και τα μυνήματα λάθους του compiler θα είναι διαφορετικά.

myArray := [...]int{10,100,200}     // Αυτό είναι array
mySlice := []int{10,100,200}        // Αυτό είναι slice!!!

Οι πιο προχωρημένοι μπορούν να διαβασουν αυτό το κομμάτι κώδικα που χρησιμοποιεί το πακέτο reflect για να βρει αν η μεταβλητή είναι array ή slice.

Αρχικοποίηση με συγκεκριμένα μόνο στοιχεία

Υπάρχει και μία περίπτωση όπου κατά την αρχικοποίηση της array θέλουμε σε κάποια στοιχεία να βάλουμε δικές μας default τιμές και σε κάποια άλλα δεν μας πειράζει να χρησιμοποιήσει την αντίστοιχη μηδενική τιμή που βάζει ο compiler. Για παράδειγμα αν θέλουμε να φτιάξουμε μία integer array με 5 στοιχεία όπου η [2] και η [4] έχουν δικές μας τιμές – ενώ όλες οι άλλες είναι μηδενικές, τότε γράφουμε:

myArray := [5]int{2: 34, 4:644}
// [0  0  34  0  644]
//  ^  ^      ^       
// Στα [0],[1],[3] ο compiler εβαλε μηδενικά αφου το 0 είναι η μηδενικη τιμη για τυπο integer

Το παραπάνω μπορούμε να το συνδυάσουμε με την τεμπέλικη μέθοδο, αρκεί βέβαια να βάλουμε τιμή στην τελευταία θέση έτσι ώστε να ξέρει η Go πόσο μεγάλη να φτιάξει την array μας:

myArray := [...]int{2: 34, 4:644}
// [0  0  34  0  644]

Διάβασμα μιας array

Χρησιμοποιώντας το fmt

To fmt πακέτο της standard library της Go, μπορεί να διαβάσει μια array χωρίς να χρειάζεται να του πείτε συγκεκριμένα το στοιχείο που σας ενδιαφέρει. Απλά ζητήστε του να κάνει Println δίνοντας μόνο το όνομα της array και αυτό θα σας εμφανίσει στην οθόνη ολόκληρη την array μαζί με όλα τα στοιχεία της.

myArray := [...]int{1,2,3,4,5}
fmt.Println(myArray)        // [1 2 3 4 5]

Να σημειώσουμε ότι χρησιμοποιώντας την σύνταξη %#v στις Printf και Sprintf τότε θα σας εμφανίσει την σύνταξη του array literal η οποία είναι και αυτή που βλέπει ο compiler στον κώδικα:

myArray := [...]int{1, 2, 3, 4, 5}
fmt.Printf("%#v", myArray)  // [5]int{1, 2, 3, 4, 5}

Με μία απλή for loop

Στην περίπτωση που θέλουμε να επέμβουμε στα στοιχεία της array ένα-προς-ένα, αλλά δεν θέλουμε να αφιερώσουμε μία γραμμή κώδικά για κάθε στοιχείο, τότε χρησιμοποιούμε μία απλή λούπα επανάληψης. Για παράδειγμα αν θέλουμε να εμφανίσουμε όλες τις νότες του πενταγράμμου, όπου θα δώσουμε και κάποιο μύνημα γύρω από αυτές, τότε θα το κάναμε κάπως έτσι:

notes := [7]string{"C", "D", "E", "F", "G", "A", "B"}
for i := 0; i < 7; i++ {
    fmt.Printf("Η %d νότα είναι %s.\n", i, notes[i])
}

To output θα είναι:

Η 0 νότα είναι C.
Η 1 νότα είναι D.
Η 2 νότα είναι E.
Η 3 νότα είναι F.
Η 4 νότα είναι G.
Η 5 νότα είναι A.
Η 6 νότα είναι B.

Σημείωση: Προφανώς η φράση 0 νότα χτυπάει κάπως άσχημα. Σαν άσκηση, σκεφτείτε πως θα μπορούσατε να το διορθώσετε (σημ: παρακάτω σε άλλο παράδειγμα θα δείτε τη λύση).

TIP: Για να αποφύγουμε τυχόν λάθη στον index, δηλαδή τον αριθμό 7 του παραπάνω παραδείγματος, μπορούμε να χρησιμοποιήσουμε την build-in συνάρτηση len η οποία μας επιστρέφει το μέγεθος της array (δηλαδή τον αριθμό των στοιχείων που περιέχει).

notes := [7]string{"C", "D", "E", "F", "G", "A", "B"}
fmt.Printf("Η μουσική έχει %d νότες\n", len(notes)) // Η μουσική έχει 7 νότες
for i := 0; i < len(notes); i++ {                   // i < len(notes) είναι το ίδιο με i < 7
    fmt.Printf("Η %d νότα είναι %s.\n", i, notes[i])
}

Προσοχή: Ένα κλασσικό λάθος είναι να βάζουμε λάθος συνθήκη για την λούπα (πχ i <= 7 αντί για i<7) το οποίο θα έχει ως αποτέλεσμα να προσπαθήσει το πρόγραμμα να διαβάσει το notes[7] το οποίο δεν υπάρχει και βρίσκεται εκτός των ορίων της array με αποτέλεσμα να προκληθεί panic σφάλμα. Αυτα τα σφάλματα δεν σκάνε κατά το compiling αλλά κατά το runtime (καθώς τρέχει το πρόγραμμα). Είναι περιττό να αναφέρουμε ότι panic errors πρέπει να αποφεύγουμε όσο το δυνατόν γίνεται.

Μία ασφαλέστερη for loop

Για να αποφύγουμε τα λάθη, είτε με τον index, είτε με την συνθήκη ελέγχου (το αιώνιο ερώτημα των φοιτητών, να βάλω < ή <=), η Go έχει μία καλύτερη και πιο ασφαλής λύση. Θα χρησιμοποιήσουμε την σύνταξη for...range και θα δώσουμε δύο μεταβλητές, η μία που θα δείχνει την θέση στην οποία βρισκόμαστε, και η άλλη την τιμή στην θέση αυτή. Βέβαια σε αυτή την περίπτωση η loop θα διαβάσει ολόκληρη την array, ενώ στις προηγούμενες περιπτώσεις είχαμε την ευκαιρία να φτιάξουμε μικρότερες λούπες όπου θα διάβαζαν μέχρι ένα συγκεκριμένο σημείο. Νομίζω όμως πως στην περίπτωση που θέλετε να διακόψετε το διάβασμα της λούπας, λογικά θα μπορείτε να κάνετε break.

for index, value := range myArray {
    // Γράψε κώδικα
}

Η εξήγηση:

Για παράδειγμα:

notes := [7]string{"C", "D", "E", "F", "G", "A", "B"}
for thesi, nota := range notes {
    fmt.Printf("Η %d νότα είναι %s.\n", thesi+1, nota)  // το thesi+1 μπήκε για ευνόητους λόγους
}

Και το output:

Η 1 νότα είναι C.
Η 2 νότα είναι D.
Η 3 νότα είναι E.
Η 4 νότα είναι F.
Η 5 νότα είναι G.
Η 6 νότα είναι A.
Η 7 νότα είναι B.

Χρησιμοποιώντας λοιπόν την range, η Go μας προφυλάσει από τυχόν λάθη, καθώς δεν γράφουμε καμία συνθήκη και δεν ασχολούμαστε με το index (στο παράδειγμα το όνομασα thesi). Οι θέσεις και οι τιμές της array εκχωρούνται αυτόματα από την γλώσσα προγραμματισμού. Στην περίπτωση που δεν μας ενδιαφέρει να αποθηκεύσουμε την τιμή της θέσης σε κάποια μεταβλητή, τότε απλά δεν την χρησιμοποιούμε:

notes := [7]string{"C", "D", "E", "F", "G", "A", "B"}
for thesi := range notes {
    fmt.Printf("Η %d νότα είναι %s.\n", thesi+1, notes[thesi])
}

Σημαντικό:: Αν δεν θέλουμε να αποθηκεύσουμε την θέση σε κάποια μεταβλητή, τότε πρέπει να χρησιμοποιήσουμε τον χαρακτήρα _ καθώς η Go κρατάει την σειρα των μεταβλητών. Η πρώτη μεταβλητή αναπαριστά την θέση και η δεύτερη την τιμή του στοιχείου στη θέση αυτή. Οπότε αν δεν μας ενδιαφέρει η τιμή, μπορούμε να την παραλήψουμε, γράφοντας μόνο την πρώτη μεταβλητή (θέση). Στην αντίθετη περίπτωση, αν δηλαδή μας απασχολεί μόνο η τιμή των στοιχείων και όχι η θέση τους, τότε έχουμε πρόβλημα, επειδή είναι η πρώτη μεταβλητή στη σειρά. Για αυτό χρησιμοποιούμε το _ ώστε να δηλώσουμε στην Go ότι δεν μας ενδιαφέρει (δείτε παρακάτω στην ενότητα με τα κλασσικά σφάλματα το παράδειγμα).

Αντιγραφή μιας array σε μία άλλη

Είναι ακριβώς το ίδιο όπως θα ήταν αν είχαμε απλές μεταβλητές, με την διαφορά ότι οι 2 arrays πρέπει να είναι ακριβώς ίδιου τύπου και ακριβώς ίδιου μεγέθους:

// Φτιάξε μία string array με μέγεθος 5 στοιχείων
var array1 [5]string

// Φτιάξε μία δεύτερη string array με μέγεθος 5 στοιχείων
// Αρχικοποίησε τα στοιχεία με ονόματα χρωμάτων
array2 := [5]string{"black", "white", "red", "green", "blue"}

// Αντέγραψε την array2 στην array1
array1 = array2

// Μετά την αντιγραφή
fmt.Println(array1) // [black white red green blue]
fmt.Println(array2) // [black white red green blue]

Σημαντικό: Κατά την αντιγραφή, οι arrays έχουν πλέον ίδιες τιμές. Όμως αυτό δεν σημαίνει ότι έχουν και την ίδια διεύθυνση μνήμης. Εχουν δημιουργηθεί δύο διαφορετικά μέρη στη μνήμη του υπολογιστή.

fmt.Println(&array1[0]) // 0x43c0c0
fmt.Println(&array2[0]) // 0x43c0f0

Δυο συγχρονισμένες arrays

Έστω ότι αντιγράφουμε μία array σε μία άλλη. Τότε αυτόματα θα είναι ίσες, αφού θα έχουν ίδιο μέγεθος, τύπο και ίδια στοιχεία. Αν αλλάξουμε όμως τα στοιχεία της πρώτης, τότε θα πάψουν πλέον να είναι ίσες:

// Φτιάξε μία string array με μέγεθος 3 στοιχείων
var array1 [3]string

// Φτιάξε μία δεύτερη string array με μέγεθος 3 στοιχείων
// Αρχικοποίησε τα στοιχεία με ονόματα χρωμάτων
array2 := [3]string{"red", "green", "blue"}

// Αντέγραψε την array2 στην array1
array1 = array2

// Μετά την αντιγραφή σύγκρινε μεταξύ τους
if array1 == array2 {
    fmt.Println("Ίσες")     // Αυτό θα δείξει η έξοδος
} else {
    fmt.Println("Άνισες")
}

// Τώρα άλλαξε το δεύτερο στοιχείο της array2
array2[1] = "Purple"

// Σύγκρινε ξανά
if array1 == array2 {
    fmt.Println("Ίσες")
} else {
    fmt.Println("Άνισες") // Αυτό θα δείξει η έξοδος
}

fmt.Println(array1) // [red green blue]
fmt.Println(array2) // [red Purple blue]

Αν θέλουμε να φτιάξουμε δύο arrays που θα είναι για πάντα ίσες μεταξύ τους, δηλαδή ότι συμβαίνει στην μία, το ίδιο να συμβαίνει αυτόματα και στα στοιχεία της άλλης, τότε χρειαζόμαστε 2 arrays pointers, όπου θα δείχνουν στις ίδιες διευθύνσεις μνήμης.

array1 := [3]*string{}  // Φτιάξε μία string pointer array με 3 στοιχεία
array2 := [3]*string{new(string), new(string), new(string)} // Φτιάξε και αρχικοποιήσε μία string pointer array με 3 στοιχεία

// Βάλε τιμές στα στοιχεία
*array2[0] = "Red"
*array2[1] = "Green"
*array2[2] = "Blue"

// Αντέγραψε τη μία array στην άλλη
array1 = array2

for i := 0; i < len(array1); i++ {
    fmt.Printf("Το στοιχείο array1[%d] δείχνει στη διεύθυνση %p και έχει την τιμή %s\n", i, array1[i], *array1[i])
    fmt.Printf("Το στοιχείο array2[%d] δείχνει στη διεύθυνση %p και έχει την τιμή %s\n", i, array2[i], *array2[i])
    fmt.Println("--")
}

if array1 == array2 {
    fmt.Println("Ίσες")
} else {
    fmt.Println("Άνισες")
}

*array2[1] = "Purple"   // Αλλαζω το 2ο στοιχείο στην array2

for i := 0; i < len(array1); i++ {
    fmt.Printf("Το στοιχείο array1[%d] δείχνει στη διεύθυνση %p και έχει την τιμή %s\n", i, array1[i], *array1[i])
    fmt.Printf("Το στοιχείο array2[%d] δείχνει στη διεύθυνση %p και έχει την τιμή %s\n", i, array2[i], *array2[i])
    fmt.Println("--")
}

if array1 == array2 {
    fmt.Println("Ίσες") // Θα είναι πάλι ίσες
} else {
    fmt.Println("Άνισες")
}

Η έξοδος θα είναι:

Το στοιχείο array1[0] δείχνει στη διεύθυνση 0x40c138 και έχει την τιμή Red
Το στοιχείο array2[0] δείχνει στη διεύθυνση 0x40c138 και έχει την τιμή Red
--
Το στοιχείο array1[1] δείχνει στη διεύθυνση 0x40c140 και έχει την τιμή Green
Το στοιχείο array2[1] δείχνει στη διεύθυνση 0x40c140 και έχει την τιμή Green
--
Το στοιχείο array1[2] δείχνει στη διεύθυνση 0x40c148 και έχει την τιμή Blue
Το στοιχείο array2[2] δείχνει στη διεύθυνση 0x40c148 και έχει την τιμή Blue
--
Ίσες
Το στοιχείο array1[0] δείχνει στη διεύθυνση 0x40c138 και έχει την τιμή Red
Το στοιχείο array2[0] δείχνει στη διεύθυνση 0x40c138 και έχει την τιμή Red
--
Το στοιχείο array1[1] δείχνει στη διεύθυνση 0x40c140 και έχει την τιμή Purple
Το στοιχείο array2[1] δείχνει στη διεύθυνση 0x40c140 και έχει την τιμή Purple
--
Το στοιχείο array1[2] δείχνει στη διεύθυνση 0x40c148 και έχει την τιμή Blue
Το στοιχείο array2[2] δείχνει στη διεύθυνση 0x40c148 και έχει την τιμή Blue
--
Ίσες

Πέρασμα arrays σε functions

By value

By default όταν περνάμε μία array σε μία function, τότε αυτόματα περνάει ένα αντίγραφο αυτής. Δηλαδή στέλνει μόνο τις τιμές, με αποτέλεσμα οι οποισδήποτε αλλαγές γίνουν στην array μέσα στην function να μην έχουν κανένα αποτέλεσμα στην array που βρίσκεται εκτός της function.

func edit(colors [3]string) {
	for i := range colors {
		colors[i] = "Edited " + colors[i]
	}
	fmt.Println(colors) // [Edited Red Edited Green Edited Blue]
}

func main() {
	colors := [3]string{
		"Red",
		"Green",
		"Blue",
    }
    
	fmt.Println(colors) // [Red Green Blue]
	edit(colors)
	fmt.Println(colors) // [Red Green Blue] -- δεν άλλαξε

}

Οπότε η συνάρτηση edit στο παραπάνω παράδειγμα είναι σχετικά άχρηστη, αφού οι αλλαγές που κάνει στην array δεν πραγματοποιούνται στην πραγματικότητα αλλά γίνονται μόνο τοπικά μέσα στο namespace της συνάρτησης και δεν περνάνε πίσω στην main. Για να περάσουμε τις αλλαγές, θα πρέπει να καλέσουμε την array με την διεύθυνσή της χρησιμοποιώντας pointer.

By Pointer

func edit(colors *[3]string) {  // Φτιάξε έναν pointer για να αποθηκεύσεις την διεύθυνση της array
	for i := range colors {
		colors[i] = "Edited " + colors[i]
	}
	fmt.Println(colors) // &[Edited Red Edited Green Edited Blue]
}

func main() {
	colors := [3]string{
		"Red",
		"Green",
		"Blue",
	}
	fmt.Println(colors) // [Red Green Blue]

	edit(&colors)       // Πέρνα την διεύθυνση της array
	fmt.Println(colors) // [Edited Red Edited Green Edited Blue] -- άλλαξε
}

Σημαντικό: Να σημειώσουμε μία διαφορά:

colors *[3]string // Pointer σε string array 3 στοιχείων
colors [3]*string // Array με 3 pointers σε string στοιχεία

Αν βάλουμε το αστεράκι σε λάθος θέση, τότε θα έχουμε 2 σφάλματα:

./prog.go:9:25: invalid operation: "Edited " + colors[i] (mismatched types string and *string)
./prog.go:22:6: cannot use colors (type [3]string) as type [3]*string in argument to edit

Γιατί είναι καλύτερη αυτή η μέθοδος;

Άσχετα με το αν μας ενδιαφέρει να πειράξουμε την συνάρτησή μας ή όχι, υπάρχει κι ένας ακόμα λόγος που κάνει την μέθοδο pass by pointer πιο αποδοτική από την pass by value και αυτός είναι ο χώρος της μνήμης. Φανταστείτε ότι έχουμε μία array των 8 megabyte. Για να το πετύχουμε αυτό θα χρησιμοποιήσουμε την συντόμευση 1e6 το οποίο σημαίνει 1*10^6 που μας κάνει 1.000.000. Ένα εκατομμύριο integers (όπου σε 64-bit CPU ο ένας integer θέλει 8 byte) είναι περίπου 8.000.000 bytes, δηλαδή περίπου 8 megabyte.

// Φτιάξε μία array που καταλαμβάνει 8 mb στη μνήμη
var myBigFatArray [1e6]int

// Πάσαρε την array σε μία συνάρτηση
foo(myBigFatArray)

// Η function foo υποδέχεται την array των 8.000.000 integers
func foo(array [1e6]int) {}

Και μόλις τώρα σπαταλήσαμε 8 mb επιπλέον, δηλαδή το πρόγραμμά μας χρειάζεται 16mb μνήμης για να τρέξει. Ο πιο αποδοτικός τρόπος θα ήταν να χρησιμοποιήσουμε pointers, όπου εκεί αντί να αντιγράψουμε 8mb, θα αντιγράψουμε μόλις 8 bytes.

var myBigFatArray [1e6]int // Δεσμεύει 8 mb
foo(&myBigFatArray) // Στέλνει 8 byte
fun foo(array *[1e6]int) // υποδέχεται 8 byte

Υπενθύμιση: Στις συναρτήσεις δεν χρειάζεται να χρησιμοποιούμε τα ίδια ονόματα των παραμέτρων/ορισμάτων.

Κλασσικά σφάλματα

Όπως είπαμε νωρίτερα, οι arrays είναι συλλογές στοιχείων με καθορισμένο αριθμό (μέγεθος) και data type, όπου δεν μπορεί να αλλάξει κατόπιν της δημιουργίας τους. Δεν μπορούμε δηλαδή να βάλουμε σε μία string array έναν integer, ούτε να βάλουμε 5 στοιχεία σε μία array με μέγεθος 4. Πάμε να δούμε τα κλασσικά μυνήματα λάθους που προκύπτουν στις παραπάνω περιπτώσεις:

invalid array index

Στην περίπτωση που προσπαθήσουμε να αποκτήσουμε πρόσβαση σε θέση (index) που είναι μεγαλύτερη από το πλήθος των στοιχείων της array τότε το πρόγραμμά κρασάρει με τα παρακάτω μυνήματα λάθους:

Σημείωση: Στο παρακάτω παράδειγμα η array μας έχει μέγεθος 4, αλλά το 4ο στοιχείο βρίσκεται στη θέση [3]. Ναι, είναι λίγο παράξενο αυτό, αλλά να θυμάστε ότι οι arrays – όπως και τα strings – ξεκινάνε να μετράνε από το μηδέν.

var myArray [4]string
fmt.Println(myArray[4])     // invalid array index 4 (out of bounds for 4-element array)
myArray[8] = "Kati tetoio"  // invalid array index 8 (out of bounds for 4-element array)

Σε μερικές γλώσσες προγραμματισμού, το να χρησιμοποιήσουμε αρνητικούς αριθμούς ως index είναι κάτι φυσιολογικό και ερμηνεύεται ως η αντίστροφη θέση, ξεκινώντας από το τέλος. Πχ στην Python το [-1] είναι ίδιο στοιχείο με το τελευταιο, δηλαδή [3]

# Φτιάξε μία array με 4 στοιχεία τύπου string
>>> myArray = ["proto", "deutero", "trito", "tetarto"]
>>> print(myArray[3])   # Εμφάνισε το τελευταίο (4ο στοιχείο)
tetarto
>>> print(myArray[-1])  # Λειτουργεί κανονικά και αντιστοιχεί με το τελευταίο στοιχείο
tetarto

Στην Go όμως δεν μπορούμε να κάνουμε κάτι αντίστοιχο. Δεν επιτρέπεται λοιπόν να χρησιμοποιούμε αρνητικούς αριθμούς για να αποκτήσουμε πρόσβαση στα στοιχεία μιας array.

var myArray [4]string
fmt.Println(myArray[-1])    // invalid array index -1 (index must be non-negative)

Επίσης, μιλώντας για την τιμή του index, δηλαδή της θέσης, αυτή πρέπει να είναι ακέραιο νούμερο, διαφορετικά παίρνουμε μύνημα σφάλματος. Επίσης, αν θέλουμε να αναφερθούμε σε περισσότερες από μία θέσεις (πχ στην [0] και την [1] τότε θέλει λίγο προσοχή):

var myArray [4]string
fmt.Println(myArray["a"])     // non-integer array index "a"
fmt.Println(myArray[0,1])     // syntax error: unexpected comma, expecting 

Αν θέλουμε να αναφερθούμε στα στοιχεία στις θέσεις 0 και 1 θα πρέπει να το κάνουμε χρησιμοποιώντας ξανά την λέξη myArray, δηλαδή: fmt.Println(myArray[0], myArray[1]).

cannot use (type X) as type Y in assignment

Αν έχουμε μία array που χωράει μήλα, δεν μπορούμε να πάμε και να βάλουμε στις ίδιες θέσεις καρπούζια. Αντίστοιχα και εδώ, αν έχουμε μία array που δέχεται ένα συγκεκριμένο data type, δεν μπορούμε να πάμε και να βάλουμε κάποιο άλλο. Υπενθυμίζουμε ότι το data type δεν αλλάζει στις arrays.

var myArray [4]string       // Φτιάξε μία array με string στοιχεία

// Τώρα δοκίμασε να βάλεις στην 2η θέση έναν integer, το 5
myArray[2] = 5              // cannot use 5 (type int) as type string in assignment

myArray is not a type

Υπενθυμίζουμε ξανά ότι η πρόσβαση στα στοιχεία της Array γίνεται χρησιμοποιώντας square brackets []. Διαφορετικά θα πάρουμε μυνήματα λαθους:

var myArray [4]string
fmt.Println(myArray{0}) // myArray is not a type
fmt.Println(myArray(0)) // cannot call non-function myArray (type [4]string)

declared and not used

Στην περίπτωση που χρησιμοποιούμε την σύνταξη for...range και δεν χρειαστούμε να χρησιμοποιήσουμε την μεταβλητή που αναλογεί στην θέση (index), τότε θα πάρουμε το παρακάτω σφάλμα:

for thesi, nota := range notes {
    fmt.Println(nota)
}
// thesi declared and not used

Για να το φτιάξουμε τότε απλά βάζουμε στην θέση της μεταβλητής τον blank identifier δηλαδή το _ το οποίο δηλώνει ότι δεν θα χρησιμοποιήσουμε αυτή τη μεταβλητή. Συνεπώς ο κώδικας θα γραφόταν:

for _, nota := range notes {
    fmt.Println(nota)
}

cannot use array (type Χ) as type Υ in assignment

Όταν προσπαθούμε να αντιγράψουμε μία array σε μία άλλη, τότε πρέπει και οι δυο να είναι ακριβώς το ίδιο type και να έχουν το ίδιο μέγεθος. Διαφορετικά, ακόμα κι αν αντιγράφουμε την μικρότερη στην μεγαλύτερη, τότε θα πάρουμε σφάλμα:

var array1 [10]string
var array2 [5]string
array1 = array2 // cannot use array2 (type [5]string) as type [10]string in assignment
array2 = array1 // cannot use array1 (type [10]string) as type [5]string in assignment

invalid operation (mismatched types X and Y)

Κατά την σύγκριση δύο array πρέπει να είναι και πάλι ακριβώς ίδιου τύπου και ακριβώς ίδιου μεγέθους. Διαφορετικά θα δούμε μύνημα σφάλματος:

	var array1 [10]string
	var array2 [5]string
	if array1 == array2 {       // invalid operation: array1 == array2 (mismatched types [10]string and [5]string)
		fmt.Println("Ises")
	} else {
		fmt.Println("Anises")
    }
Σχόλια powered by Disqus