Använda metoder i Go

Slutförd

En metod i Go är en särskild typ av funktion med en enkel skillnad: du måste inkludera en extra parameter före funktionsnamnet. Den här extra parametern kallas mottagaren.

Metoder är användbara när du vill gruppera funktioner och koppla dem till en anpassad typ. Den här metoden i Go liknar att skapa en klass i andra programmeringsspråk, eftersom den gör att du kan implementera vissa funktioner från den objektorienterade programmeringsmodellen (OOP), till exempel inbäddning, överlagring och inkapsling.

För att förstå varför metoder är viktiga i Go börjar vi med hur du deklarerar en.

Deklarera metoder

Hittills har du bara använt structs som en annan anpassad typ som du kan skapa i Go. I den här modulen får du lära dig att du genom att lägga till metoder kan lägga till beteenden i de strukturer som du skapar.

Syntaxen för att deklarera en metod är ungefär så här:

func (variable type) MethodName(parameters ...) {
    // method functionality
}

Men innan du kan deklarera en metod måste du skapa en struct. Anta att du vill skapa ett geometripaket och som en del av paketet bestämmer du dig för att skapa en triangel struct med namnet triangle. Sedan vill du använda en metod för att beräkna triangelns perimeter. Du kan representera den i Go så här:

type triangle struct {
    size int
}

func (t triangle) perimeter() int {
    return t.size * 3
}

Structen ser ut som en normal, men perimeter() funktionen har en extra parameter av typen triangle före funktionsnamnet. Den här mottagaren innebär att när du använder structen kan du anropa funktionen så här:

func main() {
    t := triangle{3}
    fmt.Println("Perimeter:", t.perimeter())
}

Om du försöker anropa perimeter() funktionen som normalt fungerar den inte eftersom funktionens signatur säger att den behöver en mottagare. Det enda sättet att anropa den metoden är att deklarera en struct först, vilket ger dig åtkomst till metoden. Du kan till och med ha samma namn för en metod så länge den tillhör en annan struct. Du kan till exempel deklarera en square struct med en perimeter() funktion, så här:

package main

import "fmt"

type triangle struct {
    size int
}

type square struct {
    size int
}

func (t triangle) perimeter() int {
    return t.size * 3
}

func (s square) perimeter() int {
    return s.size * 4
}

func main() {
    t := triangle{3}
    s := square{4}
    fmt.Println("Perimeter (triangle):", t.perimeter())
    fmt.Println("Perimeter (square):", s.perimeter())
}

När du kör föregående kod ser du att det inte finns något fel och att du får följande utdata:

Perimeter (triangle): 9
Perimeter (square): 16

Från de två anropen perimeter() till funktionen avgör kompilatorn vilken funktion som ska anropas baserat på mottagartypen. Det här beteendet hjälper till att hålla konsekvens och korta namn i funktioner mellan paket och undviker att inkludera paketnamnet som ett prefix. Vi kommer att prata om varför det här beteendet kan vara viktigt när vi går igenom gränssnitt i nästa lektion.

Pekare i metoder

Det kommer att finnas tillfällen då en metod behöver uppdatera en variabel. Om argumentet till metoden är för stort kanske du vill undvika att kopiera det. I dessa fall måste du använda pekare för att skicka adressen till en variabel. I en tidigare modul, när vi diskuterade pekare, sa vi att varje gång du anropar en funktion i Go gör Go en kopia av varje argumentvärde för att använda den.

Samma beteende finns när du behöver uppdatera mottagarvariabeln i en metod. Anta till exempel att du vill skapa en ny metod för att dubbla triangelstorleken. Du måste använda en pekare i mottagarvariabeln, så här:

func (t *triangle) doubleSize() {
    t.size *= 2
}

Du kan bevisa att metoden fungerar så här:

func main() {
    t := triangle{3}
    t.doubleSize()
    fmt.Println("Size:", t.size)
    fmt.Println("Perimeter:", t.perimeter())
}

När du kör föregående kod bör du få följande utdata:

Size: 6
Perimeter: 18

Du behöver ingen pekare i mottagarvariabeln när metoden bara kommer åt mottagarens information. Go-konventionen föreskriver dock att om någon metod för en struct har en pekarmottagare måste alla metoder för den structen ha en pekarmottagare. Även om en metod för structen inte behöver den.

Deklarera metoder för andra typer

En viktig aspekt av metoderna är att definiera dem för alla typer, inte bara för anpassade typer som structs. Du kan dock inte definiera en struct från en typ som tillhör ett annat paket. Därför kan du inte skapa en metod för en grundläggande typ, till exempel en string.

Ändå kan du använda ett hack för att skapa en anpassad typ från en grundläggande typ och sedan använda den som om det var den grundläggande typen. Anta till exempel att du vill skapa en metod för att transformera en sträng från gemener till versaler. Du kan skriva ungefär så här:

package main

import (
    "fmt"
    "strings"
)

type upperstring string

func (s upperstring) Upper() string {
    return strings.ToUpper(string(s))
}

func main() {
    s := upperstring("Learning Go!")
    fmt.Println(s)
    fmt.Println(s.Upper())
}

När du kör föregående kod får du följande utdata:

Learning Go!
LEARNING GO!

Observera hur du kan använda det nya objektet s som om det vore en sträng när du först skriver ut dess värde. När du anropar Upper metoden s skriver du sedan ut alla versaler av typen sträng.

Inbäddningsmetoder

I en tidigare modul lärde du dig att du kan använda en egenskap i en struct och bädda in samma egenskap i en annan struct. Det innebär att du kan återanvända egenskaper från en struct för att undvika upprepning och behålla konsekvens i din kodbas. En liknande idé gäller för metoder. Du kan anropa metoder för den inbäddade structen även om mottagaren är annorlunda.

Anta till exempel att du vill skapa en ny triangel struct med logik för att inkludera en färg. Dessutom vill du fortsätta att använda triangeln som du deklarerade tidigare. Så skulle den färgade triangeln struct se ut så här:

type coloredTriangle struct {
    triangle
    color string
}

Du kan sedan initiera structen coloredTriangle och anropa perimeter() metoden från triangle struct (och till och med komma åt dess fält), så här:

func main() {
    t := coloredTriangle{triangle{3}, "blue"}
    fmt.Println("Size:", t.size)
    fmt.Println("Perimeter", t.perimeter())
}

Gå vidare och ta med de föregående ändringarna i programmet för att se hur inbäddning fungerar. När du kör programmet med en main() metod som den föregående bör du få följande utdata:

Size: 3
Perimeter 9

Om du är bekant med ett OOP-språk som Java eller C++, kanske du tror att structen triangle ser ut som en basklass och coloredTriangle är en underklass (till exempel arv), men det är inte korrekt. Det som händer i verkligheten är att Go-kompilatorn marknadsför perimeter() metoden genom att skapa en omslutningsmetod som ser ut ungefär så här:

func (t coloredTriangle) perimeter() int {
    return t.triangle.perimeter()
}

Observera att mottagaren är coloredTriangle, som anropar perimeter() metoden från triangelfältet. Den goda nyheten är att du inte behöver skapa föregående metod. Det kan du, men Go gör det åt dig under huven. Vi inkluderade endast föregående exempel i utbildningssyfte.

Överlagringsmetoder

Nu ska vi gå tillbaka till exemplet triangle som vi diskuterade tidigare. Vad händer om du vill ändra implementeringen av perimeter() metoden i structen coloredTriangle ? Du kan inte ha två funktioner med samma namn. Men eftersom metoderna behöver en extra parameter (mottagaren) får du ha en metod med samma namn så länge den är specifik för den mottagare som du vill använda. Att använda den här skillnaden är hur du överbelastar metoder.

Med andra ord kan du skriva omslutningsmetoden som vi diskuterade om du vill ändra dess beteende. Om perimetern för en färgad triangel är dubbelt så stor som omkretsen för en normal triangel, skulle koden vara ungefär så här:

func (t coloredTriangle) perimeter() int {
    return t.size * 3 * 2
}

Nu, utan att ändra något annat i metoden main() du skrev tidigare, skulle det se ut så här:

func main() {
    t := coloredTriangle{triangle{3}, "blue"}
    fmt.Println("Size:", t.size)
    fmt.Println("Perimeter", t.perimeter())
}

När du kör den får du olika utdata:

Size: 3
Perimeter 18

Men om du fortfarande behöver anropa perimeter() metoden från structen triangle kan du göra det genom att komma åt den explicit, så här:

func main() {
    t := coloredTriangle{triangle{3}, "blue"}
    fmt.Println("Size:", t.size)
    fmt.Println("Perimeter (colored)", t.perimeter())
    fmt.Println("Perimeter (normal)", t.triangle.perimeter())
}

När du kör den här koden bör du få följande utdata:

Size: 3
Perimeter (colored) 18
Perimeter (normal) 9

Som du kanske har märkt kan du i Go åsidosätta en metod och fortfarande komma åt den ursprungliga metoden om du behöver den.

Inkapsling i metoder

Inkapsling innebär att en metod inte är tillgänglig för anroparen (klienten) för ett objekt. I andra programmeringsspråk placerar du vanligtvis nyckelorden private eller public före metodnamnet. I Go behöver du bara använda en versaliserad identifierare för att göra en metod offentlig och en icke-kapitaliserad identifierare för att göra en metod privat.

Inkapsling i Go börjar endast gälla mellan paket. Med andra ord kan du bara dölja implementeringsinformation från ett annat paket, inte själva paketet.

Om du vill prova det skapar du ett nytt paket geometry och flyttar triangeln där, så här:

package geometry

type Triangle struct {
    size int
}

func (t *Triangle) doubleSize() {
    t.size *= 2
}

func (t *Triangle) SetSize(size int) {
    t.size = size
}

func (t *Triangle) Perimeter() int {
    t.doubleSize()
    return t.size * 3
}

Du kan använda föregående paket, så här:

func main() {
    t := geometry.Triangle{}
    t.SetSize(3)
    fmt.Println("Perimeter", t.Perimeter())
}

Och du bör få följande utdata:

Perimeter 18

Om du försöker anropa fältet size eller doubleSize() metoden från main() funktionen får programmet panik, så här:

func main() {
    t := geometry.Triangle{}
    t.SetSize(3)
    fmt.Println("Size", t.size)
    fmt.Println("Perimeter", t.Perimeter())
}

När du kör föregående kod får du följande fel:

./main.go:12:23: t.size undefined (cannot refer to unexported field or method size)