Использование интерфейсов в Go
Интерфейсы в Go — это тип данных, который представляет поведение других типов. Это нечто вроде макета или контракта, которому должен соответствовать объект. Благодаря интерфейсам база кода становится более гибкой и адаптируемой, так как ваш код не привязан к определенной реализации. Это позволяет быстро расширять функциональные возможности программы. Вы понимаете, почему в этом модуле.
В отличие от других языков программирования, интерфейсы в Go реализуются неявно. Go не предлагает ключевые слова для реализации интерфейса. Таким образом, если вы знакомы с интерфейсами на других языках программирования, но не знакомы с Go, эта идея может быть запутана.
В этом модуле мы работаем с несколькими примерами, чтобы изучить интерфейсы в Go и продемонстрировать, как сделать большую часть из них.
Объявление интерфейса
Интерфейс в Go похож на схему. Абстрактный тип, включающий только методы, которыми должен обладать конкретный тип или реализовать.
Предположим, что в пакете для геометрических расчетов необходимо создать интерфейс, определяющий методы, которые должна реализовывать фигура. Определить интерфейс можно следующим образом:
type Shape interface {
Perimeter() float64
Area() float64
}
Интерфейс Shape
указывает, что у любого типа фигуры (Shape
) должны быть методы Perimeter()
и Area()
. Например, при создании структуры Square
необходимо реализовать оба метода, а не один из них. Кроме того, обратите внимание, что интерфейс не содержит особенностей реализации этих методов (для вычисления периметра и площади фигуры). Это просто контракт. У разных фигур (треугольника, круга, квадрата) площадь и периметр вычисляются по-разному.
Реализация интерфейса
Как уже говорилось, в Go нет ключевого слова для реализации интерфейса. Интерфейс в Go реализуется неявно, если у типа есть все методы, которых требует интерфейс.
Давайте создадим структуру Square
, которая имеет оба метода интерфейса Shape
, как в следующем примере кода:
type Square struct {
size float64
}
func (s Square) Area() float64 {
return s.size * s.size
}
func (s Square) Perimeter() float64 {
return s.size * 4
}
Обратите внимание, что сигнатура методов структуры Square
соответствует сигнатуре интерфейса Shape
. Однако те же методы могут быть у другого интерфейса. Как Go определяет, какой интерфейс реализуется конкретным типом? Это происходит во время выполнения при использовании типа.
Чтобы продемонстрировать, как используются интерфейсы, можно написать следующий код:
func main() {
var s Shape = Square{3}
fmt.Printf("%T\n", s)
fmt.Println("Area: ", s.Area())
fmt.Println("Perimeter:", s.Perimeter())
}
При выполнении приведенной выше программы получается следующий результат:
main.Square
Area: 9
Perimeter: 12
Пока интерфейс не играет особой роли. Давайте создадим еще один тип, например Circle
, и посмотрим, почему интерфейсы так полезны. Вот код структуры 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
}
Теперь давайте рефакторинг main()
функции и создадим функцию, которая может распечатать тип получаемого объекта, а также область и периметр, как показано ниже.
func printInformation(s Shape) {
fmt.Printf("%T\n", s)
fmt.Println("Area: ", s.Area())
fmt.Println("Perimeter:", s.Perimeter())
fmt.Println()
}
Обратите внимание, что у функции printInformation
есть параметр Shape
. Вы можете отправить Square
объект или Circle
объект в эту функцию, и он работает, хотя выходные данные отличаются. Теперь функция main()
выглядит так:
func main() {
var s Shape = Square{3}
printInformation(s)
c := Circle{6}
printInformation(c)
}
Обратите внимание, что для объекта c
не указан тип Shape
. Однако функция printInformation
ожидает объект, который реализует методы, определенные в интерфейсе Shape
.
После выполнения программы вы получите следующий результат:
main.Square
Area: 9
Perimeter: 12
main.Circle
Area: 113.09733552923255
Perimeter: 37.69911184307752
Обратите внимание, что ошибки не происходит, а выходные данные зависят от типа передаваемого объекта. Кроме того, можно заметить, что тип объекта выводится без указания интерфейса Shape
.
Преимущество интерфейсов заключается в том, что для каждого нового типа или реализации Shape
не нужно изменять функцию printInformation
. Как мы уже говорили ранее, код становится более гибким и проще расширить при использовании интерфейсов.
Реализация интерфейса Stringer
Расширение существующей функциональности можно легко продемонстрировать на примере интерфейса Stringer
с методом String()
:
type Stringer interface {
String() string
}
Функция fmt.Printf
использует этот интерфейс для печати значений, что означает, что можно написать пользовательский String()
метод для печати пользовательской строки, как показано ниже.
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)
}
При выполнении приведенной выше программы получается следующий результат:
John Doe is from USA
Mark Collins is from United Kingdom
Как видно, вы использовали пользовательский тип (структуру) для записи пользовательской String()
версии метода. Этот метод является общим способом реализации интерфейса в Go, и вы найдете примеры его во многих программах, так как мы собираемся изучить.
Расширение существующей реализации
Допустим, у вас есть приведенный ниже код и вы хотите расширить его функциональность, написав пользовательскую реализацию метода Writer
для обработки некоторых данных.
С помощью следующего кода можно создать программу, использующую API GitHub для получения трех репозиториев Майкрософт:
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)
}
При выполнении этого кода результат будет примерно следующим (для удобства он сокращен):
[{"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
....
Обратите внимание, что io.Copy(os.Stdout, resp.Body)
вызов — это тот, который печатает содержимое терминала, полученное из вызова API GitHub. Предположим, вы хотите написать собственную реализацию, чтобы сократить содержимое, отображаемое в терминале. При просмотре источника io.Copy
функции вы увидите:
func Copy(dst Writer, src Reader) (written int64, err error)
Если получить более подробные сведения о первом параметре, обратите внимание, dst Writer
что Writer
это интерфейс:
type Writer interface {
Write(p []byte) (n int, err error)
}
Вы можете продолжить изучение io
исходного кода пакета, пока не найдете, где Copy
вызывается Write
метод, но давайте оставим это исследование в одиночку.
Так как Writer
— это интерфейс и именно такой объект ожидает функция Copy
, можно написать пользовательскую реализацию метода Write
. Таким образом, вы можете настроить содержимое, выводимое в терминале.
Первое, что нужно сделать, чтобы реализовать интерфейс, — создать пользовательский тип. В этом случае можно создать пустую структуру, так как вам нужно просто написать собственный метод Write
:
type customWriter struct{}
Теперь можно перейти к написанию пользовательской функции Write
. Кроме того, необходимо написать структуру для преобразования ответа от API в формате JSON в объект Golang. Чтобы создать структуру на основе полезных данных JSON, можно использовать сайт JSON-to-Go. В этом случае метод Write
будет выглядеть так:
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
}
Наконец, необходимо изменить функцию main()
, чтобы в ней использовался пользовательский объект:
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)
}
После выполнения программы вы получите следующий результат:
microsoft/aed-blockchain-learn-content
microsoft/aed-content-nasa-su20
microsoft/aed-external-learn-template
microsoft/aed-go-learn-content
microsoft/aed-learn-template
Теперь выходные данные выглядят лучше благодаря пользовательскому методу Write
. Вот окончательная версия программы:
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)
}
Написание пользовательского серверного интерфейса API
Наконец, давайте рассмотрим еще один вариант использования интерфейсов, который может оказаться полезным при создании серверного интерфейса API. Для создания веб-сервера обычно используется интерфейс http.Handler
из пакета net/http
, который выглядит следующим образом (этот код писать не нужно):
package http
type Handler interface {
ServeHTTP(w ResponseWriter, r *Request)
}
func ListenAndServe(address string, h Handler) error
Обратите внимание, что ListenAndServe
функция ожидает адрес сервера, например http://localhost:8000
экземпляр Handler
, который отправляет ответ от вызова на адрес сервера.
Давайте создадим, а затем разберем следующую программу:
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))
}
Прежде чем изучать этот код, давайте выполним его следующим образом:
go run main.go
Если выходных данных нет, это хороший признак. Теперь откройте адрес http://localhost:8000
в новом окне браузера или выполните следующую команду в терминале:
curl http://localhost:8000
На этот раз результат должен быть следующим:
Go T-Shirt: $25.00
Go Jacket: $55.00
Давайте последовательно разберем приведенный выше код, чтобы понять, что он делает, и убедиться в потенциале интерфейсов Go. Во-первых, сначала создайте пользовательский тип для float32
типа с идеей написания пользовательской String()
реализации метода, который вы используете позже.
type dollars float32
func (d dollars) String() string {
return fmt.Sprintf("$%.2f", d)
}
Затем мы написали реализацию метода ServeHTTP
, которая может использоваться интерфейсом http.Handler
. Обратите внимание, что мы снова создали пользовательский тип, но на этот раз карту, а не структуру. Далее мы написали метод ServeHTTP
, ресивером которого является тип database
. Реализация этого метода использует данные из приемника, циклит его и выводит каждый элемент.
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)
}
}
Наконец, в функции main()
мы создали экземпляр типа database
и инициализировали его рядом значений. Мы запустили HTTP-сервер с помощью функции http.ListenAndServe
, в которой был определен адрес сервера, включая используемый порт и объект db
, реализующий пользовательскую версию метода ServeHTTP
. При запуске программы Go использует реализацию этого метода, и это то, как вы используете и реализуете интерфейс в API сервера.
func main() {
db := database{"Go T-Shirt": 25, "Go Jacket": 55}
log.Fatal(http.ListenAndServe("localhost:8000", db))
}
Еще один вариант использования интерфейсов в серверном API связан с функцией http.Handle
. Дополнительные сведения см. в статье Writing web applications (Написание веб-приложений) на сайте Go.