Korzystanie z interfejsów w języku Go

Ukończone

Interfejsy w języku Go to typ danych używanych do reprezentowania zachowania innych typów. Interfejs jest jak strategia lub kontrakt, który powinien spełniać obiekt. W przypadku korzystania z interfejsów baza kodu staje się bardziej elastyczna i dostosowywalna, ponieważ piszesz kod, który nie jest powiązany z określoną implementacją. W związku z tym można szybko rozszerzyć funkcjonalność programu. Rozumiesz, dlaczego w tym module.

W przeciwieństwie do interfejsów w innych językach programowania interfejsy w języku Go są zadowalające niejawnie. Język Go nie oferuje słów kluczowych do zaimplementowania interfejsu. Dlatego jeśli znasz interfejsy w innych językach programowania, ale dopiero zaczynasz korzystać z języka Go, ten pomysł może być mylący.

W tym module pracujemy z wieloma przykładami, aby eksplorować interfejsy w języku Go i pokazać, jak jak najwięcej z nich wykorzystać.

Deklarowanie interfejsu

Interfejs w języku Go jest jak strategia. Typ abstrakcyjny, który zawiera tylko metody, które konkretny typ musi posiadać lub zaimplementować.

Załóżmy, że chcesz utworzyć interfejs w pakiecie geometrycznym, który wskazuje, jakie metody musi implementować kształt. Możesz zdefiniować interfejs podobny do następującego:

type Shape interface {
    Perimeter() float64
    Area() float64
}

Interfejs Shape oznacza, że każdy typ, który chcesz wziąć pod uwagę Shape , musi mieć zarówno metody , jak Perimeter() i Area() . Na przykład podczas tworzenia Square struktury musi implementować obie metody, a nie tylko jedną. Należy również zauważyć, że interfejs nie zawiera szczegółów implementacji dla tych metod (na przykład do obliczania obwodu i obszaru kształtu). Są to po prostu kontrakt. Kształty, takie jak trójkąty, okręgi i kwadraty, mają różne sposoby obliczania obszaru i obwodu.

Implementowanie interfejsu

Jak wspomniano wcześniej, w języku Go nie masz słowa kluczowego do zaimplementowania interfejsu. Interfejs w języku Go jest niejawnie spełniony przez typ, gdy ma wszystkie metody wymagane przez interfejs.

Utwórzmy strukturę zawierającą Square obie metody z interfejsu Shape , jak pokazano w poniższym przykładowym kodzie:

type Square struct {
    size float64
}

func (s Square) Area() float64 {
    return s.size * s.size
}

func (s Square) Perimeter() float64 {
    return s.size * 4
}

Zwróć uwagę, że sygnatura Square metody struktury jest zgodna z podpisem interfejsu Shape . Jednak inny interfejs może mieć inną nazwę, ale te same metody. Jak lub kiedy język Go wie, który interfejs implementuje konkretny typ? Go wie, kiedy używasz go, w czasie wykonywania.

Aby zademonstrować sposób użycia interfejsów, możesz napisać następujący kod:

func main() {
    var s Shape = Square{3}
    fmt.Printf("%T\n", s)
    fmt.Println("Area: ", s.Area())
    fmt.Println("Perimeter:", s.Perimeter())
}

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

main.Square
Area:  9
Perimeter: 12

W tym momencie nie ma znaczenia, czy używasz interfejsu. Utwórzmy inny typ, taki jak Circle, a następnie sprawdźmy, dlaczego interfejsy są przydatne. Oto kod struktury Circle :

type Circle struct {
    radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.radius * c.radius
}

func (c Circle) Perimeter() float64 {
    return 2 * math.Pi * c.radius
}

Teraz refaktoryzujemy main() funkcję i utwórzmy funkcję, która może wydrukować typ odbieranego obiektu wraz z obszarem i obwodem, w następujący sposób:

func printInformation(s Shape) {
    fmt.Printf("%T\n", s)
    fmt.Println("Area: ", s.Area())
    fmt.Println("Perimeter:", s.Perimeter())
    fmt.Println()
}

Zwróć uwagę, printInformation jak funkcja ma Shape jako parametr. Można wysłać obiekt Square lub Circle do tej funkcji i działa, chociaż dane wyjściowe są inne. Funkcja main() wygląda teraz następująco:

func main() {
    var s Shape = Square{3}
    printInformation(s)

    c := Circle{6}
    printInformation(c)
}

Zwróć uwagę, że w przypadku c obiektu nie określamy, że jest Shape to obiekt. printInformation Jednak funkcja oczekuje obiektu, który implementuje metody zdefiniowane w interfejsieShape.

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

main.Square
Area:  9
Perimeter: 12

main.Circle
Area:  113.09733552923255
Perimeter: 37.69911184307752

Zwróć uwagę, że nie otrzymujesz błędu, a dane wyjściowe różnią się w zależności od odbieranego typu obiektu. Widać również, że typ obiektu w danych wyjściowych nie mówi nic o interfejsie Shape .

Piękno korzystania z interfejsów polega na tym, że dla każdego nowego typu lub implementacji ShapeprintInformation funkcji funkcja nie musi się zmieniać. Jak wspomniano wcześniej, twój kod staje się bardziej elastyczny i łatwiejszy do rozszerzenia, gdy używasz interfejsów.

Implementowanie interfejsu stringer

Prostym przykładem rozszerzania istniejących funkcji jest użycie interfejsu Stringer, który ma metodę podobną do następującej String() :

type Stringer interface {
    String() string
}

Funkcja fmt.Printf używa tego interfejsu do drukowania wartości, co oznacza, że można napisać metodę niestandardową, aby wydrukować ciąg niestandardowy String() , w następujący sposób:

package main

import "fmt"

type Person struct {
    Name, Country string
}

func (p Person) String() string {
    return fmt.Sprintf("%v is from %v", p.Name, p.Country)
}
func main() {
    rs := Person{"John Doe", "USA"}
    ab := Person{"Mark Collins", "United Kingdom"}
    fmt.Printf("%s\n%s\n", rs, ab)
}

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

John Doe is from USA
Mark Collins is from United Kingdom

Jak widać, użyto typu niestandardowego (struktury), aby napisać niestandardową wersję String() metody. Ta technika jest typowym sposobem implementowania interfejsu w języku Go i można znaleźć przykłady w wielu programach, ponieważ chcemy je eksplorować.

Rozszerzanie istniejącej implementacji

Załóżmy, że masz następujący kod i chcesz rozszerzyć jego funkcjonalność, pisząc niestandardową implementację Writer metody, która jest odpowiedzialna za manipulowanie niektórymi danymi.

Korzystając z następującego kodu, można utworzyć program, który korzysta z interfejsu API usługi GitHub, aby uzyskać trzy repozytoria od firmy Microsoft:

package main

import (
    "fmt"
    "io"
    "net/http"
    "os"
)

func main() {
    resp, err := http.Get("https://api.github.com/users/microsoft/repos?page=15&per_page=3")
    if err != nil {
        fmt.Println("Error:", err)
        os.Exit(1)
    }

    io.Copy(os.Stdout, resp.Body)
}

Po uruchomieniu poprzedniego kodu uzyskasz dane wyjściowe podobne do następujących (skrócone w celu zapewnienia czytelności):

[{"id":276496384,"node_id":"MDEwOlJlcG9zaXRvcnkyNzY0OTYzODQ=","name":"-Users-deepakdahiya-Desktop-juhibubash-test21zzzzzzzzzzz","full_name":"microsoft/-Users-deepakdahiya-Desktop-juhibubash-test21zzzzzzzzzzz","private":false,"owner":{"login":"microsoft","id":6154722,"node_id":"MDEyOk9yZ2FuaXphdGlvbjYxNTQ3MjI=","avatar_url":"https://avatars2.githubusercontent.com/u/6154722?v=4","gravatar_id":"","url":"https://api.github.com/users/microsoft","html_url":"https://github.com/micro
....

Zwróć uwagę, że io.Copy(os.Stdout, resp.Body) wywołanie jest tym, które drukuje w terminalu zawartość uzyskaną z wywołania interfejsu API usługi GitHub. Załóżmy, że chcesz napisać własną implementację, aby skrócić zawartość widoczną w terminalu. Gdy przyjrzysz się źródle io.Copy funkcji, zobaczysz:

func Copy(dst Writer, src Reader) (written int64, err error)

Jeśli bardziej szczegółowo poznać szczegóły pierwszego parametru, zauważysz, dst Writerże Writer jest to interfejs:

type Writer interface {
    Write(p []byte) (n int, err error)
}

Możesz kontynuować eksplorowanie kodu źródłowego io pakietu, dopóki nie znajdziesz miejsca wywołania Write Copy metody, ale pozostawmy tę eksplorację na razie.

Ponieważ Writer jest interfejsem i jest to obiekt, którego Copy oczekuje funkcja, możesz napisać niestandardową implementację Write metody . W związku z tym możesz dostosować zawartość drukowaną w terminalu.

Pierwszą rzeczą, którą należy zaimplementować, jest utworzenie typu niestandardowego. W takim przypadku możesz utworzyć pustą strukturę, ponieważ wystarczy napisać metodę niestandardową Write w następujący sposób:

type customWriter struct{}

Teraz możesz napisać funkcję niestandardową Write . Musisz również napisać strukturę, aby przeanalizować odpowiedź interfejsu API w formacie JSON do obiektu Golang. Możesz użyć witryny JSON-to-Go, aby utworzyć strukturę na podstawie ładunku JSON. Write Dlatego metoda może wyglądać następująco:

type GitHubResponse []struct {
    FullName string `json:"full_name"`
}

func (w customWriter) Write(p []byte) (n int, err error) {
    var resp GitHubResponse
    json.Unmarshal(p, &resp)
    for _, r := range resp {
        fmt.Println(r.FullName)
    }
    return len(p), nil
}

Na koniec należy zmodyfikować main() funkcję tak, aby korzystała z obiektu niestandardowego, w następujący sposób:

func main() {
    resp, err := http.Get("https://api.github.com/users/microsoft/repos?page=15&per_page=5")
    if err != nil {
        fmt.Println("Error:", err)
        os.Exit(1)
    }

    writer := customWriter{}
    io.Copy(writer, resp.Body)
}

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

microsoft/aed-blockchain-learn-content
microsoft/aed-content-nasa-su20
microsoft/aed-external-learn-template
microsoft/aed-go-learn-content
microsoft/aed-learn-template

Dane wyjściowe wyglądają teraz lepiej dzięki napisanej metodzie niestandardowej Write . Oto ostateczna wersja programu:

package main

import (
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
)

type GitHubResponse []struct {
    FullName string `json:"full_name"`
}

type customWriter struct{}

func (w customWriter) Write(p []byte) (n int, err error) {
    var resp GitHubResponse
    json.Unmarshal(p, &resp)
    for _, r := range resp {
        fmt.Println(r.FullName)
    }
    return len(p), nil
}

func main() {
    resp, err := http.Get("https://api.github.com/users/microsoft/repos?page=15&per_page=5")
    if err != nil {
        fmt.Println("Error:", err)
        os.Exit(1)
    }

    writer := customWriter{}
    io.Copy(writer, resp.Body)
}

Pisanie niestandardowego interfejsu API serwera

Na koniec przyjrzyjmy się innemu przypadku użycia interfejsów, które mogą okazać się przydatne w przypadku tworzenia interfejsu API serwera. Typowym sposobem pisania serwera internetowego jest użycie http.Handler interfejsu net/http z pakietu, który wygląda następująco (nie trzeba pisać tego kodu):

package http

type Handler interface {
    ServeHTTP(w ResponseWriter, r *Request)
}

func ListenAndServe(address string, h Handler) error

Zwróć uwagę, że ListenAndServe funkcja oczekuje adresu serwera, takiego jak http://localhost:8000, i wystąpienia Handler , które wysyła odpowiedź z wywołania do adresu serwera.

Utwórzmy, a następnie zapoznajmy się z następującym programem:

package main

import (
    "fmt"
    "log"
    "net/http"
)

type dollars float32

func (d dollars) String() string {
    return fmt.Sprintf("$%.2f", d)
}

type database map[string]dollars

func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    for item, price := range db {
        fmt.Fprintf(w, "%s: %s\n", item, price)
    }
}

func main() {
    db := database{"Go T-Shirt": 25, "Go Jacket": 55}
    log.Fatal(http.ListenAndServe("localhost:8000", db))
}

Zanim zapoznamy się z poprzednim kodem, uruchommy go w następujący sposób:

go run main.go

Jeśli nie otrzymasz żadnych danych wyjściowych, jest to dobry znak. Teraz otwórz w http://localhost:8000 nowym oknie przeglądarki lub w terminalu uruchom następujące polecenie:

curl http://localhost:8000

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

Go T-Shirt: $25.00
Go Jacket: $55.00

Przejrzyjmy poprzedni kod powoli, aby zrozumieć, co robi i obserwować możliwości interfejsów języka Go. Najpierw należy zacząć od utworzenia niestandardowego typu dla float32 typu z pomysłem pisania niestandardowej String() implementacji metody, która będzie używana później.

type dollars float32

func (d dollars) String() string {
    return fmt.Sprintf("$%.2f", d)
}

Następnie napisaliśmy implementację ServeHTTP metody , która http.Handler może być używana. Zwróć uwagę, że ponownie utworzyliśmy typ niestandardowy, ale tym razem jest to mapa, a nie struktura. Następnie napisaliśmy metodę ServeHTTP przy użyciu database typu jako odbiornika. Implementacja tej metody używa danych z odbiornika, pętli przez nią i drukuje każdy element.

type database map[string]dollars

func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    for item, price := range db {
        fmt.Fprintf(w, "%s: %s\n", item, price)
    }
}

Na koniec w main() funkcji utworzyliśmy wystąpienie database typu i zainicjowaliśmy go przy użyciu niektórych wartości. Uruchomiliśmy serwer HTTP przy użyciu http.ListenAndServe funkcji , gdzie zdefiniowaliśmy adres serwera, w tym port do użycia i db obiekt, który implementuje niestandardową wersję ServeHTTP metody. Po uruchomieniu programu język Go używa implementacji tej metody i w ten sposób używasz i implementujesz interfejs w interfejsie API serwera.

func main() {
    db := database{"Go T-Shirt": 25, "Go Jacket": 55}
    log.Fatal(http.ListenAndServe("localhost:8000", db))
}

Podczas korzystania z funkcji można znaleźć inny przypadek użycia interfejsów w interfejsie http.Handle API serwera. Aby uzyskać więcej informacji, zobacz wpis Pisanie aplikacji internetowych w witrynie Języka Go.