Použití rozhraní v Go
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 Circle
a 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 Shape
funkce 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:8000
a 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 ServeHTTP
database
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.