Как использовать интерфейсы в Go

задняя часть Go API дизайн

Эта статья переведена сHow to use interfaces in GoЕсть некоторые опущения, пожалуйста, обратитесь к исходному тексту

До того, как я начал программировать на Go, большая часть моей работы выполнялась на Python. Как программисту Python, мне очень сложно научиться использовать интерфейсы в Go. Основы просты, и я знаю, как использовать интерфейсы в стандартной библиотеке, но мне потребовалось много практики, чтобы понять, как создавать свои собственные. В этой статье я расскажу о системе типов Go, чтобы объяснить, как эффективно использовать интерфейсы.

Введение в интерфейс

Что такое интерфейс? Интерфейс имеет два значения: это набор методов, а также тип. Давайте сначала сосредоточимся на аспекте интерфейса как набора методов.

Обычно мы представляем интерфейсы на каких-то гипотетических примерах. Давайте посмотрим на этот пример:AnimalТип — это интерфейс, мы определимAnimalкак все, что может говорить. Это основная концепция системы типов Go: мы разрабатываем абстракции с точки зрения того, что тип может делать, а не типа данных, которые он может хранить.

type Animal interface {
    Speak() string
}

Очень просто: мы определяемAnimalдля любогоSpeakТип метода.SpeakМетод не имеет параметров и возвращает строку. Все типы, которые определяют этот метод, мы называем еговыполнитьохватыватьAnimalинтерфейс. Не в Goimplementsключевое слово, определяющее, реализует ли тип интерфейс, полностью автоматически. Давайте создадим несколько типов, реализующих этот интерфейс:

type Dog struct {
}

func (d Dog) Speak() string {
    return "Woof!"
}

type Cat struct {
}

func (c Cat) Speak() string {
    return "Meow!"
}

type Llama struct {
}

func (l Llama) Speak() string {
    return "?????"
}

type JavaProgrammer struct {
}

func (j JavaProgrammer) Speak() string {
    return "Design patterns!"
}

Теперь у нас есть четыре разных типа животных:Dog,Cat,LlamaиJavaProgrammer. в нашемmainфункцию, мы создаем[]Animal{Dog{}, Cat{}, Llama{}, JavaProgrammer{}}, чтобы увидеть, что сказало каждое животное:

func main() {
    animals := []Animal{Dog{}, Cat{}, Llama{}, JavaProgrammer{}}
    for _, animal := range animals {
        fmt.Println(animal.Speak())
    }
}

Отлично, теперь, когда вы знаете, как использовать интерфейсы, мне больше не нужно о них говорить, верно? нет. Давайте взглянем на некоторые из менее очевидных вещей.

interface{} тип

interface{}тип,пустой интерфейс, является источником большой путаницы.interface{}Тип — это интерфейс без методов. Поскольку нетimplementsключевое слово, поэтому все типы реализуют как минимум 0 методов, поэтомуВсе типы реализуют пустой интерфейс. Это означает, что если вы напишете функцию дляinterface{}value в качестве параметра, то вы можете предоставить любое значение функции. Например:

func DoSomething(v interface{}) {
   // ...
}

Вот где это сбивает с толку: вDoSomethingвнутри функции,vЧто такое тип? Новички будут думатьvдалюбойтипа, но это неправильно.vнетлюбойтипа, этоinterface{}тип. Да это правильно! при передаче значения вDoSomethingфункция, среда выполнения Go выполнит преобразование типов (при необходимости) и преобразует значение вinterface{}значение типа. Все значения имеют только один тип во время выполнения, в то время какvСтатический типinterface{}.

Это может заставить вас задаться вопросом: хорошо, если происходит преобразование, что именно передается в функцию какinterface{}ценность? (В приведенном выше примере это[]AnimalЧто там? )

Значение интерфейса состоит из двух слов (32 бита для 32-битных машин и 64 бита для 64-битных машин); одно слово указывает на таблицу методов базового типа значения, а другое слово указывает на фактические данные. Я не хочу говорить об этом бесконечно. Если вы понимаете, что значение интерфейса состоит из двух слов и содержит указатель на базовые данные, то этого достаточно, чтобы избежать распространенных ошибок. Если вы хотите узнать больше о реализации интерфейса. Эта статья была полезна:Описание интерфейсов Расса Кокса.

В нашем примере выше, когда мы инициализируем переменнуюanimals, нам не нужно что-то подобноеAnimal(Dog{})чтобы показать трансформацию, так как это делается автоматически. Эти элементыAnimalтипы, но их базовые типы не совпадают.

Почему это важно? Понимание того, как интерфейсы представлены в памяти, может прояснить некоторые потенциально запутанные вещи. Например, что-то вроде "Я могу преобразовать []T в []interface{} ? " На этот вопрос легко ответить. Вот несколько примеров плохого кода, представляющегоinterface{}Распространенные заблуждения о типах:

package main

import (
    "fmt"
)

func PrintAll(vals []interface{}) {
    for _, val := range vals {
        fmt.Println(val)
    }
}

func main() {
    names := []string{"stanley", "david", "oscar"}
    PrintAll(names)
}

Запустив этот код, вы получите следующую ошибку:cannot use names (type []string) as type []interface {} in argument to PrintAll. Если мы хотим, чтобы это работало, мы должны поставить[]stringПреобразовать в[]interface{}:

package main

import (
    "fmt"
)

func PrintAll(vals []interface{}) {
    for _, val := range vals {
        fmt.Println(val)
    }
}

func main() {
    names := []string{"stanley", "david", "oscar"}
    vals := make([]interface{}, len(names))
    for i, v := range names {
        vals[i] = v
    }
    PrintAll(vals)
}

некрасиво, нотакова жизнь, ничто не идеально. (На самом деле это случается не так уж часто, т.к.[]interface{}не сработало, как вы думали)

указатели и интерфейсы

Другая тонкость интерфейсов заключается в том, что в определении интерфейса не указывается, должен ли разработчик использовать приемник указателя или приемник значения для реализации интерфейса. Когда задано значение интерфейса, нет гарантии, что базовый тип является указателем. В предыдущем примере мы определили метод над получателем значения. Давайте немного изменим его,CatизSpeak()Метод изменен на приемник указателя:

func (c *Cat) Speak() string {
    return "Meow!"
}

Запустив приведенный выше код, вы получите следующую ошибку:

cannot use Cat literal (type Cat) as type Animal in array or slice literal:
	Cat does not implement Animal (Speak method has pointer receiver)

Ошибка означает: вы пытались поставитьCatПреобразовать вAnimal, но только*CatТип реализует этот интерфейс. Вы можете передать указатель (new(Cat)или&Cat{}), чтобы исправить эту ошибку.

animals := []Animal{Dog{}, new(Cat), Llama{}, JavaProgrammer{}}

Давайте сделаем противоположное: мы проходим в*Dogуказатель, но не меняетсяDogизSpeak()метод:

animals := []Animal{new(Dog), new(Cat), Llama{}, JavaProgrammer{}}

Это прекрасно работает, потому что тип указателя может получить доступ к методам типа значения через связанный с ним тип значения, но не наоборот. Это*DogЗначение типа может быть определено с помощьюDogтипSpeak()метод, в то время какCatНевозможно получить доступ к значению типа, определенному в*Catметоды на типах.

Это может звучать загадочно, но становится ясно, когда вы помните: все в Go передается по значению. При каждом вызове функции передаваемые данные копируются. Для методов с приемником значения значение копируется при вызове метода. Например, следующий метод:

func (t T)MyMethod(s string) {
    // ...
}

даfunc(T, string)вид метода. Получатели методов передаются функциям по значению, как и любой другой параметр.

Поскольку все параметры передаются по значению, это объясняет, почему*Catметод не может бытьCatВызывается значение типа. кто-нибудьCatМожет быть много значений типа*Catвведите указатель на него, если мы попытаемся передатьCatзначение типа для вызова*Catметод, вы просто не знаете, какой указатель соответствует. И наоборот, еслиDogЕсть метод по типу, via*DogЧтобы вызвать этот метод, вы можете найти, чему именно соответствует указательGogзначение типа, тем самым вызывая вышеуказанный метод. Во время выполнения Go делает это за нас автоматически, поэтому нам не нужно использовать операторы, подобные приведенным ниже в C.d->Speak().

Пример 1. Получение правильной временной метки через Twitter API

API Twitter использует следующий формат для отображения меток времени:

"Thu May 31 00:00:01 +0000 2012"

API Twitter возвращает строку json, здесь мы рассматриваем только синтаксический анализcreated_atПоле:

package main

import (
    "encoding/json"
    "fmt"
    "reflect"
)

// start with a string representation of our JSON data
var input = `
{
    "created_at": "Thu May 31 00:00:01 +0000 2012"
}
`

func main() {
    // our target will be of type map[string]interface{}, which is a
    // pretty generic type that will give us a hashtable whose keys
    // are strings, and whose values are of type interface{}
    var val map[string]interface{}

    if err := json.Unmarshal([]byte(input), &val); err != nil {
        panic(err)
    }

    fmt.Println(val)
    for k, v := range val {
        fmt.Println(k, reflect.TypeOf(v))
    }
}

Запустив приведенный выше код, выведите:

map[created_at:Thu May 31 00:00:01 +0000 2012]
created_at Thu May 31 00:00:01 +0000 2012 string

Мы получили проанализированный результат, но проанализированное время имеет строковый тип и имеет ограниченный эффект, поэтому мы хотим проанализировать его какtime.Timetype, внесите следующие изменения в код:

var val map[string]interface{} -> var val map[string]time.Time

Результат - ошибка:

panic: parsing time ""Thu May 31 00:00:01 +0000 2012"" as ""2006-01-02T15:04:05Z07:00"": cannot parse "Thu May 31 00:00:01 +0000 2012"" as "2006"

Ошибка заключается в том, что формат строки не соответствует формату времени в Go (поскольку API Twitter написан на Ruby, формат которого отличается от формата Go). Мы должны определить свой собственный тип для анализа времени.encoding/jsonПри синтаксическом анализе он будет оценивать входящиеjson.UnmarshalЯвляется ли значениеjson.Unmarshalerинтерфейс:

type Unmarshaler interface {
    UnmarshalJSON([]byte) error
}

Если он реализован, он вызоветUnmarshalJSONметод разбора (Ссылаться на), поэтому нам нужна реализация, котораяUnmarshalJSON([]byte) errorТип метода:

type Timestamp time.Time

func (t *Timestamp) UnmarshalJSON(b []byte) error {
    // ...
}

Стоит отметить, что мы используем указатель в качестве приемника метода, потому что мы хотим внести изменения в приемник внутри метода.UnmarshalJSONсередина,tпредставляет собой указаниеTimestampуказатель на значение типа через*tУ нас есть доступ к этому значению, поэтому мы можем его изменить.

мы можем использоватьtime.Parse(layout, value string) (Time, error)для анализа времени первый параметр этой функции представляет собой строку, представляющую формат времени (Дополнительные форматы строк), вторая — это строка, которую мы хотим разобрать. возвращениеtime.Timeзначение типа иerror(если ошибка синтаксического анализа). проанализированоtime.TimeПосле значения типа преобразуется вTimestampвведите и назначьте*t:

func (t *Timestamp) UnmarshalJSON(b []byte) error {
    v, err := time.Parse(time.RubyDate, string(b[1:len(b)-1]))
    if err != nil {
        return err
    }
    *t = Timestamp(v)
    return nil
}

Обратите внимание, что входящая функция[]byte— это исходные данные JSON, которые содержат кавычки, поэтому здесь нам нужно нарезать и удалить кавычки.

Пример 2: Получение объекта из HTTP-запроса

Однако мы разработали интерфейс для решения распространенной проблемы в веб-разработке: мы хотим проанализировать тело HTTP-запроса, чтобы получить нужные нам данные объекта. Например, мы определяем наш интерфейс следующим образом:

GetEntity(*http.Request) (interface{}, error)

так какinterface{}Может быть любой базовый тип, поэтому мы можем разобрать, чтобы получить все, что нам нужно. Но это плохой дизайн, мы вносим слишком много логики вGetEntityв функции,GetEntityТеперь функцию необходимо модифицировать для каждого нового типа, и нам нужно использовать утверждения типа для обработки возвращаемого значения. На практике возвратinterface{}часто раздражает, как правило, вам просто нужно помнить, чтоinterface{}в качестве параметра вместо возвратаinterface{}значение обычно лучше (Закон Постеля).

Мы также можем попробовать написать некоторые функции с явными типами возвращаемого значения, например:

GetUser(*http.Request) (User, error)

Но это недостаточно гибко, потому что для разных типов нужно писать разные функции. Что нам действительно нужно, так это такой дизайн:

type Entity interface {
	UnmarshalerHTTP(*http.Request) error
}

func GetEntity(r *http.Request, v Entity) error {
	return v.UnmarshalerHTTP(r)
}

GetEntiryМетод должен передать параметр, параметрEntityТип интерфейса, обязательно реализуйтеUnmarshalHTTPметод. Чтобы использовать этот метод, нам нужно определитьUserтип и реализацияUnmarshalHTTPметод и разобрать HTTP-запрос в методе:

type User struct {
   ...
}

func (u *User) UnmarshalHTTP(r *http.Request) error {
   // ...
}

Затем определитеUserпеременная типа и передать ее указатель наGetEntityметод:

var u User
if err := GetEntity(req, &u); err != nil {
    // ...
}

Это похоже на анализ данных JSON. Этот способ работает последовательно и безопасно, потому чтоvar u Userбудет автоматическиUserСтруктура инициализируется нулевым значением. Go не разделяет объявление и инициализацию, как другие языки. Объявив значение без его инициализации, среда выполнения выделит для значения соответствующее пространство памяти. даже нашUnmarshalHTTPМетоды не могут использовать определенные поля, которые также будут содержать действительные нулевые данные, а не мусорные данные.

Эпилог

Я надеюсь, что после прочтения этой статьи вы почувствуете себя более комфортно, используя интерфейсы в Go, имейте в виду следующие выводы:

  • Создавайте абстракции, рассматривая одну и ту же функциональность между типами данных, а не одинаковыми полями.
  • interface{}Значение не является произвольным типом, аinterface{} тип
  • Интерфейс содержит размер двух слов, что-то вроде(type, value)
  • функция может принятьinterface{}как параметр, но желательно не возвращаемыйinterface{}
  • Тип указателя может вызывать методы для значения, на которое он указывает, но не наоборот.
  • Параметры в функциях и даже получателях передаются по значению
  • Значение интерфейса — это просто интерфейс, оно не имеет ничего общего с указателями.
  • Если вы хотите изменить значение, на которое указывает указатель в методе, используйте*оператор