Używanie metod w języku Go

Ukończone

Metoda w języku Go to specjalny typ funkcji z prostą różnicą: przed nazwą funkcji należy dołączyć dodatkowy parametr. Ten dodatkowy parametr jest znany jako odbiornik.

Metody są przydatne, gdy chcesz zgrupować funkcje i powiązać je z typem niestandardowym. To podejście w języku Go jest podobne do tworzenia klasy w innych językach programowania, ponieważ umożliwia zaimplementowanie niektórych funkcji z modelu programowania obiektowego (OOP), takich jak osadzanie, przeciążanie i hermetyzacja.

Aby zrozumieć, dlaczego metody są ważne w języku Go, zacznijmy od sposobu deklarowania metody.

Deklarowanie metod

Do tej pory użyto struktur tylko jako innego typu niestandardowego, który można utworzyć w języku Go. W tym module dowiesz się, że dodając metody, można dodawać zachowania do tworzonych struktur.

Składnia do deklarowania metody jest podobna do następującej:

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

Jednak zanim będzie można zadeklarować metodę, musisz utworzyć strukturę. Załóżmy, że chcesz utworzyć pakiet geometryczny, a w ramach tego pakietu zdecydujesz się utworzyć strukturę trójkąta o nazwie triangle. Następnie chcesz użyć metody do obliczenia obwodu tego trójkąta. Możesz je przedstawić w języku Go w następujący sposób:

type triangle struct {
    size int
}

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

Struktura wygląda jak normalna, ale perimeter() funkcja ma dodatkowy parametr typu triangle przed nazwą funkcji. Ten odbiornik oznacza, że w przypadku używania struktury można wywołać funkcję w następujący sposób:

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

Jeśli spróbujesz wywołać perimeter() funkcję w zwykły sposób, nie będzie działać, ponieważ podpis funkcji mówi, że potrzebuje odbiornika. Jedynym sposobem wywołania tej metody jest zadeklarowanie najpierw struktury, która zapewnia dostęp do metody. Można nawet mieć taką samą nazwę metody, o ile należy do innej struktury. Można na przykład zadeklarować square strukturę za pomocą perimeter() funkcji, w następujący sposób:

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())
}

Po uruchomieniu poprzedniego kodu zwróć uwagę, że nie ma błędu i otrzymujesz następujące dane wyjściowe:

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

Z dwóch wywołań funkcji perimeter() kompilator określa, która funkcja ma być wywoływana na podstawie typu odbiorcy. To zachowanie pomaga zachować spójność i krótkie nazwy w funkcjach między pakietami i unikać dołączania nazwy pakietu jako prefiksu. Omówimy, dlaczego to zachowanie może być ważne podczas omawiania interfejsów w następnej lekcji.

Wskaźniki w metodach

Czasami metoda musi zaktualizować zmienną. Ewentualnie, jeśli argument metody jest zbyt duży, możesz uniknąć kopiowania. W tych wystąpieniach należy użyć wskaźników, aby przekazać adres zmiennej. W poprzednim module, kiedy omówiliśmy wskaźniki, powiedzieliśmy, że za każdym razem, gdy wywołujesz funkcję w języku Go, narzędzie Go tworzy kopię każdej wartości argumentu do użycia.

To samo zachowanie występuje, gdy trzeba zaktualizować zmienną odbiornika w metodzie. Załóżmy na przykład, że chcesz utworzyć nową metodę, aby podwoić rozmiar trójkąta. Należy użyć wskaźnika w zmiennej odbiornika w następujący sposób:

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

Możesz udowodnić, że metoda działa w następujący sposób:

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

Po uruchomieniu poprzedniego kodu powinny zostać wyświetlone następujące dane wyjściowe:

Size: 6
Perimeter: 18

Nie potrzebujesz wskaźnika w zmiennej odbiornika, gdy metoda uzyskuje tylko dostęp do informacji odbiorcy. Jednak konwencja Języka Go określa, że jeśli jakakolwiek metoda struktury ma odbiornik wskaźnika, wszystkie metody tej struktury muszą mieć odbiornik wskaźnika. Nawet jeśli metoda struktury nie potrzebuje jej.

Deklarowanie metod dla innych typów

Jednym z kluczowych aspektów metod jest zdefiniowanie ich dla dowolnego typu, a nie tylko dla typów niestandardowych, takich jak struktury. Nie można jednak zdefiniować struktury z typu należącego do innego pakietu. W związku z tym nie można utworzyć metody dla typu podstawowego, takiego jak string.

Niemniej jednak można użyć hack, aby utworzyć typ niestandardowy na podstawie typu podstawowego, a następnie użyć go tak, jakby był to typ podstawowy. Załóżmy na przykład, że chcesz utworzyć metodę przekształcania ciągu z małych liter na wielkie litery. Możesz napisać coś w następujący sposób:

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())
}

Po uruchomieniu poprzedniego kodu uzyskasz następujące dane wyjściowe:

Learning Go!
LEARNING GO!

Zwróć uwagę, jak można użyć nowego obiektu s tak, jakby był to ciąg podczas pierwszego drukowania jego wartości. Następnie, po wywołaniu Upper metody, s drukuje wszystkie wielkie litery typu ciąg.

Metody osadzania

W poprzednim module przedstawiono, że można użyć właściwości w jednej struktury i osadzić tę samą właściwość w innej struktury. Oznacza to, że można ponownie użyć właściwości z jednej struktury, aby uniknąć powtórzeń i zachować spójność w bazie kodu. Podobny pomysł dotyczy metod. Metody osadzonej struktury można wywołać, nawet jeśli odbiornik jest inny.

Załóżmy na przykład, że chcesz utworzyć nową strukturę trójkąta z logiką, aby uwzględnić kolor. Ponadto chcesz kontynuować korzystanie z zadeklarowanej wcześniej struktury trójkąta. W związku z tym kolorowa struktura trójkąta będzie wyglądać następująco:

type coloredTriangle struct {
    triangle
    color string
}

Następnie można zainicjować coloredTriangle strukturę i wywołać perimeter() metodę z triangle struktury (a nawet uzyskać dostęp do jej pól), w następujący sposób:

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

Przejdź dalej i uwzględnij poprzednie zmiany w programie, aby zobaczyć, jak działa osadzanie. Po uruchomieniu programu z metodą podobną main() do poprzedniej należy uzyskać następujące dane wyjściowe:

Size: 3
Perimeter 9

Jeśli znasz język OOP, taki jak Java lub C++, możesz pomyśleć, że triangle struktura wygląda jak klasa bazowa i coloredTriangle jest podklasą (taką jak dziedziczenie), ale nie jest to poprawne. Dzieje się tak w rzeczywistości, że kompilator Języka Go promuje perimeter() metodę, tworząc metodę otoki, która wygląda mniej więcej tak:

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

Zwróć uwagę, że odbiornik to coloredTriangle, który wywołuje metodę perimeter() z pola trójkąta. Dobrą wiadomością jest to, że nie trzeba tworzyć poprzedniej metody. Możesz, ale Go robi to dla Ciebie pod kapturem. Dołączyliśmy poprzedni przykład tylko do celów szkoleniowych.

Metody przeciążenia

Wróćmy do omówionego wcześniej przykładu triangle . Co się stanie, jeśli chcesz zmienić implementację perimeter() metody w coloredTriangle struktury? Nie można mieć dwóch funkcji o tej samej nazwie. Jednak ze względu na to, że metody wymagają dodatkowego parametru (odbiornika), możesz mieć metodę o tej samej nazwie, o ile jest ona specyficzna dla odbiornika, którego chcesz użyć. Zastosowanie tego rozróżnienia polega na tym, jak przeciążać metody.

Innymi słowy, możesz napisać metodę otoki, którą omówiliśmy, jeśli chcesz zmienić jej zachowanie. Jeśli obwód kolorowego trójkąta jest dwa razy obwodem normalnego trójkąta, kod będzie podobny do następującego:

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

Teraz bez zmiany niczego innego w main() metodzie, którą wcześniej napisałeś, wyglądałoby to następująco:

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

Po uruchomieniu uzyskasz inne dane wyjściowe:

Size: 3
Perimeter 18

Jeśli jednak nadal musisz wywołać metodę perimeter() z triangle struktury, możesz to zrobić, jawnie korzystając z niej, w następujący sposób:

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())
}

Po uruchomieniu tego kodu powinny zostać wyświetlone następujące dane wyjściowe:

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

Jak można zauważyć, w języku Go możesz zastąpić metodę i nadal uzyskać dostęp do oryginalnej metody, jeśli jej potrzebujesz.

Hermetyzacja w metodach

Hermetyzacja oznacza, że metoda jest niedostępna dla obiektu wywołującego (klienta). Zazwyczaj w innych językach programowania należy umieścić private słowa kluczowe lub public przed nazwą metody. W języku Go należy użyć tylko wielkich identyfikatorów, aby upublicznić metodę i niekapitalizowany identyfikator, aby utworzyć prywatną metodę.

Hermetyzacja w języku Go ma zastosowanie tylko między pakietami. Innymi słowy, można ukryć tylko szczegóły implementacji z innego pakietu, a nie samego pakietu.

Aby nadać jej próbę, utwórz nowy pakiet geometry i przenieś tam strukturę trójkąta, w następujący sposób:

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
}

Możesz użyć poprzedniego pakietu w następujący sposób:

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

Powinny zostać wyświetlone następujące dane wyjściowe:

Perimeter 18

Jeśli spróbujesz wywołać size pole lub doubleSize() metodę z main() funkcji, program będzie panikować w następujący sposób:

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

Po uruchomieniu poprzedniego kodu zostanie wyświetlony następujący błąd:

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