Эта статья переведена с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.Time
type, внесите следующие изменения в код:
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{}
- Тип указателя может вызывать методы для значения, на которое он указывает, но не наоборот.
- Параметры в функциях и даже получателях передаются по значению
- Значение интерфейса — это просто интерфейс, оно не имеет ничего общего с указателями.
- Если вы хотите изменить значение, на которое указывает указатель в методе, используйте
*
оператор