Поля интерфейса Go

106

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

Например:

// Interface
type Giver interface {
    Give() int64
}

// One implementation
type FiveGiver struct {}

func (fg *FiveGiver) Give() int64 {
    return 5
}

// Another implementation
type VarGiver struct {
    number int64
}

func (vg *VarGiver) Give() int64 {
    return vg.number
}

Теперь мы можем использовать интерфейс и его реализации:

// A function that uses the interface
func GetSomething(aGiver Giver) {
    fmt.Println("The Giver gives: ", aGiver.Give())
}

// Bring it all together
func main() {
    fg := &FiveGiver{}
    vg := &VarGiver{3}
    GetSomething(fg)
    GetSomething(vg)
}

/*
Resulting output:
5
3
*/

Теперь вы не можете сделать что-то вроде этого:

type Person interface {
    Name string
    Age int64
}

type Bob struct implements Person { // Not Go syntax!
    ...
}

func PrintName(aPerson Person) {
    fmt.Println("Person's name is: ", aPerson.Name)
}

func main() {
    b := &Bob{"Bob", 23}
    PrintName(b)
}

Однако, поигравшись с интерфейсами и встроенными структурами, я обнаружил способ сделать это в некоторой степени:

type PersonProvider interface {
    GetPerson() *Person
}

type Person struct {
    Name string
    Age  int64
}

func (p *Person) GetPerson() *Person {
    return p
}

type Bob struct {
    FavoriteNumber int64
    Person
}

Благодаря встроенной структуре у Боба есть все, что есть у Person. Он также реализует интерфейс PersonProvider, поэтому мы можем передать Боба функциям, которые предназначены для использования этого интерфейса.

func DoBirthday(pp PersonProvider) {
    pers := pp.GetPerson()
    pers.Age += 1
}

func SayHi(pp PersonProvider) {
    fmt.Printf("Hello, %v!\r", pp.GetPerson().Name)
}

func main() {
    b := &Bob{
        5,
        Person{"Bob", 23},
    }
    DoBirthday(b)
    SayHi(b)
    fmt.Printf("You're %v years old now!", b.Age)
}

Вот игровая площадка Go , демонстрирующая приведенный выше код.

Используя этот метод, я могу создать интерфейс, который определяет данные, а не поведение, и который может быть реализован любой структурой, просто встраивая эти данные. Вы можете определять функции, которые явно взаимодействуют с этими встроенными данными и не знают о природе внешней структуры. И все проверяется во время компиляции! (Только так вы могли бы испортить, что я могу видеть, будет встраивание интерфейса PersonProviderв Bob, а не бетон Person. Было бы компилировать и не во время выполнения.)

Теперь вот мой вопрос: это изящный трюк, или я должен делать это по-другому?

Мэтт Мак
источник
4
«Я могу создать интерфейс, который определяет данные, а не поведение». Я бы сказал, что у вас есть поведение, которое возвращает данные.
jmaloney
Я напишу ответ; Я думаю, это нормально, если вам это нужно и вы знаете последствия, но есть последствия, и я бы не стал делать это постоянно.
twotwotwo
@jmaloney Думаю, вы правы, если хотите посмотреть на это прямо. Но в целом, с разными частями, которые я показал, семантика становится «эта функция принимает любую структуру, в составе которой есть ___». По крайней мере, я так задумал.
Мэтт Мак
1
Это не «ответный» материал. Я получил ответ на ваш вопрос, набрав в Google строку "interface as struct property golang". Я нашел аналогичный подход, установив структуру, которая реализует интерфейс как свойство другой структуры. Вот площадка, play.golang.org/p/KLzREXk9xo Спасибо , что поделились идеями.
Дейл
1
Оглядываясь назад и спустя 5 лет использования Go, мне становится ясно, что приведенное выше не является идиоматическим Go. Тяга к дженерикам. Если вы чувствуете искушение сделать что-то подобное, я советую вам переосмыслить архитектуру вашей системы. Принимайте интерфейсы и возвращайте структуры, делитесь посредством общения и радуйтесь.
Мэтт Мак

Ответы:

55

Это определенно изящный трюк. Однако раскрытие указателей по-прежнему делает доступным прямой доступ к данным, поэтому дает вам лишь ограниченную дополнительную гибкость для будущих изменений. Кроме того, соглашения Go не требуют, чтобы вы всегда помещали абстракцию перед атрибутами данных .

Взяв все это вместе, я бы склонялся к той или иной крайности для данного варианта использования: либо а) просто создайте общедоступный атрибут (используя встраивание, если применимо) и передавайте конкретные типы, либо б) если кажется, что раскрытие данных будет вызовет проблемы позже, выставьте геттер / сеттер для более надежной абстракции.

Вы будете взвешивать это на основе атрибутов. Например, если некоторые данные зависят от реализации или вы ожидаете изменить представление по какой-либо другой причине, вы, вероятно, не захотите раскрывать атрибут напрямую, тогда как другие атрибуты данных могут быть достаточно стабильными, чтобы сделать их общедоступными - это чистый выигрыш.


Скрытие свойств за геттерами и сеттерами дает вам дополнительную гибкость для внесения обратно совместимых изменений позже. Скажем, вы когда-нибудь захотите Personсохранить не только одно поле «name», но и первый / средний / последний / префикс; если у вас есть методы Name() stringи SetName(string), вы можете Personпорадовать существующих пользователей интерфейса, добавляя новые более мелкие методы. Или вы можете захотеть пометить объект, поддерживаемый базой данных, как «грязный», если он имеет несохраненные изменения; это можно сделать, когда все обновления данных проходят через SetFoo()методы.

Итак: с помощью геттеров / сеттеров вы можете изменять поля структуры, поддерживая совместимый API, и добавлять логику для получения / набора свойств, поскольку никто не может просто обойтись p.Name = "bob"без прохождения вашего кода.

Эта гибкость более актуальна, когда тип сложный (и кодовая база большая). Если у вас есть PersonCollection, он может быть внутренне подкреплен идентификаторами базы данных sql.Rows, a []*Person, a []uintили чем-то еще. Используя правильный интерфейс, вы можете избавить вызывающих абонентов от заботы, которая такова, так как io.Readerсетевые соединения и файлы выглядят одинаково.

Одна особенность: interfaces в Go имеют особенное свойство, которое вы можете реализовать без импорта пакета, который его определяет; это может помочь вам избежать циклического импорта . Если ваш интерфейс возвращает a *Person, а не просто строки или что-то еще, все PersonProvidersдолжны импортировать пакет, в котором Personон определен. Это может быть хорошо или даже неизбежно; это просто следствие, о котором нужно знать.


Но опять же, у сообщества Go нет строгого соглашения против раскрытия членов данных в общедоступном API вашего типа . Это остается на ваше суждение , является ли это разумно использовать доступ общественности к атрибуту , как часть вашего API в данном случае, а не препятствуя любую экспозицию , так как это могло осложнить или предотвратить изменение реализации позже.

Так, например, stdlib делает такие вещи, как позволяет вам инициализировать http.Serverфайл с вашей конфигурацией и обещает, что можно использовать ноль bytes.Buffer. Это нормально - делать свои собственные вещи подобным образом, и, действительно, я не думаю, что вам следует упреждающе абстрагироваться от вещей, если более конкретная версия с раскрытием данных, вероятно, сработает. Это просто осознание компромиссов.

два, два
источник
Еще одна вещь: подход встраивания больше похож на наследование, верно? Вы получаете любые поля и методы, которые есть у встроенной структуры, и можете использовать ее интерфейс, чтобы любая надстройка соответствовала требованиям, без повторной реализации наборов интерфейсов.
Мэтт Мак,
Да - очень похоже на виртуальное наследование на других языках. Вы можете использовать внедрение для реализации интерфейса, независимо от того, определен ли он в терминах геттеров и сеттеров или указателя на данные (или, третий вариант для доступа только для чтения к крошечным структурам, копии структуры).
twotwotwo
Я должен сказать, что это заставляет меня вернуться в 1999 год и научиться писать множество шаблонных геттеров и сеттеров на Java.
Том
Жаль, что собственная стандартная библиотека Go не всегда это делает. Я пытаюсь имитировать некоторые вызовы os.Process для модульных тестов. Я не могу просто обернуть объект процесса в интерфейс, поскольку доступ к переменной-члену Pid осуществляется напрямую, а интерфейсы Go не поддерживают переменные-члены.
Алекс Янсен
1
@Tom Это правда. Я действительно думаю, что геттеры / сеттеры добавляют больше гибкости, чем предоставление указателя, но я также не думаю, что все должны получать / устанавливать все (или что соответствует типичному стилю Go). Раньше у меня было несколько слов, указывающих на это, но я изменил начало и конец, чтобы еще больше подчеркнуть это.
twotwotwo,
2

Если я правильно понимаю, вы хотите заполнить одно поле структуры в другое. Мое мнение - не использовать интерфейсы для расширения. Вы легко можете сделать это с помощью следующего подхода.

package main

import (
    "fmt"
)

type Person struct {
    Name        string
    Age         int
    Citizenship string
}

type Bob struct {
    SSN string
    Person
}

func main() {
    bob := &Bob{}

    bob.Name = "Bob"
    bob.Age = 15
    bob.Citizenship = "US"

    bob.SSN = "BobSecret"

    fmt.Printf("%+v", bob)
}

https://play.golang.org/p/aBJ5fq3uXtt

Отметьте Personв Bobдекларации. Это сделает включенное поле структуры доступным в Bobструктуре напрямую с некоторым синтаксическим сахаром.

Игорь Александрович Мелехин
источник