Korzystanie z interfejsów w języku Go
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 Shape
printInformation
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.