Как добавить новые методы к существующему типу в Go?

129

Я хочу добавить удобный метод использования для gorilla/muxтипов Route и Router:

package util

import(
    "net/http"
    "github.com/0xor1/gorillaseed/src/server/lib/mux"
)

func (r *mux.Route) Subroute(tpl string, h http.Handler) *mux.Route{
    return r.PathPrefix("/" + tpl).Subrouter().PathPrefix("/").Handler(h)
}

func (r *mux.Router) Subroute(tpl string, h http.Handler) *mux.Route{
    return r.PathPrefix("/" + tpl).Subrouter().PathPrefix("/").Handler(h)
}

но компилятор сообщает мне

Невозможно определить новые методы для мультиплексора нелокального типа.

Итак, как мне этого добиться? Могу ли я создать новый тип структуры с анонимными полями mux.Route и mux.Router? Или что-то другое?

Дэниел Робинсон
источник
Что интересно, методы расширения считаются не объектно-ориентированными ( “extension methods are not object-oriented”) для C #, но, глядя на них сегодня, я сразу вспомнил интерфейсы Go (и его подход к переосмыслению объектной ориентации), и тогда у меня возник именно этот вопрос.
Wolf

Ответы:

174

Как упоминает компилятор, вы не можете расширять существующие типы в другом пакете. Вы можете определить свой собственный псевдоним или подпакет следующим образом:

type MyRouter mux.Router

func (m *MyRouter) F() { ... }

или встроив оригинальный роутер:

type MyRouter struct {
    *mux.Router
}

func (m *MyRouter) F() { ... }

...
r := &MyRouter{router}
r.F()
jimt
источник
10
Или просто использовать функцию ...?
Пол Ханкин
5
@Paul делает это, чтобы переопределить такие функции, как String () и MarshalJSON ()
Riking
31
Если вы выполните первую часть, то как вы приведете mux.Routerэкземпляры к MyRouters? например, если у вас есть библиотека, которая возвращается, mux.Routerно вы хотите использовать свои новые методы?
docwhat
как использовать первое решение? MyRouter (маршрутизатор)
tfzxyinhao
внедрение кажется более практичным в использовании.
ivanjovanovic
124

Я хотел расширить ответ, данный @jimt здесь . Этот ответ верен и очень помог мне разобраться в этом. Однако есть некоторые предостережения в отношении обоих методов (псевдоним, встраивание), с которыми у меня возникли проблемы.

примечание : я использую термины родительский и дочерний, хотя я не уверен, что это лучший вариант для композиции. По сути, parent - это тип, который вы хотите изменить локально. Дочерний - это новый тип, который пытается реализовать эту модификацию.

Метод 1 - определение типа

type child parent
// or
type MyThing imported.Thing
  • Предоставляет доступ к полям.
  • Не предоставляет доступа к методам.

Метод 2 - Встраивание ( официальная документация )

type child struct {
    parent
}
// or with import and pointer
type MyThing struct {
    *imported.Thing
}
  • Предоставляет доступ к полям.
  • Предоставляет доступ к методам.
  • Требует рассмотрения при инициализации.

Резюме

  • При использовании метода композиции встроенный родительский элемент не будет инициализирован, если это указатель. Родитель должен быть инициализирован отдельно.
  • Если встроенный родительский элемент является указателем и не инициализируется при инициализации дочернего элемента, произойдет ошибка разыменования нулевого указателя.
  • И определение типа, и варианты внедрения предоставляют доступ к полям родительского элемента.
  • Определение типа не разрешает доступ к родительским методам, но встраивание родительского делает.

Вы можете увидеть это в следующем коде.

рабочий пример на детской площадке

package main

import (
    "fmt"
)

type parent struct {
    attr string
}

type childAlias parent

type childObjParent struct {
    parent
}

type childPointerParent struct {
    *parent
}

func (p *parent) parentDo(s string) { fmt.Println(s) }
func (c *childAlias) childAliasDo(s string) { fmt.Println(s) }
func (c *childObjParent) childObjParentDo(s string) { fmt.Println(s) }
func (c *childPointerParent) childPointerParentDo(s string) { fmt.Println(s) }

func main() {
    p := &parent{"pAttr"}
    c1 := &childAlias{"cAliasAttr"}
    c2 := &childObjParent{}
    // When the parent is a pointer it must be initialized.
    // Otherwise, we get a nil pointer error when trying to set the attr.
    c3 := &childPointerParent{}
    c4 := &childPointerParent{&parent{}}

    c2.attr = "cObjParentAttr"
    // c3.attr = "cPointerParentAttr" // NOGO nil pointer dereference
    c4.attr = "cPointerParentAttr"

    // CAN do because we inherit parent's fields
    fmt.Println(p.attr)
    fmt.Println(c1.attr)
    fmt.Println(c2.attr)
    fmt.Println(c4.attr)

    p.parentDo("called parentDo on parent")
    c1.childAliasDo("called childAliasDo on ChildAlias")
    c2.childObjParentDo("called childObjParentDo on ChildObjParent")
    c3.childPointerParentDo("called childPointerParentDo on ChildPointerParent")
    c4.childPointerParentDo("called childPointerParentDo on ChildPointerParent")

    // CANNOT do because we don't inherit parent's methods
    // c1.parentDo("called parentDo on childAlias") // NOGO c1.parentDo undefined

    // CAN do because we inherit the parent's methods
    c2.parentDo("called parentDo on childObjParent")
    c3.parentDo("called parentDo on childPointerParent")
    c4.parentDo("called parentDo on childPointerParent")
}
TheHerk
источник
ваш пост очень полезен, так как показывает много исследований и усилий, пытающихся сравнить по пунктам каждую технологию ... позвольте мне побудить вас подумать, что происходит с точки зрения преобразования в данный интерфейс. Я имею в виду, если у вас есть структура и вы хотите, чтобы эта структура (предположим, от стороннего поставщика) вы хотите адаптироваться к заданному интерфейсу, кому вы удастся получить это? Для этого вы можете использовать псевдоним типа или встроенный тип.
Виктор
@Victor Я не понимаю ваш вопрос, но я думаю, что вы спрашиваете, как получить структуру, которую вы не контролируете, для удовлетворения заданного интерфейса. Короткий ответ: вы не вносите свой вклад в эту кодовую базу. Однако, используя материал в этом посте, вы можете создать другую структуру из первой, а затем реализовать интерфейс в этой структуре. См. Этот пример игровой площадки .
TheHerk
привет @TheHerk! Я хочу, чтобы вы указали на еще одно отличие при "расширении" структуры из другого пакета. Мне кажется, что есть два способа заархивировать это, используя псевдоним типа (ваш пример) и используя тип embed ( play.golang.org/p/psejeXYbz5T ). Мне кажется, что этот псевдоним типа упрощает преобразование, поскольку вам нужно только преобразование типа , если вы используете перенос типов, вам нужно ссылаться на «родительскую» структуру с помощью точки, таким образом, получая доступ к самому родительскому типу. Я думаю, это зависит от клиентского кода ...
Виктор
пожалуйста, ознакомьтесь с мотивацией этой темы здесь stackoverflow.com/a/28800807/903998 , следите за комментариями, и я надеюсь, что вы поймете мою точку зрения
Виктор
Хотел бы я понять, что вы имеете в виду, но у меня все еще проблемы. В ответе, на который мы пишем эти комментарии, я объясняю как встраивание, так и псевдоним, включая преимущества и недостатки каждого из них. Я не рекомендую одно другому. Возможно, вы предполагаете, что я пропустил один из этих плюсов или минусов.
TheHerk