FREE

ΕΝΟΤΗΤΑ 14 GO - Pointers

Στο σημερινό δωρεάν μάθημα GO θα εξηγήσουμε μέσα από απλά παραδείγματα τι ακριβώς είναι οι pointers στην GO και πως τους χρησιμοποιούμε με μεταβλητές αλλά και με functions.

Πριν όμως μιλήσουμε για τους pointers, πρέπει να κατανοήσουμε πως συμπεριφέρεται η GO χωρίς αυτούς. Όταν έχουμε τιμές, είδους int, bool ή ακόμα και string, τις οποίες περνάμε σε ένα function, η GO παίρνει ένα αντίγραφο αυτών των τιμών και τις χρησιμοποιεί. Αντίγραφο σημαίνει ότι όποιες αλλαγές και αν κάνει εσωτερικά η function στην μεταβλητή, η τιμή της αρχικής μεταβλητής (εκτός function) παραμένει αναλλοίωτη.

Περνώντας τιμές σε μια function με αυτό τον τρόπο, μας επιφέρει λιγότερα λάθη στην ανάπτυξη του κώδικα μας. Επίσης η GO χρησιμοποιεί ένα υποσύνολο της μνήμης που ονομάζεται stack για να διαχειριστεί τα αντίγραφα των τιμών. Το αρνητικό όμως σε αυτή την προσέγγιση είναι ότι καθώς η μια function καλεί μια άλλη αντιγράφονται οι τιμές και χρησιμοποιείται όλο και περισσότερη μνήμη. Σε μεγάλα πολύπλοκα εταιρικά προγράμματα υπάρχουν αρκετές δεκάδες functions που καλεί η μια την άλλη. Το συμπέρασμα είναι ότι πολλές φορές χρησιμοποιούμε περισσότερη μνήμη από ότι χρειαζόμαστε.

Υπάρχει μια εναλλακτική προσέγγιση σε αυτό το πρόβλημα που χρησιμοποιεί λιγότερη μνήμη και ονομάζεται pointer. Αντί να περνάμε μια τιμή στην function περνάμε την τοποθεσία της μνήμης στην οποία βρίσκεται η τιμή. Ο pointer δεν έχει τιμή από μόνος του. Απλά δίνει “οδηγίες” σε αυτούς που το χρησιμοποιούν που να βρουν την τιμή που χρειάζονται. Το συμπέρασμα σε αυτή την προσέγγιση είναι ότι δεν δημιουργείται ένα αντίγραφο της τιμής της μεταβλητής.

Η GO, τους pointers, δεν τους αποθηκεύει στο stack αλλά στο heap. Η περιοχή της μνήμης που ονομάζεται heap επιτρέπει μια τιμή να υπάρχει αποθηκευμένη εκεί μέχρι να μην υπάρχει κανένας pointer που να δείχνει σε αυτή. Όταν αυτό γίνει, τότε μια διαδικασία που ονομάζεται garbage collection και τρέχει σε τακτά χρονικά διαστήματα, έρχεται να καθαρίσει τις τιμές. Σε αυτή την διαδικασία δεν έχουμε πρόσβαση και ορίζεται εξ ολοκλήρου από το σύστημα. Οπότε, εκτός από ότι γνωρίζουμε ότι υπάρχει, δεν θα απασχολήσει ξανά.

Οι pointers γενικότερα μας βοηθάνε στο να δημιουργήσουμε καλύτερο κώδικα αν χρησιμοποιηθούν σωστά. Για παράδειγμα, μπορούμε πολύ εύκολα να ανακαλύψουμε εάν μια μεταβλητή περιέχει τιμή ή όχι ελέγχοντας εάν ο pointer είναι nil. Η τιμή nil είναι ένα ειδικό type στην GO που περιγράφει την κατάσταση μιας μεταβλητής όταν δεν περιέχει καμία τιμή. Συνήθως για να αποφύγουμε τυχόν λάθη στην εκτέλεση του προγράμματος μας κάνουμε τον εξής έλεγχο: pointer != nil.

Για να δημιουργήσουμε ένα pointer έχουμε αρκετές επιλογές. Η πρώτη είναι να δηλώσουμε μια μεταβλητή να είναι είδος pointer χρησιμοποιώντας την λέξη κλειδί var, μετά το όνομα της μεταβλητής και αμέσως μετά τον αστερίσκο * με το όνομα του type. Για παράδειγμα var myPointer *int. H αρχική τιμή της μεταβλητής pointer που ορίζεται με αυτό τον τρόπο είναι nil. Για να αναθέσουμε τον pointer που δημιουργήσαμε σε μια μεταβλητή χρησιμοποιούμε το & όπως var1 := &var2. Τέλος τυπώνοντας τον pointer θα δούμε την διεύθυνση της μνήμης στην οποία δείχνει ο pointer ενώ την τιμή στην οποία δείχνει ο pointer μπορούμε να την λάβουμε χρησιμοποιώντας *pointerName. Ας δούμε πως λειτουργεί αυτός ο τρόπος με ένα απλό παράδειγμα.

main.go


package main

import "fmt"

func main(){
    var myVariable int = 3
    var myPointer *int
    myPointer = &myVariable
    fmt.Println(myPointer)
    fmt.Println(*myPointer)
    *myPointer = 5
    fmt.Println(*myPointer)
    fmt.Println(myVariable)
    }

Output


0xc0000180b0
3
5
5

Σε αυτό το απλό παράδειγμα, δημιουργήσαμε μια int μεταβλητή με το όνομα myVariable και της αναθέσαμε την τιμή 3. Επίσης, δημιουργήσαμε έναν pointer είδος int με το όνομα myPointer. Μετά αναθέσαμε την μεταβλητή στον pointer. Τώρα μπορούμε να δείχνουμε με το όνομα του pointer την τοποθεσία της μνήμης που βρίσκεται η τιμή, ή αν βάλουμε μπροστά από το όνομα τον αστερίσκο (*) μπορούμε να δείχνουμε στην ίδια την τιμή. Αλλάζουμε την τιμή του pointer σε 5. Αυτό φυσικά επηρεάζει εκτός από τον pointer και την ίδια την μεταβλητή αφού ο pointer δείχνει σε αυτή.

Ο δεύτερος τρόπος για να ορίσουμε έναν pointer είναι να χρησιμοποιήσουμε την new function που μας προσφέρει η GO. Αυτή η function επιστρέφει μια διεύθυνση της μνήμης για οποιοδήποτε type του ορίσουμε. Ο γενικός κανόνας είναι variable :=new(type). Ας το δούμε σε ένα απλό παράδειγμα.

main.go

    
package main

import "fmt"

func main(){
    var myVariable int = 3
    myPointer :=new(int)
    myPointer = &myVariable
    fmt.Println(myPointer)
    fmt.Println(*myPointer)
    *myPointer = 5
    fmt.Println(*myPointer)
    fmt.Println(myVariable)
}
    
    

Output

    
0xc0000180b0
3
5
5
    
    

Ο τρίτος τρόπος να ορίσουμε έναν pointer είναι από μια υπάρχουσα μεταβλητή χρησιμοποιώντας το &. Ας δούμε και αυτό τον τρόπο.

main.go


package main

import "fmt"

func main(){
    var myVariable int = 3
    myPointer := &myVariable
    fmt.Println(myPointer)
    fmt.Println(*myPointer)
    *myPointer = 5
    fmt.Println(*myPointer)
    fmt.Println(myVariable)
}

Output


0xc0000180b0
3
5
5

Όπως ήδη έχουμε αναφέρει, είναι συνηθισμένο λάθος να προσπαθήσουμε να τυπώσουμε κάποια τιμή στην οποία υποθέτουμε ότι δείχνει ένας pointer αλλά στην ουσία η τιμή του είναι nil. Για αυτό το λόγο, είναι φρόνιμο και κοινή πρακτική από τους προγραμματιστές της GO να κάνουν πρώτα ένα μικρό έλεγχο για την κατάσταση του pointer πριν ζητήσουμε κάποιες τιμές.

main.go


package main

import "fmt"

func main(){
    var myVariable int = 3
    myPointer := &myVariable
    if myPointer != nil {
        fmt.Println(*myPointer)
    }
    *myPointer = 5
    if myPointer != nil {
        fmt.Println(*myPointer)
        fmt.Println(myVariable)
    }
}

Output


3
5
5

Αφού έχουμε αποκτήσει μια μικρή εμπειρία στο πως λειτουργούν οι pointers, ας τους δούμε τώρα σε συνδυασμό με τα functions. Στο παρακάτω παράδειγμα, δημιουργούμε δύο functions. Η πρώτη function δέχεται έναν αριθμό μέσω μιας μεταβλητής (τον συνηθισμένο τρόπο), προσθέτει τον αριθμό 10, και μετά τυπώνει το αποτέλεσμα στην κονσόλα. Η δεύτερη function δέχεται έναν pointer, προσθέτει 10 και μετά τυπώνει το αποτέλεσμα στην κονσόλα. Μετά το τέλος της εκτέλεσης της κάθε function, τυπώνουμε την τιμή της αρχικής μεταβλητής για να δούμε πως η κάθε function την έχει επηρεάσει.

main.go


package main

import "fmt"

func main(){
    var mainVariable int
    changeValue(mainVariable)
    fmt.Println("mainVariable is: ", mainVariable)
    changeValuePointer(&mainVariable)
    fmt.Println("mainVariable is: ", mainVariable)
}

func changeValue(myVar int){
    myVar += 10
    fmt.Println("from changeValue")
    fmt.Println("myVar value is :", myVar)
}

func changeValuePointer(myVar *int){
    *myVar += 10
    fmt.Println("from changeValuePointer")
    fmt.Println("myVar value is :", *myVar)
}
    

Output


from changeValue
myVar value is : 10
mainVariable is:  0
from changeValuePointer
myVar value is : 10
mainVariable is:  10