Använda gränssnitt i Go

Slutförd

Gränssnitt i Go är en typ av data som används för att representera beteendet för andra typer. Ett gränssnitt är som en skiss eller ett kontrakt som ett objekt ska uppfylla. När du använder gränssnitt blir din kodbas mer flexibel och anpassningsbar eftersom du skriver kod som inte är kopplad till en viss implementering. Därför kan du snabbt utöka funktionerna i ett program. Du förstår varför i den här modulen.

Till skillnad från gränssnitt i andra programmeringsspråk uppfylls gränssnitten i Go implicit. Go erbjuder inte nyckelord för att implementera ett gränssnitt. Om du är bekant med gränssnitt i andra programmeringsspråk men är nybörjare på Go kan den här idén vara förvirrande.

I den här modulen arbetar vi med flera exempel för att utforska gränssnitt i Go och visa hur du får ut mesta möjliga av dem.

Deklarera ett gränssnitt

Ett gränssnitt i Go är som en skiss. En abstrakt typ som endast innehåller de metoder som en betongtyp måste ha eller implementera.

Anta att du vill skapa ett gränssnitt i geometripaketet som anger vilka metoder en form måste implementera. Du kan definiera ett gränssnitt som det här:

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

Gränssnittet Shape innebär att alla typer som du vill överväga Shape måste ha både Perimeter() metoderna och Area() . När du till exempel skapar en Square struct måste den implementera båda metoderna, inte bara en. Observera också att ett gränssnitt inte innehåller implementeringsinformation för dessa metoder (till exempel för att beräkna perimetern och området för en form). De är bara ett kontrakt. Former som trianglar, cirklar och rutor har olika sätt att beräkna område och perimeter.

Implementera ett gränssnitt

Som vi beskrev tidigare i Go har du inget nyckelord för att implementera ett gränssnitt. Ett gränssnitt i Go uppfylls implicit av en typ när det har alla metoder som ett gränssnitt kräver.

Nu ska vi skapa en Square struct som har båda metoderna från Shape gränssnittet, som du ser i följande exempelkod:

type Square struct {
    size float64
}

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

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

Observera hur metodens signatur för struct matchar Square gränssnittets signatur Shape . Ett annat gränssnitt kan dock ha ett annat namn men samma metoder. Hur eller när vet Go vilket gränssnitt en konkret typ implementerar? Go vet det när du använder det, vid körning.

Om du vill visa hur gränssnitt används kan du skriva följande kod:

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

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

main.Square
Area:  9
Perimeter: 12

I det här läget spelar det ingen roll om du använder ett gränssnitt eller inte. Nu ska vi skapa en annan typ, till exempel Circle, och sedan utforska varför gränssnitt är användbara. Här är koden för struct: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
}

Nu ska vi omstrukturera main() funktionen och skapa en funktion som kan skriva ut den typ av objekt som den tar emot, tillsammans med dess område och perimeter, så här:

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

Observera hur printInformation funktionen har Shape som parameter. Du kan skicka ett Square eller ett Circle objekt till den här funktionen, och det fungerar, även om utdata är annorlunda. Funktionen main() ser nu ut så här:

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

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

Observera att för c objektet anger vi inte att det är ett Shape objekt. Funktionen förväntar sig dock printInformation ett objekt som implementerar de metoder som definieras i Shape gränssnittet.

När du kör programmet bör du få följande utdata:

main.Square
Area:  9
Perimeter: 12

main.Circle
Area:  113.09733552923255
Perimeter: 37.69911184307752

Observera att du inte får något fel, och utdata varierar beroende på vilken objekttyp det tar emot. Du kan också se att objekttypen i utdata inte säger något om Shape gränssnittet.

Det fina med att använda gränssnitt är att för varje ny typ eller implementering av ShapeprintInformation behöver funktionen inte ändras. Som vi sa tidigare blir koden mer flexibel och enklare att utöka när du använder gränssnitt.

Implementera ett Stringer-gränssnitt

Ett enkelt exempel på att utöka befintliga funktioner är att använda ett Stringer, som är ett gränssnitt som har en String() metod, så här:

type Stringer interface {
    String() string
}

Funktionen fmt.Printf använder det här gränssnittet för att skriva ut värden, vilket innebär att du kan skriva din anpassade String() metod för att skriva ut en anpassad sträng, så här:

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

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

John Doe is from USA
Mark Collins is from United Kingdom

Som du ser använde du en anpassad typ (en struct) för att skriva en anpassad version av String() metoden. Den här tekniken är ett vanligt sätt att implementera ett gränssnitt i Go, och du hittar exempel på det i många program, som vi är på väg att utforska.

Utöka en befintlig implementering

Anta att du har följande kod och vill utöka dess funktioner genom att skriva en anpassad implementering av en Writer metod som ansvarar för att manipulera vissa data.

Med hjälp av följande kod kan du skapa ett program som använder GitHub-API:et för att hämta tre lagringsplatser från 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)
}

När du kör föregående kod får du ungefär följande utdata (förkortad för läsbarhet):

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

Observera att anropet io.Copy(os.Stdout, resp.Body) är det som skriver ut det innehåll som du fick från anropet till GitHub-API:et till terminalen. Anta att du vill skriva en egen implementering för att förkorta innehållet du ser i terminalen. När du tittar på funktionens io.Copykälla ser du:

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

Om du går djupare in på informationen om den första parametern, dst Writermärker du att det Writer är ett gränssnitt:

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

Du kan fortsätta utforska io paketets källkod tills du hittar var Copy anropar metoden, men låt oss lämna den här utforskningen Write ensam tills vidare.

Eftersom Writer är ett gränssnitt och det är ett objekt som Copy funktionen förväntar sig kan du skriva din anpassade implementering av Write metoden. Därför kan du anpassa innehållet som du skriver ut till terminalen.

Det första du behöver för att implementera ett gränssnitt är att skapa en anpassad typ. I det här fallet kan du skapa en tom struct eftersom du helt enkelt behöver skriva din anpassade Write metod, så här:

type customWriter struct{}

Nu är du redo att skriva din anpassade Write funktion. Du måste också skriva en struct för att parsa API-svaret i JSON-format till ett Golang-objekt. Du kan använda JSON-to-Go-webbplatsen för att skapa en struct från en JSON-nyttolast. Write Så metoden kan se ut så här:

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
}

Slutligen måste du ändra funktionen så att den main() använder ditt anpassade objekt, så här:

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

När du kör programmet bör du få följande utdata:

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

Utdata ser bättre ut nu tack vare den anpassade Write metod som du skrev. Här är den slutliga versionen av programmet:

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

Skriva ett anpassat server-API

Slutligen ska vi utforska ett annat användningsfall för gränssnitt som du kan ha nytta av om du skapar ett server-API. Det vanliga sättet att skriva en webbserver är att använda http.Handler gränssnittet från net/http paketet, som ser ut så här (du behöver inte skriva den här koden):

package http

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

func ListenAndServe(address string, h Handler) error

Observera hur ListenAndServe funktionen förväntar sig en serveradress, till exempel http://localhost:8000, och en instans av Handler som skickar svaret från anropet till serveradressen.

Nu ska vi skapa och sedan utforska följande 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))
}

Innan vi utforskar föregående kod ska vi köra den så här:

go run main.go

Om du inte får några utdata är det ett gott tecken. http://localhost:8000 Öppna nu i ett nytt webbläsarfönster eller kör följande kommando i terminalen:

curl http://localhost:8000

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

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

Nu ska vi granska föregående kod långsamt för att förstå vad den gör och för att se kraften i Go-gränssnitt. Först börjar du med att skapa en anpassad typ för en float32 typ, med tanken att skriva en anpassad implementering av String() metoden, som du använder senare.

type dollars float32

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

Sedan skrev vi implementeringen av den ServeHTTP metod som http.Handler kunde användas. Observera hur vi skapade en anpassad typ igen, men den här gången är det en karta, inte en struct. Sedan skrev ServeHTTP vi metoden med hjälp database av typen som mottagare. Den här metodens implementering använder data från mottagaren, loopar igenom dem och skriver ut varje objekt.

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

Slutligen instansierade vi en database typ i main() funktionen och initierade den med vissa värden. Vi startade HTTP-servern med hjälp http.ListenAndServe av funktionen, där vi definierade serveradressen, inklusive porten som ska användas och objektet db som implementerar en anpassad version av ServeHTTP metoden. När du kör programmet använder Go din implementering av den metoden, och det är så du använder och implementerar ett gränssnitt i ett server-API.

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

Du hittar ett annat användningsfall för gränssnitt i ett server-API när du använder http.Handle funktionen. Mer information finns i inlägget Skriva webbprogram på Go-webbplatsen.