Použití rozhraní v Go

Dokončeno

Rozhraní v go jsou typ dat, která se používají k reprezentaci chování jiných typů. Rozhraní je jako podrobný plán nebo kontrakt, který by měl objekt splňovat. Když používáte rozhraní, stane se základ kódu flexibilnější a přizpůsobitelný, protože píšete kód, který není svázaný s konkrétní implementací. Proto můžete funkce programu rychle rozšířit. V tomto modulu rozumíte tomu, proč.

Na rozdíl od rozhraní v jiných programovacích jazycích jsou rozhraní v jazyce Go implicitně splněna. Go nenabízí klíčová slova pro implementaci rozhraní. Pokud tedy znáte rozhraní v jiných programovacích jazycích, ale s Jazykem Go začínáte, může být tento nápad matoucí.

V tomto modulu pracujeme s několika příklady, abychom prozkoumali rozhraní v Go a ukázali, jak je využít na maximum.

Deklarace rozhraní

Rozhraní v Go je jako podrobný plán. Abstraktní typ, který zahrnuje pouze metody, které konkrétní typ musí mít nebo implementovat.

Řekněme, že chcete vytvořit rozhraní v balíčku geometrie, které indikuje, jaké metody musí obrazec implementovat. Rozhraní můžete definovat takto:

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

Rozhraní Shape znamená, že každý typ, který chcete zvážit Shape , musí mít obě Perimeter() metody i Area() metody. Když například vytvoříte Square strukturu, musí implementovat obě metody, nejen jednu. Všimněte si také, že rozhraní neobsahuje podrobnosti implementace pro tyto metody (například pro výpočet obvodu a oblasti obrazce). Jsou to prostě kontrakt. Obrazce, jako jsou trojúhelníky, kruhy a čtverce, mají různé způsoby výpočtu oblasti a obvodu.

Implementace rozhraní

Jak jsme probrali dříve, v Go nemáte klíčové slovo pro implementaci rozhraní. Rozhraní v Go je implicitně splněno typem, pokud má všechny metody, které rozhraní vyžaduje.

Pojďme vytvořit Square strukturu, která má obě metody z Shape rozhraní, jak je znázorněno v následujícím ukázkovém kódu:

type Square struct {
    size float64
}

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

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

Všimněte si, jak podpis Square metody struktury odpovídá podpisu Shape rozhraní. Jiné rozhraní však může mít jiný název, ale stejné metody. Jak nebo kdy Go zjistí, které rozhraní konkrétní typ implementuje? Go to ví, když ho používáte, za běhu.

Abyste si ukázali, jak se používají rozhraní, můžete napsat následující kód:

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

Když spustíte předchozí program, získáte následující výstup:

main.Square
Area:  9
Perimeter: 12

V tomto okamžiku nezáleží na tom, jestli rozhraní používáte nebo ne. Pojďme vytvořit jiný typ, například Circlea pak prozkoumat, proč jsou rozhraní užitečná. Tady je kód 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
}

Teď pojďme funkci refaktorovat main() a vytvořit funkci, která může vytisknout typ objektu, který přijímá, spolu s jeho oblastí a obvodem, například takto:

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

Všimněte si, že printInformation funkce má Shape jako parametr. Do této funkce můžete odeslat Square objekt nebo Circle objekt, který funguje, i když se výstup liší. Vaše main() funkce teď vypadá takto:

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

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

Všimněte si, že pro c objekt neurčíme, že se jedná o Shape objekt. Funkce však očekává objekt, printInformation který implementuje metody definované v Shape rozhraní.

Při spuštění programu byste měli získat následující výstup:

main.Square
Area:  9
Perimeter: 12

main.Circle
Area:  113.09733552923255
Perimeter: 37.69911184307752

Všimněte si, že se nezobrazí chyba a výstup se liší v závislosti na typu objektu, který obdrží. Můžete také vidět, že typ objektu ve výstupu o rozhraní nic neřekne Shape .

Výhodou použití rozhraní je, že pro každý nový typ nebo implementaci Shapefunkce printInformation se nemusí měnit. Jak jsme řekli dříve, váš kód se stává flexibilnější a jednodušší rozšířit při použití rozhraní.

Implementace rozhraní Stringeru

Jednoduchým příkladem rozšíření stávajících funkcí je použití Stringer, což je rozhraní, které má metodu String() , například takto:

type Stringer interface {
    String() string
}

Tato fmt.Printf funkce používá toto rozhraní k tisku hodnot, což znamená, že můžete napsat vlastní String() metodu pro tisk vlastního řetězce, například takto:

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

Když spustíte předchozí program, získáte následující výstup:

John Doe is from USA
Mark Collins is from United Kingdom

Jak vidíte, k zápisu vlastní verze String() metody jste použili vlastní typ (strukturu). Tato technika je běžným způsobem implementace rozhraní v Go a najdete příklady v mnoha programech, jak se chystáme prozkoumat.

Rozšíření existující implementace

Řekněme, že máte následující kód a chcete rozšířit jeho funkce tím, že napíšete vlastní implementaci Writer metody, která má na starosti manipulaci s některými daty.

Pomocí následujícího kódu můžete vytvořit program, který využívá rozhraní API GitHubu k získání tří úložišť od Microsoftu:

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

Když spustíte předchozí kód, získáte něco jako následující výstup (zkrácený pro čitelnost):

[{"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
....

Všimněte si, že io.Copy(os.Stdout, resp.Body) volání je ten, který se vytiskne do terminálu s obsahem, který jste získali z volání rozhraní API GitHubu. Řekněme, že chcete napsat vlastní implementaci, abyste zkrátili obsah, který vidíte v terminálu. Když se podíváte na zdroj io.Copy funkce, uvidíte:

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

Pokud se podrobněji ponoříte do podrobností prvního parametru, všimněte si, dst Writerže Writer se jedná o rozhraní:

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

Můžete pokračovat ve zkoumání zdrojového io kódu balíčku, dokud nenajdete, kam Write volá, ale prozatím necháme toto zkoumání samotné.

Protože Writer je rozhraní a jedná se o objekt, který Copy funkce očekává, můžete napsat vlastní implementaci Write metody. Proto můžete přizpůsobit obsah, který vytisknete do terminálu.

První věcí, kterou potřebujete implementovat rozhraní, je vytvoření vlastního typu. V tomto případě můžete vytvořit prázdnou strukturu, protože stačí napsat vlastní Write metodu, například takto:

type customWriter struct{}

Teď jste připraveni napsat vlastní Write funkci. Musíte také napsat strukturu, která parsuje odpověď rozhraní API ve formátu JSON na objekt Jazyk Go. Pomocí webu JSON-to-Go můžete vytvořit strukturu z datové části JSON. Write Metoda by tedy mohla vypadat takto:

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
}

Nakonec musíte upravit main() funkci tak, aby používala vlastní objekt, například takto:

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

Při spuštění programu byste měli získat následující výstup:

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

Výstup teď vypadá lépe díky vlastní Write metodě, kterou jste napsali. Tady je konečná verze 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)
}

Vytvoření vlastního rozhraní API serveru

Nakonec se podíváme na další případ použití pro rozhraní, která by mohla být užitečná, pokud vytváříte serverové rozhraní API. Typický způsob psaní webového serveru spočívá v použití http.Handler rozhraní z net/http balíčku, který vypadá takto (tento kód nemusíte psát):

package http

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

func ListenAndServe(address string, h Handler) error

Všimněte si, že ListenAndServe funkce očekává adresu serveru, například http://localhost:8000a instanci Handler , která odešle odpověď z volání na adresu serveru.

Pojďme vytvořit a prozkoumat následující program:

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

Než prozkoumáme předchozí kód, pojďme ho spustit takto:

go run main.go

Pokud se vám nezobrazí žádný výstup, je to dobré znamení. Nyní otevřete http://localhost:8000 v novém okně prohlížeče nebo v terminálu spusťte následující příkaz:

curl http://localhost:8000

Teď byste měli získat následující výstup:

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

Pojďme se pomalu podívat na předchozí kód, abychom pochopili, co dělá, a sledujte výkon rozhraní Go. Nejprve začnete vytvořením vlastního typu pro float32 typ s myšlenkou vytvoření vlastní implementace String() metody, kterou použijete později.

type dollars float32

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

Pak jsme napsali implementaci ServeHTTP metody, kterou http.Handler by bylo možné použít. Všimněte si, jak jsme znovu vytvořili vlastní typ, ale tentokrát se jedná o mapu, nikoli strukturu. Dále jsme napsali metodu ServeHTTPdatabase pomocí typu jako příjemce. Implementace této metody používá data z příjemce, prochází je a vytiskne jednotlivé položky.

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

Nakonec jsme ve main() funkci vytvořili database instanci typu a inicializovali ho s některými hodnotami. Server HTTP jsme spustili pomocí http.ListenAndServe funkce, kde jsme definovali adresu serveru, včetně portu, který se má použít, a db objektu, který implementuje vlastní verzi ServeHTTP metody. Při spuštění programu go použije vaši implementaci této metody a to je způsob, jakým používáte a implementujete rozhraní v serverovém rozhraní API.

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

Při použití http.Handle funkce můžete najít jiný případ použití pro rozhraní v serverovém rozhraní API. Další informace najdete v příspěvku Psaní webových aplikací na webu Go.