Go: all: unterstützt die schrittweise Codereparatur beim Verschieben eines Typs zwischen Paketen

Erstellt am 1. Dez. 2016  ·  225Kommentare  ·  Quelle: golang/go

Originaltitel: Vorschlag: Unterstützung der schrittweisen Codereparatur beim Verschieben eines Typs zwischen Paketen

Go sollte die Möglichkeit hinzufügen, alternative äquivalente Namen für Typen zu erstellen, um eine schrittweise Codereparatur während des Refactorings der Codebasis zu ermöglichen. Dies war das Ziel des Go 1.8-Alias-Features, das in # 16339 vorgeschlagen, aber von Go 1.8 zurückgehalten wurde. Da wir das Problem für Go 1.8 nicht gelöst haben, bleibt es ein Problem, und ich hoffe, wir können es für Go 1.9 lösen.

Bei der Erörterung des Alias-Vorschlags gab es viele Fragen, warum diese Möglichkeit, insbesondere für Typen alternative Namen zu erstellen, wichtig ist. Als neuen Versuch, diese Fragen zu beantworten, habe ich den Artikel „ Codebase Refactoring (mit Hilfe von Go) “ geschrieben und gepostet. Bitte lesen Sie diesen Artikel, wenn Sie Fragen zur Motivation haben. (Eine alternative, kürzere Präsentation finden Sie in Roberts Gophercon Lightning Talk . Leider war dieses Video erst am 9. Oktober online verfügbar. Update, 16. Dezember: Hier ist mein GothamGo-Talk , der im Wesentlichen der erste Entwurf des Artikels war.)

Dieses Problem schlägt _keine_ spezifische Lösung vor. Stattdessen möchte ich Feedback von der Go-Community über den Raum möglicher Lösungen einholen. Ein möglicher Weg besteht darin, Aliase auf Typen zu beschränken, wie am Ende des Artikels erwähnt. Es mag andere geben, die wir ebenfalls in Betracht ziehen sollten.

Bitte posten Sie hier Gedanken zu Typaliasen oder anderen Lösungen als Kommentare.

Dankeschön.

Update, 16. Dezember : Design-Dokument für Typaliase veröffentlicht .
Update, 9. Januar : Vorschlag angenommen, dev.typealias-Repository erstellt, Implementierung zu Beginn des Go 1.9-Zyklus für Experimente fällig.


Diskussionszusammenfassung (zuletzt aktualisiert 02.02.2017)

Erwarten wir eine allgemeine Lösung, die für alle Deklarationen funktioniert?

Wenn Typaliase zu 100 % erforderlich sind, sind var-Aliasnamen möglicherweise zu 10 % erforderlich, func-Aliasnamen sind zu 1 % erforderlich und konstante Aliase sind zu 0 % erforderlich. Da const bereits = hat und func auch = plausibel verwenden könnte, ist die entscheidende Frage, ob var-Aliasnamen wichtig genug sind, um sie zu planen oder zu implementieren.

Wie von @rogpeppe (https://github.com/golang/go/issues/16339#issuecomment-258771806) und @ianlancetaylor (https://github.com/golang/go/issues/16339#issuecomment-233644777) argumentiert. Im ursprünglichen Alias-Vorschlag und wie im Artikel erwähnt, ist eine mutierende globale var normalerweise ein Fehler. Es ist wahrscheinlich nicht sinnvoll, die Lösung zu verkomplizieren, um einen Fehler zu berücksichtigen. (Wenn wir herausfinden könnten, wie das geht, würde es mich nicht überraschen, wenn Go langfristig dazu tendiert, globale Variablen zu verlangen, dass sie unveränderlich sind.)

Da reichhaltigere var-Aliasse wahrscheinlich nicht wichtig genug sind, um sie einzuplanen, scheint es hier die richtige Wahl zu sein, sich nur auf Typaliase zu konzentrieren. Die meisten Kommentare hier scheinen zuzustimmen. Ich werde nicht alle auflisten.

Brauchen wir eine neue Syntax (= vs => vs export)?

Das stärkste Argument für die neue Syntax ist die Notwendigkeit, var-Aliasnamen jetzt oder in Zukunft zu unterstützen (https://github.com/golang/go/issues/18130#issuecomment-264232763 von @Merovius). Es scheint in Ordnung zu sein, keine var-Aliasnamen zu verwenden (siehe vorheriger Abschnitt).

Ohne var-Aliasnamen ist die Wiederverwendung von = einfacher als die Einführung einer neuen Syntax, egal ob => wie im Aliasvorschlag, ~ (https://github.com/golang/go/issues/18130#issuecomment-264185142 von @joegrasse) oder export (https://github.com/golang/go/issues/18130#issuecomment-264152427 von @cznic).

Die Verwendung von = in würde auch genau der Syntax von Typaliasen in Pascal und Rust entsprechen. Soweit andere Sprachen die gleichen Konzepte haben, ist es schön, die gleiche Syntax zu verwenden.

Vorausschauend könnte es ein zukünftiges Go geben, in dem auch Funkaliase existieren (siehe https://github.com/golang/go/issues/18130#issuecomment-264324306 by @nigeltao), und dann würden alle Deklarationen die gleiche Form zulassen :

const C2 = C1
func F2 = F1
type T2 = T1
var V2 = V1

Die einzige davon, die keinen echten Alias ​​erstellen würde, wäre die var-Deklaration, da V2 und V1 unabhängig voneinander während der Programmausführung neu definiert werden können (im Gegensatz zu den const-, func- und type-Deklarationen, die unveränderlich sind). Da ein Hauptgrund für Variablen darin besteht, dass sie variieren können, wäre diese Ausnahme zumindest leicht zu erklären. Wenn Go sich auf unveränderliche globale Variablen zubewegt, würde sogar diese Ausnahme verschwinden.

Um es klar zu sagen, ich schlage hier keine Funkaliase oder unveränderliche globale Variablen vor, sondern arbeite nur die Auswirkungen solcher zukünftigen Ergänzungen durch.

@jimmyfrasche schlug (https://github.com/golang/go/issues/18130#issuecomment-264278398) Aliase für alles außer consts vor, sodass const die Ausnahme anstelle von var wäre:

const C2 = C1 // no => form
func F2 => F1
type T2 => T1
var V2 => V1
var V2 = V1 // different from => form

Inkonsistenzen sowohl bei const als auch bei var zu haben, scheint schwieriger zu erklären, als nur bei var eine Inkonsistenz zu haben.

Kann dies eine Tooling- oder Compiler-only-Änderung anstelle einer Sprachänderung sein?

Es lohnt sich auf jeden Fall zu fragen, ob die schrittweise Codereparatur nur durch an den Compiler gelieferte Nebeninformationen aktiviert werden kann (z. B. https://github.com/golang/go/issues/18130#issuecomment-264205929 von @btracey).

Oder vielleicht, wenn der Compiler eine Art regelbasierte Vorverarbeitung anwenden kann, um Eingabedateien vor der Kompilierung zu transformieren (z. B. https://github.com/golang/go/issues/18130#issuecomment-264329924 von @tux21b).

Leider nein, die Veränderung lässt sich so wirklich nicht eingrenzen. Es gibt mindestens zwei Compiler (gc und gccgo), die koordiniert werden müssen, aber auch alle anderen Tools, die Programme analysieren, wie go vet, guru, goimports, gocode (Codevervollständigung) und andere.

Wie @bcmills sagte (https://github.com/golang/go/issues/18130#issuecomment-264275574), „ist ein ‚non-language- Sprachänderung – es ist nur eines mit schlechterer Dokumentation.“

Welche anderen Verwendungen könnten Aliase haben?

Uns ist folgendes bekannt. Da insbesondere Typaliase als wichtig genug für die Aufnahme in Pascal und Rust angesehen wurden, gibt es wahrscheinlich noch andere.

  1. Aliase (oder einfach Aliase eingeben) würden das Erstellen von Drop-In-Ersetzungen ermöglichen, die andere Pakete erweitern. Siehe zum Beispiel https://go-review.googlesource.com/#/c/32145/ , insbesondere die Erklärung in der Commit-Nachricht.

  2. Aliase (oder einfach Typaliase) würden es ermöglichen, ein Paket mit einer kleinen API-Oberfläche, aber einer großen Implementierung als Sammlung von Paketen für eine bessere interne Struktur zu strukturieren, aber dennoch nur ein Paket zum Importieren und Verwenden von Clients bereitzustellen. Es gibt ein etwas abstraktes Beispiel, das unter https://github.com/golang/go/issues/16339#issuecomment -232813695 beschrieben ist.

  3. Protokollpuffer verfügen über eine Funktion "öffentlichen importieren", deren Semantik in generiertem C++-Code trivial, in generiertem Go-Code jedoch unmöglich zu implementieren ist. Dies führt zu Frustration für Autoren von Protokollpufferdefinitionen, die von C++- und Go-Clients gemeinsam genutzt werden. Typaliase würden Go eine Möglichkeit bieten, diese Funktion zu implementieren. Tatsächlich war der ursprüngliche Anwendungsfall für Import Public die schrittweise Code-Reparatur . Ähnliche Probleme können bei anderen Arten von Codegeneratoren auftreten.

  4. Abkürzen von langen Namen. Lokale (nicht exportierte oder nicht auf Pakete beschränkte) Aliase können praktisch sein, um einen langen Typnamen abzukürzen, ohne den Overhead eines ganz neuen Typs einzuführen. Wie bei all diesen Verwendungen würde die Klarheit des endgültigen Codes einen starken Einfluss darauf haben, ob es sich um eine vorgeschlagene Verwendung handelt.

Welche anderen Probleme muss ein Vorschlag für Typaliase berücksichtigen?

Auflistung dieser als Referenz. Ich versuche nicht, sie in diesem Abschnitt zu lösen oder zu diskutieren, obwohl einige später besprochen wurden und unten in separaten Abschnitten zusammengefasst sind.

  1. Handhabung in godoc. (https://github.com/golang/go/issues/18130#issuecomment-264323137 von @nigeltao und https://github.com/golang/go/issues/18130#issuecomment-264326437 von @jimmyfrasche)

  2. Können Methoden für Typen mit Aliasnamen definiert werden? (https://github.com/golang/go/issues/18130#issuecomment-265077877 von @ulikunitz)

  3. Wie gehen wir mit Alias-Zyklen um, wenn Aliase zu Alias ​​zulässig ist? (https://github.com/golang/go/issues/18130#issuecomment-264494658 von @thwd)

  4. Sollten Aliase nicht exportierte Bezeichner exportieren können? (https://github.com/golang/go/issues/18130#issuecomment-264494658 von @thwd)

  5. Was passiert, wenn Sie einen Alias ​​einbetten (wie greifen Sie auf das eingebettete Feld zu)? (https://github.com/golang/go/issues/18130#issuecomment-264494658 von @thwd , auch #17746)

  6. Sind Aliase als Symbole im erstellten Programm verfügbar? (https://github.com/golang/go/issues/18130#issuecomment-264494658 von @thwd)

  7. Ldflags-String-Injection: Was ist, wenn wir auf einen Alias ​​verweisen? (https://github.com/golang/go/issues/18130#issuecomment-264494658 von @thwd; dies tritt nur auf, wenn es var-Aliasse gibt.)

Ist Versionierung eine Lösung für sich?

"In diesem Fall ist die Versionierung vielleicht die ganze Antwort, nicht Typaliase."
(https://github.com/golang/go/issues/18130#issuecomment-264573088 von @iainmerrick)

Wie im Artikel erwähnt , ist die Versionierung meiner Meinung nach ein komplementäres Anliegen. Die Unterstützung der schrittweisen Codereparatur, beispielsweise mit Typaliasen, gibt einem Versionierungssystem mehr Flexibilität bei der Erstellung eines großen Programms, was den Unterschied zwischen der Fähigkeit zur Erstellung des Programms oder der Nicht-Erstellung ausmachen kann.

Kann das größere Refactoring-Problem stattdessen gelöst werden?

In https://github.com/golang/go/issues/18130#issuecomment -265052639 weist @niemeyer darauf hin, dass es beim Verschieben von os.Error tatsächlich zwei Änderungen gab: Der Name hat sich geändert, aber auch die Definition (der aktuelle Error-Methode war früher eine String-Methode).

@niemeyer schlägt vor, dass wir vielleicht eine Lösung für das umfassendere Refactoring-Problem finden können, das das Verschieben von Typen zwischen Paketen als Sonderfall behebt, aber auch Dinge wie das Ändern von Methodennamen behandelt, und er schlägt eine Lösung vor, die auf "Adaptern" basiert.

In den Kommentaren gibt es eine Menge Diskussionen, die ich hier nicht einfach zusammenfassen kann. Die Diskussion ist noch nicht beendet, aber bisher ist unklar, ob "Adapter" in die Sprache passen oder in die Praxis umgesetzt werden können. Es scheint klar zu sein, dass Adapter mindestens eine Größenordnung komplexer sind als Typaliase.

Adapter benötigen auch eine kohärente Lösung für die unten aufgeführten Subtyping-Probleme.

Können Methoden für Aliastypen deklariert werden?

Aliase erlauben es sicherlich nicht, die üblichen Beschränkungen der Methodendefinition zu umgehen: Wenn ein Paket den Typ T1 = otherpkg.T2 definiert, kann es keine Methoden auf T1 definieren, ebenso wie es keine Methoden direkt auf otherpkg.T2 definieren kann. Das heißt, wenn Typ T1 = otherpkg.T2 ist, dann ist func (T1) M() äquivalent zu func (otherpkg.T2) M(), die heute ungültig ist und ungültig bleibt. Wenn ein Paket jedoch den Typ T1 = T2 (beide im selben Paket) definiert, ist die Antwort weniger klar. In diesem Fall wäre func (T1) M() äquivalent zu func (T2) M(); da letzteres zulässig ist, gibt es ein Argument, ersteres zuzulassen. Die aktuelle Konstruktionsdokumentation macht hier keine Einschränkung (im Sinne der generellen Vermeidung von Restriktionen), so dass func (T1) M() in dieser Situation gültig ist.

In https://github.com/golang/go/issues/18130#issuecomment -267694112 schlägt @jimmyfrasche vor, dass stattdessen die Definition von "keine Verwendung von Aliasen in Methodendefinitionen" eine klare Regel wäre und nicht wissen muss, was T definiert ist um zu wissen, ob func (T) M() gültig ist. In https://github.com/golang/go/issues/18130#issuecomment -267997124 weist @rsc darauf hin, dass es auch heute noch bestimmte T gibt, für die func (T) M() nicht gültig ist: https://play .golang.org/p/bci2qnldej. In der Praxis kommt das nicht vor, weil Leute vernünftigen Code schreiben.

Wir werden diese mögliche Einschränkung im Hinterkopf behalten, aber warten Sie mit der Einführung ab, bis starke Beweise dafür vorliegen, dass sie erforderlich ist.

Gibt es einen saubereren Weg, das Einbetten und allgemeiner das Umbenennen von Feldern zu handhaben?

In https://github.com/golang/go/issues/18130#issuecomment -267691816 weist @Merovius darauf hin, dass ein eingebetteter Typ, der seinen Namen während einer Paketverschiebung ändert, Probleme verursachen wird, wenn dieser neue Name schließlich am Websites verwenden. Wenn beispielsweise der Benutzertyp U einen eingebetteten io.ByteBuffer hat, der in bytes.Buffer verschoben wird, dann ist der Feldname, während U io.ByteBuffer einbettet, U.ByteBuffer, aber wenn U aktualisiert wird, um auf bytes.Buffer zu verweisen, ist der Feldname notwendigerweise Änderungen an U.Buffer.

In https://github.com/golang/go/issues/18130#issuecomment -267710478 weist @neild darauf hin, dass es zumindest einen Workaround gibt, wenn Verweise auf io.ByteBuffer ausgeschnitten werden müssen: Das Paket P, das U definiert, kann auch definiere 'type ByteBuffer = bytes.Buffer' und bette diesen Typ in U ein. Dann hat U immer noch einen U.ByteBuffer, auch nachdem io.ByteBuffer vollständig verschwunden ist.

In https://github.com/golang/go/issues/18130#issuecomment -267703067 schlägt @bcmills die Idee von type U struct { bytes.Buffer; ByteBuffer = Buffer } anstatt den Typalias der obersten Ebene erstellen zu müssen.

In https://github.com/golang/go/issues/18130#issuecomment -268001111 wirft @rsc noch eine weitere Möglichkeit auf: eine Syntax für 'diesen Typ mit diesem Namen einbetten', damit es möglich ist, ein Byte einzubetten. Buffer als Feldname ByteBuffer, ohne dass ein Typ der obersten Ebene oder ein alternativer Name erforderlich ist. Wenn dies vorhanden wäre, könnte der Typname von io.ByteBuffer auf bytes.Buffer aktualisiert werden, während der ursprüngliche Name beibehalten wird (und kein zweiter oder ungeschickter exportierter Typ eingeführt wird).

All dies scheint es wert zu sein, untersucht zu werden, sobald wir mehr Beweise für umfangreiche Refactorings haben, die durch Probleme mit der Namensänderung von Feldern blockiert werden. Wie @rsc schrieb: "Wenn

Es gab einen Vorschlag, die Verwendung von Aliasen in eingebetteten Feldern einzuschränken oder den eingebetteten Namen zu ändern, um den Namen des Zieltyps zu verwenden, aber diese führen dazu, dass die Alias-Einführung vorhandene Definitionen bricht, die dann atomar fixiert werden müssen, was im Wesentlichen jede schrittweise Reparatur verhindert. @rsc : "Wir haben dies in #17746 ausführlich besprochen. Ich war ursprünglich auf der Seite des Namens eines eingebetteten io.ByteBuffer-Alias, der Buffer ist, aber das obige Argument hat mich überzeugt, dass ich falsch lag. Insbesondere @jimmyfrasche hat etwas Gutes

Wie wirkt sich dies auf Programme aus, die Reflexion verwenden?

Programme, die Reflektion verwenden, sehen Aliase durch. In https://github.com/golang/go/issues/18130#issuecomment -267903649 weist @atdiar darauf hin, dass wenn ein Programm Reflektion verwendet, um beispielsweise das Paket zu finden, in dem ein Typ definiert ist, oder sogar den Namen eines Typs wird die Änderung beim Verschieben des Typs beobachtet, selbst wenn ein Weiterleitungsalias zurückgelassen wird. In https://github.com/golang/go/issues/18130#issuecomment -268001410 bestätigte @rsc dies und schrieb: „Wie die Situation beim Einbetten ist es nicht perfekt. Im Gegensatz zur Situation beim Einbetten habe ich keine Antworten, außer vielleicht sollte Code nicht mit Reflect geschrieben werden, um so empfindlich auf diese Details zu reagieren."

Die heutige Verwendung von Paketen von Anbietern ändert auch die von Reflect erkannten Paketimportpfade, und wir wurden nicht auf bedeutende Probleme aufmerksam gemacht, die durch diese Mehrdeutigkeit verursacht werden. Dies deutet darauf hin, dass Programme reflect.Type.PkgPath normalerweise nicht auf eine Weise untersuchen, die durch die Verwendung von Aliasen unterbrochen würde. Trotzdem ist es eine potenzielle Lücke, genau wie beim Einbetten.

Wie wirkt sich die separate Kompilierung von Programmen und Plugins aus?

In https://github.com/golang/go/issues/18130#issuecomment -268524504 wirft @atdiar die Frage nach der Auswirkung auf Objektdateien und separate Kompilierung auf. In https://github.com/golang/go/issues/18130#issuecomment -268560180 antwortet @rsc , dass hier keine Änderungen erforderlich sein sollten: Wenn X Y- und Y-Änderungen importiert und neu kompiliert wird, muss X dies tun auch neu kompiliert werden. Das gilt heute ohne Aliase und wird auch mit Aliasen so bleiben. Getrennte Kompilierung bedeutet, X und Y in unterschiedlichen Schritten kompilieren zu können (der Compiler muss sie nicht im selben Aufruf verarbeiten), nicht dass es möglich ist, Y zu ändern, ohne X neu zu kompilieren.

Wären Summentypen oder eine Art Untertypisierung eine alternative Lösung?

In https://github.com/golang/go/issues/18130#issuecomment -264413439 schlägt @iand " https://github.com/golang/go/issues/18130#issuecomment -268072274 schlägt @j7b vor, algebraische Typen zu verwenden, "damit wir auch ein leeres Schnittstellenäquivalent mit Kompilierzeit-Typüberprüfung als Bonus erhalten". Andere Bezeichnungen für dieses Konzept sind Summentypen und Variantentypen.

Im Allgemeinen reicht dies nicht aus, um das Verschieben von Typen mit schrittweiser Codereparatur zu ermöglichen. Es gibt zwei Möglichkeiten, darüber nachzudenken.

In https://github.com/golang/go/issues/18130#issuecomment -268075680 geht @bcmills den konkreten Weg und weist darauf hin, dass algebraische Typen eine andere Darstellung als das Original haben, was eine Behandlung der Summe nicht möglich macht und das Original als austauschbar: Letzteres hat Typenschilder.

In https://github.com/golang/go/issues/18130#issuecomment -268585497 geht @rsc den theoretischen Weg und erweitert auf https://github.com/golang/go/issues/18130#issuecomment -265211655 von @gri weist darauf hin, dass bei einer schrittweisen

Neben der Tatsache, dass das Problem der schrittweisen Codereparatur nicht gelöst wird, sind algebraische Typen / Summentypen / Vereinigungstypen / Variantentypen für sich allein schwer zu Go hinzuzufügen. Sehen
die FAQ-Antwort und die Go 1.6 AMA-Diskussion für mehr.

In https://github.com/golang/go/issues/18130#issuecomment -265206780 schlägt @thwd vor, dass Go eine Untertypbeziehung zwischen konkreten Typen und Schnittstellen hat (bytes.Buffer kann als Untertyp von io.Reader angesehen werden). ) und zwischen Schnittstellen (io.ReadWriter ist in gleicher Weise ein Untertyp von io.Reader), die Schnittstellen "rekursiv kovariant (gemäß den aktuellen Varianzregeln) bis auf ihre Methodenargumente" zu machen, würde das Problem lösen, vorausgesetzt, dass alle zukünftigen Pakete nur Verwenden Sie Schnittstellen, niemals konkrete Typen wie structs ("fördert auch gutes Design").

Als Lösung gibt es drei Probleme. Erstens hat es die oben genannten Subtyping-Probleme, sodass die schrittweise Codereparatur nicht gelöst wird. Zweitens gilt es nicht für vorhandenen Code, wie @thwd in diesem Vorschlag angemerkt hat. Drittens ist es möglicherweise kein gutes Design, die Verwendung von Schnittstellen überall zu erzwingen, und führt zu Performance-Overheads (siehe zum Beispiel https://github.com/golang/go/issues/18130#issuecomment-265211726 von @Merovius und https://github .com/golang/go/issues/18130#issuecomment-265224652 von @zombiezen).

Einschränkungen

Dieser Abschnitt enthält vorgeschlagene Einschränkungen als Referenz, aber denken Sie daran, dass Einschränkungen die Komplexität erhöhen. Wie ich in https://github.com/golang/go/issues/18130#issuecomment -264195616 geschrieben habe, „sollten wir diese Einschränkungen wahrscheinlich erst nach tatsächlichen Erfahrungen mit dem uneingeschränkten, einfacheren Design umsetzen, das uns hilft zu verstehen, ob die Einschränkung genug bringen würde Vorteile, um seine Kosten zu bezahlen."

Anders ausgedrückt, jede Einschränkung müsste durch Beweise dafür gerechtfertigt werden, dass sie ernsthaften Missbrauch oder Verwechslungen verhindern würden. Da wir noch keine Lösung implementiert haben, gibt es solche Beweise nicht. Wenn die Erfahrung diesen Beweis erbracht hat, lohnt es sich, darauf zurückzukommen.

Beschränkung? Aliase von Standardbibliothekstypen können nur in der Standardbibliothek deklariert werden.

(https://github.com/golang/go/issues/18130#issuecomment-264165833 und https://github.com/golang/go/issues/18130#issuecomment-264171370 von @iand)

Das Problem ist "Code, der Standardbibliothekskonzepte umbenannt hat, um einer benutzerdefinierten Namenskonvention zu entsprechen", oder "lange Spaghettiketten von Aliasen über mehrere Pakete hinweg, die wieder in der Standardbibliothek landen" oder "Aliasing-Dinge wie Interface{} und Fehler" .

Wie bereits erwähnt, würde die Einschränkung den oben beschriebenen Fall "Erweiterungspaket" mit x/image/draw nicht zulassen.

Es ist unklar, warum die Standardbibliothek etwas Besonderes sein sollte: Die Probleme würden bei jedem Code bestehen. Außerdem ist weder interface{} noch error ein Typ aus der Standardbibliothek. Eine Umformulierung der Einschränkung in "Vordefinierte Typen aliasieren" würde Aliasing-Fehler verbieten, aber die Notwendigkeit, Alias-Fehler zu erstellen, war eines der motivierenden Beispiele in dem Artikel.

Beschränkung? Alias-Ziel muss ein paketqualifizierter Bezeichner sein.

(https://github.com/golang/go/issues/18130#issuecomment-264188282 von @jba)

Dies würde es unmöglich machen, beim Umbenennen eines Typs innerhalb eines Pakets einen Alias ​​zu erstellen, der weit genug verwendet werden kann, um eine schrittweise Reparatur erforderlich zu machen (https://github.com/golang/go/issues/18130#issuecomment-264274714 von @ bcmills).

Es würde auch Aliasing-Fehler wie im Artikel verbieten.

Beschränkung? Das Aliasziel muss ein paketqualifizierter Bezeichner mit demselben Namen wie der Alias ​​sein.

(vorgeschlagen während der Alias-Diskussion in Go 1.8)

Zusätzlich zu den Problemen des vorherigen Abschnitts mit der Beschränkung auf paketqualifizierte Bezeichner würde das Erzwingen des Beibehaltens des Namens die Konvertierung von io.ByteBuffer in bytes.Buffer im Artikel verbieten.

Beschränkung? Aliasnamen sollten in irgendeiner Weise vermieden werden.

"Wie wäre es, Aliase hinter einem Import zu verstecken, genau wie bei "C" und "unsafe", um dessen Verwendung weiter zu entmutigen? Ebenso möchte ich, dass die Alias-Syntax ausführlich ist und sich als Gerüst für die weitere Umgestaltung herausstellt ." - https://github.com/golang/go/issues/18130#issuecomment -264289940 von @xiegeo

"Sollten wir auch automatisch folgern, dass ein Alias-Typ veraltet ist und durch den neuen Typ ersetzt werden sollte? Wenn wir golint, godoc und ähnliche Tools erzwingen, um den alten Typ als veraltet zu visualisieren, würde dies den Missbrauch von Typ-Aliasing sehr stark einschränken. Und die letzte Sorge, dass die Aliasing-Funktion missbraucht wird, wäre ausgeräumt." - https://github.com/golang/go/issues/18130#issuecomment -265062154 von @rakyll

Bis wir wissen, dass sie falsch verwendet werden, erscheint es verfrüht, von der Verwendung abzuraten. Es kann gute, nicht vorübergehende Verwendungen geben (siehe oben).

Auch im Fall einer Codereparatur kann während des Übergangs entweder der alte oder der neue Typ der Alias ​​sein, abhängig von den Einschränkungen, die durch den Importgraphen auferlegt werden. Ein Alias ​​zu sein bedeutet nicht, dass der Name veraltet ist.

Es gibt bereits einen Mechanismus, um bestimmte Deklarationen als veraltet zu markieren (siehe https://github.com/golang/go/issues/18130#issuecomment-265294564 von @jimmyfrasche).

Beschränkung? Aliase müssen auf benannte Typen abzielen.

"Aliase sollten nicht für unbenannte Typen gelten. Es gibt keine "Code-Reparatur"-Geschichte beim Wechsel von einem unbenannten Typ zu einem anderen. Aliase für unbenannte Typen zuzulassen bedeutet, dass ich Go nicht mehr einfach als benannte und unbenannte Typen beibringen kann." - https://github.com/golang/go/issues/18130#issuecomment -276864903 von @davecheney

Bis wir wissen, dass sie falsch verwendet werden, erscheint es verfrüht, von der Verwendung abzuraten. Es kann gute Verwendungen mit unbenannten Zielen geben (siehe oben).

Wie im Design-Dokument erwähnt, erwarten wir eine Änderung der Terminologie, um die Situation klarer zu machen.

FrozenDueToAge Proposal Proposal-Accepted

Hilfreichster Kommentar

@cznic , @iand , andere: Bitte beachten Sie, dass _Einschränkungen die Komplexität erhöhen_. Sie verkomplizieren die Erklärung der Funktion und erhöhen die kognitive Belastung für jeden Benutzer der Funktion: Wenn Sie eine Einschränkung vergessen, müssen Sie sich überlegen, warum etwas, von dem Sie dachten, dass es funktionieren sollte, nicht funktioniert.

Es ist oft ein Fehler, Beschränkungen für einen Versuch eines Designs nur aufgrund von hypothetischem Missbrauch zu implementieren. Dies geschah in den Diskussionen über den Alias-Vorschlag und machte die Aliase in der Testversion nicht in der Lage, die Konvertierung von io.ByteBuffer => bytes.Buffer aus dem Artikel zu verarbeiten. Ein Teil des Ziels des Schreibens des Artikels besteht darin, einige Fälle zu definieren, von denen wir wissen, dass wir sie bewältigen wollen, damit wir sie nicht versehentlich ausschließen.

Als weiteres Beispiel wäre es einfach, ein Missbrauchsargument zu verwenden, um Nicht-Zeiger-Empfänger oder Methoden für Nicht-Struct-Typen zu verbieten. Wenn wir beides getan hätten, könnten Sie keine Aufzählungen mit String()-Methoden erstellen, um sich selbst zu drucken, und Sie könnten nicht http.Headers sowohl eine einfache Map als auch Hilfsmethoden bereitstellen. Missbrauch kann man sich oft leicht vorstellen; Es kann länger dauern, bis überzeugende positive Anwendungen erscheinen, und es ist wichtig, Raum für Experimente zu schaffen.

Als weiteres Beispiel wurde beim ursprünglichen Entwurf und der ursprünglichen Implementierung für Zeiger- und Wertmethoden nicht zwischen den Methodensätzen auf T und *T unterschieden: Wenn Sie ein *T hätten, könnten Sie die Wertmethoden (Empfänger T) aufrufen, und wenn Sie a T können Sie die Zeigermethoden aufrufen (Empfänger *T). Dies war einfach, ohne Einschränkungen zu erklären. Aber dann hat uns die tatsächliche Erfahrung gezeigt, dass das Zulassen von Zeigermethodenaufrufen auf Werte zu einer bestimmten Klasse verwirrender, überraschender Fehler führte. Du könntest zum Beispiel schreiben:

var buf bytes.Buffer
io.Copy(buf, reader)

und io.Copy würde erfolgreich sein, aber buf hätte nichts darin. Wir mussten uns entscheiden, ob wir erklären, warum dieses Programm nicht richtig lief, oder erklären, warum dieses Programm nicht kompiliert wurde. So oder so würde es Fragen geben, aber wir waren auf der Seite, eine falsche Ausführung zu vermeiden. Trotzdem mussten wir noch einen FAQ-Eintrag darüber

Bitte denken Sie auch hier daran, dass Einschränkungen die Komplexität erhöhen. Wie jede Komplexität bedürfen auch Beschränkungen einer erheblichen Begründung. In dieser Phase des Designprozesses ist es gut, über Einschränkungen nachzudenken, die für ein bestimmtes Design angemessen sein könnten, aber wir sollten diese Einschränkungen wahrscheinlich erst implementieren, nachdem tatsächliche Erfahrungen mit dem uneingeschränkten, einfacheren Design uns helfen zu verstehen, ob die Einschränkung genügend Vorteile bringt seine Kosten zu bezahlen.

Alle 225 Kommentare

Ich mag, wie optisch einheitlich das aussieht.

const OldAPI => NewPackage.API
func  OldAPI => NewPackage.API
var   OldAPI => NewPackage.API
type  OldAPI => NewPackage.API

Aber da wir die meisten Elemente fast nach und nach verschieben können, vielleicht das einfachste
Lösung _ist_ nur um = für Typen zuzulassen.

const OldAPI = NewPackage.API
func  OldAPI() { NewPackage.API() }
var   OldAPI = NewPackage.API
type  OldAPI = NewPackage.API

Also zuerst wollte ich mich nur für diese hervorragende Beschreibung bedanken. Ich denke, die beste Lösung besteht darin, Typaliase mit einem Zuweisungsoperator einzuführen. Dies erfordert keine neuen Schlüsselwörter/Operatoren, verwendet eine bekannte Syntax und sollte das Refactoring-Problem für große Codebasen lösen.

Wie der Artikel von Russ betont, muss jede aliasähnliche Lösung https://github.com/golang/go/issues/17746 und https://github.com/golang/go/issues/17784 elegant lösen

Vielen Dank für das Verfassen dieses Artikels.

Ich finde die Nur-Typ-Aliasnamen mit dem Zuweisungsoperator am besten:

type OldAPI = NewPackage.API

Meine Gründe:

  • Es ist einfacher.
    Die alternative Lösung => eine subtil unterschiedliche Bedeutung basierend auf ihrem Operanden zu haben, fühlt sich für Go fehl am Platz an.
  • Es ist fokussiert und konservativ.
    Das vorliegende Problem mit Typen ist gelöst und Sie müssen sich keine Sorgen machen, wie kompliziert die verallgemeinerte Lösung ist.
  • Es ist ästhetisch.
    Ich finde es sieht gefälliger aus.

All dies oben: Das Ergebnis, das einfach, fokussiert, konservativ und ästhetisch ist, macht es mir leicht, mir vorzustellen, dass es ein Teil von Go ist.

Wenn die Lösung nur auf Typen beschränkt wäre, dann ist die Syntax

type NewFoo = old.Foo

schon vorher in Betracht gezogen, wie im Artikel von @rsc besprochen, sieht für mich sehr gut aus.

Wenn wir das Gleiche für Konstanten, Variablen und Funktionen machen möchten, wäre meine bevorzugte Syntax (wie zuvor vorgeschlagen)

package newfmt

import (
    "fmt"
)

// No renaming.
export fmt.Printf // Note: Same as `export Printf fmt.Printf`.

export (
        fmt.Sprintf
        fmt.Formatter
)

// Renaming.
export Foo fmt.Errorf // Foo must be exported, ie. `export foo fmt.Errorf` would be invalid.

export (
    Bar fmt.Fprintf
    Qux fmt.State
)

Der Nachteil besteht, wie bereits erwähnt, darin, dass ein neues Top-Level-Only-Keyword eingeführt wird, was zugegebenermaßen umständlich ist, obwohl es technisch machbar und vollständig abwärtskompatibel ist. Ich mag diese Syntax, weil sie das Importmuster widerspiegelt. Es erscheint mir selbstverständlich, dass Exporte nur in dem Abschnitt erlaubt werden, in dem auch Importe erlaubt sind, d.h. zwischen der Paketklausel und einer beliebigen Variablen-, Typ-, Konstanten- oder Funktions-TLD.

Die Umbenennungsbezeichner würden im Paketbereich deklariert, die neuen Namen sind jedoch in dem Paket, das sie deklariert (newfmt im obigen Beispiel), oben in Bezug auf die erneute Deklaration, die wie üblich nicht zulässig ist,

var v = Printf // undefined: Printf.
var Printf int // Printf redeclared, previous declaration at newfmt.go:8.

Im importierenden Paket sind die Umbenennungskennungen normal sichtbar, wie alle anderen exportierten Kennungen des Paketblocks (newftm).

package foo

import "newfmt"

type bar interface {
    baz(qux newfmt.Qux) // qux type is identical to fmt.State.
}

Zusammenfassend lässt sich sagen, dass dieser Ansatz keine neue lokale Namensbindung in newfmt einführt, was meiner Meinung nach zumindest einige der in #17746 diskutierten Probleme vermeidet und #17784 vollständig löst.

Meine erste Vorliebe ist für ein Nur-Schreiben type NewFoo = old.Foo .

Wenn eine allgemeinere Lösung gewünscht wird, stimme ich @cznic zu, dass ein dediziertes Schlüsselwort besser ist als ein neuer Operator (insbesondere ein asymmetrischer Operator mit verwirrender Direktionalität[1]). Davon abgesehen glaube ich nicht, dass das Schlüsselwort export die richtige Bedeutung hat. Weder die Syntax noch die Semantik spiegeln import wider. Was ist mit alias ?

Ich verstehe, warum @cznic nicht möchte, dass die neuen Namen in dem Paket verfügbar sind, in dem sie deklariert werden, aber zumindest für mich fühlt sich diese Einschränkung unerwartet und künstlich an (obwohl ich den Grund dafür sehr gut verstehe).

[1] Ich benutze Unix seit fast 20 Jahren und kann immer noch keinen Symlink beim ersten Versuch erstellen. Und ich scheitere meist auch beim zweiten Versuch, nachdem ich das Handbuch gelesen habe.

Ich möchte eine zusätzliche Einschränkung vorschlagen: Typaliase für Standardbibliothekstypen dürfen nur in der Standardbibliothek deklariert werden.

Meine Begründung ist, dass ich nicht mit Code arbeiten möchte, der Standardbibliothekskonzepte umbenannt hat, um einer benutzerdefinierten Namenskonvention zu entsprechen. Ich möchte auch nicht mit langen Spaghettiketten von Aliasen über mehrere Pakete hinweg umgehen, die wieder in der Standardbibliothek landen.

@iand : Diese Einschränkung würde die Verwendung dieser Funktion blockieren, um alles in die Standardbibliothek zu migrieren. Ein typisches Beispiel ist die aktuelle Migration von Context in die Standardbibliothek. Die alte Heimat von Context sollte ein Alias ​​für die Context in der Standardbibliothek werden.

@quentinmit das stimmt leider. Es begrenzt auch den Anwendungsfall für golang.org/x/image/draw in dieser CL https://go-review.googlesource.com/#/c/32145/

Meine eigentliche Sorge gilt den Leuten, die Dinge wie interface{} und error

Wenn beschlossen wird, einen neuen Operator einzuführen, möchte ich ~ vorschlagen. In der englischen Sprache wird es im Allgemeinen als "ähnlich", "ungefähr", "etwa" oder "rund" verstanden. Wie @4ad oben erwähnt hat, ist => ein asymmetrischer Operator mit verwirrender Direktionalität.

Zum Beispiel:

const OldAPI ~ NewPackage.API
func  OldAPI ~ NewPackage.API
var   OldAPI ~ NewPackage.API
type  OldAPI ~ NewPackage.API

@ianund wenn wir die rechte Seite auf einen ausräumen .

Es würde auch bedeuten, dass Sie keine Aliase für Typen im aktuellen Paket oder für lange Typausdrücke wie map[string]map[int]interface{} könnten. Aber diese Verwendungen haben nichts mit dem Hauptziel der schrittweisen Codereparatur zu tun, also sind sie vielleicht kein großer Verlust.

@cznic , @iand , andere: Bitte beachten Sie, dass _Einschränkungen die Komplexität erhöhen_. Sie verkomplizieren die Erklärung der Funktion und erhöhen die kognitive Belastung für jeden Benutzer der Funktion: Wenn Sie eine Einschränkung vergessen, müssen Sie sich überlegen, warum etwas, von dem Sie dachten, dass es funktionieren sollte, nicht funktioniert.

Es ist oft ein Fehler, Beschränkungen für einen Versuch eines Designs nur aufgrund von hypothetischem Missbrauch zu implementieren. Dies geschah in den Diskussionen über den Alias-Vorschlag und machte die Aliase in der Testversion nicht in der Lage, die Konvertierung von io.ByteBuffer => bytes.Buffer aus dem Artikel zu verarbeiten. Ein Teil des Ziels des Schreibens des Artikels besteht darin, einige Fälle zu definieren, von denen wir wissen, dass wir sie bewältigen wollen, damit wir sie nicht versehentlich ausschließen.

Als weiteres Beispiel wäre es einfach, ein Missbrauchsargument zu verwenden, um Nicht-Zeiger-Empfänger oder Methoden für Nicht-Struct-Typen zu verbieten. Wenn wir beides getan hätten, könnten Sie keine Aufzählungen mit String()-Methoden erstellen, um sich selbst zu drucken, und Sie könnten nicht http.Headers sowohl eine einfache Map als auch Hilfsmethoden bereitstellen. Missbrauch kann man sich oft leicht vorstellen; Es kann länger dauern, bis überzeugende positive Anwendungen erscheinen, und es ist wichtig, Raum für Experimente zu schaffen.

Als weiteres Beispiel wurde beim ursprünglichen Entwurf und der ursprünglichen Implementierung für Zeiger- und Wertmethoden nicht zwischen den Methodensätzen auf T und *T unterschieden: Wenn Sie ein *T hätten, könnten Sie die Wertmethoden (Empfänger T) aufrufen, und wenn Sie a T können Sie die Zeigermethoden aufrufen (Empfänger *T). Dies war einfach, ohne Einschränkungen zu erklären. Aber dann hat uns die tatsächliche Erfahrung gezeigt, dass das Zulassen von Zeigermethodenaufrufen auf Werte zu einer bestimmten Klasse verwirrender, überraschender Fehler führte. Du könntest zum Beispiel schreiben:

var buf bytes.Buffer
io.Copy(buf, reader)

und io.Copy würde erfolgreich sein, aber buf hätte nichts darin. Wir mussten uns entscheiden, ob wir erklären, warum dieses Programm nicht richtig lief, oder erklären, warum dieses Programm nicht kompiliert wurde. So oder so würde es Fragen geben, aber wir waren auf der Seite, eine falsche Ausführung zu vermeiden. Trotzdem mussten wir noch einen FAQ-Eintrag darüber

Bitte denken Sie auch hier daran, dass Einschränkungen die Komplexität erhöhen. Wie jede Komplexität bedürfen auch Beschränkungen einer erheblichen Begründung. In dieser Phase des Designprozesses ist es gut, über Einschränkungen nachzudenken, die für ein bestimmtes Design angemessen sein könnten, aber wir sollten diese Einschränkungen wahrscheinlich erst implementieren, nachdem tatsächliche Erfahrungen mit dem uneingeschränkten, einfacheren Design uns helfen zu verstehen, ob die Einschränkung genügend Vorteile bringt seine Kosten zu bezahlen.

Außerdem hoffe ich, dass wir zu Beginn des Go 1.9-Zyklus (idealerweise am Tag, an dem der Zyklus beginnt) eine vorläufige Entscheidung darüber treffen können, was wir versuchen sollen, und dann etwas zum Experimentieren bereithalten. Mehr Zeit zum Experimentieren zu haben, hat viele Vorteile, darunter die Möglichkeit zu erfahren, ob eine bestimmte Einschränkung zwingend ist. Ein Fehler mit Alias ​​bestand darin, dass erst gegen Ende des Go 1.8-Zyklus eine vollständige Implementierung vorgenommen wurde.

Eine Sache des ursprünglichen Alias-Vorschlags ist, dass die tatsächliche Verwendung des Alias-Typs im beabsichtigten Anwendungsfall (ermöglicht Refactoring) nur temporär sein sollte. Im Beispiel protobuffer wurde der Stub io.BytesBuffer nach Abschluss der schrittweisen Reparatur gelöscht.

Wenn der Alias-Mechanismus nur temporär zu sehen sein soll, ist dann tatsächlich ein Sprachwechsel erforderlich? Vielleicht könnte es stattdessen einen Mechanismus geben, um gc mit einer Liste von "Aliasnamen" zu versorgen. gc könnte die Ersetzungen vorübergehend vornehmen, und der Autor der nachgelagerten Codebasis könnte Elemente in dieser Datei nach und nach entfernen, wenn Fixes zusammengeführt werden. Mir ist klar, dass dieser Vorschlag auch knifflige Konsequenzen hat, aber er fördert zumindest einen vorübergehenden Mechanismus.

Ich werde nicht am Bikeshedding über Syntax teilnehmen (ist mir im Grunde egal), mit einer Ausnahme: Wenn Aliase hinzugefügt und auf Typen beschränkt werden soll, verwenden Sie bitte eine Syntax, die konsequent auf mindestens var , wenn nicht auch func und const (alle vorgeschlagenen syntaktischen Konstrukte erlauben alle, außer type Foo = pkg.Bar ). Der Grund ist, dass, obwohl ich zustimme, dass Fälle, in denen Aliase für var den Unterschied machen, selten sind, ich aber nicht glaube, dass sie nicht existieren, und daher glaube, dass wir uns irgendwann dazu entschließen könnten, hinzuzufügen Sie auch. An diesem Punkt möchten wir definitiv, dass alle Alias-Deklarationen konsistent sind, es wäre schlecht, wenn es type Foo = pkg.Bar und var Foo => pkg.Bar .

Ich würde auch leicht dafür argumentieren, alle vier zu haben. Die Gründe sind

1) Es gibt eine Unterscheidung für var und ich benutze sie manchmal. Zum Beispiel mache ich oft ein globales var Debug *log.Logger verfügbar oder weise globale Singletons wie http.DefaultServeMux zu, um Registrierungen von Paketen abzufangen/zu entfernen, die ihm Handler hinzufügen.

2) Ich denke auch, dass, während func Foo() { pkg.Bar() } dasselbe wie func Foo => pkg.Bar func Foo() { pkg.Bar() } tut, die Absicht von letzterem viel klarer ist (besonders wenn Sie bereits über Aliase Bescheid wissen). Es heißt eindeutig "das soll nicht wirklich hier sein". Obwohl technisch identisch, kann die Alias-Syntax als Dokumentation dienen.

Es ist jedoch nicht der Hügel, auf dem ich sterben würde; Nur Typ-Aliasnamen wären für mich in Ordnung, solange es die Möglichkeit gibt, sie später zu erweitern.

Ich bin auch super froh, dass dies so geschrieben wurde, wie es war. Es fasst eine Reihe von Meinungen zusammen, die ich eine Weile über API-Design und -Stabilität hatte und wird in Zukunft auch als einfache Referenz dienen, um Leute zu verlinken :)

Allerdings möchte ich auch betonen , dass dort , wo zusätzliche Anwendungsfälle durch Aliase abgedeckt , die aus dem doc verschieden sind (und AIUI die allgemeinere Absicht dieser Ausgabe, die einige Lösung zu lösen schrittweise Reparatur zu finden ist). Ich freue mich sehr, wenn sich die Community auf das Konzept einigen kann, eine schrittweise Reparatur zu ermöglichen, aber wenn eine andere Entscheidung als Aliase getroffen wird, um es zu erreichen, denke ich auch, dass in diesem Fall gleichzeitig darüber gesprochen werden sollte, ob und wie man unterstützt wird Dinge wie die öffentlichen Protobuf-Importe oder der x/image/draw Anwendungsfall von Drop-in-Ersatzpaketen (beide mir auch etwas am Herzen) mit einer anderen Lösung. Der Vorschlag von @btracey für ein go-tool/gc-Flag für Aliase ist ein Beispiel, bei dem ich glaube, dass es zwar die schrittweise Reparatur relativ gut abdeckt, aber für diese anderen Anwendungsfälle nicht wirklich akzeptabel ist. Sie können nicht wirklich von jedem erwarten, der etwas kompilieren möchte, das x/image/draw , um diese Flags zu übergeben, er sollte nur in der Lage sein, go get .

@jba

@ianund wenn wir die rechte Seite auf einen ausräumen .

Es würde auch bedeuten, dass Sie im aktuellen Paket keine Aliasnamen für einen Typ haben können, […]. Aber diese Verwendungen haben nichts mit dem Hauptziel der schrittweisen Codereparatur zu tun, also sind sie vielleicht kein großer Verlust.

Das Umbenennen innerhalb eines Pakets (zB in einen idiomatischeren oder konsistenteren Namen) ist sicherlich eine Art von Refactoring, die man vernünftigerweise durchführen möchte, und wenn das Paket weit verbreitet ist, erfordert dies eine schrittweise Reparatur.

Ich denke, eine Beschränkung auf nur paketqualifizierte Namen wäre ein Fehler. (Eine Beschränkung auf nur exportierte Namen ist möglicherweise erträglicher.)

@btracey

Vielleicht könnte es stattdessen einen Mechanismus geben, der gc mit einer Liste von "Aliasnamen" versorgt. gc könnte die Ersetzungen vorübergehend vornehmen, und der Autor der nachgelagerten Codebasis könnte Elemente in dieser Datei nach und nach entfernen, wenn Fixes zusammengeführt werden.

Ein Mechanismus für gc würde entweder bedeuten, dass der Code während des Reparaturvorgangs nur mit gc baubar ist oder dass der Mechanismus von den anderen Compilern unterstützt werden müsste (zB gccgo und llgo ). Ein Mechanismus "ohne Sprachänderung", der von allen Implementierungen unterstützt werden muss, ist de facto eine Sprachänderung - es ist nur einer mit schlechterer Dokumentation.

@btracey und @bcmills , und nicht nur die Compiler: jedes Tool, das Quellcode analysiert, wie guru oder alles andere, was Leute gebaut haben. Es ist sicherlich eine Sprachänderung, egal wie Sie es schneiden.

Okay danke.

Eine andere Möglichkeit sind Aliase für alles außer consts (und @rsc bitte verzeihen Sie mir, dass ich eine Einschränkung vorschlage!)

Für consts ist => wirklich nur ein längerer Weg, um = zu schreiben. Es gibt keine neue Semantik wie bei Typen und Vars. Es gibt keine gespeicherten Tastenanschläge wie bei funcs.

Das würde zumindest #17784 lösen.

Das Gegenargument wäre, dass Werkzeuge die Fälle unterschiedlich behandeln könnten und dass sie ein Indikator für die Absicht sein könnten. Das ist ein gutes Gegenargument, aber ich denke nicht, dass es die Tatsache aufwiegt, dass es im Grunde zwei Möglichkeiten gibt, genau dasselbe zu tun.

Das heißt, ich bin im Moment damit einverstanden, nur Aliase zu schreiben, sie sind sicherlich die wichtigsten. Ich stimme @Merovius definitiv zu, dass wir dringend erwägen sollten, die Option zum Hinzufügen von beizubehalten , auch wenn dies für einige Zeit nicht der Fall ist.

Wie wäre es, Aliase hinter einem Import zu verstecken, genau wie bei "C" und "unsafe", um dessen Verwendung weiter zu verhindern? Ebenso möchte ich, dass die Alias-Syntax ausführlich ist und sich als Gerüst für das weitere Refactoring abhebt.

Als Versuch, den Gestaltungsraum ein wenig zu öffnen, hier einige Ideen. Sie sind nicht ausgearbeitet. Sie sind wahrscheinlich schlecht und/oder unmöglich; die Hoffnung besteht hauptsächlich darin, bei anderen neue/bessere Ideen auszulösen. Und wenn Interesse besteht, können wir weiter forschen.

Die motivierende Idee für (1) und (2) ist, irgendwie Konvertierung anstelle von Aliasen zu verwenden. In #17746 traten bei Aliasen Probleme auf, die mit mehreren Namen für denselben Typ (oder mehreren Möglichkeiten, denselben Namen zu buchstabieren, abhängig davon, ob Sie sich Aliase als #define oder als harte Links vorstellen) zu tun hatten. Die Verwendung der Konvertierung umgeht dies, indem die Typen unterschieden werden.

  1. Fügen Sie weitere automatische Konvertierungen hinzu.

Wenn Sie fmt.Println("abc") aufrufen oder var e interface{} = "abc" schreiben, wird "abc" automatisch in interface{} . Wir könnten die Sprache so ändern, dass, wenn Sie type T struct { S } deklariert haben und T keine nicht hochgestuften Methoden hat, der Compiler bei Bedarf automatisch zwischen S und T konvertiert, auch rekursiv innerhalb anderer Strukturen. T könnte dann als De-facto-Alias ​​von S (oder umgekehrt) für allmähliche Refactoring-Zwecke dienen.

  1. Fügen Sie einen neuen Typ von "sieht aus wie" hinzu.

Lassen Sie type T ~S einen neuen Typ T deklarieren, der ein Typ ist, der "wie S aussieht". Genauer gesagt ist T "jeder Typ, der in und von Typ S umwandelbar ist". (Wie immer könnte die Syntax später erörtert werden.) Wie bei Schnittstellentypen kann T keine Methoden haben; um im Grunde überhaupt etwas mit T zu tun, müssen Sie es in S konvertieren (oder einen Typ, der in / von S umwandelbar ist). Im Gegensatz zu Schnittstellentypen gibt es keinen "konkreten Typ", die Konvertierung zwischen S nach T und T nach S beinhaltet keine Repräsentationsänderungen. Für die schrittweise Umgestaltung würden diese "wie"-Typen es Autoren ermöglichen, APIs zu schreiben, die sowohl alte als auch neue Typen akzeptieren. ("Sieht aus wie" Typen sind im Grunde ein stark eingeschränkter, vereinfachter Union-Typ.)

  1. Typ-Tags

Bonus super abscheuliche Idee. (Bitte sagen Sie mir nicht, dass das schrecklich ist – ich weiß es. Ich versuche nur, andere zu neuen Ideen anzuregen.) Was wäre, wenn wir Typ-Tags (wie struct-Tags) einführen und spezielle Typ-Tags zum Einrichten verwenden? und Steueraliasnamen, wie sagen wir type T S "alias:\"T\"" . Typ-Tags haben auch andere Verwendungszwecke und bieten mehr Spielraum für die Angabe von Aliasen durch den Paketautor als nur "dieser Typ ist ein Alias"; Beispielsweise könnte der Autor des Codes das Einbettungsverhalten festlegen.

Wenn wir es erneut mit Aliasen versuchen, lohnt es sich möglicherweise, über "was macht godoc" nachzudenken, ähnlich wie bei den Problemen "was macht iota" und "was macht die Einbettung".

Insbesondere, wenn wir

type  OldAPI => NewPackage.API

und NewPackage.API einen Doc-Kommentar hat, wird erwartet, dass wir diesen Kommentar neben "type OldAPI" kopieren/einfügen, wird erwartet, dass wir ihn unkommentiert lassen (wobei godoc automatisch einen Link bereitstellt oder automatisch kopiert/einfügt) oder werden? Gibt es eine andere Konvention?

Etwas tangential, obwohl die primäre Motivation die schrittweise Codereparatur ist und sein sollte, könnte ein kleiner Anwendungsfall (zurück zum Alias-Vorschlag, da dies ein konkreter Vorschlag ist) darin bestehen, einen doppelten Funktionsaufruf-Overhead bei der Präsentation einer einzelnen Funktion zu vermeiden unterstützt durch mehrere Build-Tag-abhängige Implementierungen. Ich winke im Moment nur mit der Hand, aber ich habe das Gefühl, dass Aliase in der jüngsten https://groups.google.com/d/topic/golang-nuts/wb5I2tjrwoc/discussion "Vermeidung von Funktionsaufruf-Overhead in Paketen" nützlich gewesen sein könnten mit go+asm-Implementierungen" Diskussion.

@nigeltao re godoc, ich denke:

Es sollte immer auf das Original verweisen, unabhängig davon.

Wenn der Alias ​​Dokumente enthält, sollten diese trotzdem angezeigt werden.

Wenn der Alias ​​keine Dokumente enthält, ist es verlockend, dass godoc die Originaldokumente anzeigt, aber der Name des Typs wäre falsch, wenn der Alias ​​auch den Namen ändert, könnten die Dokumente auf Elemente verweisen, die nicht im aktuellen Paket enthalten sind, und Wenn es für das schrittweise Refactoring verwendet wird, könnte beim Betrachten von X die Meldung "Deprecated: use X" angezeigt werden.

Dies würde jedoch für die meisten Anwendungsfälle möglicherweise keine Rolle spielen. Das sind Dinge, die schief gehen könnten, nicht Dinge, die schiefgehen werden. Und einige von ihnen könnten durch Linting erkannt werden, wie zum Beispiel umbenannte Aliase und versehentliches Kopieren von Warnungen zu veralteten Versionen.

Ich bin mir nicht sicher, ob die folgende Idee schon einmal gepostet wurde, aber was ist mit einem meist toolbasierten Ansatz wie "gofix" / "gorename"? Zum Ausarbeiten:

  • Jedes Paket kann eine Reihe von Umschreibungsregeln enthalten (zB Mapping pkg.Ident => otherpkg.Ident )
  • diese Umschreibregeln können mit //+rewrite ... Tags in beliebigen go-Dateien angegeben werden
  • diese Umschreibungsregeln sind nicht auf ABI-kompatible Änderungen beschränkt, es ist auch möglich, andere Dinge zu tun (zB pkg.MyFunc(a) => pkg.MyFunc(context.Contex(), a) )
  • ein gofix-ähnliches Tool kann verwendet werden, um alle Transformationen auf das aktuelle Repository anzuwenden. Dies macht es Benutzern eines Pakets leicht, ihren Code zu aktualisieren.
  • Es ist nicht notwendig, das gofix-Tool aufzurufen, um erfolgreich zu kompilieren. Eine Bibliothek, die immer noch die alte API einer Abhängigkeit X verwenden möchte (um mit alten und neuen Versionen von X kompatibel zu bleiben), kann dies weiterhin tun. Der Befehl go build sollte die Transformationen (angegeben in den rewrite-Tags von Paket X) im laufenden Betrieb anwenden, ohne die Dateien auf der Festplatte zu ändern.

Die letzten Schritte können den Compiler etwas komplizieren / verlangsamen, aber im Grunde ist es nur ein Präprozessor und die Anzahl der Rewrite-Regeln sollte sowieso klein gehalten werden. So, genug Brainstorming für heute :)

Die Verwendung von Aliasen, um den Overhead von Funktionsaufrufen zu vermeiden, scheint ein Hack zu sein, um die Unfähigkeit des Compilers zu umgehen, Nicht-Blatt-Funktionen inline einzubinden. Ich denke nicht, dass Implementierungsmängel die Sprachspezifikation beeinflussen sollten.

@josharian Während Sie sie nicht als vollständige Vorschläge beabsichtigten, lassen Sie mich antworten (wenn auch nur, damit jeder, der von Ihnen inspiriert ist, die unmittelbare Kritik berücksichtigen kann):

  1. Löst das Problem nicht wirklich, da Konvertierungen nicht wirklich das Problem sind. x/net/context.Context ist zu context.Context zuweisbar/konvertierbar/was auch immer. Das Problem sind Typen höherer Ordnung; nämlich die Typen func (ctx x/net/context.Context) und func (ctx context.Context) sind nicht gleich, obwohl die Argumente zuweisbar sind. Damit 1 das Problem lösen kann, müsste type T struct { S } bedeuten, dass T und S identische Typen sind. Was bedeutet, dass Sie für Aliase einfach eine andere Syntax verwenden (nur dass diese Syntax bereits eine andere Bedeutung hat).

  2. Hat wieder ein Problem mit Typen höherer Ordnung, da zuweisbare/konvertierbare Typen nicht unbedingt dieselbe Speicherdarstellung haben (und wenn ja, kann sich die Interpretation erheblich ändern). Zum Beispiel ist ein uint8 in ein uint64 umwandelbar und umgekehrt. Das würde aber bedeuten, dass zB mit type T ~uint8 der Compiler nicht wissen kann, wie man ein func(T) ; Muss es 1, 2, 4 oder 8 Bytes auf den Stack schieben? Es mag Möglichkeiten geben, dieses Problem zu umgehen, aber es klingt für mich ziemlich kompliziert (und schwerer zu verstehen als Aliase).

Danke, @Merovius.

  1. Ja, ich habe hier die Zufriedenheit mit der Benutzeroberfläche vermisst. Du hast Recht, das macht den Job nicht.

  2. Ich hatte im Hinterkopf "die gleiche Speicherdarstellung haben". Cabrio hin und her ist eindeutig nicht die richtige Erklärung dafür - danke.

@uluyol ja, es geht hauptsächlich um die Unfähigkeit des Compilers, Nicht-Blatt-Funktionen zu inlinen, aber explizites Aliasing könnte weniger überraschend sein, wenn es darum geht, ob Inline-Aufrufe an Nicht-Blätter in Stack-Traces, Runtime.Callers usw.

Jedenfalls ist es, wie gesagt, eine kleine Tangente.

@josharian Ähnliches Problem: [2]uintptr und interface{} haben dieselbe Speicherdarstellung; Wenn Sie sich also nur auf die Speicherdarstellung verlassen, können Sie die Typsicherheit umgehen. uint64 und float64 haben beide die gleiche Speicherdarstellung und sind hin und her konvertierbar, würden aber zumindest zu wirklich seltsamen Ergebnissen führen, wenn Sie nicht wissen, welches welches ist.

Sie könnten jedoch mit "dem gleichen zugrunde liegenden Typ" davonkommen. Ich bin mir nicht sicher, welche Auswirkungen das hätte. Das kann zu Unrecht führen, wenn ein Typ zum Beispiel in Feldern verwendet wird. Wenn Sie type S1 struct { T1 } und type S2 struct { T2 } (wobei T1 und T2 denselben zugrunde liegenden Typ haben), dann könnten unter type L1 ~T1 beide funktionieren als type S struct { L1 } , aber da T1 und T2 immer noch einen anderen (obwohl gleich aussehenden) zugrunde liegenden Typ haben, haben Sie mit type L2 ~S1 kein S2 sieht gleich aus S1 und kann nicht als L2 .

Sie müssten also an einer Reihe von Stellen in der Spezifikation "identische Typen" durch "gleichen zugrunde liegenden Typ" ersetzen oder ergänzen, damit dies funktioniert, was unhandlich erscheint und wahrscheinlich unvorhergesehene Folgen für die Typsicherheit haben wird. "Look-Alike"-Typen scheinen auch ein noch größeres Missbrauchs- und Verwirrungspotential zu haben als Aliase, die IMHO die Hauptargumente gegen Aliase zu sein scheinen.

Wenn jemand dafür eine einfache Regel aufstellen kann, die diese Probleme nicht hat, sollte sie auf jeden Fall als Alternative in Betracht gezogen werden :)

In Anlehnung an @josharians Idee ist hier eine Variation seiner Nummer 2:

Erlauben Sie die Angabe von "ersetzbaren Typen". Dies ist eine Liste von Typen, die den benannten Typ in Funktionsargumenten, Rückgabewerten usw. ersetzen können. Der Compiler würde den Aufruf einer Funktion mit einem Argument des benannten Typs oder einem seiner Ersetzungen ermöglichen. Die Ersatztypen müssen eine kompatible Definition mit dem benannten Typ haben. Kompatibel bedeutet hier identische Speicherdarstellungen und identische Deklarationen, nachdem andere Ersatztypen in der Deklaration berücksichtigt wurden.

Ein unmittelbares Problem besteht darin, dass die Direktionalität dieser Beziehung dem Alias-Vorschlag entgegengesetzt ist, der den Abhängigkeitsgraphen invertiert. Dies allein könnte es unbrauchbar machen, aber ich schlage es hier vor, weil andere vielleicht einen Weg finden, dies zu umgehen. Eine Möglichkeit könnte darin bestehen, Ersatzelemente als //go-Kommentare zu deklarieren, anstatt über den Importgraphen. Auf diese Weise werden sie vielleicht eher zu Makros.

Umgekehrt hat diese Richtungsumkehr einige Vorteile:

  • die Menge der ersetzbaren Typen wird vom Autor des neuen Pakets kontrolliert, der besser in der Lage ist, die Semantik zu gewährleisten
  • Im Originalpaket sind keine Codeänderungen erforderlich, sodass Kunden erst aktualisieren müssen, wenn sie das neue Paket verwenden

Anwenden auf das Context-Refactoring: Das Kontextpaket der Standardbibliothek würde deklarieren, dass context.Context durch golang.org/x/net/context.Context . Dies bedeutet jede Verwendung, die Kontext akzeptiert. Context kann stattdessen auch ein golang.org/x/net/context.Context akzeptieren. Funktionen im Kontextpaket, die einen Kontext zurückgeben, würden jedoch immer ein context.Context .

Dieser Vorschlag umgeht das Einbettungsproblem (#17746), da sich der Name des eingebetteten Typs nie ändert. Ein eingebetteter Typ könnte jedoch mit einem Wert eines Ersatztyps initialisiert werden.

@ian und @josharian Sie fragen nach einer bestimmten Variante von kovarianten Typen.

@josharian , danke für die Vorschläge.

Zu type T struct { S } , das sieht aus wie eine andere Syntax für Alias, und nicht unbedingt eine klarere.

Zu type T ~S , ich bin mir entweder nicht sicher, wie es sich vom Alias ​​unterscheidet oder wie es beim Refactoring hilft. Ich denke, in einem Refactoring (sagen wir io.ByteBuffer -> bytes.Buffer) würden Sie schreiben:

package io
type ByteBuffer ~bytes.Buffer

aber wenn dann, wie Sie sagen, "um im Grunde überhaupt etwas mit T zu tun, Sie es in S konvertieren müssen", dann bricht der gesamte Code, der etwas mit io.ByteBuffer macht, immer noch ab.

Zu type T S "alias" : Ein wichtiger Punkt von @bcmills oben ist, dass mehrere äquivalente Namen für Typen eine

@Merovius FWIW, ich würde sagen, dass [2]uintptr und interface{} nicht die gleiche Speicherdarstellung haben. Eine Schnittstelle{} ist ein [2]unsafe.Pointer, kein [2]uintptr. Ein uintptr und ein Zeiger sind unterschiedliche Darstellungen. Aber ich denke, Ihre allgemeine Aussage ist richtig, dass wir nicht unbedingt eine direkte Umwandlung von solchen Dingen zulassen wollen. Ich meine, kannst du auch von Interface{} in [2]*Byte konvertieren? Hier ist viel mehr als nötig.

@jimmyfrasche und @nigeltao , re godoc: Ich stimme zu, dass wir das auch früh brauchen. Ich stimme zu, dass wir die Annahme "das neue Feature - was auch immer es ist - nur für das Refactoring der Codebasis verwendet wird" nicht fest codieren sollten. Es kann andere wichtige Verwendungen haben, wie Nigel gefunden hat, um beim Schreiben eines Draw-Erweiterungspakets mit Aliasnamen zu helfen. Ich gehe davon aus, dass veraltete Dinge in ihren Doc-Kommentaren explizit als veraltet gekennzeichnet werden, wie Jimmy sagte. Ich habe darüber nachgedacht, automatisch einen Doc-Kommentar zu generieren, wenn einer nicht vorhanden ist, aber es gibt nichts Offensichtliches zu sagen, das nicht bereits aus der Syntax (allgemein gesprochen) klar sein sollte. Betrachten Sie als konkretes Beispiel die alten Go 1.8-Aliasnamen. Gegeben

type ByteBuffer => bytes.Buffer

Wir könnten einen Doc-Kommentar synthetisieren, der sagt "ByteBuffer ist ein Alias ​​für bytes.Buffer", aber das scheint mit der Anzeige der Definition überflüssig zu sein. Wenn heute jemand "type X struct{}" schreibt, synthetisieren wir nicht "X ist ein benannter Typ für eine Struktur{}".

@iand , danke. Es hört sich so an, als ob Ihr Vorschlag vom Autor des neuen Pakets verlangt, die genaue Definition aus dem alten Paket zu schreiben und dann auch eine Deklaration, die die beiden verknüpft, wie (Syntax):

package old
type T { x int }

package new
import "old"
type T1 { x int }
substitutable T1 <- old.T

Ich stimme zu, dass die Importumkehr problematisch ist und an sich schon ein Showstopper sein kann, aber lassen Sie uns das überspringen. An diesem Punkt scheint die Codebasis in einem fragilen Zustand zu sein: Jetzt kann das Paket new durch eine Änderung unterbrochen werden, um ein Strukturfeld in Paket alt hinzuzufügen. Angesichts der ersetzbaren Zeile gibt es für T1 nur eine mögliche Definition: genau dieselbe wie old.T. Wenn die beiden Typen noch unterschiedliche Definitionen haben, müssen Sie sich auch um die Methoden kümmern: Müssen die Methodenimplementierungen auch übereinstimmen? Wenn nicht, was passiert, wenn Sie ein T in eine Schnittstelle{} einfügen und es dann mit einer Typzusicherung als T1 herausziehen und M() aufrufen? Bekommst du T1.M? Was ist, wenn Sie es als Schnittstelle { M() } herausziehen, ohne T1 direkt zu benennen, und M() aufrufen? Bekommst du TM? Es gibt eine Menge Komplexität, die durch die Mehrdeutigkeit der beiden Definitionen im Quellbaum verursacht wird.

Natürlich könnte man sagen, dass die ersetzbare Zeile den Rest überflüssig macht und keine Definition für den Typ T1 oder irgendwelche Methoden erfordert. Aber das ist im Grunde dasselbe wie das Schreiben (in der alten Alias-Syntax) type T1 => old.T .

Zurück zum Problem des Importdiagramms, obwohl die Beispiele in dem Artikel alle den alten Code in Bezug auf den neuen Code definiert haben, ist es genauso effektiv, die Umleitung in den neuen Code zu setzen, wenn der Paketdiagramm so wäre, dass neu stattdessen alt importiert werden muss neues Paket während des Übergangs.

Ich denke, dies zeigt, dass es bei einem solchen Übergang wahrscheinlich keine sinnvolle Unterscheidung zwischen dem Autor des neuen Pakets und dem Autor des alten Pakets gibt. Am Ende ist das Ziel, dass Code zu neuem hinzugefügt und aus altem gelöscht wurde, sodass beide Autoren (wenn sie unterschiedlich sind) dann einbezogen werden müssen. Und die beiden brauchen auch in der Mitte eine Art koordinierte Kompatibilität, sei es explizit (eine Art Umleitung) oder implizit (Typdefinitionen müssen genau übereinstimmen, wie in der Ersetzbarkeitsanforderung).

@rsc Dieses Bruchszenario legt nahe, dass jedes

@iand Wenn es nur eine Definition gibt (weil die andere "dasselbe wie _das_ eine" sagt), besteht keine Sorge, dass sie nicht synchron sind.

In #13467 weist @joegrasse darauf hin, dass es schön wäre, wenn dieser Vorschlag einen Mechanismus zur Verfügung stellen würde, der es erlaubt, dass identische C-Typen zu identischen Go-Typen werden, wenn cgo in mehreren Paketen verwendet wird. Das ist nicht dasselbe Problem wie das, für das dieses Problem gedacht ist, aber beide Probleme hängen mit dem Typaliasing zusammen.

Gibt es eine Zusammenfassung der vorgeschlagenen/akzeptierten/abgelehnten Beschränkungen/Einschränkungen von Aliasnamen? Einige Fragen, die mir in den Sinn kommen, sind:

  • Ist der RHS immer voll qualifiziert?
  • Wie gehen wir mit Alias-Zyklen um, wenn Aliase zu Alias ​​zulässig ist?
  • Sollten Aliase nicht exportierte Bezeichner exportieren können?
  • Was passiert, wenn Sie einen Alias ​​einbetten? (wie greifen Sie auf das eingebettete Feld zu)
  • Sind Aliase als Symbole im erstellten Programm verfügbar?
  • ldflags-String-Injection: Was ist, wenn wir auf einen Alias ​​verweisen?

@rsc Ich möchte die Unterhaltung nicht zu sehr umleiten, aber unter dem Alias-Vorschlag, wenn "neu" ein Feld entfernt, auf das sich "alt" stützte, können Clients von "alt" jetzt nicht kompiliert werden.

Ich denke jedoch, dass im Rahmen des Ersatzvorschlags so arrangiert werden könnte, dass nur Clients, die sowohl alt als auch neu zusammen verwenden, kaputt gehen. Damit dies möglich ist, müsste die Substitutionsdirektive nur dann validiert werden, wenn der Compiler eine Verwendung von "alten" Typen im "neuen" Paket erkennt.

@thwd Ich glaube, es gibt noch keinen guten Bericht. Meine Notizen:

  • Alias-Zyklen sind kein Thema. Bei paketübergreifenden Aliasen ist ein Zyklus bereits wegen eines Import-Zyklus unzulässig. Im Fall von nicht paketübergreifenden Aliasnamen müssen sie offensichtlich nicht zugelassen werden, was Zyklen in der Initialisierungsreihenfolge sehr ähnlich ist. Persönlich würde ich gerne Aliasse zu Aliasen haben, da ich nicht denke, dass sie auf Anwendungsfälle mit schrittweiser Reparatur beschränkt sein sollten (siehe meinen Kommentar oben) und es traurig wäre, wenn Paket A kaputt gehen könnte, wenn jemand einen Typ einzieht Paket B mit einem Alias ​​(stellen Sie sich vor, x/image/draw.Image alias draw.Image und dann beschließt jemand, draw.Image image.Draw über einen Alias ​​in x/image/draw bricht ab, da Aliasnamen zu Aliasnamen nicht erlaubt sind).
  • Ich denke, frühere Befürworter von Aliasnamen waren sich einig, dass der Alias-Export nicht exportierter Bezeichner aufgrund der dadurch möglicherweise verursachten Seltsamkeit eine schlechte Idee ist. Effektiv bedeutet dies, dass Aliase für nicht exportierte Bezeichner nutzlos sind und möglicherweise vollständig verboten werden.
  • Die Einbettungsfrage, AFAIK, ist noch ungelöst. Es gibt eine ganze Diskussion in #17746, ich gehe davon aus, dass diese Diskussion fortgesetzt wird, wenn / wenn / bevor beschlossen wird, mit Aliasen fortzufahren (aber es gibt immer noch die Möglichkeit einer alternativen Lösung oder die Entscheidung, schrittweise Reparaturen nicht zum Ziel zu machen) überhaupt)

@iand , re "nur Clients, die sowohl alt als auch neu zusammen verwenden, würden kaputt gehen", das ist der einzige interessante Fall. Es sind die gemischten Clients, die es zu einer schrittweisen Codereparatur machen. Clients, die nur den neuen Code oder nur den alten Code verwenden, funktionieren heute.

Es gibt noch etwas zu beachten, das ich an anderer Stelle noch nicht erwähnt gesehen habe:

Da ein explizites Ziel hier darin besteht, große, schrittweise Refactorings in großen dezentralen Codebasen zu ermöglichen, wird es Situationen geben, in denen ein Bibliotheksbesitzer eine Art Bereinigung durchführen möchte, die eine unbekannte Anzahl von Clients erfordert, ihren Code zu ändern (in der endgültigen " die alte API zurückziehen"). Eine übliche Methode besteht darin, eine Warnung vor der Veraltung hinzuzufügen, aber der Go-Compiler enthält keine Warnungen.

Wie kann ein Bibliotheksbesitzer ohne irgendeine Art von Compilerwarnung sicher sein, dass das Refactoring sicher ist?

Eine Antwort könnte eine Art Versionsverwaltungsschema sein – es ist eine neue Version der Bibliothek mit einer neuen inkompatiblen API. In diesem Fall ist die Versionierung vielleicht die ganze Antwort, nicht Typaliase.

Wie wäre es alternativ dazu, dem Bibliotheksautor zu erlauben, eine "Veraltungswarnung" hinzuzufügen, die tatsächlich einen Kompilierungsfehler für Clients verursacht, jedoch mit einem expliziten Algorithmus für das Refactoring, das sie durchführen müssen? Ich stelle mir so etwas vor:

Error: os.time is obsolete, use time.time instead. Run "go upgrade" to fix this.

Für Typaliase würde der Refactoring-Algorithmus wohl nur "alle Instanzen von OldType durch NewType ersetzen" lauten, aber es könnte Feinheiten geben, ich bin mir nicht sicher.

Wie auch immer, das würde es dem Bibliotheksautor ermöglichen, nach besten Kräften alle Clients zu warnen, dass ihr Code bald kaputt geht, und ihnen eine einfache Möglichkeit geben, das Problem zu beheben, bevor die alte API vollständig gelöscht wird.

@iainmerrick Es gibt offene Fehler für diese: golang/lint#238 und golang/gddo#456

Das Lösen des Problems der schrittweisen Codereparatur , wie im Artikel von

Dies erfordert entweder ein Werkzeug oder eine Änderung der Sprache.

Da das Austauschen von zwei Typen per Definition die Funktionsweise der Sprache ändert, wäre jedes Werkzeug ein Mechanismus zum Simulieren der Äquivalenz außerhalb des Compilers, wahrscheinlich durch Umschreiben aller Instanzen des alten Typs in den neuen Typ. Dies bedeutet jedoch, dass ein solches Tool Code umschreiben muss, der Ihnen nicht gehört, wie ein Paket von Anbietern, das golang.org/x/net/context anstelle des stdlib-Kontextpakets verwendet. Die Spezifikation für die Änderung müsste entweder in einer separaten Manifestdatei oder in einem maschinenlesbaren Kommentar enthalten sein. Wenn Sie das Tool nicht ausführen, erhalten Sie Build-Fehler. Das alles wird unübersichtlich. Es scheint, als würde ein Tool genauso viele Probleme verursachen wie es löst. Es wäre immer noch ein Problem, mit dem sich jeder, der diese Pakete verwendet, auseinandersetzen muss, wenn auch etwas schöner, da ein Teil automatisiert ist.

Wenn die Sprache geändert wird, muss der Code nur von seinen Betreuern geändert werden, und für die meisten Leute funktioniert es einfach. Tools zur Unterstützung der Betreuer sind immer noch eine Option, aber es wäre viel einfacher, da die Quelle die Spezifikation ist und nur die Betreuer eines Pakets sie aufrufen müssten.

Wie @griesemer betonte (ich erinnere mich nicht, wo es so viele Threads dazu gab) hat Go bereits Aliasing, für Sachen wie byteuint8 und wenn Sie ein Paket importieren zweimal mit unterschiedlichen lokalen Namen in dieselbe Quelldatei.

Das Hinzufügen einer Möglichkeit zum expliziten Aliasing von Typen in der Sprache erlaubt uns nur, bereits vorhandene Semantiken zu verwenden. Dadurch wird ein echtes Problem auf überschaubare Weise gelöst.

Eine Sprachumstellung ist noch eine große Sache und es muss noch viel geklärt werden, aber ich denke, dass es hier letztendlich richtig ist.

Soweit mir bekannt ist, ist ein "Elefant im Raum" die Tatsache, dass bei Typaliasen deren Einführung nicht-temporäre (dh "nicht refaktorierende") Verwendungen ermöglicht. Ich habe die nebenbei erwähnten gesehen (z. B. "Reexportieren von Typbezeichnern in einem anderen Paket, um die API zu vereinfachen"). Um mit der guten Tradition früherer Vorschläge Schritt zu halten, listen Sie bitte

Zusätzliche Frage: Bitte klären Sie, ob Typaliase das Hinzufügen von Methoden zum Typ im neuen Paket ermöglichen (wobei die Kapselung wohl durchbrochen wird) oder nicht? Würde das neue Paket auch Zugriff auf private Felder alter Strukturen erhalten oder nicht?

Zusätzliche Frage: Bitte klären Sie, ob Typaliase das Hinzufügen von Methoden zum Typ im neuen Paket ermöglichen (wobei die Kapselung wohl durchbrochen wird) oder nicht? Würde das neue Paket auch Zugriff auf private Felder alter Strukturen erhalten oder nicht?

Ein Alias ​​ist nur ein anderer Name für einen Typ. Es ändert nicht das Paket des Typs. Also nein zu Ihren beiden Fragen (außer neues Paket == altes Paket).

@akavel Derzeit gibt es überhaupt keinen Vorschlag. Aber wir kennen zwei interessante Möglichkeiten, die sich während der Go 1.8-Alias-Tests ergeben haben.

  1. Aliase (oder einfach Aliase eingeben) würden das Erstellen von Drop-In-Ersetzungen ermöglichen, die andere Pakete erweitern. Siehe zum Beispiel https://go-review.googlesource.com/#/c/32145/ , insbesondere die Erklärung in der Commit-Nachricht.

  2. Aliase (oder einfach Typaliase) würden es ermöglichen, ein Paket mit einer kleinen API-Oberfläche, aber einer großen Implementierung als Sammlung von Paketen für eine bessere interne Struktur zu strukturieren, aber dennoch nur ein Paket zum Importieren und Verwenden von Clients bereitzustellen. Es gibt ein etwas abstraktes Beispiel, das unter https://github.com/golang/go/issues/16339#issuecomment -232813695 beschrieben ist.

Das zugrunde liegende Ziel von Aliasen ist großartig, aber es hört sich immer noch so an, als ob wir dem Ziel des Refactoring von Code nicht ganz ehrlich sind, obwohl es der Hauptmotivator für das Feature ist. Einige der Vorschläge schlagen vor, den Namen zu sperren, und ich habe noch nicht erwähnt, dass Typen bei solchen Refactorings normalerweise auch ihre Oberfläche ändern. Sogar das Beispiel von os.Error => error oft in Zusammenhang mit Aliasen erwähnt wird, ignoriert die Tatsache, dass os.Error eine String Methode hatte und nicht Error . Wenn wir nur den Typ verschieben und umbenennen, würde der gesamte Fehlerbehandlungscode trotzdem beschädigt werden. Das ist bei Refactorings üblich. Alte Methoden werden umbenannt, verschoben, gelöscht, und wir wollen sie nicht im neuen Typ haben, da dies die Inkompatibilität mit neuem Code bewahren würde.

Im Interesse der Hilfe hier ist eine Startidee: Was wäre, wenn wir das Problem in Bezug auf Adapter anstelle von Aliasnamen betrachten würden? Ein Adapter würde einem vorhandenen Typ einen alternativen Namen _und eine Schnittstelle_ geben und er kann an Stellen, an denen der ursprüngliche Typ zuvor gesehen wurde, ungeschminkt verwendet werden. Der Adapter müsste die unterstützten Methoden explizit definieren, anstatt davon auszugehen, dass dieselbe Schnittstelle des zugrunde liegenden angepassten Typs vorhanden ist. Dies würde dem bestehenden Verhalten von type foo bar sehr ähnlich sein, jedoch mit einigen zusätzlichen Semantiken.

io.ByteBuffer

Hier ist zum Beispiel ein Beispielskelett, das den Fall io.ByteBuffer anspricht, wobei vorerst das temporäre Schlüsselwort "adapts" verwendet wird:

type ByteBuffer adapts bytes.Buffer

func (old *ByteBuffer) Write(b []byte) (n int, err error) {
        buf := (*bytes.Buffer)(old)
        return buf.Write(b)
}

(... etc ...)

Mit diesem Adapter wäre also dieser Code gültig:

func newfunc(b *bytes.Buffer) { ... }
func oldfunc(b *io.ByteBuffer) { ... }

func main() {
        var newvar bytes.Buffer
        var oldvar io.BytesBuffer

        // New code using the new type obviously just works.
        newfunc(&newvar)

        // New code using the old type receive the underlying value that was adapted.
        newfunc(&oldvar)

        // Old code using the old type receive the adapted value unchanged.
        oldfunc(&oldvar)

        // Old code gets new variable adapted on the way in. 
        oldfunc(&newvar)
}

Die Schnittstellen von newfunc und oldfunc sind kompatibel. Beide akzeptieren tatsächlich *bytes.Buffer , wobei oldfunc es auf dem Weg dorthin an *io.BytesBuffer anpasst. Das gleiche Konzept funktioniert für Aufgaben, Ergebnisse usw.

os.Fehler

Die gleiche Logik wird wahrscheinlich auch für die Schnittstelle verwendet, obwohl die Compiler-Implementierung etwas schwieriger ist. Hier ist ein Beispiel für os.Error => error , das die Umbenennung der Methode behandelt:

package os

type Error adapts error

func (e Error) String() string { return error(e).Error() }

Dieser Fall bedarf jedoch weiterer Überlegungen, da Methoden wie:

func (v *T) Read(b []byte) (int, os.Error) { ... }`

Gibt einen Typ zurück, der eine String Methode hat, daher möchten wir normalerweise in die entgegengesetzte Richtung anpassen, damit der Code schrittweise korrigiert werden kann.

_AKTUALISIERT: Muss weiter nachgedacht werden._

Einbettungsproblem

In Bezug auf den Einbettungsfehler, der das Feature aus 1.8 herausgezogen hat, ist das Ergebnis bei Adaptern etwas klarer, da es sich nicht nur um neue Namen für dasselbe handelt: Wenn der Adapter eingebettet ist, wird der Feldname von . verwendet die so alte Adapterlogik funktioniert weiterhin, und der Zugriff auf das Feld wird die Adapterschnittstelle verwenden, es sei denn, sie wird explizit in einen Kontext übergeben, der den zugrunde liegenden Typ übernimmt. Wird der nicht angepasste Typ eingebettet, passiert das Übliche.

Kubernetes, Docker

Die im Beitrag genannten Probleme scheinen Variationen der oben genannten Probleme zu sein und werden durch den Vorschlag gelöst.

vars, consts

In diesem Szenario wäre es wenig sinnvoll, Variablen oder Konstanten anzupassen, da wir ihnen Methoden nicht direkt zuordnen können. Es sind ihre Typen, die angepasst werden würden oder nicht.

godoc

Wir weisen ausdrücklich darauf hin, dass es sich bei dem Ding um einen Adapter handelt, und zeigen die Dokumentation dazu wie üblich, da es eine vom angepassten Ding unabhängige Schnittstelle enthält.

Syntax

Bitte wählen Sie etwas Schönes aus. ;)

@iainmerrick @zombiezen

Sollten wir auch automatisch ableiten, dass ein Alias-Typ veraltet ist und durch den neuen Typ ersetzt werden sollte? Wenn wir golint, godoc und ähnliche Tools erzwingen, um den alten Typ als veraltet zu visualisieren, würde dies den Missbrauch von Typ-Aliasing sehr stark einschränken. Und die letzte Sorge, dass die Aliasing-Funktion missbraucht wird, wäre gelöst.

Zwei Beobachtungen:

1. Die Semantik von Typreferenzen hängt vom unterstützten Refactoring-Anwendungsfall ab

Gustavos Vorschlag zeigt, dass an dem Anwendungsfall für Typreferenzen und der daraus resultierenden Semantik noch mehr gearbeitet werden muss.

Ross' neuer Vorschlag enthält eine neue Syntax type OldAPI = newpkg.newAPI . Aber was ist die Semantik? Ist es unmöglich, OldAPI mit älteren öffentlichen Methoden oder Feldern zu erweitern? Angenommen ja als Antwort, die erfordert, dass die newAPI alle öffentlichen Methoden und Felder von OldAPI unterstützt, um die Kompatibilität zu gewährleisten. Bitte beachten Sie, dass jeder Code im Paket mit OldAPI, der auf privaten Feldern und Methoden basiert, umgeschrieben werden muss, um nur die öffentliche newAPI zu verwenden, vorausgesetzt, dass das Ändern der Sichtbarkeitsbeschränkungen von Paketen nicht möglich ist.

Der alternative Pfad besteht darin, die Definition zusätzlicher Methoden für OldAPI zuzulassen. Dies könnte NewAPI entlasten, alle öffentlichen alten Methoden bereitzustellen. Aber das würde OldAPI zu einem anderen Typ machen als NewAPI. Eine gewisse Zuordenbarkeit zwischen Werten der beiden Typen muss aufrechterhalten werden, aber die Regeln würden komplex werden. Das Zulassen des Hinzufügens von Feldern würde zu einer höheren Komplexität führen.

2. Paket mit NewAPI kann kein Paket mit OldAPI importieren

Die Neudefinition der OldAPI erfordert, dass Paket O, das die Definition von OldAPI enthält, Paket N mit der NewAPI importiert. Das bedeutet, dass Paket N O nicht importieren kann. Vielleicht ist es so offensichtlich, dass es nicht erwähnt wurde, aber es scheint mir eine wichtige Einschränkung für den Anwendungsfall Refactoring zu sein.

Update: Paket N kann keine Abhängigkeit von Paket O haben. Es kann beispielsweise kein Paket importieren, das O importiert.

@niemeyer Änderungen wie das Umbenennen einer Methode sind bereits nach und nach möglich: a) Neue Methode

@rakyll Persönlich, wenn ich Aliase für etwas nicht Refactoring als nützlich erachten würde (wie Wrapper-Pakete, die ich für einen ausgezeichneten Anwendungsfall halte), würde ich sie einfach verwenden, Verdammungswarnungen. Ich wäre sauer auf den, der sie künstlich verkrüppelt und für meine Benutzer verwirrend gemacht hat, aber ich würde mich nicht entmutigen lassen.

Ich denke , irgendwann es diskutiert werden muss , ob wir Wrapper-Pakete tatsächlich prüfen, protobuf öffentliche Ein- oder Aussetzen internes Paket-APIs so eine schlechte Sache (und ich weiß nicht , wie man am beste Debatte etwas die subjektiven ohne eine Seite nur zu wiederholen immer wieder, dass sie unlesbar sind und der andere sagt: "Nein, das sind sie nicht". Hier gibt es nicht viele objektive Argumente, wie mir scheint).

Ich denke zumindest (offensichtlich), dass sie eine gute Sache sind und ich bin auch der Meinung, dass das Hinzufügen einer Sprachfunktion und die künstliche Beschränkung auf nur einen Anwendungsfall eine schlechte Sache ist; Eine orthogonale, gut gestaltete Sprache ermöglicht es Ihnen, mit so wenig Funktionen wie möglich so viel wie möglich zu machen. Sie möchten, dass Ihre Funktionen den "überspannten Vektorraum möglicher Programme" so weit wie möglich erweitern, daher erscheint mir das Hinzufügen einer Funktion, die dem Raum nur einen einzigen Punkt hinzufügt, seltsam.

Ich möchte, dass ein anderer, etwas anderer Anwendungsfall berücksichtigt wird, wenn ein Typ-Alias-Vorschlag entwickelt wird.

Obwohl der Hauptanwendungsfall, den wir in dieser Ausgabe besprechen, der Typ _replacement_ ist, wären Typaliase auch sehr nützlich, um einen Codekörper von einer Abhängigkeit von einem Typ zu entwöhnen.

Angenommen, ein Typ hat sich als "instabil" herausgestellt (dh er wird ständig geändert, möglicherweise auf inkompatible Weise). Dann möchten einige Benutzer möglicherweise zu einem "stabilen" Ersatztyp migrieren. Ich denke da an Entwicklung auf Github etc. wo die Besitzer eines Typs und seine Benutzer nicht unbedingt eng zusammenarbeiten oder sich auf das Ziel der Stabilität einigen.

Andere Beispiele wären, wo nur ein einzelner Typ das Löschen einer Abhängigkeit von einem großen oder problematischen Paket verhindert, zB wenn eine Lizenzinkompatibilität entdeckt wurde.

Der Ablauf hier wäre also:

  1. Definieren Sie den Typalias
  2. Ändern Sie den relevanten Codekörper, um den Typalias zu verwenden
  3. Ersetzen Sie den Typalias durch eine Typdefinition.

Am Ende dieses Prozesses gäbe es zwei unabhängige Typen, die sich frei in ihre eigenen Richtungen entwickeln könnten.

Beachten Sie in diesem Anwendungsfall Folgendes:

  • Es gibt keine Möglichkeit, das Paket, das die ursprüngliche Typdefinition enthält, zu ändern, um dort einen Typalias hinzuzufügen (da die Eigentümer dem wahrscheinlich nicht zustimmen werden)
  • der ursprüngliche Typ ist nicht veraltet (obwohl er im Codekörper als solcher angesehen werden könnte, während er vom Typ "entwöhnt" wird).

@Merovius In dem Moment, in dem Sie die alte Methode löschen oder umbenennen,

In den meisten Fällen finde ich diese Kritik unfair, da das Go-Team große Anstrengungen unternimmt, um das Projekt für externe Parteien zugänglich zu machen, aber in dem Moment, in dem Sie annehmen, dass Sie Zugriff auf jede einzelne Codezeile haben, die ein bestimmtes Paket aufruft, ist das eine Mauer Garten, der nicht in den Kontext einer Open-Source-Community passt. Das Hinzufügen einer Refactoring-Funktion auf Sprachebene, die nur innerhalb von Walled Gardens funktioniert, wäre, gelinde gesagt, untypisch.

@niemeyer Ich habe mich anscheinend nicht klar

  1. Neue API hinzufügen, austauschbar mit alter API
  2. Stellen Sie die Verbraucher nach und nach auf die neue API um
    3a. Sobald alles migriert ist oder die Einstellungsfrist abgelaufen ist, löschen Sie die alte API
    3b. Sorgen Sie für unbegrenzte Stabilität, indem Sie beide APIs für immer beibehalten (siehe z. B. diesen Teil des Artikels ).

Du scheinst über 3a vs. 3b zu streiten. Aber was ich darauf hingewiesen habe, dass 1. bereits für Methodennamen möglich ist, aber nicht für Typen, darum geht es hier.

Allerdings merke ich jetzt, dass ich dich falsch verstanden habe :) Du hast vielleicht darauf hingewiesen, dass os.Error unterschiedliche Interface-Definitionen sind, so dass der Umzug nicht wirklich aufgeht. Ich denke, das ist wahr; Wenn Sie das Entfernen von APIs verbieten, würden Typaliase es nicht ermöglichen, Methoden von Schnittstellentypen umzubenennen.

Vielleicht kannst Du mir aber etwas zu Deiner Adapteridee erklären: Würde das nicht auch erlauben (zB im os.Error-Fall) jeden fmt.Stringer als os.Error zu verwenden?

Die Adapteridee scheint auf jeden Fall lohnenswert weiterzuentwickeln, auch wenn ich da etwas skeptisch bin. Es ist jedoch ein gutes Ziel, Schnittstellen schrittweise umzugestalten, ohne mögliche Implementierer und/oder Verbraucher zu beschädigen.

@niemeyer Ja, Sie bringen auch einen guten Punkt dazu, dass sich der Methodenname irrtümlich ändert. Das bringt viele Komplikationen mit sich, und es ist nicht etwas, was ich hier versuche anzugehen. Da nur ein Bruchteil des Codes, der error/os.Error erwähnt, die Methode tatsächlich aufruft, war der Umzug der schmerzhaftere Teil als die Methodenänderung. Ich denke, wir können die Umbenennung von Methoden als ein unabhängiges Problem von der Änderung des Codespeicherorts behandeln. Wenn der Umzug heute stattfinden würde und wir die Paketreorganisation nahtlos durchführen könnten, aber beim alten Methodennamen bleiben könnten, wäre das immer noch ein erheblicher Fortschritt. Die Fokussierung dieses Problems auf die Codelokalisierung soll versuchen zu vereinfachen.

Ich stimme zu, dass es großartig wäre, wenn es eine allgemeine Korrektur gäbe, die beide Arten von Änderungen behandelt. Ich sehe nicht, was diese Korrektur ist. Insbesondere verstehe ich nicht, wie Typwechsel mit den von Ihnen beschriebenen Adaptern funktionieren: Wird der Wert während des Typwechsels irgendwie automatisch konvertiert? Was ist mit Reflexion? Wenn Sie nur einen Typ mit zwei Namen haben, werden viele Probleme vermieden, die bei zwei Typen entstehen, die automatisch hin und her konvertieren.

@rsc Ja, der Adapter würde in jeder Situation konsistent automatisch konvertieren, sodass interface{} ohne zu wissen, wie wir dorthin gekommen sind, wenn das Sinn macht.

@Merovius Meine beiden obigen Kommentare gehen genau auf die Punkte ein, die Sie immer noch machen. Wenn Sie heute einen Typ verschieben, brechen Sie Code, der repariert werden muss. Wenn Sie eine Methode umbenennen, beschädigen Sie Code, der repariert werden muss. Wenn Sie eine Methode löschen, ihre Argumente ändern, beschädigen Sie Code, der repariert werden muss. Beim Refactoring von Code in einem dieser Fälle müssen die Fixes atomar mit dem Bruch in jeder Aufruf-Site durchgeführt werden, damit die Dinge weiterhin funktionieren. Das Verschieben des Typs, aber völlig unberührt, ist ein sehr begrenzter Fall von Refactoring, der IMO keine Sprachfunktion rechtfertigt.

@niemeyer Das würde mit den konkreten Typen umgehen. Was ist mit einer Typzusicherung für .(interface{String() string}) vs. .(interface{Error() string}) oder was auch immer sich an bestimmten Schnittstellenteilen geändert hat? Muss der Check irgendwie beide möglichen Underlying-Typen berücksichtigen?

@niemeyer Nein. Das Umbenennen einer Methode ist nicht-atomar möglich. Um zB eine Methode von A.Foo nach A.Bar , mach

  1. Fügen Sie die Methode A.Bar als Wrapper um A.Foo
  2. Migrieren Sie Benutzer, um nur A.Bar über beliebig viele Commits aufzurufen
  3. Löschen Sie entweder A.Foo oder nicht, je nachdem, ob Sie eine Einstellung erzwingen möchten.

Das Ändern von Funktionsargumenten ist nicht-atomar möglich. Um zB einen Parameter x int zu einem func Foo() hinzuzufügen, tun Sie

  1. func FooWithInt(x int) { Foo(); // use x somehow; } Add
  2. Migrieren Sie Benutzer, um den Parameter über beliebig viele Commits hinzuzufügen
  3. Wenn Sie nicht bereit sind, eine Einstellung zu erzwingen (oder Sie sich nicht daran stören, WithInt zu haben), sind Sie fertig. Andernfalls ändern Sie Foo in func Foo(x int) { FooWithInt(x) } .
  4. Migrieren Sie Benutzer mit s/FooWithInt/Foo/g über beliebig viele Commits.
  5. Löschen Sie FooWithInt .

Dasselbe funktioniert für so ziemlich alle Fälle, mit Ausnahme von Bewegungstypen (und genau genommen vars). Atomarität ist nicht erforderlich. Entweder brechen Sie die Kompatibilität, wenn Sie die Veraltung erzwingen, oder Sie tun es nicht, aber das ist völlig orthogonal zur Atomarität. Die Möglichkeit, zwei verschiedene Namen zu verwenden, um sich auf dasselbe zu beziehen, ermöglicht es Ihnen, die Atomarität zu umgehen, wenn Sie im Grunde willkürliche Änderungen vornehmen, und Sie haben diese Fähigkeit für alle Fälle außer für Typen. Ja, um eine tatsächliche Verschiebung anstelle einer Änderung durchzuführen, müssen Sie bereit sein, die Einstellung durchzusetzen (also den Build von potenziell unbekanntem Code zu unterbrechen, was bedeutet, dass dies eine umfassende und rechtzeitige Ankündigung erfordert). Aber selbst wenn dies nicht der Fall ist, hängt die Fähigkeit, APIs mit einem bequemeren Namen oder einer anderen nützlichen Umhüllung (siehe x/image/draw) zu erweitern, auch von der Fähigkeit ab, auf das alte Ding mit dem neuen Namen zu verweisen und umgekehrt.

Der Unterschied zwischen dem Verschieben von Typen heute und dem Umbenennen einer Funktion heute besteht darin, dass Sie im ersteren Fall tatsächlich eine atomare Änderung benötigen, während Sie im letzteren Fall die Änderung schrittweise über unabhängige Repos und Commits vornehmen können. Nicht als "Ich mache einen Commit, der s/Foo/Bar/ macht", aber es gibt einen Prozess dafür.

Trotzdem. Ich weiß nicht, wo wir anscheinend aneinander vorbeireden. Ich finde das Dokument von @rsc ziemlich klar, um meinen POV zu vermitteln, und verstehe deines nicht wirklich :)

@rsc Ich kann zwei vernünftige Antworten sehen. Die einfache, dass die Schnittstelle den Typ trägt, der eingegeben wurde, Adapter oder andere, und die übliche Semantik gilt für die Schnittstellenbehauptung. Der andere ist, dass der Wert möglicherweise nicht angepasst ist, wenn er die Schnittstelle nicht erfüllt, der zugrunde liegende Wert jedoch. Ersteres ist einfacher und vielleicht ausreichend für die Refactoring-Anwendungsfälle, die wir im Sinn haben, während letzteres vielleicht eher mit der Idee übereinstimmt, dass wir es auch auf den zugrunde liegenden Typ typisieren können.

@Merovius Sicher, das Umbenennen einer Methode ist möglich, solange _Sie sie nicht wirklich umbenennen_ und Anrufseiten zwingen, stattdessen eine neue API zu verwenden. Ebenso ist das Verschieben eines Typs möglich, solange _Sie ihn nicht tatsächlich verschieben_ und Aufruf-Sites zwingen, stattdessen eine neue API zu verwenden. Wir alle tun beides seit Jahren, um den alten Code funktionsfähig zu erhalten.

@niemeyer Aber noch einmal: Für Typen kann man nicht einmal anständig Dinge hinzufügen. Siehe x/Bild/Zeichnung. Und nicht jeder hat vielleicht eine so absolute Ansicht von Stabilität; Ich selbst finde es gut zu sagen "in 6,12,... Monaten wird $function,$type,... weggehen, stellen Sie sicher, dass Sie zu diesem Zeitpunkt davon migriert sind" und dann einfach nicht gewarteten Code zu zerstören, der nicht funktioniert schaffen, dieser Einstellungsmitteilung zu folgen (wenn jemand der Meinung ist, dass er langfristigen Support für APIs benötigt, kann er sicherlich jemanden finden, der dafür bezahlt wird). Ich würde sogar behaupten, dass die meisten Leute nicht diese absolute Meinung über Stabilität haben; Sehen Sie sich den jüngsten Vorstoß nach semantischen Versionen an, der nur dann wirklich sinnvoll ist, wenn Sie die Option haben möchten, die Kompatibilität zu unterbrechen. Und der Doc argumentiert sehr gut, wie Sie selbst in diesem Fall noch von der Möglichkeit einer schrittweisen Reparatur profitieren würden und wie sie das Problem der Diamantenabhängigkeit lindern, wenn nicht sogar lösen kann.

Sie können die meisten Anwendungsfälle von Aliasnamen für schrittweise Reparaturen ablehnen, da Ihre Haltung zur Stabilität absolut ist. Aber ich würde behaupten, dass das für die meisten der Go-Community anders ist, dass es einen Wunsch nach Brüchen gibt und einen Nutzen, um sie so reibungslos wie möglich zu machen, wenn sie passieren.

@niemeyer @rsc @Merovius Ich habe deine Diskussion (und die gesamte Diskussion) verfolgt und möchte diesen Beitrag direkt mittendrin klatschen.

Je mehr wir über das Problem iterieren, desto näher kommen wir einer Form der erweiterten Kovarianz-Semantik. Hier ein Gedanke: Wir haben bereits eine Subtyp-Semantik ("is-a") definiert, die von konkreten Typen zu Schnittstellen und zwischen Schnittstellen definiert ist. Mein Vorschlag ist, Schnittstellen rekursiv kovariant (gemäß den aktuellen Varianzregeln) bis hin zu ihren Methodenargumenten zu machen.

Dies löst das Problem nicht für alle aktuellen Pakete. Aber es kann das Problem für alle zukünftigen Pakete lösen, die noch geschrieben werden müssen, indem die "beweglichen Teile" der API Schnittstellen sein können (fördert auch gutes Design).

Ich denke, wir können alle Anforderungen lösen, indem wir auf diese Weise Schnittstellen (ab)benutzen. Brechen wir Go 1.0? Ich weiß es nicht, aber ich glaube, wir sind es nicht.

@thwd Ich denke, Sie müssen genauer definieren, was Sie mit "Schnittstellen rekursiv kovariant machen" meinen. Normalerweise müssen sich bei der Subtypisierung Methodenargumente auf kontravariante Weise und die Ergebnisse auf kovariante Weise ändern. Außerdem würde dies nach dem, was Sie sagen, kein bestehendes Problem mit konkreten (Nicht-Schnittstellen-)Typen lösen.

@thwd Ich bin anderer Meinung, dass Schnittstellen (auch kovariante) eine gute Lösung für eines dieser Probleme sind (nur für sehr spezifische Fälle davon). Um sie zu einem zu machen, müssten Sie alles in Ihrer API zu einer Schnittstelle machen (weil Sie nie wissen, was Sie irgendwann verschieben/ändern möchten), einschließlich vars/consts/funcs/… und ich denke nicht an alles, das ist gutes Design (das habe ich in Java gesehen. Es ärgert mich). Wenn etwas eine Struktur ist, machen Sie es einfach zu einer Struktur. Alles andere fügt Ihrem Paket nur seltsamen syntaktischen Overhead hinzu und jede umgekehrte Abhängigkeit praktisch ohne Nutzen. Es ist auch der einzige Weg, um gesund zu bleiben, wenn Sie anfangen; Beginnen Sie einfach und gehen Sie später zu etwas Allgemeineren über. Viele Komplikationen in der API, die ich bisher gesehen habe, sind darauf zurückzuführen, dass die Leute über das API-Design nachdenken und viel mehr Allgemeinheit planen, als sie jemals benötigt werden. Und dann passiert in 80% (diese Zahl ist eine offensichtliche Lüge) der Fälle überhaupt nichts, weil es kein "sauberes API-Design" gibt.

(Um es klar zu sagen: Ich sage nicht, dass kovariante Schnittstellen keine gute Idee sind. Ich sage nur, dass sie keine gute Lösung für diese Probleme sind)

Um den Punkt von @ Merovius zu ergänzen, haben viele schrittweise

package foo

type Authority struct {
  Host string
  Port int
}

Im Laufe der Zeit wächst das Paket foo und es gewinnt mehr Verantwortung (und Codegröße) als jemand, der nur den Typ Authority wirklich will. Es ist also ein wünschenswerter Anwendungsfall, ein fooauthority Paket zu erstellen, das nur Authority und vorhandene Benutzer von foo.Authority weiterhin arbeiten zu lassen. Beachten Sie, dass eine Lösung, die nur Schnittstellentypen berücksichtigt, hier nicht hilfreich wäre.

@Merovius Ihr letzter Kommentar war völlig subjektiv und spricht mich persönlich anstelle meines Vorschlags an. Das wird nicht gut enden, deshalb werde ich diese Diskussion hier beenden.

@griesemer @Merovius Ich stimme euch beiden zu. Um die Schleife zu schließen, können wir uns dann darauf einigen, dass die bisherige Diskussion uns zu einem Begriff von Subtypen/Kovarianz geführt hat. Außerdem sollte jede Implementierung davon keine Laufzeitumleitung verursachen. Das hat @niemeyer vorgeschlagen (wenn ich ihn richtig verstanden habe). Aber ich würde gerne mehr Ideen lesen. Ich werde auch über das Problem nachdenken.

@niemeyer In den Kommentaren von @Merovius war nichts _ad hominem_. Seine Behauptung, dass "Ihre Haltung zur Stabilität absolut ist", ist eine Beobachtung über Ihre Position, nicht über Sie, und ist eine vernünftige Schlussfolgerung aus einigen Ihrer Aussagen, wie z

In dem Moment, in dem Sie die alte Methode löschen oder umbenennen, beenden Sie jeden Client, der sie verwendet, auf einmal.

und

Sicher, das Umbenennen einer Methode ist möglich, solange Sie sie nicht tatsächlich umbenennen und Aufrufsites zwingen, stattdessen eine neue API zu verwenden. Ebenso ist das Verschieben eines Typs möglich, solange Sie ihn nicht tatsächlich verschieben und stattdessen erzwingen, dass Aufrufsites eine neue API verwenden. Wir alle tun beides seit Jahren, um den alten Code funktionsfähig zu erhalten.

Ich hatte bei diesen Aussagen den gleichen Eindruck wie Merovius – dass Sie nicht mitfühlen, wenn Sie etwas eine Zeit lang missbilligen und es dann schließlich entfernen; dass Sie sich dafür einsetzen, dass Code auf unbestimmte Zeit in freier Wildbahn funktioniert; dass "Ihre Haltung zur Stabilität absolut ist". (Und um weiteren Missverständnissen vorzubeugen, verwende ich "Sie", um auf Ihre Ideen zu verweisen, nicht auf Ihre Persönlichkeit.)

@niemeyer Die adapts Ihnen vorgeschlagene Deklaration instance aus Haskell-Typklassen verwandt zu sein. Wenn man das locker in Go übersetzt, könnte es etwa so aussehen:

package os

type Error interface {
  String() string
}

instance error Error (
  func (e error) String() string { return e.Error() }
)

Leider (wie @zombiezen bemerkt) ist nicht klar, wie dies für Nicht-Schnittstellentypen helfen würde.

Es ist mir auch nicht klar, wie es mit Funktionstypen (Argumenten und Rückgabewerten) interagieren würde; Wie würde beispielsweise die Semantik von adapts bei der Migration von Context in die Standardbibliothek helfen?

Ich habe durch diese Aussagen den gleichen Eindruck wie Merovius gewonnen – dass Sie nicht mitfühlen, wenn Sie etwas für eine Weile ablehnen

@jba Dies sind absolute Fakten, keine absoluten Meinungen. Wenn Sie eine Methode oder einen Typ löschen, wird der Go-Code, der diese verwendet, unterbrochen, sodass diese Änderungen atomar durchgeführt werden müssen. In meinem Vorschlag geht es jedoch um die schrittweise Umgestaltung des Codes, was hier das Thema ist und die Veraltung impliziert. Dieser Prozess der Abwertung ist jedoch keine Frage der Sympathie. Ich habe mehrere öffentliche Go-Pakete mit jeweils Tausenden von Abhängigkeiten und mehrere unabhängige APIs aufgrund dieser schrittweisen Entwicklung. Wenn wir eine API kaputt machen, ist es gut, solche Unterbrechungen in Batches durchzuführen, anstatt sie zu streamen, wenn wir erwarten, dass die Leute nicht verrückt werden. Es sei denn, Sie leben in einem ummauerten Garten und können sich an jede Anrufstelle wenden, um das Problem zu beheben. Aber ich wiederhole mich.. all das kann man im ursprünglichen Vorschlag oben artikulierter lesen.

@Merovius

Persönlich, wenn ich Aliase für nützlich halten würde für etwas, das nicht refactoring ist (wie Wrapper-Pakete, was ich für einen ausgezeichneten Anwendungsfall halte), würde ich sie einfach verwenden, Veraltungswarnungen sind verdammt.

Wir pflegen Pakete mit einer enorm großen Anzahl neuer und veralteter APIs und Aliase ohne klare Erklärung des Zustands des alten (aliasierten) Typs wird die schrittweise Codereparatur nicht unterstützen und nur zur Überwältigung der vergrößerten API-Oberfläche beitragen. Ich stimme @niemeyer zu, dass unsere Lösung die Anforderungen einer verteilten Entwickler-Community erfüllen muss, die derzeit keine anderen Signale hat als den freien godoc-Text, der besagt, dass eine API "veraltet" ist. Das Hinzufügen einer Sprachfunktion, um alte Typen zu veralten, ist das Thema dieses Threads, daher stellt sich natürlich die Frage nach dem Zustand des alten (aliasierten) Typs.

Ich würde gerne Typ-Aliasing unter einem anderen Thema diskutieren, z. B. das Bereitstellen von Erweiterungen für einen Typ oder Teilpakete, aber nicht in diesem Thread. Dieses Thema selbst weist verschiedene kapselungsspezifische Probleme auf, die vor jeder Betrachtung angegangen werden müssen.

Ein bestimmter Operator oder die Andeutung, dass der Alias-Typ etwas ersetzt wird, könnte sinnvoll sein, um den Benutzern mitzuteilen, dass sie wechseln müssen. Eine solche Unterscheidbarkeit würde es Tools ermöglichen, die ersetzten APIs automatisch zu melden.

Um es klarzustellen, ist die Richtlinie für die veraltete Version für Typen außerhalb der Standardbibliothek technisch nicht möglich. Ein Typ ist nur aus der Sicht eines Aliasing-Pakets alt. Da wir dies im Ökosystem niemals erzwingen können, möchte ich immer noch, dass Standardbibliotheksaliase eine strikte Veraltung implizieren (angezeigt durch entsprechende Verfallshinweise).

Ich schlage auch vor, dass wir den Begriff der Deprecation in einer parallelen Diskussion standardisieren und sie in unseren Kerntools (golint, godoc usw.) unterstützen. Das Fehlen von veralteten Hinweisen ist das größte Problem im Go-Ökosystem und ist weiter verbreitet als das Problem der schrittweisen Codereparatur.

@rakyll Ich bin mit dem Anwendungsfall von computerlesbaren

Zu a), abgesehen von der Tatsache, dass ich Aliase produktiv für andere Dinge als für Bewegungen verwenden möchte, würde dies auch nur für einen sehr kleinen Satz von Deprecations gelten. Angenommen, ich möchte in einigen Versionen einige Parameter aus einer Funktion entfernen; Ich kann keine Aliase verwenden, weil die Signatur der neuen API unterschiedlich sein wird, aber ich möchte das trotzdem ankündigen. Zu b) sind IMHO-Compiler-Warnungen allgemein schlecht. Ich denke, dass dies größtenteils mit dem übereinstimmt, was go bereits tut, also denke ich nicht, dass es einer Rechtfertigung bedarf.

Ich stimme allem zu, was Sie zu den Einstellungshinweisen sagen. Dafür gibt es anscheinend bereits eine Syntax: #10909. Der nächste Schritt, um es nützlicher zu machen, wäre also, die Werkzeugunterstützung zu verbessern, indem man sie in godoc hervorhebt und eine Überprüfung hat, die vor ihrer Verwendung warnt (z. B. go vet, golint oder ein separates Werkzeug insgesamt).

@rakyll Ich stimme zu, dass die stdlib mit einer konservativen Verwendung von Typaliasen beginnen sollte, sollten sie eingeführt werden.


Seitenleiste:

Hintergrund für diejenigen, die den Status von veralteten Kommentaren in Go und verwandten Tools nicht kennen, da er ziemlich weit verbreitet ist:

Wie @Merovius oben erwähnt, gibt es eine Standardkonvention zum Markieren von Elementen als veraltet, #10909, siehe https://blog.golang.org/godoc-documenting-go-code

TL;DR: Erstellen Sie einen Absatz in den Dokumenten des veralteten Elements, der mit "Deprecated: " beginnt und erklärt, was der Ersatz ist.

Es gibt einen akzeptierten Vorschlag für godoc, veraltete Elemente auf nützlichere Weise anzuzeigen: #17056.

@rakyll schlug vor, dass golint warnt, wenn veraltete Elemente verwendet werden: golang/lint#238.


Selbst wenn die stdlib eine konservative Haltung zur Verwendung von Aliasen innerhalb der stdlib einnimmt, denke ich nicht, dass die Existenz eines Typalias (in irgendeiner Weise, die mechanisch erkannt oder visuell gekennzeichnet wird) implizieren sollte, dass der alte Typ veraltet ist, sogar wenn es das in der Praxis immer bedeutet.

Dies würde Folgendes bedeuten:

  • Scannen anderer stdlib-Pakete, um festzustellen, ob ein Typ, der nicht explizit als veraltet markiert ist, an anderer Stelle mit einem Alias ​​versehen ist
  • Hardcoding aller stdlib-Aliasse in automatisierte Tools
  • nur melden, dass der alte Typ veraltet ist, wenn Sie bereits nach seinem Ersatz suchen, was nicht hilft, die Suche zu erleichtern

Wenn ein Typalias eingeführt wird, weil der alte Typ veraltet ist, muss er behandelt werden, indem der alte Typ als veraltet markiert wird, mit einem Verweis auf den neuen Typ, unabhängig davon.

Dies ermöglicht bessere Tools, indem es einfacher und allgemeiner ist: Es muss keine Sonderfälle oder sogar Typaliase kennen: Es muss nur mit "Deprecated: " in Doc-Kommentaren übereinstimmen.

Eine offizielle, wenn auch vielleicht vorübergehende, Richtlinie, dass ein Alias ​​in der stdlib nur zur Veraltung dient, ist gut, aber sie sollte nur mit den standardmäßigen Veraltungskommentaren durchgesetzt werden und indem andere Verwendungen untersagt werden, um sie über die Codeüberprüfung hinaus zu lassen.

@niemeyer Meine vorherige Antwort ist aufgrund von Stromausfall verloren gegangen :( außer Betrieb:

Aber ich wiederhole mich..

FWIW, ich fand deine letzte Antwort ziemlich hilfreich. Es hat mich überzeugt, dass wir uns mehr einig sind, als es zuvor den Anschein hatte (und es Ihnen vielleicht immer noch scheinen mag). Irgendwo scheint es aber immer noch Missverständnisse zu geben.

In meinem Vorschlag geht es jedoch um eine schrittweise Umgestaltung des Codes

Das ist, glaube ich, nicht strittig. :) Ich stimmte von Anfang an zu, dass Ihr Vorschlag eine interessante Alternative ist, um das Problem anzugehen. Was mich verwirrt, sind Aussagen wie diese:

Wenn Sie eine Methode oder einen Typ löschen, wird der Go-Code, der diese verwendet, unterbrochen, sodass diese Änderungen atomar durchgeführt werden müssen.

Ich frage mich immer noch, was Ihre Argumentation hier ist. Ich verstehe die Einheit der Atomizität als ein einzelnes Commit. Mit dieser Annahme verstehe ich einfach nicht, warum Sie davon überzeugt sind, dass das Löschen einer Methode oder eines Typs nicht zuerst in separaten, beliebig vielen Commits in den abhängigen Repositorys erfolgen kann und dann, sobald kein offensichtlicher Benutzer mehr vorhanden ist (und eine reichliche Deprekation). Intervall abgelaufen ist) wird die Methode oder der Typ in einem Commit-Upstream gelöscht (ohne etwas zu unterbrechen, da niemand mehr davon abhängt). Ich stimme zu, dass es einen gewissen Unschärfefaktor bei umgekehrten Abhängigkeiten gibt, die nicht der Veraltung entsprechen oder den Sie nicht finden (oder vernünftigerweise beheben können), aber das scheint mir weitgehend unabhängig von der vorliegenden Angelegenheit zu sein; Sie werden dieses Problem immer haben, wenn Sie eine Breaking Change anwenden und egal wie Sie versuchen, sie zu orchestrieren.

Und um fair zu sein: Sätze wie helfen der Verwirrung nicht wirklich

Es sei denn, Sie leben in einem ummauerten Garten und können sich an jede Anrufstelle wenden, um das Problem zu beheben.

Wenn etwas, was ich gesagt habe, Ihnen den Eindruck vermittelt hat, dass dies der Punkt ist, von dem aus ich argumentiere, hoffe ich, dass Sie einen Schritt zurücktreten und es vielleicht noch einmal lesen können, unter der Annahme, dass ich vollständig aus der Position der Open Source argumentiere community (wenn du mir nicht glaubst, schaue gerne in meinen vorherigen Beiträgen zu diesem Thema nach; ich bin immer der Erste, der darauf hinweist, dass dies bei weitem mehr ein Community-Problem als ein Monorepo-Problem ist. Monorepos haben Möglichkeiten, dies zu umgehen , wie gesagt).

Trotzdem. Ich finde das genauso ätzend wie du. Ich hoffe aber, dass ich deine Position irgendwann verstehe.

gleichzeitig darüber sprechen, ob und wie man Dinge wie die protobuf öffentlichen Importe unterstützen kann ...
Ich denke, es muss irgendwann diskutiert werden, ob wir Wrapper-Pakete, öffentliche Protobuf-Importe oder das Offenlegen interner Paket-APIs für so schlecht halten

nit: Ich glaube nicht, dass öffentliche Protobuf-Importe als besonderer sekundärer Anwendungsfall erwähnt werden müssen. Sie wurden für die schrittweise Codereparatur entwickelt, wie sowohl in der internen Designdokumentation als auch in der öffentlichen Dokumentation ausdrücklich

@Merovius Danke für die produktive Antwort. Lassen Sie mich versuchen, einen Kontext zu liefern:

Ich frage mich immer noch, was Ihre Argumentation hier ist. Ich verstehe die Einheit der Atomizität als ein einzelnes Commit. Mit dieser Annahme verstehe ich einfach nicht, warum Sie davon überzeugt sind, dass das Löschen einer Methode oder eines Typs nicht zuerst einzeln erfolgen kann,

Habe nie gesagt, dass es nicht passieren kann. Lassen Sie mich einen Schritt zurücktreten und es klarer formulieren.

Wir sind uns wahrscheinlich alle einig, dass das Endziel zweierlei ist: Wir wollen funktionierende Software und wir wollen die Software verbessern, damit wir vernünftig weiter daran arbeiten können. Einige der letzteren sind bahnbrechende Veränderungen und stehen im Widerspruch zum früheren Ziel. Es gibt also Spannung, was bedeutet, dass es eine gewisse Subjektivität gibt, wo der Sweet Spot liegt. Der interessante Teil unserer Debatte liegt hier.

Eine hilfreiche Möglichkeit, nach diesem Sweet Spot zu suchen, besteht darin, über menschliche Eingriffe nachzudenken. Das heißt, sobald Sie etwas tun, bei dem die Benutzer den Code manuell ändern müssen, damit er funktioniert, tritt Trägheit auf. Es dauert lange, bis der relevante Teil aller abhängigen Codebasen diesen Prozess durchlaufen hat. Wir bitten vielbeschäftigte Menschen, Dinge zu tun, die sie in den meisten Fällen lieber nicht stören würden.

Eine andere Möglichkeit, diesen Sweet Spot zu betrachten, ist die Wahrscheinlichkeit einer funktionierenden Software. Es spielt keine Rolle, wie sehr wir die Leute bitten, keine veraltete Methode zu verwenden. Wenn es leicht zugänglich ist und ihr Problem hier und jetzt löst, werden die meisten Entwickler es einfach verwenden. Das gängige Gegenargument ist hier: _oh, aber dann ist es ihr Problem, wenn es kaputt geht!_ Aber das widerspricht dem erklärten Ziel: Wir wollen funktionierende Software, nicht Recht haben.

Hoffentlich bietet dies einen besseren Einblick, warum das einfache Verschieben eines Typs nicht hilfreich ist. Damit Menschen diesen neuen Typ tatsächlich in seinem neuen Zuhause verwenden können, brauchen wir menschliches Eingreifen. Wenn sich die Leute die Mühe machen, ihren Code manuell zu ändern, ist es am besten, eine Intervention zu haben, die _den neuen Typ verwendet_, anstatt etwas, das sich in der kommenden Zukunft bald wieder ändern wird. Wenn wir uns die Mühe machen, ein Sprachfeature hinzuzufügen, um bei Refactorings zu helfen, würde es den Leuten im Idealfall ermöglichen, ihren Code aus den oben genannten Gründen nach und nach _in diesen neuen Typ_ zu verschieben, nicht einfach in ein neues Zuhause.

Danke für die Erklärung. Ich glaube, ich verstehe Ihre Position jetzt besser und stimme Ihren Annahmen zu (nämlich, dass die Leute auf jeden Fall veraltete Dinge verwenden werden, daher ist es von größter Bedeutung, jede mögliche Hilfe zu leisten, um sie zum Ersatz zu führen). FWIW, mein naiver Plan, mit diesem Problem umzugehen (egal welche Lösung für die schrittweise Reparatur wir verwenden werden), ist ein Go-Fix-ähnliches Tool, um Code automatisch Paket für Paket in der Verfallsphase zu migrieren, aber ich gebe frei zu, dass Wie und ob das in der Praxis funktioniert, habe ich noch nicht ausprobiert.

@niemeyer Ich glaube nicht, dass Ihr Vorschlag ohne eine ernsthafte Störung des Go-Typsystems durchführbar ist.

Betrachten Sie das Dilemma, das dieser Code darstellt:

package old
import "new"
type A adapts new.A
func (a A) NewA() {}

package new
type A struct{}
func (a A) OldA() {}

package main
import (
    "new"
    "old"
    "reflect"
)
func main() {
    oldv := reflect.ValueOf(old.A{})
    newv := reflect.ValueOf(new.A{})
    if oldv.Type() == newv.Type() {
        // The two types are equal, therefore they must
        // have exactly the same method set, so either
        // oldv doesn't have the OldA method or newv doesn't
        // have the NewA method - both of which imply a contradiction
        // in the type system.
    } else {
         // The two types are not equal, which means that the
         // old adapted type is not fully compatible with the old
         // one. Any type that includes either new.A or new.B will
         // be incompatible as one of its components will likewise be
         // unequal, so any code that relies on dynamic type checking
         // will fail when presented with the type that's not using the
         // expected version.
    }
 }

Eines der aktuellen Axiome des Reflect-Pakets lautet, dass, wenn zwei Typen gleich sind, ihre Reflect.Type-Werte gleich sind. Dies ist eine der Grundlagen für die Effizienz der Laufzeittypkonvertierung von Go. Soweit ich sehen kann, gibt es keine Möglichkeit, das Schlüsselwort "adapts" zu implementieren, ohne dies zu unterbrechen.

@rogpeppe Siehe das Gespräch mit @rsc über Reflexion oben. Die beiden Typen sind nicht identisch, daher würde Reflect nur die Wahrheit sagen und Details zum Adapter angeben, wenn er danach gefragt wird.

@niemeyer Wenn die beiden Typen nicht gleich sind, glaube ich nicht, dass wir die schrittweise

Wir könnten tun:

package newimage
import "image"
type RGBA adapts image.RGB
func (r *RGBA) At(x, y) color.Color {
    return (*image.Buffer)(r).At(x, y)
}
etc for all the methods

Angesichts des Ziels der schrittweisen Codereparatur halte ich es für vernünftig, dies zu erwarten
ein im neuen Paket erstelltes Bild ist mit bestehenden Funktionen kompatibel
die den alten Bildtyp verwenden.

Nehmen wir zur Argumentation an, dass das Paket image/png
wurde konvertiert, um newimage zu verwenden, aber image/jpeg nicht.

Ich glaube, dass wir erwarten sollten, dass dieser Code funktioniert:

img, err := png.Decode(r)
if err != nil { ... }
err = jpeg.Encode(w, img, nil)

aber da es eine Typ-Assertion gegen *image.RGBA und nicht gegen *newimage.RGBA macht,
es wird AFAICS fehlschlagen, weil die Typen unterschiedlich sind.

Angenommen, wir haben die obige Typzusicherung erfolgreich gemacht, unabhängig davon, ob der Typ *image.RGBA . ist
oder nicht. Das würde die aktuelle Invariante brechen, die:

Reflect.TypeOf(x) == Reflect.TypeOf(x.(anyStaticType))

Das heißt, die Verwendung einer statischen Typzusicherung würde nicht nur den statischen Typ von a . bestätigen
Wert, aber manchmal würde es ihn tatsächlich ändern.

Sagen wir, wir haben entschieden, dass das in Ordnung ist, dann würden wir vermutlich auch brauchen
um die Konvertierung eines angepassten Typs in jede beliebige Schnittstelle zu ermöglichen, die mit einer seiner kompatiblen
Unterstützung für angepasste Typen, sonst würde entweder neuer oder alter Code aufhören
funktioniert bei der Konvertierung in Schnittstellentypen, die mit dem
Typ, den sie verwenden.

Dies führt zu einer weiteren widersprüchlichen Situation:

// oldInterface is some interface with methods that
// are only supported by the old type.
type oldInterface interface {
    OldMethod()
}
var x = interface{} = newpackage.Type{}
switch x.(type) {
case oldInterface:
    // This would fail because the newpackage.Type
    // does not implement OldMethod, even though we
    // we just supposedly checked that x implements OldMethod.
    reflect.TypeOf(x).Method("OldMethod")
}

Insgesamt denke ich, dass es zwei Typen gibt, die beide gleich, aber unterschiedlich sind
würde zu einem sehr schwer zu erklärenden Typensystem und unerwarteten Inkompatibilitäten führen
in Code, der dynamische Typen verwendet.

Ich unterstütze den Vorschlag "Typ X = Y". Es ist einfach zu erklären und tut es nicht
stören das Typensystem zu sehr.

@rogpeppe : Ich glaube, dass @niemeyers Vorschlag darin besteht, einen angepassten Typ implizit in seinen Basistyp zu konvertieren , ähnlich den früheren Vorschlägen von @josharian .

Damit dies für die schrittweise Umgestaltung funktioniert, müsste es auch implizit Funktionen mit Argumenten angepasster Typen konvertieren; im Wesentlichen würde es erfordern, der Sprache Kovarianz hinzuzufügen. Das ist sicherlich keine unmögliche Aufgabe – viele Sprachen erlauben Kovarianz, insbesondere für Typen mit derselben zugrunde liegenden Struktur – aber es erhöht die Komplexität des Typsystems, insbesondere für Schnittstellentypen .

Das führt zu einigen interessanten Randfällen, wie Sie bemerkt haben, aber sie sind nicht unbedingt "widersprüchlich" an sich:

type oldInterface interface {
    OldMethod()
}
var x = interface{} = newpackage.Type{}
switch y := x.(type) {
case oldInterface:
    reflect.TypeOf(y).Method("OldMethod")  // ok
    reflect.TypeOf(x).Method("NewMethod")  // ok

    // This would fail because y has been implicitly converted to oldInterface.
    reflect.TypeOf(y).Method("NewMethod")

    // This would fail because accessing OldMethod on newpackage.Type requires
    // a conversion to oldInterface.
    reflect.TypeOf(x).Method("OldMethod")
}
// This would fail because accessing OldMethod on newpackage.Type requires
// a conversion to oldInterface.

Das erscheint mir immer noch widersprüchlich. Das aktuelle Modell ist sehr einfach: Ein Schnittstellenwert hat einen gut definierten zugrunde liegenden statischen Typ. Im obigen Code leiten wir etwas über diesen zugrunde liegenden Typ ab, aber wenn wir uns den Wert ansehen, sieht er nicht so aus, wie wir ihn abgeleitet haben. Dies ist aus meiner Sicht eine schwerwiegende (und schwer zu erklärende) Änderung der Sprache.

Hier scheint die Diskussion ins Stocken zu geraten. Basierend auf einem Vorschlag von @egonelbre in https://github.com/golang/go/issues/16339#issuecomment -247536289 habe ich den ursprünglichen Kommentar zum Problem (oben) aktualisiert, um eine verlinkte Zusammenfassung der Diskussion einzufügen weit. Ich werde jedes Mal, wenn ich die Zusammenfassung aktualisiert habe, einen neuen Kommentar wie diesen posten.

Insgesamt scheint die Stimmung hier eher für Typaliase als für verallgemeinerte Aliase zu gelten. Möglicherweise wird die Adapteridee von Gustavo Typaliase verdrängen, aber möglicherweise nicht. Es erscheint im Moment etwas komplex, obwohl vielleicht am Ende der Diskussion eine einfachere Form erreicht wird. Ich schlage vor, dass die Diskussion noch eine Weile andauert.

Ich bin immer noch nicht davon überzeugt, dass veränderliche globale Variablen "normalerweise ein Fehler" sind (und in den Fällen, in denen es sich um einen Fehler handelt, ist der Race Detector das Werkzeug der Wahl, um solche Fehler zu finden). Ich würde darum bitten, dass, wenn dieses Argument verwendet wird, um das Fehlen einer erweiterbaren Syntax zu rechtfertigen, ein Vet-Check implementiert wird, der - sagen wir - auf Zuweisungen zu globalen Variablen in Code prüft, der nicht ausschließlich von init() oder deren Deklarationen erreichbar ist. Ich würde naiv denken, dass dies nicht besonders schwer zu implementieren ist und es nicht viel Arbeit sein sollte, es über - sagen wir - alle bei godoc.org registrierten Pakete zu testen, um zu sehen, was die Anwendungsfälle für veränderliche globale Variablen sind und ob wir dies tun betrachten alle von ihnen Fehler.

(Ich würde auch gerne glauben, dass, wenn go unveränderliche globale Variablen wachsen lässt, diese Teil von const-Deklarationen sein sollten, weil sie das konzeptionell sind und weil dies abwärtskompatibel wäre, aber ich erkenne an, dass dies wahrscheinlich zu Komplikationen, welche Art von Ausdrücken zum Beispiel in Array-Typen verwendet werden können und mehr Nachdenken erfordern würden)

Re "Einschränkung? Aliase von Standardbibliothekstypen können nur in Standardbibliotheken deklariert werden." -- insbesondere würde dies den Drop-In-Usecase für x/image/draw , ein bestehendes Paket, das Interesse an der Verwendung von Aliasen bekundet hat. Ich könnte mir zum Beispiel auch sehr gut vorstellen, dass Router-Pakete oder ähnliches in ähnlicher Weise Aliase in net/http ( winkt mit den Händen ).

Ich stimme auch den Gegenargumenten zu all den Einschränkungen zu, dh ich bin dafür, keine davon zu haben.

@Merovius , was ist mit veränderlichen _exported_ globalen Variablen? Es stimmt, dass eine nicht exportierte globale Datei in Ordnung sein kann, da der gesamte Code im Paket weiß, wie sie richtig zu handhaben ist. Es ist weniger offensichtlich, dass exportierte veränderliche Globals jemals Sinn machen. Diesen Fehler haben wir selbst in der Standardbibliothek mehrfach gemacht. Es gibt beispielsweise keine völlig sichere Möglichkeit, runtime.MemProfileRate zu aktualisieren. Das Beste, was Sie tun können, ist, es früh in Ihrem Programm zu setzen und zu hoffen, dass kein importiertes Paket eine Initialisierungs-Goroutine gestartet hat, die möglicherweise Speicher zuweist. Mit var vs const haben Sie vielleicht recht, aber das können wir auf einen anderen Tag belassen.

Guter Punkt zu x/image/draw. Wird beim nächsten Update zur Zusammenfassung hinzugefügt.

Ich würde sehr gerne einen repräsentativen Korpus von Go-Code zusammenstellen, den wir analysieren könnten, um Fragen wie die von Ihnen gestellten zu beantworten. Ich habe vor ein paar Wochen angefangen, dies zu versuchen, und bin auf einige Probleme gestoßen. Es ist ein bisschen mehr Arbeit, als es scheint, aber es ist sehr wichtig, diesen Datensatz zu haben, und ich gehe davon aus, dass wir es schaffen werden.

@rsc Ihre GothamGo-Präsentation zu diesem Thema wurde auf youtube https://www.youtube.com/watch?v=h6Cw9iCDVcU veröffentlicht und wäre eine gute Ergänzung zum ersten Beitrag.

Im Abschnitt "Welche anderen Probleme muss ein Vorschlag für Typaliase berücksichtigen?" Abschnitt wäre es hilfreich, anzugeben, dass die Antwort auf "Können Methoden für Typen mit Aliasnamen definiert werden?" ist ein hartes Nein. Mir ist klar, dass das gegen den verordneten Geist der Sektion verstößt, aber mir ist aufgefallen, dass es in vielen Gesprächen über Aliase hier und anderswo Leute gibt, die das Konzept sofort ablehnen, weil sie glauben, dass Aliase dies notwendigerweise zulassen und damit verursachen würden Probleme als es löst. Es ist in der Definition implizit enthalten, aber eine explizite Erwähnung würde viel unnötiges Hin und Her kurzschließen. Obwohl das vielleicht in eine Alias-FAQ im neuen Vorschlag für Aliase gehört, sollte das das Ergebnis dieses Threads sein.

@Merovius jede exportierte

Gegeben Version n eines Pakets p ,

package p
var Global = 0

ab Version n+1 können Getter und Setter eingeführt und die Variable veraltet sein

package p
//Deprecated: use GetGlobal and SetGlobal.
var Global = 0
func GetGlobal() int {
    return Global
}
func SetGlobal(n int) {
   Global = n
}

und Version n + 2 könnte Global mehr exportieren

package p
var global = 0
func GetGlobal() int {
    return global
}
func SetGlobal(n int) {
   global = n
}

(Übung links für den Leser: Sie könnten auch den Zugriff auf global in einen Mutex in n + 2 verpacken und GetGlobal() zugunsten des idiomatischeren Global() abwerten.)

Das ist keine schnelle Lösung, aber es reduziert das Problem, sodass nur Funktionsaliase (oder deren derzeitige Problemumgehung) für die schrittweise Codereparatur unbedingt erforderlich sind.

@rsc Eine triviale Verwendung für Aliase, die Sie in Ihrer Zusammenfassung

@jimmyfrasche Du hast

Es gibt einen Punkt bezüglich der Nicht-Reparatur-Nutzung von Aliasen (z. B. das Erstellen von Drop-In-Ersatzpaketen), aber ich gebe zu, dass dies die Argumentation für var-Aliasnamen abschwächt.

@Merovius stimmte in allen Punkten zu. Ich bin auch nicht glücklich darüber, aber ich muss der Logik folgen v☹v

@niemeyer können Sie klären, wie Adapter die Migration von Typen unterstützen würden, bei denen sowohl alte als auch neue eine Methode mit demselben Namen, aber unterschiedlichen Signaturen haben. Das Hinzufügen eines Arguments zu einer Methode oder das Ändern des Typs eines Arguments scheinen gängige Entwicklungen einer Codebasis zu sein.

@rogpeppe Beachten Sie, dass es heute genau so passiert:

type two one

Dies macht one und two unabhängigen Typen, und ob Sie ein interface{} spiegeln oder unter einem one und two konvertieren. Der obige Adaptervorschlag macht diesen letzten Schritt für Adapter automatisch. Der Vorschlag mag Ihnen aus mehreren Gründen nicht gefallen, aber daran ist nichts widersprüchlich.

@iand Wie im Fall von type two one haben die beiden Typen völlig unabhängige Methodensätze, daher gibt es nichts Besonderes an übereinstimmenden Namen. Bevor alte Codebasen migriert werden, würden sie die alte Signatur unter dem vorherigen Typ (jetzt ein Adapter) verwenden. Neuer Code, der den neuen Typ verwendet, würde die neue Signatur verwenden. Das Übergeben eines Wertes des neuen Typs in alten Code passt diesen automatisch an, da der Compiler weiß, dass letzterer ein Adapter von ersterem ist, und daher den entsprechenden Methodensatz verwendet.

@niemeyer Es scheint, als ob sich hinter diesen Adaptern eine Menge Komplexität

@jimmyfrasche In Bezug auf Methoden auf Aliasnamen erlauben Aliase sicherlich nicht, die üblichen Methodendefinitionsbeschränkungen zu umgehen: Wenn ein Paket den Typ T1 = otherpkg.T2 definiert, kann es keine Methoden auf T1 definieren, genauso wie es keine Methoden direkt auf otherpkg.T2 definieren kann. Wenn ein Paket jedoch den Typ T1 = T2 (beide im selben Paket) definiert, ist die Antwort weniger klar. Wir könnten eine Einschränkung einführen, aber dafür besteht (noch) keine offensichtliche Notwendigkeit.

Diskussionszusammenfassung auf oberster Ebene aktualisiert . Änderungen:

  • Link zum GothamGo-Video hinzugefügt
  • "Lange Namen abkürzen" als mögliche Verwendung hinzugefügt, per @jba.
  • x/image/draw als Argument gegen die Beschränkung der Standardbibliothek hinzugefügt, gemäß @Merovius.
  • Mehr Text zu Methoden für Aliase hinzugefügt, per @jimmyfrasche.

Designdokument hinzugefügt:

Wie schon vor einer Woche scheint es bei Typaliasen noch einen allgemeinen Konsens zu geben. Robert und ich haben ein formales Designdokument entworfen, das ich gerade eingecheckt habe (Link oben).

Nach dem Vorschlag Prozess , schrieben qualitative Anmerkungen zu dem Vorschlag _here_ zu diesem Thema. Rechtschreibung/Grammatik/etc finden Sie auf der Gerrit-Codereview-Seite https://go-review.googlesource.com/#/c/34592/. Vielen Dank.

Ich würde gerne den "Effekt auf die Einbettung" überdenken lassen. Es schränkt die Verwendbarkeit von Typaliasen für die schrittweise Codereparatur ein. Wenn p1 einen Typ in type T1 = T2 umbenennen möchte und das Paket p2 p1.T2 in eine Struktur einbettet, können sie diese Definition nie in p1.T1 aktualisieren p3 auf die eingebettete Struktur mit dem Namen verweisen könnte. p2 kann dann nicht zu p1.T1 wechseln, ohne p3 zu brechen; p3 kann den Namen nicht auf p1.T1 aktualisieren, ohne mit dem aktuellen p2 zu brechen.

Ein Ausweg wäre, a) im Allgemeinen jedes Kompatibilitäts-/Ablaufversprechen auf Code zu beschränken, der sich nicht namentlich auf eingebettete Felder bezieht, oder b) eine separate Einstellungsstufe hinzuzufügen, also p1 fügt type T1 = T2 und veraltet T2 , dann wird p2 Bezugnahme auf (sagen wir) s2.T2 nach dem Namen veraltet, alle Importeure von p2 werden repariert um das nicht zu tun, dann macht p2 den Wechsel.

Theoretisch kann das Problem auf unbestimmte Zeit wieder auftreten; p4 könnte p3 importieren, das selbst den Typ von p2 einbettet; es scheint mir, dass p3 auch eine Verfallsfrist haben muss, um auf das doppelt eingebettete Feld namentlich zu verweisen? In diesem Fall wird der innerste Verfallszeitraum verschwindend klein oder der äußerste wird unendlich. Aber auch ohne das Problem als rekursiv zu betrachten, scheint mir, dass b) ziemlich schwer zu bemessen wäre (die Verfallsperiode von p2 müsste vollständig in der Verfallsperiode von p1 Wenn T also ein "Standard-Deprecation-Zeitraum" ist, müssen Sie beim Umbenennen von Typen mindestens 2T auswählen, damit die Releases aufeinander abgestimmt sind.

a) erscheint mir auch unpraktisch; ZB wenn ein Typ ein *byte.Buffer einbettet und ich dieses Feld setzen möchte (oder diesen Puffer an eine andere Funktion übergeben möchte), gibt es einfach keine Möglichkeit, dies zu tun, ohne darauf mit Namen zu verweisen (außer mit struct-Initialisatoren ohne Namen, wodurch auch Kompatibilitätsgarantien verloren gehen :) ).

Ich verstehe, wie attraktiv es ist, mit byte und rune als Aliasnamen kompatibel zu sein. Aber persönlich würde ich dies als zweitrangig betrachten, um die Nützlichkeit von Typaliasen für schrittweise Reparaturen zu erhalten. Ein (wahrscheinlich schlechtes) Beispiel für eine Idee, beides zu bekommen, wäre, für exportierte Namen die Verwendung eines beliebigen Alias ​​für den Verweis auf ein eingebettetes Feld zu ermöglichen und für nicht exportierte Namen (inhärent auf dasselbe Paket beschränkt, also unter mehr Kontrolle des Autors) ) die aktuell vorgeschlagene Semantik beibehalten? Ja, diese Unterscheidung gefällt mir auch nicht. Vielleicht hat jemand eine bessere Idee.

@rsc re-Methoden auf einem Alias

Wenn Sie einen Typ S haben, der ein Alias ​​für Typ T ist, beide im selben Paket definiert sind, und Sie das Definieren von Methoden für S zulassen, was ist, wenn T ein Alias ​​für pF ist, der in einem anderen Paket definiert ist? Auch dies sollte offensichtlich fehlschlagen, aber es sind Feinheiten bei der Durchsetzung, Implementierung und Lesbarkeit der Quelle zu berücksichtigen (Wenn sich T in einer anderen Datei als S befindet, ist es nicht sofort klar, ob Sie eine Methode für T definieren können, indem Sie sich die Definition von T).

Die Regel – wenn Sie type T = S , können Sie keine Methoden auf T deklarieren – ist absolut und aus dieser einzelnen Zeile in der Quelle wird klar, dass sie zutrifft, ohne dass die Quelle von S, wie Sie es in der Alias- oder Alias-Situation tun würden.

Darüber hinaus verwischt das Zulassen von Methoden für einen lokalen Typalias die Unterscheidung zwischen einem Typalias und einer Typdefinition. Da die Methoden ohnehin sowohl auf S als auch auf T definiert wären, schränkt die Einschränkung, dass sie nur auf eine geschrieben werden können, nicht ein, was ausgedrückt werden kann. Es hält die Dinge einfach einfacher und einheitlicher..

@jimmyfrasche Wenn wir type T1 = T2 schreiben und sich T2 im selben Paket befindet, dann verwerfen wir wahrscheinlich den Namen T2. In diesem Fall möchten wir, dass T2 im godoc so wenig wie möglich vorkommt. Daher möchten wir alle Methoden als func (T1) M() deklarieren.

@jba eine godoc-Änderung, um die Methoden eines Alias ​​als für diesen Alias ​​deklariert zu melden, würde diese Anforderung erfüllen, ohne die Lesbarkeit der Quelle zu ändern. Im Allgemeinen wäre es schön, wenn godoc den vollständigen Methodensatz eines Typs anzeigen würde, wenn es um Aliasing und/oder Einbettung geht, insbesondere wenn der Typ aus einem anderen Paket stammt. Das Problem sollte mit intelligenteren Tools und nicht mit mehr Sprachsemantik gelöst werden.

@jba Warum würden Sie in diesem Fall nicht einfach die Richtung des Alias ​​umkehren? type T2 = T1 können Sie bereits Methoden auf T1 mit derselben Paketstruktur definieren; der einzige Unterschied ist der Typname, der vom reflect Paket gemeldet wird, und Sie können die Migration starten, indem Sie die namensabhängigen Anruf-Sites als namenunempfindlich festlegen, bevor Sie den Alias ​​hinzufügen.

@jimmyfrasche Aus dem Vorschlagsdokument :

"Da T1 nur eine andere Art ist, T2 zu schreiben, hat es keinen eigenen Satz von Methodendeklarationen. Stattdessen ist der Methodensatz von T1 derselbe wie der von T2. ​​Zumindest für den ersten Versuch gibt es keine Einschränkung gegen Methodendeklarationen mit T1 als ein Empfängertyp, der mit T2 in derselben Deklaration bereitgestellt wird, wäre gültig. "

Die Verwendung von pF als Methodenempfängertyp ist niemals gültig.

@mdempsky Ich war nicht sehr klar, aber ich habe gesagt, es sei ungültig.

Mein Punkt ist, dass es weniger offensichtlich ist, ob es gültig ist oder nicht, wenn man sich nur diese bestimmte Codezeile ansieht.

Bei type S = T müssen Sie sich auch T ansehen, um sicherzustellen, dass es sich nicht auch um einen Alias ​​handelt, der einen Typ in einem anderen Paket als Alias ​​bezeichnet. Der einzige Gewinn ist die Komplexität.

Methoden für einen Alias ​​immer zu verbieten ist einfacher und leichter zu lesen und Sie verlieren nichts. Ich kann mir nicht vorstellen, dass sehr oft ein verwirrender Fall auftritt, aber es ist nicht nötig, die Möglichkeit einzuführen, wenn Sie nichts gewinnen, was nicht anderswo oder mit einem anderen, aber gleichwertigen Ansatz besser zu handhaben ist.

@Merovius

Wenn p1 einen Typ vom Typ T1 = T2 umbenennen möchte und das Paket p2 p1.T2 in eine Struktur einbettet, können sie diese Definition nie in p1.T1 aktualisieren, da ein Importer p3 möglicherweise mit dem Namen auf die eingebettete Struktur verweist.

In vielen Fällen ist es heute möglich, dieses Problem zu umgehen, indem Sie das anonyme Feld in ein benanntes Feld ändern und die Methoden explizit weiterleiten. Dies würde jedoch nicht für nicht exportierte Methoden funktionieren.

Eine andere Option könnte darin bestehen, ein zweites Feature hinzuzufügen, um dies zu kompensieren. Wenn Sie das Methodenset eines Felds übernehmen könnten, ohne es anonym zu machen (oder mit expliziter Umbenennung), würde dies ermöglichen, dass der Feldname auch bei einer Änderung des zugrunde liegenden Typs unverändert bleibt.

Betrachten Sie die Deklaration aus Ihrem Beispiel:

package p2

type S struct {
  p1.T2
}

Eine kompensierende Funktion könnten "Feldaliase" sein, die einer ähnlichen Syntax wie Typaliasen folgen würden:

package p2

type S struct {
  p1.T1
  T2 = T1  // field T2 is an alias for field T1.
}

var s S  // &s.T2 == &s.T1

Ein weiteres kompensierendes Feature könnte die "Delegation" sein, die explizit den Methodensatz eines anonymen Felds übernimmt:

package p2

type S struct {
  T2 p1.T1 delegated  // T2 is a field of type T1.
  // The method set of S includes the method set of T1 and forwards those calls to field T2.
}

Ich glaube, ich bevorzuge selbst Feld-Aliasnamen, weil sie auch eine andere Art der schrittweisen Reparatur ermöglichen würden: das Umbenennen der Felder einer Struktur, ohne Pointer-Aliasing oder Konsistenzfehler einzuführen.

@Merovius Das Hauptproblem ist, wenn der Typ durch einen Alias ​​umbenannt wird.

Ich habe dies nicht vollständig betrachtet – nur beiläufig, nur ein zufälliger Gedanke:

Was ist, wenn Sie einen Alias ​​in Ihr Paket einführen, der es zurück benennt, und diesen einbetten?

Ich weiß nicht, ob das etwas behebt, aber vielleicht verschafft es etwas Zeit, um die Schleife zu durchbrechen?

@bcmills Ich habe nicht an diese

@Merovius Je mehr ich darüber nachdenke, desto mehr mag ich die Idee von Feldaliasen.

Die explizite Weiterleitung der Methoden ist selbst dann mühsam, wenn sie exportiert werden, und unterbricht andere Arten des Refactorings (zB das Hinzufügen von Methoden zum eingebetteten Typ und die Erwartung, dass der Typ, der ihn einbettet, weiterhin dieselbe Schnittstelle erfüllt). Und das Umbenennen von Struct-Feldern fällt auch unter den allgemeinen Rahmen der schrittweisen Code-Reparatur.

@Merovius

Wenn p1 einen Typ vom Typ T1 = T2 umbenennen möchte und das Paket p2 p1.T2 in eine Struktur einbettet, können sie diese Definition nie in p1.T1 aktualisieren, da ein Importer p3 möglicherweise mit dem Namen auf die eingebettete Struktur verweist. p2 kann dann nicht zu p1.T1 wechseln, ohne p3 zu unterbrechen; p3 kann den Namen nicht auf p1.T1 aktualisieren, ohne mit dem aktuellen p2 zu brechen.

Wenn ich Ihr Beispiel verstehe, haben wir:

package p1

type T2 struct {}
type T1 = T2
package p2

import "p1"

type S struct {
  p1.T2
  F2 string // see below
}

Ich glaube, dass dies nur ein spezifisches Beispiel für den allgemeinen Fall ist, in dem wir ein Strukturfeld umbenennen möchten; das gleiche Problem tritt auf, wenn wir S.F2 in S.F1 umbenennen möchten.

In diesem speziellen Fall können wir das Paket p2 aktualisieren, um die neue API von p1 mit einem lokalen Typalias zu verwenden:

package p2

import "p1"

type T2 = p1.T1

type S struct {
  T2
}

Dies ist natürlich keine gute langfristige Lösung. Ich glaube jedoch, dass es keinen Ausweg gibt, dass p2 seine exportierte API ändern muss, um den T2-Namen zu entfernen, was auf die gleiche Weise wie bei jeder Feldumbenennung erfolgt.

Nur eine Anmerkung zum Thema "Typen zwischen Paketen verschieben". Ist diese Formulierung nicht etwas problematisch?

Soweit ich weiß, erlaubt der Vorschlag, auf eine Objektdefinition, die in einem anderen Paket liegt, über einen neuen Namen "zu verweisen".

Es verschiebt die Objektdefinition nicht, oder? (es sei denn, man schreibt Code in erster Linie mit Aliasnamen, in diesem Fall kann der Benutzer ändern, worauf sich der Alias ​​bezieht, genau wie im Draw-Paket).

@atdiar Der Verweis auf einen Typ in einem anderen Paket kann als Schritt zum Verschieben des Typs verwendet werden. Ja, ein Alias ​​verschiebt den Text nicht, kann aber als Werkzeug dafür verwendet werden.

@Merovius Dies führt wahrscheinlich zu Reflexionen und Plugins.

@atdiar Es tut mir leid, aber ich verstehe nicht, was du sagen willst. Hast du den Originalkommentar dieses Threads, den darin verlinkten Artikel über schrittweise Reparaturen und die bisherige Diskussion gelesen? Wenn Sie versuchen, der Diskussion ein bisher nicht berücksichtigtes Argument hinzuzufügen, müssen Sie meiner Meinung nach klarer sein.

Endlich ein nützlicher und gut geschriebener Vorschlag. Wir brauchen Typ-Alias, ich habe große Probleme beim Erstellen einer einzelnen API ohne Typ-Alias. Bisher muss ich meinen Code so schreiben, dass ich das nicht so gerne mache. Dies sollte in go v1.8 enthalten sein, aber es ist nie zu spät, also fahren Sie mit 1.9 fort.

@Merovius
Ich spreche ausdrücklich vom "Verschieben von Typen" zwischen Paketen. Es ändert die Objektdefinition. In pkg reflect sind beispielsweise einige Informationen an das Paket gebunden, in dem ein Objekt definiert wurde.
Wenn Sie die Definition verschieben, kann sie beschädigt werden.

@kataras Es geht nicht wirklich um gutes Dokument und Kommentare, es geht nur darum, dass Typdefinitionen nicht verschoben werden sollten. So sehr ich den Alias-Vorschlag auch schätze, ich bin vorsichtig, dass die Leute denken, dass sie das einfach tun können.

@atdiar , bitte lest den Artikel aus dem Originalkommentar und die bisherige Diskussion. Das Verschieben von Typen und wie Sie Ihre Bedenken angehen können, sind das Hauptanliegen dieses Threads. Wenn Sie der Meinung sind, dass der Artikel von Russ Ihre Bedenken nicht ausreichend berücksichtigt, sagen Sie bitte genau, warum seine Erklärung nicht zufriedenstellend ist. :)

@kataras Obwohl ich persönlich zustimme, halte ich es nicht für besonders hilfreich, einfach zu behaupten, wie wichtig wir diese Funktion finden. Es muss konstruktiv argumentiert werden, um auf die Bedenken der Menschen einzugehen. :)

@Merovius Ich habe das Dokument gelesen. Es beantwortet meine Frage nicht. Ich glaube, ich war deutlich genug. Es hängt mit dem gleichen Problem zusammen, das uns davon abgehalten hat, den früheren Alias-Vorschlag umzusetzen.

@atdiar verstehe ich zumindest nicht. Sie sagen, dass das Verschieben eines Typs Dinge zerstören würde; Der Vorschlag betrifft die Vermeidung solcher Brüche durch schrittweise Reparatur, indem ein Alias ​​verwendet wird, dann jede umgekehrte Abhängigkeit aktualisiert wird, bis kein Code den alten Typ verwendet, und dann den alten Typ entfernt. Ich verstehe nicht, wie Ihre Behauptung, dass "Reflektion und Plugins" gebrochen sind, unter diesen Annahmen gilt. Wenn Sie die Annahmen in Frage stellen wollen, wurde das bereits diskutiert.

Ich sehe auch nicht, wie eines der Probleme, die Aliase daran hindern, 1.8 einzugeben, mit dem verbunden ist, was Sie gesagt haben. Die jeweiligen Ausgaben lauten meines Wissens nach #17746 und #17784. Wenn Sie sich auf das Einbettungsproblem beziehen (das so interpretiert werden könnte, dass es sich auf Brüche oder Reflexion bezieht, obwohl ich nicht einverstanden bin), dann wird dies im formellen Vorschlag angesprochen (obwohl, siehe oben, ich glaube, dass die vorgeschlagene Lösung mehr Diskussion verdient). und Sie sollten genau sagen, warum Sie dies nicht glauben.

Es tut mir leid, aber nein, Sie waren nicht genau genug. Haben Sie eine Problemnummer für "dasselbe Problem, das uns davon abgehalten hat, den früheren Alias-Vorschlag umzusetzen", auf das Sie sich beziehen, das sich auf das bezieht, was Sie bisher erwähnt haben, um das Verständnis zu erleichtern? Können Sie ein konkretes Beispiel für die Brüche nennen, von denen Sie sprechen? Wenn Sie möchten, dass Ihre Bedenken angesprochen werden, müssen Sie zuerst anderen helfen, sie zu verstehen.

@Merovius Was passiert also im Fall von transitiven Abhängigkeiten, bei denen eine dieser Abhängigkeiten reflect.Type.PkgPath() betrachtet?
Das gleiche Problem tritt beim Einbettungsproblem auf.

@atdiar Es tut mir leid, ich sehe in Anbetracht der bisherigen Diskussion in diesem Thread und dem, worum es in diesem Vorschlag geht, nicht, wie dies in irgendeiner Weise ein verständliches Anliegen ist. Ich werde jetzt aus diesem speziellen Subthread heraustreten und anderen, die Ihren Einwand vielleicht besser verstehen, die Möglichkeit geben, ihn anzusprechen.

Lassen Sie es mich prägnant umformulieren:

Das Problem betrifft die Typgleichheit angesichts der Tatsache, dass die Typdefinition ihren eigenen Speicherort hartcodiert.
Da die Typgleichheit zur Laufzeit getestet werden kann und wird, sehe ich nicht, wie das Verschieben von Typen so einfach ist.

Ich warne nur, dass dieser Anwendungsfall von "Moving Types" potenziell viele Pakete in freier Wildbahn aus der Ferne brechen kann. Ähnliche Sorgen mit Plugins.

(Auf die gleiche Weise würde das Ändern des Typs eines Zeigers in einem Paket viele andere Pakete zerstören, wenn diese Parallele die Dinge klarer machen kann.)

@atdiar Auch hier geht es um das Verschieben von Typen in zwei Schritten, indem Sie zuerst den alten Speicherort verwerfen und die umgekehrten Abhängigkeiten aktualisieren, _dann_ den Typ verschieben. _Natürlich_ geht alles kaputt, wenn Sie nur Typen verschieben, aber darum geht es in dieser Ausgabe überhaupt nicht. Hier geht es darum, eine schrittweise, mehrstufige Lösung dafür zu ermöglichen. Wenn Sie Bedenken haben, dass eine der hier vorgeschlagenen Lösungen diesen mehrstufigen Prozess nicht ermöglicht, seien Sie bitte präzise und beschreiben Sie eine Situation, in der keine vernünftige Abfolge von schrittweisen Reparatur-Commits einen Bruch verhindern kann.

@niemeyer

Dies macht ein und zwei unabhängige Typen, und ob spiegelnd oder unter einer Schnittstelle{}, das ist es
du siehst. Sie können auch zwischen eins und zwei konvertieren. Der obige Adaptervorschlag macht das nur zum Letzten
Schrittautomatik für Adapter. Der Vorschlag mag Ihnen aus mehreren Gründen nicht gefallen, aber es gibt nichts
darüber widersprüchlich.

Sie können nicht zwischen konvertieren

 func() one

und

func() two

@Merovius Sie können unmöglich alle Importeure eines Code-reparierten Pakets ändern, die in freier Wildbahn existieren. Und ich bin nicht sehr daran interessiert, mich hier mit der Paketversionierung zu befassen.

Um es klar zu sagen, ich bin nicht gegen den Alias-Vorschlag, sondern gegen die Formulierung "Moving Types between Packages", die einen Anwendungsfall impliziert, der noch nicht nachweislich sicher ist.

@jimmyfrasche re Vorhersagbarkeit der Method-on-Alias-Gültigkeit:

Es ist bereits so, dass func (t T) M() manchmal gültig, manchmal ungültig ist. Es kommt nicht viel heraus, weil die Leute diese Grenzen nicht oft überschreiten. Das heißt, es funktioniert in der Praxis gut. https://play.golang.org/p/bci2qnldej. Auf jeden Fall steht dies auf der Liste der _möglichen_ Einschränkungen. Wie alle möglichen Einschränkungen erhöht dies die Komplexität, und wir möchten konkrete Beweise aus der realen Welt sehen, bevor wir diese Komplexität hinzufügen.

@Merovius , Namen neu einbetten:

Ich stimme zu, dass die Situation nicht perfekt ist. Wenn ich jedoch eine Codebasis voller Verweise auf io.ByteBuffer habe und sie in bytes.Buffer verschieben möchte, möchte ich in der Lage sein, einzuführen

package io
type ByteBuffer = bytes.Buffer

_ohne_ eine der vorhandenen Referenzen auf io.ByteBuffer zu aktualisieren. Wenn alle Orte, an denen io.ByteBuffer eingebettet ist, automatisch den Namen des Felds in Buffer ändern, wenn eine Typdefinition durch einen Alias ​​ersetzt wird, dann habe ich die Welt kaputt gemacht und es gibt keine schrittweise Reparatur. Im Gegensatz dazu, wenn der Name eines eingebetteten io.ByteBuffer immer noch ByteBuffer ist, können die Verwendungen einzeln in ihren eigenen schrittweisen Reparaturen aktualisiert werden (möglicherweise müssen mehrere Schritte durchgeführt werden; wiederum nicht ideal).

Wir haben dies in #17746 ausführlich besprochen. Ich war ursprünglich auf der Seite des Namens eines eingebetteten io.ByteBuffer-Alias, der Buffer ist, aber das obige Argument überzeugte mich, dass ich falsch lag. Insbesondere @jimmyfrasche hat einige gute Argumente dafür

Beachten Sie, dass es in Ihrem Beispiel in p2 eine Problemumgehung gibt. Wenn p2 wirklich ein eingebettetes Feld namens ByteBuffer möchte, ohne auf io.ByteBuffer zu verweisen, kann es Folgendes definieren:

type ByteBuffer = bytes.Buffer

und betten Sie dann einen ByteBuffer (d. h. einen p2.ByteBuffer) anstelle eines io.ByteBuffer ein. Das ist auch nicht perfekt, aber es bedeutet, dass die Reparaturen fortgesetzt werden können.

Es ist definitiv so, dass dies nicht perfekt ist und Feldumbenennungen im Allgemeinen von diesem Vorschlag nicht berücksichtigt werden. Es könnte sein, dass die Einbettung nicht auf den zugrunde liegenden Namen achten sollte, dass es eine Art Syntax für 'Einbetten von X als Name N' geben sollte. Es könnte auch sein, dass wir später Feldaliase hinzufügen sollten. Beide scheinen a priori vernünftige Ideen zu sein, und beide sollten wahrscheinlich getrennt sein, spätere Vorschläge werden auf der Grundlage echter Beweise für einen Bedarf bewertet. Wenn uns Typaliase dabei helfen, den Punkt zu erreichen, an dem das Fehlen von Feldaliasen die nächste große Hürde für umfangreiche Refactorings ist, dann ist das ein Fortschritt!

(/cc @neild und @bcmills)

@atdiar , ja, es ist wahr, dass Reflect abhängt, wird er kaputt gehen. Wie die Situation beim Einbetten ist es nicht perfekt. Im Gegensatz zur Situation beim Einbetten habe ich keine Antworten, außer vielleicht sollte Code nicht mit Reflect geschrieben werden, um so empfindlich auf diese Details zu reagieren.

@rsc Was ich im Sinn hatte, war a) das Einbetten eines Alias ​​und seines definierenden Typs in dieselbe Struktur zu verbieten (um Mehrdeutigkeiten von b zu vermeiden), b) zuzulassen, auf ein Feld mit einem beliebigen Namen im Quellcode zu verweisen, c) einen auswählen oder das andere in der generierten Typinformation/Reflexion und dergleichen (egal welche).

Ich würde leichtfertig behaupten, dass dies dazu beiträgt, die Art von Brüchen zu vermeiden, die ich beschrieben habe, und gleichzeitig eine klare Wahl für den Fall zu treffen, in dem eine Wahl erforderlich ist; und mir persönlich ist es weniger wichtig, Code, der auf Reflexion beruht, nicht zu knacken, als Code, der dies nicht tut.

Ich bin mir gerade nicht sicher, ob ich dein ByteBuffer-Argument verstehe, aber ich bin auch am Ende eines langen Arbeitstages, also brauche ich nicht weiter zu erklären, wenn ich es nicht überzeugend finde, antworte ich irgendwann :)

@Merovius Ich denke, es ist sinnvoll, die einfachen Regeln auszuprobieren und zu sehen, wie weit wir kommen, bevor wir komplexere einführen. Wir können (a) und (b) bei Bedarf später hinzufügen; (c) ist gegeben, egal was passiert.

Ich stimme zu, dass (b) unter bestimmten Umständen vielleicht eine gute Idee ist, aber unter anderen vielleicht nicht. Wenn Sie Typaliase für den oben erwähnten Anwendungsfall "Strukturieren einer API aus einem Paket in mehrere Implementierungspakete" verwenden, möchten Sie möglicherweise nicht, dass die Einbettung des Alias ​​den anderen Namen (der in einem internen Paket und ansonsten für die meisten Benutzer nicht zugänglich). Ich hoffe wir können noch mehr Erfahrungen sammeln.

@rsc

Vielleicht könnte das Hinzufügen von Informationen auf Paketebene zur Aliasability zu den Objektdateien hilfreich sein.
(Unter Berücksichtigung, ob go-Plugins weiterhin ordnungsgemäß funktionieren müssen oder nicht.)

@Merovius @rsc

a) Verbieten Sie, sowohl einen Alias ​​als auch seinen definierenden Typ in dieselbe Struktur einzubetten

Beachten Sie, dass dies aufgrund der Art und Weise, wie Einbetten mit Methodensätzen interagiert, in vielen Fällen bereits verboten ist. (Wenn der eingebettete Typ einen nicht leeren Methodensatz hat und eine dieser Methoden aufgerufen wird, schlägt die Kompilierung des Programms fehl: https://play.golang.org/p/XkaB2a0_RK.)

Das Hinzufügen einer expliziten Regel, die die doppelte Einbettung verbietet, scheint also nur in einer kleinen Teilmenge von Fällen einen Unterschied zu machen; scheint mir die Komplexität nicht wert.

Warum nicht stattdessen Typaliase als algebraische Typen angehen und Aliase für eine Reihe von Typen unterstützen, damit wir auch ein leeres Interface-Äquivalent mit Typüberprüfung zur Kompilierzeit als Bonus erhalten, a la

type Stringeroonie = {string,fmt.Stringer}

@j7b

Warum nicht Typaliase stattdessen als algebraische Typen angehen und Aliase für eine Reihe von Typen unterstützen?

Aliase sind semantisch und strukturell äquivalent zum Originaltyp. Algebraische Datentypen sind dies nicht: Im allgemeinen benötigen sie zusätzlichen Speicherplatz für Typ-Tags. (Go-Schnittstellentypen enthalten diese Typinformationen bereits, Strukturen und andere Nicht-Schnittstellentypen jedoch nicht.)

@bcmills

Dies mag eine fehlerhafte Argumentation sein, aber ich dachte, das Problem könnte angegangen werden, da der Alias ​​A vom Typ T äquivalent dazu ist, A als Schnittstelle zu deklarieren{} und den Compiler Variablen vom Typ A in T in Bereichen, in denen Variablen vom Typ A deklariert werden, transparent konvertieren zu lassen , von denen ich dachte, dass sie hauptsächlich lineare Kompilierzeitkosten haben, eindeutig sind und eine Grundlage für vom Compiler verwaltete Pseudotypen einschließlich Algebraik unter Verwendung der type T = Syntax schaffen und möglicherweise auch die Implementierung von Typen wie unveränderlichen Referenzen zur Kompilierzeit ermöglichen, die wie Was den Benutzercode betrifft, wäre das nur eine Schnittstelle{}s "unter der Haube".

Mängel in diesem Gedankengang wären wahrscheinlich ein Produkt von Unwissenheit, und da ich nicht in der Lage bin, einen praktischen Proof of Concept anzubieten, akzeptiere ich ihn gerne und verschiebe ihn.

@j7b Selbst wenn ADT eine Lösung für ein allmähliches Reparaturproblem darstellt, erstellen sie dann ihre eigenen; Es ist unmöglich, Mitglieder eines ADT hinzuzufügen oder zu entfernen, ohne Abhängigkeiten zu unterbrechen. Sie würden also im Wesentlichen mehr Probleme schaffen, als Sie lösen würden.

Ihre Idee der transparenten Übersetzung von und zur Schnittstelle{} funktioniert auch nicht für höherwertige Typen wie []interface{} . Und irgendwann verlieren Sie eine der Stärken von go, nämlich den Benutzern die Kontrolle über das Datenlayout zu geben und stattdessen alles mit Java zu verpacken.

ADT sind hier nicht die Lösung.

@Merovius Ich bin mir ziemlich sicher, wenn ein algebraisches

Abgesehen davon bin ich mir sicher, dass type T = das Potenzial hat, über das Umbenennen hinaus auf intuitive, nützliche Weise überladen zu werden gibt einen vom Compiler verwalteten Meta- oder Pseudotyp an, und es werden alle Verwendungsmöglichkeiten eines vom Compiler verwalteten Typs und die Syntax berücksichtigt, die diese Verwendungen am besten ausdrückt. Da sich eine neue Syntax nicht um die Menge der global reservierten Wörter kümmern muss, wenn sie als Qualifizierer verwendet werden, wäre etwas wie type A = alias Type klar und erweiterbar.

@j7b

Abgesehen davon bin ich ein positiver Typ T = hat das Potenzial, auf intuitive, nützliche Weise über das Umbenennen hinaus überladen zu werden.

Ich hoffe das nicht. Go ist heute (meistens) schön orthogonal, und diese Orthogonalität beizubehalten ist eine gute Sache.

Die Art und Weise, wie man heute einen neuen Typ T in Go deklariert, ist type T def , wobei def die Definition des neuen Typs ist. Wenn man algebraische Datentypen (auch bekannt als getaggte Unions) implementieren würde, würde ich erwarten, dass sie dieser Syntax und nicht der Syntax für Typaliase folgen.

Ich füge gerne einen anderen Standpunkt (zur Unterstützung) von Typaliasen ein, der einen Einblick in alternative Anwendungsfälle neben dem Refactoring geben kann:

Lassen Sie uns für einen Moment einen Schritt zurücktreten und annehmen, dass wir keine regulären alten Go-Typ-Deklarationen der Form type T <a type> , sondern nur Alias-Deklarationen type A = <a type> .

(Um das Bild zu vervollständigen, nehmen wir auch an, dass Methoden irgendwie anders deklariert werden - nicht durch Assoziation zu dem benannten Typ, der als Empfänger verwendet wird, weil wir das nicht können. Zum Beispiel könnte man sich den Begriff eines Klassentyps mit den Methoden wörtlich vorstellen innerhalb und daher müssen wir uns nicht auf einen benannten Typ verlassen, um Methoden zu deklarieren. Zwei solche Typen, die strukturell identisch sind, aber unterschiedliche Methoden haben, wären unterschiedliche Typen. Die Details sind hier für dieses Gedankenexperiment nicht wichtig.)

Ich behaupte, dass wir in einer solchen Welt so ziemlich den gleichen Code schreiben könnten, den wir jetzt schreiben: Wir verwenden die (Alias-)Typnamen, damit wir uns nicht wiederholen müssen, und die Typen selbst stellen sicher, dass wir Daten in einem Typ verwenden -sicherer Weg.

Mit anderen Worten, wenn Go so konzipiert worden wäre, wäre es uns wahrscheinlich auch im Großen und Ganzen gut gegangen.

Mehr noch, in einer solchen Welt, da Typen identisch sind, wenn sie strukturell identisch sind (egal wie der Name), wären die Probleme, die wir jetzt mit dem Refactoring haben, gar nicht erst aufgetaucht, und es gäbe keine Notwendigkeit für sie Änderungen an der Sprache.

Aber wir hätten keinen Sicherheitsmechanismus, den wir im aktuellen Go haben: Wir könnten keinen Namen für einen Typ einführen und angeben, dass es sich jetzt um einen neuen, anderen Typ handeln soll. (Dennoch ist es wichtig zu bedenken, dass es sich im Wesentlichen um einen Sicherheitsmechanismus handelt.)

In anderen Programmiersprachen wird die Idee, einen neuen, unterschiedlichen Typ aus einem bestehenden Typ zu machen, "Branding" genannt: Ein Typ erhält eine Marke, die ihn von allen anderen Typen unterscheidet. In Modula-3 gab es zum Beispiel ein spezielles Schlüsselwort BRANDED , um dies zu ermöglichen (zB würde TYPE T = BRANDED REF T0 eine neue, andere Referenz auf T0) erstellen. In Haskell hat das Wort new vor einem Typ eine ähnliche Wirkung.

Wenn wir zu unserer alternativen Go-Welt zurückkehren, befinden wir uns möglicherweise in einer Position, in der wir keine Probleme mit dem Refactoring haben, aber die Sicherheit unseres Codes verbessern wollten, sodass type MyBuffer = []byte und type YourBuffer = []byte bezeichnen verschiedene Typen, damit wir nicht aus Versehen den falschen verwenden. Wir könnten vorschlagen, genau zu diesem Zweck eine Form des Type Brandings einzuführen. Zum Beispiel könnten wir type MyBuffer = new []byte oder sogar type MyBuffer = new YourBuffer schreiben, mit dem Effekt, dass MyBuffer jetzt ein anderer Typ als YourBuffer ist.

Dies ist im Wesentlichen das doppelte Problem dessen, was wir jetzt haben. Es passiert einfach, dass wir in Go vom ersten Tag an immer mit "gebrandeten" Typen gearbeitet haben, sobald sie einen Namen hatten. Mit anderen Worten, type T <a type> ist effektiv type T = new <a type> .

Zusammenfassend: In existierenden Go sind benannte Typen immer "gebrandete" Typen, und uns fehlt der Begriff nur eines Namens für einen Typ (den wir jetzt Typ-Aliasse nennen). In einigen anderen Sprachen sind Typaliase die Norm, und man muss einen "Branding"-Mechanismus verwenden, um einen explizit neuen, anderen Typ zu erstellen.

Der Punkt ist, dass beide Mechanismen von Natur aus nützlich sind, und mit Typaliasen kommen wir endlich dazu, beide zu unterstützen.

@griesemer Die Erweiterung dieser Funktion ist der ursprüngliche Alias-Vorschlag, der das Refactoring idealerweise bereinigen sollte. Ich befürchte, dass nur Typaliase aufgrund ihres eingeschränkten Geltungsbereichs schwierige Randfälle beim Refactoring verursachen würden.

Bei beiden Vorschlägen frage ich mich, ob die Zusammenarbeit mit dem Linker nicht erforderlich sein sollte, da der Name, wie Sie erklärt haben, Teil der Typdefinition in Go ist.

Ich bin mit Objektcode überhaupt nicht vertraut, daher ist es nur eine Idee, aber es scheint, dass es möglich ist, benutzerdefinierte Abschnitte zu Objektdateien hinzuzufügen. Wenn es zufällig möglich wäre, eine Art entrollte verlinkte Liste zu führen, die zur Linkzeit mit Typnamen und ihren Aliasnamen gefüllt wird, könnte das vielleicht helfen. Die Laufzeit hätte alle Informationen, die sie benötigt, ohne auf eine separate Kompilierung verzichten zu müssen.

Die Idee ist, dass die Laufzeit die verschiedenen Aliase für einen bestimmten Typ dynamisch zurückgeben kann, damit Fehlermeldungen klar bleiben (da Aliasing eine Namensdiskrepanz zwischen dem laufenden Code und dem geschriebenen Code einführt).

Eine Alternative zur Verfolgung der Aliasing-Verwendung wäre eine konkrete Versionierungsgeschichte im Großen und Ganzen, um Objektdefinitionen zwischen Paketen "verschieben" zu können, wie es für das Kontextpaket getan wurde. Aber das ist ein ganz anderes Thema.

Letztendlich ist es immer noch eine gute Idee, die strukturelle Äquivalenz den Schnittstellen und die Namensäquivalenz den Typen zu überlassen.
Angesichts der Tatsache, dass ein Typ als Schnittstelle mit mehr Einschränkungen betrachtet werden kann, scheint es, dass die Deklaration eines Alias ​​implementiert werden sollte/könnte, indem ein paketweises Slice von Slices-Typnamen-Strings beibehalten wird.

@atdiar Ich bin mir nicht sicher, ob du meinst, was ich tue, wenn du "separate Zusammenstellung" sagst. Wenn Paket P io und bytes importiert, können alle drei als separate Schritte kompiliert werden. Wenn sich jedoch io oder bytes ändern, muss P neu kompiliert werden. Es ist _nicht_ der Fall, dass Sie Änderungen an io oder bytes vornehmen und dann einfach eine alte Zusammenstellung von P verwenden können. Auch im Plugin-Modus ist dies wahr. Durch Effekte wie paketübergreifendes Inlining verändern auch nicht API-sichtbare Änderungen an der Implementierung von io oder bytes die effektive ABI, weshalb P neu kompiliert werden muss. Typaliase verschlimmern dieses Problem nicht.

@j7d , auf Typsystemebene

Ein Typkonstruktor (eine ausgefallene Art zu sagen "eine Möglichkeit, einen Typ zu verwenden") ist kovariant, wenn er die Subtypisierungsbeziehung beibehält, kontravariant, wenn er die Beziehung invertiert.

Die Verwendung eines Typs in einem Funktionsergebnis ist kovariant. Ein func()-Puffer "ist" ein func()-Reader, da das Zurückgeben eines Buffers bedeutet, dass Sie einen Reader zurückgegeben haben. Die Verwendung eines Typs in einem Funktionsargument ist _nicht_ kovariant. Ein func(Buffer) ist kein func(Reader), weil der func einen Buffer benötigt und einige Reader keine Buffer sind.

Die Verwendung eines Typs in einem Funktionsargument ist kontravariant. Ein func(Reader) ist ein func(Buffer), weil die func nur einen Reader benötigt und ein Buffer ein Reader ist. Die Verwendung eines Typs in einem Funktionsergebnis ist _nicht_ kontravariant. Ein func()-Reader ist kein func()-Puffer, da func einen Reader zurückgibt und einige Reader keine Puffer sind.

Wenn man die beiden kombiniert, ist ein func(Reader)-Reader kein func(Buffer)-Puffer, und umgekehrt, weil entweder die Argumente nicht funktionieren oder die Ergebnisse nicht funktionieren. (Die einzige Kombination in dieser Richtung, die funktioniert, wäre, dass ein func(Reader) Buffer ein func(Buffer) Reader ist.)

Im Allgemeinen, wenn func(X1) X2 ein (Untertyp von) func(X3) X4 ist, dann muss X3 ein (Untertyp von) X1 sein und X2 ist ein (Untertyp von) X4. Im Fall der Alias-Verwendung, bei der T1 und T2 austauschbar sein sollen, ist ein func(T1) T1 nur dann ein Untertyp von func(T2) T2, wenn T1 ein Untertyp von T2 ist _und_ T2 ein Untertyp von T1 ist. Das bedeutet im Grunde, dass T1 der _gleiche_ Typ wie T2 ist, kein allgemeinerer Typ.

Ich habe Funktionsargumente und -ergebnisse verwendet, weil dies das kanonische Beispiel (und ein gutes) ist, aber dasselbe gilt für andere Möglichkeiten zum Erstellen komplexer Ergebnisse. Im Allgemeinen erhalten Sie Kovarianz für Ausgaben (wie func() T oder <-chan T oder map[...]T) und Kontravarianz für Eingaben (wie func(T) oder chan<-T oder map[T ]...) und erzwungene Typgleichheit für Input+Output (wie func(T) T, oder chan T, oder *T, oder [10]T, oder []T, oder struct {Field T} oder eine Variable vom Typ T). Tatsächlich ist der häufigste Fall in Go, wie Sie an den Beispielen sehen können, Eingabe+Ausgabe.

Konkret ist ein []Puffer kein []Reader (weil Sie eine Datei in einem []Reader speichern können, aber nicht in einem []Buffer), noch ist ein []Reader ein []Puffer (weil das Abrufen von einem [] Reader kann eine Datei zurückgeben, während das Abrufen aus einem []Puffer einen Puffer zurückgeben muss).

Eine Schlussfolgerung aus all dem ist, dass Sie, wenn Sie das allgemeine Codereparaturproblem so lösen möchten, dass Code entweder T1 oder T2 verwenden kann, dies nicht mit einem Schema tun können, das T1 nur zu einem Untertyp von T2 macht (oder umgekehrt). Jeder muss ein Untertyp des anderen sein - das heißt, sie müssen vom gleichen Typ sein - oder einige dieser aufgeführten Verwendungen sind ungültig.

Das heißt, die Untertypisierung reicht nicht aus, um das Problem der schrittweisen Codereparatur zu lösen. Aus diesem Grund führen Typaliase einen neuen Namen für denselben Typ ein, sodass T1 = T2 ist, anstatt zu versuchen, Untertypen zu erstellen.

Dieser Kommentar gilt auch für @iands Vorschlag von einer Art " ersetzbaren Typen" vor zwei Wochen und im Grunde eine Erweiterung der Antwort von

Die Diskussionszusammenfassung auf oberster Ebene wurde aktualisiert. Änderungen:

  • TODO entfernt, um die Zusammenfassung der Adapterdiskussion zu aktualisieren, die scheinbar ausgeblendet ist.
  • Zusammenfassung der Diskussion über Einbettung und Feldumbenennung hinzugefügt.
  • Zusammenfassung von 'Methoden für Aliasnamen' in einen eigenen Abschnitt aus der Design-Fragenliste verschoben, erweitert um aktuelle Kommentare.
  • Zusammenfassung der Diskussion der Auswirkungen auf Programme mit Reflexion hinzugefügt.
  • Zusammenfassung der Diskussion über separate Zusammenstellung hinzugefügt.
  • Zusammenfassung der Diskussion verschiedener auf Subtypisierung basierender Ansätze hinzugefügt.

@rsc bezüglich separater Kompilierung, mein Kommentar

@atdiar Es gibt nirgendwo im System eine solche Liste von Aliasnamen. Die Laufzeit hat keinen Zugriff darauf. Aliase sind zur Laufzeit nicht vorhanden.

@rsc Huh, tut mir leid. Ich hänge mit dem ursprünglichen Alias-Vorschlag im Kopf fest und dachte über Aliasing für func nach (während ich Aliasing für Typen besprach). In diesem Fall würde es eine Diskrepanz zwischen den Namen im Code und den Namen zur Laufzeit geben.
Die Verwendung der Informationen in runtime.Frame für die Protokollierung würde in diesem Fall ein Umdenken erfordern.
Vergiss mich.

@rsc danke für die erneute Zusammenfassung. Der eingebettete Feldname irritiert mich immer noch; Alle vorgeschlagenen Problemumgehungen beruhen auf permanenten Kludges, um die alten Namen beizubehalten. Der größere Punkt in diesem Kommentar , nämlich dass dies ein Sonderfall des Umbenennens von Feldern ist, was auch nicht möglich ist, überzeugt mich jedoch, dass dies tatsächlich als separates Problem gesehen (und gelöst) werden sollte. Wäre es sinnvoll, ein separates Thema für eine Anfrage/einen Vorschlag/eine Diskussion zu eröffnen, um Feldumbenennungen für eine schrittweise Reparatur zu unterstützen (möglicherweise in derselben Go-Version behandelt)?

@Merovius , ich stimme zu, dass die schrittweise Feldumbenennung wie das nächste Problem in der Sequenz aussieht. Um diese Diskussion zu beginnen, müsste meiner Meinung nach jemand eine Reihe von Beispielen aus der Praxis zusammentragen, sowohl um einige Beweise dafür zu haben, dass es sich um ein weit verbreitetes Problem handelt, als auch um mögliche Lösungen zu überprüfen. Realistischerweise sehe ich das nicht für dieselbe Veröffentlichung.

Zurück aus zwei Wochen. Die Diskussion scheint sich angenähert zu haben. Sogar das Diskussions-Update vor zwei Wochen war ziemlich unbedeutend.

Ich schlage vor, dass wir:

  • den Typ-Alias-Vorschlag als vorläufige Lösung für das oben dargelegte Problem akzeptieren,
    vorausgesetzt, eine Implementierung kann zu Beginn von Go 1.9 (1. Februar) zum Testen bereitstehen.
  • Erstellen Sie einen dev.typealias dev-Zweig, damit CLs jetzt (Januar) überprüft und zu Beginn von Go 1.9 in den Master zusammengeführt werden können.
  • treffen Sie eine endgültige Entscheidung über die Beibehaltung von Typaliasen am Anfang des Go 1.9-Einfrierens (wie wir es für verallgemeinerte Aliase im Go 1.8-Zyklus getan haben).

+1

Ich schätze die Diskussionsgeschichte hinter dieser Änderung. Nehmen wir an, es ist implementiert. Ohne Zweifel wird es eher ein Randdetail der Sprache als ein Kernmerkmal werden. Als solches erhöht es die Komplexität der Sprache und der Tools, die in keinem Verhältnis zu ihrer tatsächlichen Nutzungshäufigkeit stehen. Es fügt auch mehr Oberfläche hinzu, in der die Sprache versehentlich missbraucht werden könnte. Aus diesem Grund ist es gut, zu vorsichtig zu sein, und ich freue mich, dass es bisher viele Diskussionen gegeben hat.

@Merovius : Entschuldigung für die Bearbeitung meines Beitrags! Ich dachte, niemand liest. Anfangs habe ich in diesem Kommentar etwas Skepsis geäußert, dass diese Sprachänderung notwendig ist, wenn es bereits Tools wie das Tool gorename .

@ jcao219 Dies wurde bereits besprochen, aber überraschenderweise kann ich das hier nicht schnell finden. Es wird ausführlich im Originalthread für allgemeine Aliase # 16339 und die zugehörigen Golang-Nuts-Threads diskutiert. Kurz gesagt: Diese Art von Tooling befasst sich nur mit der Vorbereitung der Reparatur-Commits, nicht mit der Reihenfolge der Änderungen, um Brüche zu vermeiden. Ob die Änderungen von einem Tool oder von einem Menschen vorgenommen werden, ist für das Problem unerheblich, dass es derzeit keine Abfolge von Commits gibt, die nicht den einen oder anderen Code brechen (der ursprüngliche Kommentar zu dieser Ausgabe und das zugehörige Dokument begründen diese Aussage mehr in -Tiefe).

Bei stärker automatisierten Tools (zB integriert in das Go-Tool oder ähnliches) wird dies im ursprünglichen Kommentar unter der Überschrift "Kann dies eine Tooling- oder Compiler-only-Änderung anstelle einer Sprachänderung sein?".

Lassen Sie uns abschließend sagen, dass die Änderung implementiert ist. Ohne Zweifel wird es eher ein Randdetail der Sprache als ein Kernmerkmal werden.

Ich möchte Zweifel äußern. :) Ich halte dies nicht für eine ausgemachte Sache.

@Merovius

Ich möchte Zweifel äußern. :) Ich halte dies nicht für eine ausgemachte Sache.

Ich schätze, ich meinte, dass Leute, die diese Funktion verwenden würden, hauptsächlich die Betreuer wichtiger Go-Pakete mit vielen abhängigen Clients sein werden. Mit anderen Worten, es kommt denen zugute, die bereits Go-Experten sind. Gleichzeitig bietet es eine verlockende Möglichkeit, Code für neue Go-Programmierer weniger lesbar zu machen. Die Ausnahme ist der Anwendungsfall des Umbenennens langer Namen, aber natürliche Namen vom Typ Go sind normalerweise nicht zu lang oder zu komplex.

Genau wie bei der Punktimportfunktion wäre es ratsam, dass Tutorials und Dokumente ihre Erwähnungen dieser Funktion mit einer Erklärung zu Nutzungsrichtlinien versehen.

Sagen wir zum Beispiel, ich möchte "github.com/gonum/graph/simple".DirectedGraph verwenden , und ich wollte es mit digraph aliasen , um zu vermeiden, simple.DirectedGraph , wäre das gut? Anwendungsfall? Oder sollte diese Art der Umbenennung auf unangemessen lange Namen beschränkt werden, die von Dingen wie Protobuf generiert werden?

@jcao219 , die Zusammenfassung der Diskussion oben auf dieser Seite beantwortet Ihre Fragen. Siehe insbesondere diese Abschnitte:

  • Kann dies eine Tooling- oder Compiler-only-Änderung anstelle einer Sprachänderung sein?
  • Welche anderen Verwendungen könnten Aliase haben?
  • Einschränkungen (die allgemeinen Hinweise zu diesem Abschnitt)

Zu Ihrem allgemeineren Punkt zu Go-Experten im Vergleich zu neuen Go-Programmierern: Ein explizites Ziel von Go besteht darin, die Programmierung in großen Codebasen zu vereinfachen. Ob Sie ein Experte sind, hängt nicht von der Größe der Codebasis ab, in der Sie arbeiten. (Vielleicht beginnen Sie gerade mit einem neuen Projekt, das jemand anders begonnen hat. Möglicherweise müssen Sie diese Art von Arbeit noch erledigen.)

OK, basierend auf der Einstimmigkeit / Ruhe hier werde ich (wie ich letzte Woche in https://github.com/golang/go/issues/18130#issuecomment-268614964 vorgeschlagen habe) diesen Vorschlag als genehmigt markieren und einen dev.typealias-Zweig erstellen .

Die ausgezeichnete Zusammenfassung enthält einen Abschnitt "Welche anderen Punkte muss ein Vorschlag für Typaliase ansprechen?" Welche Pläne bestehen, um diese Probleme anzugehen, nachdem der Vorschlag als angenommen erklärt wurde?

CL https://golang.org/cl/34986 erwähnt dieses Problem.

CL https://golang.org/cl/34987 erwähnt dieses Problem.

CL https://golang.org/cl/34988 erwähnt dieses Problem.

@ulikunitz zu den Problemen (alle diese Zitate aus dem Designdokument gehen von "Typ T1 = T2" aus):

  1. Handhabung in godoc. Design doc spezifiziert minimale Änderungen an godoc. Sobald das drin ist, können wir sehen, ob zusätzliche Unterstützung benötigt wird. Vielleicht, aber vielleicht auch nicht.
  2. Können Methoden für Typen mit Aliasnamen definiert werden? Jawohl. Design doc: "Da T1 nur eine andere Art ist, T2 zu schreiben, hat es keinen eigenen Satz von Methodendeklarationen. Stattdessen ist der Methodensatz von T1 derselbe wie der von T2. ​​Zumindest für den ersten Versuch gibt es keine Einschränkung gegen Methodendeklarationen die Verwendung von T1 als Empfängertyp, vorausgesetzt, die Verwendung von T2 in derselben Deklaration wäre gültig."
  3. Wenn Aliase zu Alias ​​zulässig ist, wie gehen wir mit Alias-Zyklen um? Keine Zyklen. Design doc: "In einer Typaliasdeklaration darf T2 im Gegensatz zu einer Typdeklaration niemals direkt oder indirekt auf T1 verweisen."
  4. Sollten Aliase nicht exportierte Bezeichner exportieren können? Jawohl. Design doc: "Es gibt keine Einschränkungen für die Form von T2: Es kann ein beliebiger Typ sein, einschließlich, aber nicht beschränkt auf Typen, die aus anderen Paketen importiert wurden."
  5. Was passiert, wenn Sie einen Alias ​​einbetten (wie greifen Sie auf das eingebettete Feld zu)? Der Name wird vom Alias ​​(der sichtbare Name im Programm) übernommen. Design-Dokument: https://golang.org/design/18130-type-alias#effect -on-embedding.
  6. Sind Aliase als Symbole im erstellten Programm verfügbar? Nein. Design-Dokument: "Typaliase sind zur Laufzeit meistens unsichtbar." (Antwort folgt daraus, wird aber nicht explizit genannt.)
  7. Ldflags-String-Injection: Was ist, wenn wir auf einen Alias ​​verweisen? Es gibt keine var-Aliasnamen, sodass dies nicht auftritt.

CL https://golang.org/cl/35091 erwähnt dieses Problem.

CL https://golang.org/cl/35092 erwähnt dieses Problem.

CL https://golang.org/cl/35093 erwähnt dieses Problem.

@rsc Vielen Dank für die Klarstellungen.

Angenommen:

package a

import "b"

type T1 = b.T2

Soweit ich weiß, ist T1 im Wesentlichen identisch mit b.T2 und daher ein nicht lokaler Typ und es können keine neuen Methoden definiert werden. Die Kennung T1 wird jedoch im Paket a wieder ausgeführt. Ist das eine richtige Interpretation?

@ulikunitz das ist richtig

T1 bezeichnet genau den gleichen Typ wie b.T2. Es ist einfach ein anderer Name. Ob etwas exportiert wird oder nicht, hängt allein vom Namen ab (hat nichts mit dem Typ zu tun, den es bezeichnet).

Um die Antwort von @griesemer explizit zu machen: Ja, T1 wird aus Paket a exportiert (weil es T1 ist, nicht t1).

CL https://golang.org/cl/35099 erwähnt dieses Problem.

CL https://golang.org/cl/35100 erwähnt dieses Problem.

CL https://golang.org/cl/35101 erwähnt dieses Problem.

CL https://golang.org/cl/35102 erwähnt dieses Problem.

CL https://golang.org/cl/35104 erwähnt dieses Problem.

CL https://golang.org/cl/35106 erwähnt dieses Problem.

CL https://golang.org/cl/35108 erwähnt dieses Problem.

CL https://golang.org/cl/35120 erwähnt dieses Problem.

CL https://golang.org/cl/35121 erwähnt dieses Problem.

CL https://golang.org/cl/35129 erwähnt dieses Problem.

CL https://golang.org/cl/35191 erwähnt dieses Problem.

CL https://golang.org/cl/35233 erwähnt dieses Problem.

CL https://golang.org/cl/35268 erwähnt dieses Problem.

CL https://golang.org/cl/35269 erwähnt dieses Problem.

CL https://golang.org/cl/35670 erwähnt dieses Problem.

CL https://golang.org/cl/35671 erwähnt dieses Problem.

CL https://golang.org/cl/35575 erwähnt dieses Problem.

CL https://golang.org/cl/35732 erwähnt dieses Problem.

CL https://golang.org/cl/35733 erwähnt dieses Problem.

CL https://golang.org/cl/35831 erwähnt dieses Problem.

CL https://golang.org/cl/36014 erwähnt dieses Problem.

Dies ist jetzt im Master, vor der Eröffnung von Go 1.9. Bitte zögern Sie nicht, auf Master zu synchronisieren und Dinge auszuprobieren. Vielen Dank.

Weitergeleitet von #18893

package main

import (
        "fmt"
        "q"
)

func main() {
        var a q.A
        var b q.B // i'm a named unnamed type !!!

        fmt.Printf("%T\t%T\n", a, b)
}

Was hast du erwartet zu sehen?

deadwood(~/src) % go run main.go
q.A     q.B

Was hast du stattdessen gesehen?

deadwood(~/src) % go run main.go
q.A     []int

Diskussion

Aliase sollten nicht für unbenannte Typen gelten. Es gibt keine "Code-Reparatur"-Geschichte beim Wechsel von einem unbenannten Typ zu einem anderen. Das Zulassen von Aliasnamen für unbenannte Typen bedeutet, dass ich Go nicht mehr als einfach benannte und unbenannte Typen beibringen kann. Stattdessen muss ich sagen

oh, es sei denn, es ist ein Alias, in diesem Fall müssen Sie sich daran erinnern, dass es ein unbenannter Typ _könnte_, selbst wenn Sie aus einem anderen Paket importieren.

Und schlimmer noch, es wird es den Leuten ermöglichen, Anti-Muster der Lesbarkeit zu verbreiten, wie z

type Any = interface{}

Bitte lassen Sie keine Aliasnamen für unbenannte Typen zu.

@davecheney

Es gibt keine "Code-Reparatur"-Geschichte beim Wechsel von einem unbenannten Typ zu einem anderen.

Nicht wahr. Was ist, wenn Sie den Typ eines Methodenparameters von einem benannten in einen unbenannten Typ oder umgekehrt ändern möchten? Schritt 1 besteht darin, den Alias ​​hinzuzufügen; Schritt 2 besteht darin, die Typen zu aktualisieren, die diese Methode implementieren, um den neuen Typ zu verwenden; Schritt 3 besteht darin, den Alias ​​zu entfernen.

(Es stimmt, dass Sie dies heute tun können, indem Sie die Methode zweimal umbenennen. Das doppelte Umbenennen ist bestenfalls mühsam.)

Und schlimmer noch, es wird es den Leuten ermöglichen, Anti-Muster der Lesbarkeit zu verbreiten, wie z
type Any = interface{}

Die Leute können heute schon type Any interface{} schreiben. Welchen zusätzlichen Schaden bringen Aliase in diesem Fall?

Die Leute können bereits heute Typ Any interface{} schreiben. Welchen zusätzlichen Schaden bringen Aliase in diesem Fall?

Ich habe es ein Anti-Muster genannt, weil es genau das ist. type Any interface{} , weil die Person den Code etwas kürzer _schreibt_, das macht für sie etwas mehr Sinn.

Auf der anderen Seite müssen _alle_ Leser, die im Lesen von Go-Code erfahren sind und interface{} so instinktiv erkennen wie ihr Gesicht im Spiegel, jede Variante von Any lernen und neu lernen , Object , T und ordne sie Dingen wie type Any interface{} , type Any map[interface{}]interface{} , type Any struct{} pro Paket zu.

Sicherlich stimmen Sie zu, dass paketspezifische Namen für gängige Go-Idiome für die Lesbarkeit negativ sind?

Sicherlich stimmen Sie zu, dass paketspezifische Namen für gängige Go-Idiome für die Lesbarkeit negativ sind?

Ich stimme zu, aber da das fragliche Beispiel (bei weitem das häufigste Vorkommen dieses Antimusters, auf das ich gestoßen bin) ohne Aliasse ausgeführt werden kann, verstehe ich nicht, wie sich dieses Beispiel auf den Vorschlag für Typaliase bezieht.

Die Tatsache, dass das Anti-Pattern ohne Typaliase möglich ist, bedeutet, dass wir Go-Programmierer bereits dazu erziehen müssen, es zu vermeiden, unabhängig davon, ob Aliase für unbenannte Typen existieren können.

Und tatsächlich ermöglichen Typaliase das _graduelle Entfernen_ dieses Antimusters aus Codebasen, in denen es bereits existiert.

Erwägen:

package antipattern

type Any interface{}  // not an alias

type Widget interface{
  Frozzle(Any) error
}

func Bozzle(w Widget) error {
  …
}

Heute würden Benutzer von antipattern.Bozzle mit antipattern.Any in ihren Widget Implementierungen festsitzen, und es gibt keine Möglichkeit, antipattern.Any durch schrittweise Reparaturen zu entfernen. Aber mit Typaliasen könnte der Besitzer des antipattern Pakets es so umdefinieren:

// Any is deprecated; please use interface{} directly.
type Any = interface{}

Und jetzt können die Anrufer schrittweise von Any zu interface{} migrieren, sodass der antipattern Betreuer es schließlich entfernen kann.

Mein Punkt ist, dass es keine Rechtfertigung für das Aliasing unbenannter Typen gibt
die Nichtzulassung dieser Option würde weiterhin die Unangemessenheit von untermauern
die Praxis.

Das Gegenteil, um Aliasing von unbenannten Typen zu ermöglichen, aktiviert nicht einen, sondern zwei
Formen dieses Antimusters.

Am Do, 2. Februar 2017, 16:34 schrieb Bryan C. Mills [email protected] :

Sicher stimmen Sie zu, dass paketspezifische Namen für gängige Go-Idiome sind
ein Netto-Negativ für die Lesbarkeit?

Ich stimme zu, aber da das fragliche Beispiel (bei weitem das häufigste)
Auftreten dieses Antimusters, auf das ich gestoßen bin) kann ohne
Aliasse, ich verstehe nicht, wie sich dieses Beispiel auf den Vorschlag für . bezieht
Aliase eingeben.

Die Tatsache, dass das Anti-Pattern ohne Typaliase möglich ist, bedeutet, dass
wir müssen Go-Programmierer bereits dazu erziehen, dies zu vermeiden, unabhängig davon, ob
Aliase für unbenannte Typen können vorhanden sein.


Sie erhalten dies, weil Sie erwähnt wurden.
Antworten Sie direkt auf diese E-Mail und zeigen Sie sie auf GitHub an
https://github.com/golang/go/issues/18130#issuecomment-276872714 oder stumm
der Faden
https://github.com/notifications/unsubscribe-auth/AAAcA6BGrFjjTi7eW1BPp7o81XIekbGXks5rYWr-gaJpZM4LBBEL
.

@davecheney Ich glaube, wir haben noch keine Beweise dafür, dass es schädlich ist, einem beliebigen Typliteral einen Namen zu geben. Dies ist auch nicht ein unerwartetes „Überraschung“ Feature - es wird im Detail in dem diskutiert Designdokument . An dieser Stelle ist es sinnvoll, dies für einige Zeit zu nutzen und zu sehen, wohin es uns führt.

Als Gegenbeispiel gibt es öffentliche APIs, die Typliterale nur verwenden, weil die API einen Client nicht auf einen bestimmten Typ beschränken möchte (siehe zum Beispiel https://golang.org/pkg/go/types/#Info ). Dieses explizite Typliteral kann eine nützliche Dokumentation sein. Gleichzeitig kann es aber auch ziemlich ärgerlich sein, den gleichen Typ literal überall wiederholen zu müssen; und tatsächlich ein Hindernis für die Lesbarkeit sein. In der Lage zu sein, bequem über ein IntSet anstatt über ein map[int]struct{} zu sprechen, ohne an diese eine und einzige IntSet Definition gebunden zu sein, ist für mich ein Pluspunkt. Da ist type IntSet = map[int]struct{} genau richtig.

Abschließend verweise ich gerne auf https://github.com/golang/go/issues/18130#issuecomment -268411811 zurück, falls Sie es verpasst haben. Uneingeschränkte Typdeklarationen mit = sind wirklich die "elementaren" Typdeklarationen, und ich bin froh, dass wir sie endlich in Go haben.

Vielleicht wäre type intSet = map[int]struct{} (nicht exportiert) eine bessere Möglichkeit, unbenannte Typaliase zu verwenden, aber dies klingt eher nach der Domäne von CodeReviewComments und empfohlenen Programmierpraktiken, als die Funktion einzuschränken.

Das heißt, %T ist ein praktisches Werkzeug, um Typen beim Debuggen oder Erkunden des Typsystems zu sehen. Ich frage mich, ob es ein ähnliches Formatverb geben sollte, das den Alias ​​enthält. q.B = []int im Beispiel von @davecheney .

@nathany Wie implementierst du dieses Verb? Die Alias-Informationen sind zur Laufzeit nicht vorhanden. (Was das reflect Paket betrifft, ist der Alias ​​_derselbe Typ_ wie das Ding, für das es einen Alias ​​hat.)

@bcmills Ich dachte, das könnte der Fall sein ...

Ich kann mir vorstellen, dass statische Analysetools und Editor-Plugins noch im Bild sind, um die Arbeit mit Aliasen zu erleichtern, also ist das in Ordnung.

Am 2. Februar 2017, 17:01 Uhr, schrieb "Nathan Youngman" [email protected] :

Das heißt, %T ist ein praktisches Werkzeug, um Typen beim Debuggen oder Erkunden der
Typensystem ein. Ich frage mich, ob es ein Verb mit ähnlichem Format geben sollte, das
beinhaltet den Alias? qB = []int in @davecheney
https://github.com/davecheneys Beispiel.

Ich denke, eine bessere Lösung ist es, Guru einen Abfragemodus hinzuzufügen, um dies zu beantworten
Frage:

das sind die deklarierten Aliase im gesamten GOPATH (oder einem bestimmten Paket) für
dieser angegebene Typ auf der Befehlszeile?

Ich mache mir keine Sorgen über den Missbrauch von Aliasing unbenannter Typen, aber potenzielle
duplizierte Aliase auf denselben unbenannten Typ.

@davecheney Ich habe Ihren Vorschlag zum Abschnitt "Einschränkungen" der Diskussionszusammenfassung oben hinzugefügt. Wie bei allen Beschränkungen ist unsere allgemeine Position, dass Beschränkungen die Komplexität erhöhen (siehe Anmerkungen oben) und wir wahrscheinlich tatsächliche Beweise für weit verbreiteten Schaden sehen müssten, um eine Beschränkung einzuführen. Es reicht nicht aus, die Art und Weise, wie Sie Go unterrichten, ändern zu müssen: Jede Änderung, die wir an der Sprache vornehmen, erfordert eine Änderung der Art und Weise, wie Sie Go unterrichten.

Wie im Design-Dokument und in der Mailingliste vermerkt, arbeiten wir an einer besseren Terminologie, um die Erklärungen zu erleichtern.

@minux , wie @bcmills betonte, existieren keine Alias-Informationen zur Laufzeit (völlig grundlegend für das Design). Es gibt keine Möglichkeit, ein "%T, das den Alias ​​enthält" zu implementieren.

Am 2. Februar 2017, 20:33 Uhr, schrieb "Russ Cox" [email protected] :

@minux https://github.com/minux , wie @bcmills
https://github.com/bcmills darauf hingewiesen, dass keine Alias-Informationen vorhanden sind
zur Laufzeit (völlig grundlegend für das Design). Es gibt keine Möglichkeit
implementieren Sie ein "%T, das den Alias ​​enthält".

Ich schlage einen Go-Guru (https://golang.org/x/tools/cmd/guru)-Abfragemodus vor
für Reverse-Alias-Mapping, das auf statischer Codeanalyse basiert. Es
Dabei spielt es keine Rolle, ob Alias-Informationen zur Laufzeit verfügbar sind oder nicht.

@minux , oh ich sehe, du antwortest per E-Mail und Github lässt den zitierten Text so aussehen, wie du selbst geschrieben hast. Ich habe auf den Text geantwortet, den Sie von Nathan Youngman zitiert haben, und dachte, es wäre Ihrer. Entschuldigung für die Verwirrung.

In Bezug auf Terminologie und Lehre, fand ich die Markentypen Hintergrund @griesemer geschrieben recht informativ. Dank dafür.

Bei der Erklärung von Typen und Typkonvertierungen denken Babygopher zunächst, dass ich von einem Typalias spreche, wahrscheinlich aufgrund der Vertrautheit mit anderen Sprachen.

Was auch immer die endgültige Terminologie ist, ich könnte mir vorstellen, Typaliase vor benannten (gebrandeten) Typen einzuführen, zumal die Deklaration neuer benannter Typen wahrscheinlich nach der Einführung von byte und rune in jedem Buch oder Lehrplan erfolgen wird. Ich möchte jedoch das Anliegen von @davecheney berücksichtigen , keine Anti-Muster zu fördern.

Für type intSet map[int]struct{} sagen wir, dass map[int]struct{} der _unterliegende_ Typ ist. Wie nennen wir beide Seiten von type intSet = map[int]struct{} ? Alias ​​und Alias-Typ?

Was %T , muss ich bereits erklären, dass ein byte und rune zu einem uint8 und int32 , also ist dies kein unterschiedlich.

Wenn überhaupt, denke ich, dass Typaliase byte und rune einfacher erklären werden. IMO, die Herausforderung besteht darin, zu wissen, wann benannte Typen im Vergleich zu Typaliasen verwendet werden müssen, und dies dann kommunizieren zu können.

@nathany Ich denke, es ist sehr sinnvoll, zuerst "

Die traditionelle (Nicht-Alias-)Typdeklaration leistet mehr Arbeit: Sie erstellt zuerst einen neuen Typ aus dem Typ rechts, bevor sie den Bezeichner links daran bindet. Somit sind der Bezeichner und der Typ auf der rechten Seite nicht gleich (sie haben nur denselben zugrunde liegenden Typ). Dies ist eindeutig das kompliziertere Konzept.

Für diese neu erstellten Typen benötigen wir einen neuen Begriff, da jetzt jeder Typ einen Namen haben kann. Und wir müssen in der Lage sein, auf sie zu verweisen, da es Spezifikationsregeln gibt, die sich auf sie beziehen (Typidentität, Zuweisbarkeit, Empfängerbasistypen).

Hier ist eine andere Möglichkeit, dies zu beschreiben, die in einer Unterrichtsumgebung nützlich sein kann: Ein Typ kann entweder farbig oder ungefärbt sein. Alle vordeklarierten Typen und alle Typliterale sind ungefärbt. Die einzige Möglichkeit, einen neuen farbigen Typ zu erstellen, ist über eine traditionelle (Nicht-Alias-) Typdeklaration, die zuerst den Typ rechts mit einer brandneuen, noch nie zuvor verwendeten Farbe (eine Kopie davon) malt (die alte Farbe entfernt, falls vorhanden, vollständig im Prozess), bevor Sie den Bezeichner auf der linken Seite daran binden. Auch hier sind der Bezeichner und der (implizit und unsichtbar erzeugte) Farbtyp identisch, unterscheiden sich jedoch von dem rechts aufgeschriebenen (andersfarbigen oder ungefärbten) Typ.

Mit dieser Analogie können wir auch verschiedene andere bestehende Regeln umformulieren:

  • Ein farbiger Typ unterscheidet sich immer von jedem anderen Typ (da jede Typdeklaration eine brandneue, noch nie zuvor verwendete Farbe verwendet).
  • Methoden dürfen nur mit farbigen Empfängerbasistypen verknüpft werden.
  • Der zugrunde liegende Typ eines Typs ist dieser Typ, der alle seine Farben entfernt hat.
    usw.

Wir nennen einen konstanten Namen keinen Alias ​​und den konstanten Wert die Alias-Konstante

guter punkt 👍

Ich bin mir nicht sicher, ob die Analogie zwischen farbiger und ungefärbter Farbe leichter zu verstehen ist, aber sie zeigt, dass es mehr als eine Möglichkeit gibt, die Konzepte zu erklären.

Traditionelle benannte/gebrandete/farbige Typen erfordern sicherlich mehr Erklärung. Vor allem, wenn ein benannter Typ mit einem vorhandenen benannten Typ deklariert werden kann. Es gibt ziemlich feine Unterschiede zu beachten.

type intSet map[int]struct{} // a new type with an underlying type map[int]struct{}

type myIntSet intSet // a new type with an underlying type map[int]struct{}

type otherIntSet = intSet // just another name (alias) for intSet, add methods to intSet (only in the same package)

type literalIntSet = map[int]struct{} // just another name for map[int]struct{}, no adding methods

Es ist jedoch nicht unüberwindbar. Unter der Annahme, dass dies in Go 1.9 landet, vermute ich, dass wir die 2. Auflage mehrerer Go-Bücher sehen werden. 😉

Ich verweise regelmäßig auf die Go-Spezifikation für die akzeptierte Terminologie, daher bin ich sehr gespannt, welche Begriffe am Ende ausgewählt werden.

Für diese neu erstellten Typen benötigen wir einen neuen Begriff, da jetzt jeder Typ einen Namen haben kann.

Einige Ideen:

  • "distinguished" oder "distinct" (wie in, von anderen Typen zu unterscheiden)
  • "einzigartig" (wie in, es ist ein Typ, der sich von allen anderen Typen unterscheidet)
  • "konkret" (wie in, es ist eine Entität, die in der Laufzeit existiert)
  • "identifizierbar" (wie in, der Typ hat eine Identität)

@bcmills Wir haben über verschiedene, einzigartige, eindeutige, Marken-,

Ich würde dagegen empfehlen:

  • "farbig" (in nichtprogrammierenden Kontexten trägt der Ausdruck "farbige Typen" starke rassistische Konnotationen)
  • "non-alias" (es ist verwirrend, da das Ziel des Alias ​​ein früher als "benannter Typ" bezeichneter Typ sein kann oder auch nicht)
  • "definiert" (Aliasse sind auch definiert, sie sind nur als Aliase definiert)

"gebrandmarkt" könnte funktionieren: es trägt eine "Typen als Vieh"-Konnotation, aber das erscheint mir nicht an sich schlecht.

Einzigartig und unverwechselbar scheinen bisher die herausragenden Optionen zu sein.

Sie sind einfach und ohne viel zusätzlichen Kontext oder Wissen verständlich. Wenn ich den Unterschied nicht wüsste, hätte ich, denke ich, zumindest ein allgemeines Gespür dafür, was sie bedeuten. Zu den anderen Möglichkeiten kann ich das nicht sagen.

Sobald Sie den Begriff gelernt haben, spielt es keine Rolle, aber ein konnotativer Name vermeidet unnötige Hindernisse für die Verinnerlichung der Unterscheidung.

Dies ist die Definition eines Fahrradschuppen-Arguments. Robert hat eine ausstehende CL unter https://go-review.googlesource.com/#/c/36213/ , die völlig in Ordnung zu sein scheint.

CL https://golang.org/cl/36213 erwähnt dieses Problem.

Ich möchte das Thema go fix einmal ansprechen.

Um es klar zu sagen, ich schlage nicht vor, den Alias ​​​​zu entfernen. Vielleicht ist es etwas Nützliches und Passendes für andere Jobs, das ist eine andere Geschichte.

Es ist IMO etwas sehr Wichtiges, dass es im Titel um bewegte Schrift geht. Ich möchte das Thema nicht verwirren. Unser Ziel ist es, mit einer Art Schnittstellenänderung in einem Projekt umzugehen. Wenn wir zu einer Änderung der Schnittstelle kommen, ist es nicht wahr, dass wir hoffen, dass alle Benutzer diese beiden Schnittstellen (alt und neu) irgendwann als gleich verwenden, und deshalb sagen wir "graduelle Codereparatur". Wir hoffen, dass Benutzer die Verwendung des alten entfernen/ändern.

Ich betrachte das Tool immer noch als die beste Methode, um den Code zu reparieren, so ähnlich wie die Idee, die @tux21b vorgeschlagen hat. Zum Beispiel:

$ cat "$GOROOT"/RENAME
# This file could be used for `go fix`
[package]
x/net/context=context
[type]
io.ByteBuffer=bytes.Buffer

$ go fix -rename "$GOROOT"/RENAME [packages]
# -- or --
# use a standard libraries rename table as default
$ go fix -rename [packages]
# -- or --
# include this fix as default
$ go fix [packages]

Der einzige Grund, warum @rsc hier Aber ich denke, es stimmt in diesem Arbeitsablauf nicht : Wenn ein veraltetes Paket (z. B. eine Abhängigkeit) den veralteten Namen/Pfad des Pakets verwendet, z. B. x/net/context , können wir den Code zuerst reparieren , genau wie das Dokument sagt, wie man Code über eine konfigurierbare Tabelle im Textformat auf eine neue Version migriert, aber nicht hartcodiert. Dann können Sie alle Tools verwenden, wann immer Sie möchten, genauso wie Go der neuen Version. Es gibt einen Nebeneffekt: Es wird Code ändern.

@LionNatsu , ich denke, Sie haben Recht, aber ich denke, das ist ein separates Thema:

CL https://golang.org/cl/36691 erwähnt dieses Problem.

Mit diesem Vorschlag bei tip kann ich nun dieses Paket erstellen:

package safe

import "unsafe"

type Pointer = unsafe.Pointer

wodurch Programme unsafe.Pointer Werte erstellen können, ohne unsafe direkt zu importieren:

package main

import "safe"

func main() {
    x := []int{4, 9}
    y := *(*int)(safe.Pointer(uintptr(safe.Pointer(&x[0])) + 8))
    println(y)
}

Das ursprüngliche Designdokument für Alias-Deklarationen weist darauf hin, dass dies ausdrücklich unterstützt wird. Es ist in diesem neueren Typaliasvorschlag nicht explizit, aber es funktioniert.

Bei der Alias-Deklaration lautet die Begründung dafür: _"Der Grund, warum wir Aliasing für unsafe.Pointer zulassen, ist, dass es bereits möglich ist, einen Typ mit unsafe.Pointer als zugrunde liegenden Typ zu definieren."_ https://github.com/ golang/go/issues/16339#issuecomment -232435361

Das stimmt zwar, aber ich denke, dass das Zulassen eines Alias ​​​​von unsafe.Pointer etwas Neues einführt: Programme können jetzt unsafe.Pointer Werte erstellen, ohne explizit unsichere Werte zu importieren.

Um das obige Programm vor diesem Vorschlag zu schreiben, müsste ich den safe.Pointer-Cast in ein Paket verschieben, das unsicher importiert. Dies kann es etwas schwieriger machen, Programme auf ihre Verwendung unsicherer zu überprüfen.

@crawshaw , hättest du das nicht schon früher machen können?

package safe

import (
  "reflect"
  "unsafe"
)

func Pointer(p interface {}) unsafe.Pointer {
  switch v := reflect.ValueOf(p); v.Kind() {
  case reflect.Uintptr:
    return unsafe.Pointer(uintptr(v.Uint()))
  default:
    return unsafe.Pointer(v.Pointer())
  }
}

Ich glaube, das würde genau das gleiche Programm kompilieren lassen, mit dem gleichen Mangel an Import im Paket main .

(Es wäre nicht unbedingt ein gültiges Programm: Die uintptr -zu- Pointer Konvertierung enthält einen Funktionsaufruf, daher erfüllt sie nicht die unsafe Paketbeschränkung, die " beide Konvertierungen müssen in demselben Ausdruck erscheinen, mit nur der dazwischenliegenden Arithmetik". Ich vermute jedoch, dass es möglich wäre, ein äquivalentes, gültiges Programm zu konstruieren, ohne unsafe aus main zu importieren, indem Verwendung von Dingen wie reflect.SliceHeader .)

Scheint, als ob das Exportieren eines versteckten unsicheren Typs nur eine weitere Regel ist, die dem Audit hinzugefügt werden muss.

Ja, ich wollte darauf hinweisen, dass das direkte Aliasing von "unsafe.Pointer" das Auditieren von Code erschwert, so dass ich hoffe, dass niemand dies tut.

@crawshaw Laut meinem Kommentar war dies auch vor dem Typ-Aliasing der Fall. Es gilt:

package a

import "unsafe"

type P unsafe.Pointer
package main

import "./a"
import "fmt"

var x uint64 = 0xfedcba9876543210
var h = *(*uint32)(a.P(uintptr(a.P(&x)) + 4))

func main() {
    fmt.Printf("%x\n", h)
}

Das heißt, im Paket main kann ich unsichere Arithmetik mit a.P ausführen, obwohl es kein unsafe Paket gibt und a.P kein Alias ​​ist. Dies war immer möglich.

Gibt es noch etwas, auf das Sie sich beziehen?

Mein Fehler. Ich dachte, das geht nicht. (Ich hatte den Eindruck, dass die Sonderregeln für unsafe.Pointer nicht auf neue, daraus definierte Typen übertragen würden.)

Die Spezifikation ist diesbezüglich nicht eindeutig. Wenn man sich die Implementierung von go/types ansieht, stellt sich heraus, dass meine anfängliche Implementierung genau unsafe.Pointer erforderte, nicht nur einen Typ, der zufällig einen zugrunde liegenden Typ von unsafe.Pointer . Ich habe gerade #6326 gefunden, als ich go/types geändert habe, um gc-konform zu sein.

Vielleicht sollten wir dies für reguläre Typdefinitionen verbieten und auch Aliase von unsafe.Pointer verbieten. Ich kann keinen guten Grund sehen, dies zuzulassen, und es beeinträchtigt die Offenheit, unsafe für unsicheren Code importieren zu müssen.

Das ist passiert. Ich glaube, hier ist nichts übrig geblieben.

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen

Verwandte Themen

OneOfOne picture OneOfOne  ·  3Kommentare

bradfitz picture bradfitz  ·  3Kommentare

natefinch picture natefinch  ·  3Kommentare

Miserlou picture Miserlou  ·  3Kommentare

ajstarks picture ajstarks  ·  3Kommentare