Använda gränssnitt i Go
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 Shape
printInformation
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.Copy
kä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 Writer
mä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.