Go Home Page
Die Programmiersprache Go

Frequently Asked Questions (FAQ) — Deutsche Übersetzung

Das Original:
https://golang.org/doc/go_faq.html
Version of June 17, 2017 (go1.9)
Diese Übersetzung:
http://www.bitloeffel.de/DOC/golang/go_faq_20170825_de.html
Stand: 27.06.2017
© 2014-17 Hans-Werner Heinzen @ Bitloeffel.de
Die Nutzung dieses Werks ist unter den Bedingungen der "Creative Commons Attribution 3.0"-Lizenz erlaubt.
Für Fachbegriffe und ähnliches gibt es hier noch eine Wörterliste.

Oft gestellte Fragen (FAQ)

Ursprünge

Warum so ein Projekt?

Seit mehr als zehn Jahren sind für Systementwicklung keine Sprachen von Belang mehr entstanden. Im selben Zeitraum jedoch hat sich die Rechnerlandschaft gewaltig verändert. Folgende Trends sind zu erkennen:

Wir meinen, es lohnt sich, es mit einer neuen Sprache zu versuchen, einer mit Nebenläufigkeit und mit Automatischer Speicherbereinigung und mit schneller Kompilierung. Was die oben aufgeführten Punkte angeht:

Eine sehr viel ausführlichere Antwort auf diese Frage gibt es in Form eines Aufsatzes: "Go at Google: Language Design in the Service of Software Engineering".

Wo steht das Projekt heute?

Go ging am 10. November 2009 als "Open Source"-Projekt an die Öffentlichkeit. Dann, nach mehreren Jahren sehr intensiver Gestaltung und Entwicklung, war schließlich Stabilität gefragt und Go 1 wurde am 28. März 2012 ausgeliefert. Mit einer Sprachbeschreibung (de), einer Standardbibliothek und einem eigenen Werkzeugkasten bietet Go 1 ein stabiles Fundament zum Erstellen verlässlicher Produkte, Projekte und Veröffentlichungen.

Mit dieser Stabilität im Rücken arbeiten wir nun weniger an Sprach- oder Bibliotheksänderungen, sondern benutzen Go, um Programme, Produkte und Werkzeuge zu entwickeln. Ziel von Go 1 ist tatsächlich, eine langfristige Stabilität zu gewährleisten. Rückwärts-unverträgliche Änderungen wird es in keiner Go-1-Punkt-Auslieferung geben. Wir wollen nutzen was wir haben, um herauszufinden, wie eine zukünftige Version von Go aussehen könnte. Und zwar ohne dabei mit der Sprache zu spielen.

Aber klar, auch Go selbst wird weiter entwickelt, aber mit dem Fokus auf Performanz, Zuverlässigkeit, Übertragbarkeit und zusätzlicher Funktionalität, wie zum Beispiel besserer Unterstützung von Internationalisierung.

Eines Tages mag es ein Go 2 geben, aber frühestens in ein paar Jahren, und das, was wir heute mit Go 1 lernen, wird Einfluss darauf haben.

Woher stammt das Maskottchen?

Maskottchen und Logo wurden gestaltet von Renée French, von der auch Glenda, das Häschen von Plan 9 stammt. Der Ziesel (gopher) ist abgeleitet von einem anderen, den sie für ein WFMU-T-Shirt gestaltet hatte. Logo und Maskottchen sind geschützt durch eine "Creative Commons Attribution 3.0"-Lizenz.

Wie ist das Projekt entstanden?

Am 21. September 2007 begannen Robert Griesemer, Rob Pike und Ken Thompson, die Ziele für eine neue Programmiersprache auf einer Tafel zu skizzieren. Innerhalb weniger Tage hatten sich diese Ziele zu dem Plan verfestigt, etwas zu tun, und in eine recht gute Vorstellung davon, was das sein könnte. Die weitere Planung geschah in Teilzeit parallel zu anderer Arbeit. Januar 2008 kam, und Ken Thompson hatte mit der Arbeit an einem Compiler begonnen, um damit Ideen zu erforschen; dieser Compiler erzeugte C-Kode. Bis Mitte des Jahres war die Arbeit an der Sprache zum Vollzeitprojekt geworden. Und die Zeit war reif, es mit einem produktiven Compiler zu versuchen. Unabhängig davon hatte im Mai 2008 Ian Taylor auf der Grundlage der Spezifikationsskizze mit einem GCC-Frontend für Go begonnen. Ende 2008 kam Russ Cox dazu, und half Sprache und Bibliotheken vom Prototypstadium auf den Weg in die Wirklichkeit.

Go wurde am 10.November 2009 zum "Open Source"-Projekt. Seitdem haben viele aus der Go-Gemeinde Ideen, Diskussionen und Kode beigesteuert.

Warum eine neue Sprache?

Go entstand aus Frust über die existierenden Sprachen und Umgebungen zur Systementwicklung. Programmieren war zu schwierig geworden, und das lag zu Teil an der Wahl der Sprachen. Man konnte nur zwischen effizientem Kompilieren, effizienter Ausführung oder einfachem Entwickeln wählen; alle drei zusammen waren nicht bei einer der üblichen Sprachen anzutreffen. Wer konnte, zog einfaches Entwickeln der Sicherheit und Effizienz zur Laufzeit vor, und wanderte von C++ ab in Richtung der Sprachen mit dynamischen Typen, wie Python oder Javascript, in geringerem Maß in Richtung Java.

Die Sprache Go ist der Versuch, die Bequemlichkeit einer interpretierten Sprache mit dynamischen Typen zu verbinden mit der Effizienz und Sicherheit einer kompilierten Sprache mit statischen Typen. Sie soll modern sein, also mit Unterstützung für Netzwerk- und Mehrkernrechnen. Schließlich soll das Arbeiten mit Go schnell sein: es sollte höchstens ein paar Sekunden brauchen, um ein großes Laufzeitmodul auf einem einzelnen Rechner zu erzeugen. Um alle diese Ziele zu erreichen, mussten eine Reihe linguistischer Aufgaben bewältigt werden: ein ausdrucksstarkes aber leichtgewichtiges Typsystem, Nebenläufigkeit und Automatische Speicherbereinigung, ein rigides Abhängigkeitsmodell und so weiter... Das konnte durch Bibliotheken nur schwer erreicht werden, also war eine neue Sprache gefragt.

Der Artikel "Go at Google" erörtert Hintergrund und Motivation hinter der Architektur von Go, und liefert viele Details zu anderen Antworten dieser Fragensammlung.

Welche Vorfahren hat Go?

Go gehört hauptsächlich der C-Familie an (grundlegende Syntax), aber mit deutlichem Einfluss der Pascal-Modula-Oberon-Familie (Deklarationen, Pakete), plus einiger Ideen aus Sprachen, die von Tony Hoares CSP inspiriert wurden; das sind Newsqueak und Limbo (Nebenläufigkeit). Wie auch immer, es ist eine rundum neue Sprache. Jeder Aspekt der Sprache wurde konzipiert mit dem Gedanken an die Programmierer, was diese tun, und wie wir das Programmieren — zumindest unsere Art des Programmierens — effektiver machen können; wir wollen wieder mehr Spaß haben.

Welches sind die Gestaltungsleitlinien?

Programmierung heute bedeutet viel zu viel Buchhaltung, Wiederholung und rituelles Arbeiten. Wie Dick Gabriel sagte: "Alte Programme lesen sich wie eine ruhige Unterhaltung zwischen einem gepflegt sprechenden Forscher und einem erfahrenen mechanischen Kollegen, und nicht wie eine Auseinandersetzung mit dem Compiler. Wer hätte gedacht, dass Raffinesse mit soviel Lärm erkauft wird?" Die Raffinesse ist erstrebenswert — niemand will zurück zu den alten Sprachen — aber geht das nicht leiser?

Go versucht den Aufwand fürs Tippen ebenso zu verringern wie den fürs Typisieren. Während der Designphase haben wir immer wieder versucht, Wirrwarr und Komplexität klein zu halten. Es gibt keine Vorwärtsdeklarationen und keine Header-Dateien; alles wird genau einmal deklariert. Vorbelegungen sind aussagekräftig, automatisch und leicht zu benutzen. Die Syntax ist sauber mit wenigen Schlüsselwörtern. Gestottere (wie: foo.Foo* myFoo = new(foo.Foo)) wird verringert durch die einfache Typableitung mithilfe des Deklarations- und Zuweisungskonstrukts :=. Und die vielleicht radikalste Entscheidung: es gibt keine Typenhierarchie; Typen sind einfach, sie müssen ihre Beziehungen nicht bekannt geben. All diese Vereinfachungen erlauben Go, sowohl ausdrucksstark als auch verständlich zu bleiben, ohne dabei Raffinesse zu opfern.

Ein wichtiges Prinzip lautet: Halte die Konzepte orthogonal zueinander. Methoden können für jeden Typ implementiert werden; Strukturen stehen (nur) für Daten, Interfaces (nur) für Abstraktionen; und so weiter. Mit Orthogonalität ist leichter zu verstehen was geschieht, wenn Dinge kombiniert werden.

Anwendung

Wird Go bei Google intern genutzt?

Ja. Inzwischen wurden eine ganze Reihe von Go-Programmen bei Google in Produktion übergeben. Ein offensichtliches Beispiel ist der Serverprozess hinter golang.org. Er ist nicht anderes als der godoc-Dokumentationsserver, der konfiguriert für die Produktion auf der Google App Engine läuft.

Weitere Beispiele wären das Vitess-System für große SQL-Installationen, oder Googles Download-Server dl.google.com, der Binärdateien für Chrome oder andere große Dateien wie z.B. apt-get-Pakete ausliefert.

Es gibt zwei Implementierungen des Go-Compilers, gc und gccgo. Gc hat eine eigene Aufrufnorm und einen eigenen Linker und kann deshalb nur mit C-Programmen gebunden werden, die denselben Regeln folgen. Es gibt einen solchen C-Compiler, aber keinen für C++. Gccgo ist ein "Front-End" für GCC, das mit etwas Mühe Go-Programme mit GCC-kompilierten C- und C++-Programmen binden kann.

Das Kommando cgo stellt eine "Fremdfunktionsschnittestelle" zur Verfügung, die sicheres Rufen aus Go heraus in C-Bibliotheken ermöglicht. SWIG dehnt diese Fähigkeit auf C++-Bibliotheken aus.

Unterstützt Go Googles Protocol-Buffers?

Die nötige Compiler-Erweiterung und Bibliothek wird durch ein separates "Open Source"-Projekt bereitgestellt. Es steht unter github.com/golang/protobuf/ zur Verfügung.

Darf ich die Go-Internetseiten in eine andere Sprache übersetzen?

Aber sicher doch. Wir ermutigen Entwickler dazu, Go-Internetseiten in ihrer Muttersprache zu erstellen. Allerdings, wenn Sie die Absicht haben, das Google-Logo zu verwenden (auf golang.org kommt es nicht vor), so müssen Sie sich an die Regeln in www.google.com/permissions/ halten.

Design

Hat Go eine Laufzeitumgebung?

Go hat eine umfangreiche Bibliothek namens runtime, die Bestandeil jedes Go-Programms ist. Die Laufzeitumgebung implementiert Müllabfuhr (garbage collection), Nebenläufigkeit (concurrency), Verwaltung der Kellerspeicher (stacks) und weitere wichtige Fähigkeiten von Go. Go's Laufzeitumgebung entspricht der C-Bibliothek libc, ist aber näher an der Sprache dran.

Wichtig fürs Verständnis ist allerdings, dass Go's Laufzeitumgebung keine virtuelle Maschine enthält wie etwa die Laufzeitumgebung von Java. Go-Programme werden zu Maschinenkode kompiliert. Obwohl der Begriff oft benutzt wird, um eine virtuelle Umgebung zu beschreiben, so ist in Go das Wort "runtime" nur ein Name für die Bibliothek, die die wichtigen Sprachdienste bereitstellt.

Was hat es mit den Unicode-Bezeichnern auf sich?

Es war wichtig, den Raum für Bezeichner aus der Beschränkung auf ASCII zu befreien. Die Regel in Go lautet: Bezeichner bestehen aus Zeichen, die in Unicode als Buchstaben und Ziffern definiert sind. Das ist einfach zu verstehen und einfach zu implementieren, aber auch hier gibt es Grenzen. Zum Beispiel werden zusammengesetzte Zeichen bewusst ausgeschlossen. Solange es keinen von Go unabhängigen Konsens darüber gibt, was ein Bezeichner ist, und keine Normalisierung von Bezeichnern, welche Eindeutigkeit garantiert, solange bleiben zusammengesetzte Zeichen besser außen vor. Dadurch haben wir eine einfache Regel, die später einmal erweitert werden kann, ohne dass Programme ungültig werden, eine Regel, die Fehlern vorbeugt, die mit Sicherheit aufträten, würden uneindeutige Bezeichner erlaubt.

Ähnlich ist es mit den Buchstaben in einigen Sprachen. Weil exportierte Bezeichner mit einem Großbuchstaben beginnen müssen, können Bezeichner aus Buchstaben bestimmter Sprachen nicht exportiert werden. Bis auf weiteres ist die einzige Lösung etwas in der Art von X日本語. Das ist natürlich unschön — wir denken auch über Alternativen nach. Es ist aber unwahrscheinlich, dass wir die Großbuchstabenregel nochmal ändern; sie gehört für uns zu Go's wertvollsten Eigenschaften.

Warum hat Go nicht die Fähigkeit X?

Jede Sprache bringt neue Eigenheiten mit sich, lässt hingegen jemandes Lieblingseigenschaft aus. Go wurde entworfen mit Blick auf Freude am Programmieren, schnelles Kompilieren, orthogonale Konzepte sowie dem Bedürfnis, neue Fähigkeiten wie Nebenläufigkeit und Müllabfuhr zu unterstützen. Sie mögen die Ihnen liebste Fähigkeit vermissen, weil sie nicht hineinpasst, weil sie das Kompilieren behindern würde, weil sie das klare Design unklarer machen würde, oder weil sie das zugrunde liegende Modell verkomplizieren würde.

Ärgern Sie sich nicht, weil Go kein X hat. Verzeihen Sie uns und erforschen lieber die Fähigkeiten, die Go hat. Sie könnten entdecken, dass diese das fehlende X auf interessante Weise kompensieren.

Warum hat Go keine generischen Typen?

Generische Typen kommen vielleicht irgendwann. Uns ist es nicht eilig damit, auch wenn wir verstehen, dass manche Programmierer das anders sehen.

Generische Typen sind praktisch, doch sie sind auch "teuer" wegen der Komplexität des Typ- und des Laufzeitsystems. Wir denken weiter darüber nach, doch bisher sind wir noch auf kein Konzept gestoßen, dessen Mehrwert die Komplexität aufwiegen würde. Bis dahin gibt es Go's eingebauten Maps und Slices, sowie die Möglichkeit, mit leeren Interfaces Container zu bauen (mit explizitem "Unboxing"). Das heißt, dass in vielen Fällen Kode möglich ist, der tut, was generische Typen tun würden, wenn auch vielleicht etwas umständlicher.

Dieser Punkt bleibt offen. Mehrere erfolglose Versuche, eine zu Go passende Generik zu entwerfen, sind hier beschrieben.

Warum hat Go keine "Exceptions"?

Wir glauben, dass das Koppeln von Ausnahmebedingungen an Kontrollanweisungen, wie dem try-catch-finally-Idiom, zu Kode-Wirrwarr führt. Programmierer werden außerdem ermuntert, viel zu viele der gewöhnlichen Fehler, z.B. das Scheitern beim Dateiöffnen, als Ausnahmebedingung zu behandeln.

Go geht die Sache anders an. Gewöhnliche Fehler werden durch Go's Mehrfachrückgabewerte ganz einfach gemeldet, ohne dass ein Rückgabewert überladen werden müsste. Ein standardisierter Fehlertyp zusammen mit weiteren Eigenschaften von Go ergibt eine sehr angenehme Art der Fehlerbehandlung, nur anders als in anderen Sprachen.

Darüber hinaus gibt es in Go eine Reihe eingebauter Funktionen, zum Signalisieren der echten Ausnahmebedingungen und zum Wiederherstellen danach. Der Wiederherstellmechanismus wird nur durchlaufen, wenn eine Funktion nach einem Fehler sich in ungültigem Zustand befindet; das genügt, um Katastrophen zu managen, und führt im besten Fall zu sauberem Fehlerhandhabungs-Kode.

Man lese "Defer, Panic, and Recover" für weitere Einzelheiten.

Warum hat Go keine Zusicherungen?

Go kennt keine Zusicherungen (assertions). Die wären zweifellos praktisch, doch Erfahrung sagt uns, dass Programmierer sie gewöhnlich als Krücke benutzen, damit sie nicht weiter über das Handhaben und Melden der Fehler nachdenken müssen. Sachgemäße Fehlerbehandlung bedeutet, dass Server statt abzustürzen weiterarbeiten, solange ein Fehler nicht zerstörerisch ist. Sachgemäßes Fehlermelden bedeutet, dass die Fehlermeldung klar und treffend ist, was dem Programmierer erspart, den länglichen "Trace" eines Absturzes zu interpretieren. Präzise Fehlermeldungen sind besonders wichtig, wenn sie für Programmierer bestimmt sind, die mit dem Kode nicht vertraut sind.

Wir wissen, dass dieses Thema umstritten ist. In Go, in der Sprache wie in der Bibliothek, gibt es vieles, was von der heutigen Praxis abweicht; wir meinen nämlich, dass sich manchmal der Versuch lohnt, etwas anderes zu tun.

Warum wird Nebenläufigkeit auf die Ideen von CSP aufgebaut?

Nebenläufigkeit und Mehrstrang-Programmierung gelten als schwierig. Wir glauben, dass das teilweise am komplizierten Aufbau liegt, wie bei "pthreads", und teilweise an zu starker Betonung kleinteiliger Details, wie Zugriffssperren (mutexes), Statusvariablen und Speichergrenzen. Höher abstrahierte Schnittstellen ermöglichen viel einfacheren Kode, auch wenn unter der Haube dann doch nur Zugriffssperren arbeiten.

Eins der erfolgreichsten Modelle mit abstrakter Sprachunterstützung für Nebenläufigkeit stammt von den "Communicating Sequential Processes", kurz CSP, von Hoare. Occam und Erlang sind zwei bekannte Sprachen, die von CSP abstammen. Go's Primitive für Nebenläufigkeit entstammen einem anderen Zweig dieses Stammbaums, dessen wichtigster Beitrag die kraftvolle Idee von Kanälen als erstrangigen Objekten ist. Erfahrungen mit früheren Sprachen haben gezeigt, dass das CSP-Modell gut in eine prozedurale Sprache hineinpasst.

Warum Goroutinen und nicht "Threads"?

Nebenläufigkeit soll einfach zu benutzen sein, Goroutinen gehören dazu. Der (nicht ganz neue) Gedanke ist, unabhängig arbeitende Funktionen/Koroutinen auf eine Menge von "Threads" zu verteilen. Sobald eine Koroutine blockiert, z.B. durch den Ruf einer blockierenden Systemfunktion, verschiebt das Laufzeitsystem [englisch: run-time, A.d.Ü.] die anderen Koroutinen desselben Verarbeitungsstrangs zu einem anderen, lauffähigen Strang, so dass sie nicht mit blockiert werden. Der Programmierer merkt nichts davon; so soll es sein. Das Ergebnis dieser Überlegung — wir nennen sie Goroutine — kann sehr "billig" sein: es gibt kaum Aufwand außer dem Kellerspeicher (stack), und der braucht nur wenige Kilobyte.

Um Stapel klein zu halten, benutzt das Go-Laufzeitsystem größenveränderliche, begrenzte Stapel. Jede neue Ausprägung einer Goroutine erhält ein paar Kilobyte, und die genügen fast immer. Wenn nicht, so vergrößert (und verkleinert) das Laufzeitsystem den Stapelspeicher automatisch, so dass viele Goroutinen in einem annehmbar großen Speicher Platz finden. Der CPU-Aufwand liegt im Schnitt bei drei "billigen" Anweisungen pro Funktionsaufruf. Hunderttausende Goroutinen in einem Adressbereich sind machbar; wären Goroutinen "Threads", wären die Systemressource weit früher erschöpft.

Warum sind Map-Operationen nicht unteilbar (atomic)?

Nach langer Diskussion wurde entschieden, dass für die typische Anwendung von Maps der sichere Zugriff von mehreren Goroutinen aus nicht nötig ist, und dass in Fällen, in denen er doch nötig ist, die Map wahrscheinlich Teil einer größeren Struktur ist, die bereits anderweitig synchronisiert wird. Jedwede Map-Operation automatisch mit einer Zugriffssperre abzusichern, würde die meisten Programme verlangsamen und nur wenigen zusätzliche Sicherheit bieten. Die Entscheidung fiel uns nicht leicht, weil damit unkontrollierter Zugriff auf Maps zu Programmabbrüchen führen kann.

Die Sprache verhindert atomisches Schreiben in Maps nicht. Wenn nötig, etwa wenn vertrauensunwürdige Programme laufen sollen, kann die Implementierung den Zugriff auf Maps sperren. [?, A.d.Ü.]

Wird mein Änderungsvorschlag berücksichtigt?

Oft werden Verbesserungen für die Sprache vorgeschlagen — die golang-nuts-Verteilerliste enthält eine reiche Geschichte solcher Diskussionen — doch nur wenige dieser Änderungen sind akzeptiert worden.

Go ist ein quelloffenes Projekt, aber Sprache und Standardbibliothek sind geschützt durch eine Kompatibilitätsgarantie, welche verhindert, dass existierende Programme ungültig werden. Wenn Ihr Vorschlag die Go-1-Spezifikation verletzt, so können wir sie nicht mal in Erwägung ziehen, ungeachtet aller möglichen Vorzüge. Eine zukünftige Hauptauslieferung von Go mag mit Go 1 inkompatibel sein; wir sind aber noch nicht zu einer Diskussion darüber bereit.

Selbst wenn Ihr Vorschlag mit der Go-1-Spezifikation kompatibel ist, so ist sie es vielleicht nicht mit dem Geist von Go's Design-Zielen. Der Aufsatz " Go at Google: Language Design in the Service of Software Engineering" erklärt Go's Wurzeln und die Motivation hinter seinem Design.

Typen

Ist Go eine objektorientierte Sprache?

Ja und Nein. Go hat Typen und Methoden und erlaubt damit einen objektorientierten Programmierstil, aber es gibt keine Typhierarchie. Das Konzept "Interface" in Go ermöglicht eine etwas andere Herangehensweise; wir meinen, diese ist einfach zu benutzen und in gewisser Hinsicht auch allgemeiner. Zudem können Typen in anderen Typen eingebettet werden, was etwas bietet, das analog aber nicht identisch zu Unterklassen ist. Außerdem sind Methoden in Go allgemeiner als in C++ oder Java: Sie können für jede Art Daten definiert werden, sogar für Standardtypen wie gewöhnliche Ganzzahlen; also nicht nicht nur für Strukturen.

Aber der Typ für die Methode muss lokal definiert sein, sonst meckert der Compiler "cannot define new methods on non-local type int"; mit einem lokalen Typ type Int int funktioniert's dann. A.d.Ü.

Das Fehlen einer Typhierarchie lässt "Objekte" in Go viel leichtgewichtiger erscheinen als in den Sprachen C++ und Java.

Wie erreiche ich einen dynamischen Aufruf von Methoden?

Der einzige Weg dynamisch aufzurufen führt über ein Interface. Methoden auf ein Struktur oder einen anderen konkreten Typ werden immer statisch aufgelöst.

Warum gibt es keine Vererbung?

Objektorientierte Programmierung, zumindest in den bekanntesten Sprachen, bringen viel zu viel Diskussion um die Beziehungen zwischen Typen mit sich, Beziehungen die oft automatisch abgeleitet werden könnten. Go geht die Sache anders an.

Anstatt vom Programmierer zu verlangen, im Vorhinein zu deklarieren, dass zwei Typen verwandt sind, genügt in Go jeder Typ automatisch jedem Interface, das eine Untermenge seiner Methoden fordert. Abgesehen von weniger "Buchhaltung" hat dieser Ansatz echte Vorteile. Typen können vielen Interfaces gleichzeitig genügen, und zwar ohne die Komplexität traditioneller Mehrfachvererbung. Interfaces können sehr leichtgewichtig sein, ohne den ursprünglichen Typ anfassen zu müssen: ein Interface mit nur einer oder sogar ohne Methode kann ein sehr nützliches Werkzeug sein; Interfaces können im Nachhinein hinzugefügt werden, wenn eine neue Idee auftaucht oder auch nur fürs Testen. Und zwar deshalb weil es keine explizite Verbindungen gibt zwischen Typ und Interface; es ist keine Typhierarchie zu managen.

Auf Grundlage dieser Idee ist es möglich, so etwas wie typsichere Unix-Pipelines zu konstruieren. Schauen Sie sich zum Beispiel an, wie fmt.Fprintf formatiertes Drucken zu einer beliebigen Ausgabe ermöglicht, nicht nur in eine Datei. Oder wie das Paket bufio komplett unabhängig von Datei-Ein-/Ausgabe sein kann. Oder wie das Paket image komprimierte Bilddateien erzeugt. All dies hängt an nur einem Interface (io.Writer), welches für nur eine Methode (Write) steht. Und wir kratzen hier nur an der Oberfläche. Interfaces in Go haben einen starken Einfluss darauf, wie Programme strukturiert werden.

Es braucht etwas Gewöhnung, doch ist diese implizite Typabhängigkeit eine der produktivsten Eigenschaften in Go.

Warum ist len eine Funktion und keine Methode?

Wir haben darüber diskutiert und dann entschieden: len & Co. als Funktionen zu implementieren, ist für die Praxis gut genug und vermeidet kompliziertere Fragen zu Interfaces von Basistypen.

Warum unterstützt Go nicht das Überladen von Methoden und Operatoren?

Methoden sind leichter zu handhaben, wenn nicht auch noch Typen abgeglichen werden müssen. Aus Experimenten mit anderen Sprachen wissen wir, dass es manchmal nützlich sein kann, mehrere Methoden mit demselben Namen aber verschiedenen Signaturen zu erlauben, dass es aber in der Praxis auch verwirrend und fehleranfällig sein kann. Durch Beschränken des Abgleichs auf den Namen plus Konsistenzprüfung der Typen wurde Go's Typsystem stark vereinfacht.

Was Überladen von Operatoren angeht, so scheint das eher eine Frage der Bequemlichkeit denn einer Notwendigkeit zu sein. Auch hier geht's leichter ohne.

Warum gibt es keine "implements"-Deklaration?

In Go genügt ein Typ einem Inteface, indem er die Methoden dieses Interfaces implementiert; mehr braucht's nicht. Diese Eigenschaft erlaubt es, Interfaces zu definieren und zu benutzen, ohne existierenden Kode verändern zu müssen. Sie ermöglicht eine Art struktureller Typisierung, die eine Trennung nach Problembereichen fördert, die Wiederverwendung von Kode verbessert, und es ganz allgemein einfacher macht, Programmiermuster (patterns) aufzubauen, die sich erst herausbilden während der Kode sich entwickelt. Die Semantik der Interfaces trägt wesentlich zu dem wendigen und leichtgewichtigen Eindruck bei, den Go hinterlässt.

Für weitere Details siehe Antwort zu der Frage zu Vererbung.

Wie kann ich sicherstellen, dass mein Typ einem Interface genügt?

Bitten Sie doch den Compiler, zu prüfen, ob der Typ T dem Interface I genügt, indem Sie ihm eine Zuweisung mit dem Nullwert von T (oder Zeiger auf T) vorspielen:

type T struct{}
var _ I = T{}       // Verifizieren, dass I durch T implementiert wird.
var _ I = (*T)(nil) // Verifizieren, dass I durch *T implementiert wird.

Wenn T (bzw. *T) nicht I implementiert, dann gibt's einen Umwandlungsfehler.

Wenn Sie die Nutzer eines Interfaces zwingen wollen, explizit zu erklären, dass sie es implementieren, können Sie der Methodenmenge des Interfaces eine Methode mit sprechendem Namen hinzufügen. Zum Beispiel:

type Macher interface {
    Mach()
    ImplementsMacher()
}

Also muss jeder Typ, um ein Macher zu sein, die Methode ImplementsMacher implementieren, wodurch diese Tatsache deutlich dokumentiert und in der Ausgabe von godoc verkündet wird.

type Dings struct{}
func (d Dings) ImplementsMacher() {}
func (d Dings) Mach() {}

Üblich sind solche Restriktionen nicht, weil sie die Nützlichkeit der Interface-Idee einschränken; aber manchmal nötig, um Mehrdeutigkeiten zwischen ähnlichen Interfaces aufzulösen.

Warum genügt T nicht dem Equal-Interface?

Gegeben sei dieses einfache Interface, das für ein Objekt steht, welches sich selbst mit einem anderen Wert vergleichen kann:

type Equaler interface {
    Equal(Equaler) bool
}

Gegeben sei weiter dieser Typ T:

type T int
func (t T) Equal(u T) bool { return t == u } // genügt nicht dem Equaler

Anders als in analogen Situationen in einigen polymorphen Typsystemen implementiert T hier nicht den Equaler; der Argumenttyp von T.Equal ist nämlich T und nicht der geforderte Typ Equaler.

Das Typsystem in Go befördert das Argument von Equal nicht. Dafür ist der Programmierer verantwortlich, wie dieses Beispiel eines Typs T2 zeigt, welcher Equaler tatsächlich implementiert:

type T2 int
func (t T2) Equal(u Equaler) bool { return t == u.(T2) }  // genügt dem Equaler

Selbst das ist anders als in anderen Typsystemen, weil in Go jeder Typ, der dem Equaler genügt, als Argument an T2.Equal übergeben werden kann und wir zur Laufzeit sicherstellen müssen, dass das Argument vom Typ T2 ist. Manche Sprachen garantieren das zum Umwandlungszeitpunkt.

Hier ein weiteres, ähnliches Beispiel:

type Opener interface {
   Open() Reader
}

func (t T3) Open() *os.File

In Go genügt T3 nicht dem Opener-Interface; in anderen Sprachen mag das anders sein.

Gos Typsystem tut in solchen Fällen weniger für die Programmierer als andere. Doch der Verzicht auf Subtypen macht die Regel zur Interface-Implementierung sehr einfach: Sind Name und Signatur der Funktionen exakt die des Interfaces? Die Regel ist auch leicht zu implementieren. Wir meinen, dass diese Vorteile das Fehlen automatischer Typpromotion leicht wettmachen. Sollte Go eines Tages eine Art polymorpher Typisierung einführen, dann sollte es auch einen Weg geben, die Idee hinter diesen Beispielen zu formulieren und das Ganze statisch zu prüfen.

Kann ich []T zu []interface{} konvertieren?

Nicht direkt, weil ihre Darstellung im Speicher unterschiedlich ist. Man muss die Elemente einzeln zum Ziel-Slice kopieren. Folgendes Beispiel konvertiert ein Slice aus int-Werten in ein Slice aus interface{}-Werten:

t := []int{1, 2, 3, 4}
s := make([]interface{}, len(t))
for i, v := range t {
    s[i] = v
}

Kann ich []T1 nach []T2 konvertieren, wenn T1 und T2 vom selben darunterliegenden Typ sind?

Die letzte Zeile im folgenden Kode wird nicht kompiliert.

type T1 int
type T2 int
var t1 T1
var x = T2(t1) // OK
var st1 []T1
var sx = ([]T2)(st1) // NICHT OK

In Go hängen Typen und ihre Methoden insofern sehr eng zusammen, als jeder namensbehaftete Typ eine (möglicherweise leere) Methodenmenge besitzt. Im Allgemeinen können Sie den Namen eines Typs durch Konversion ändern (und damit vielleicht auch seine Methodenmenge), doch das geht nicht bei zusammengesetzen Typen. In Go muss man Typkonversion explizit machen.

Warum ist der Wert meines Nil-Errors nicht nil?

Unter der Haube werden Interface-Werte durch zwei Elemente implementiert, den Typ und den Wert. Der Wert, der auch dynamischer Wert des Interfaces genannt wird, ist ein beliebiger konkreter Wert; der Typ ist der Typ dieses Wertes. Für den int-Wert 3 ist der Interface-Wert so etwas wie (int, 3).

Ein Interface-Wert ist nur dann nil, wenn darin weder Wert noch Typ gesetzt sind — (nil, nil). Ein nil-Interface enthält also immer nil für den Typ. Wenn wir zum Beispiel einen nil-Zeiger vom Typ *int in einem Interface-Wert speichern, so wird der Typ darin *int sein, ganz unabhängig vom Wert des Zeigers: (*int, nil). So ein Interface-Wert ist also auch dann non-nil, wenn der Zeigerwert darin nil ist.

Das kann verwirren, und es kommt dann vor, wenn ein nil-Wert in einem Interface gespeichert wird, beispielsweise in einem zurückgegebenen error:

func returnsError() error {
    var p *MyError = nil
    if bad() {
        p = ErrBad
    }
    return p // Gibt immer einen non-nil error zurück.
}

Wenn alles gut geht, gibt die Funktion ein nil-p zurück, also ist der Rückgabewert ein error-Interface, das (*MyError, nil) enthält. Wenn nun der Aufrufer den error-Rückgabewert mit nil vergleicht, sieht es immer so aus, als ob ein Fehler aufgetreten wäre, auch wenn das nicht der Fall war. Um also korrekt einen nil-error an den Rufer zurückzugeben, muss die Funktion explizit nil zurückgeben:

func returnsError() error {
	if bad() {
		return ErrBad
	}
	return nil
}

Es ist sinnvoll, wenn Funktionen, die Fehler zurückgeben, immer den Typ error in ihrer Signatur nutzen (wie wir es gerade getan haben) und nicht den konkreten Fehlertyp wie *MyError; das garantiert auch, dass der Fehler korrekt erzeugt wird. Beispielsweise gibt os.Open einen error zurück, auch wenn der, wenn nicht nil, immer vom konkreten Typ *os.PathError ist.

Ähnliches kann immer passieren, wenn ein Interface benutzt werden. Merken Sie sich nur, dass, wenn ein beliebiger konkreter Wert darin gespeichert wurde, das Interface nicht mehr nil ist. Mehr zu diesem Thema gibt's im Artikel "The Laws of Reflection".

Warum gibt es keine unmarkierten Unions wie in C?

Unmarkierte Unions würden Go's Speichersicherheit verletzen.

Warum hat Go keine Variant-Typen?

Variant-Typen, auch bekannt als algebraische Datentypen, ermöglichen, dass ein Wert einen beliebigen Typ aus einer Typmenge — aber nur daraus — sein kann. Üblich wäre, etwa in der Systementwicklung die Fehler als, sagen wir, Netzwerkfehler, Sicherheitsfehler und Anwendungsfehler zu klassifizieren; der Rufende kann dann die Fehlerquelle anhand des Fehlertyps unterscheiden. Ein weiteres Beispiel wäre ein Syntaxbaum, in dem jeder Knoten einem anderen Typ angehören kann: Vereinbarung, Anweisung, Zuweisung und so weiter.

Wir haben erwogen, Variant-Typen in Go zuzulassen, haben dann aber doch darauf verzichtet, weil sie in verwirrender Weise mit Interfaces überlappen. Was würde passieren, wenn Elemente eines Variant-Typs Interfaces wären?

Außerdem ist Einiges von dem, wofür Variant-Typen gedacht sind, durch die Sprache bereits abgedeckt. Das Fehler-Beispiel kann leicht mit einem Interface abgebildet werden, welches den Fehler enthält, plus einem Typ-Switch, der die Fälle unterscheidet. Das Beispiel mit dem Syntaxbaum ist ebenfalls machbar, wenn auch weniger elegant.

Warum hat Go keine kovarianten Ergebnistypen?

Kovariante Ergebnistypen würden bedeuten, dass

type Copyable interface {
	Copy() interface{}
}

durch eine Methode

func (v Value) Copy() Value

befriedigt würde, weil ja Value das Leere Interface implementiert. Aber in Go müssen die Typen exakt übereinstimmen, so dass Value also nicht Copyable implementiert. Go trennt scharf zwischen dem, was ein Typ tut — seine Methoden — von der Implementierung. Wenn zwei Methoden verschiedene Typen zurückliefern, dann tun sie eben nicht dasselbe. Programmierer, die sich kovariante Ergebnistypen wünschen, versuchen nur zu oft eine Typhierarchie mit Interfaces zu konstruieren. In Go ist es natürlicher, sauber zwischen Interface und Implementierung zu unterscheiden.

Werte

Warum bietet Go keine impliziten numerischen Konversionen?

Die Bequemlichkeit der automatischen numerischen Typkonversion in C wird konterkariert durch die Verwirrung, die sie stiftet. Wann hat ein Ausdruck kein Vorzeichen? Wie groß ist der Wert? Gibt es einen Überlauf? Ist das Ergebnis portierbar, sprich: unabhängig von der Maschine, auf der es erzeugt wird? Auch wird der Compiler komplexer; die "üblichen arithmetischen Konversionen" sind weder einfach noch konsistent über Architekturgrenzen hinweg. Wegen der Portabilität haben wir uns entschieden, die Sache klar und schlicht zu halten, auf Kosten der (wenigen) expliziten Konversionen im Kode. Außerordentlich hilfreich erweist sich dabei die Konstantendefinition in Go — es sind Werte beliebiger Genauigkeit ohne Festlegung von Vorzeichen oder Größe.

Damit hängt zusammen, dass, anders als in C, int und int64 unterschiedliche Typen sind, selbst wenn int ein 64-Bit-Typ ist. int ist ein architekturabhängiger Typ; wenn die Länge einer Ganzzahl wichtig für Sie ist, ermuntert Go dazu, explizit zu sein.

Ein Blog-Beitrag mit dem Titel "Constants" bietet eine detailliertere Untersuchung.

Warum sind Maps Standardtypen?

Aus dem gleichen Grund wie Strings: es sind so wichtige und wirkmächtige Datenstrukturen, dass eine gute Implementierung mit Unterstützung durch die Syntax das Programmieren nur angenehmer machen kann. Wir halten Go's Implementierung der Maps für stark genug für die überwiegende Mehrheit der Anwendungsfälle. Falls eine besondere Anwendung von einer maßgeschneiderten Implementierung profitieren kann, so ist eine solche möglich, aber sie wird syntaktisch weniger bequem sein. Uns scheint das ein annehmbarer Kompromiss zu sein.

Warum dürfen in Maps Slices nicht Schlüssel sein?

Nachschlagen in einer Map braucht einen Gleichheitsoperator, und den gibt's für Slices nicht. Gleichheit ist dort nicht implementiert, weil sie für diese Typen nicht eindeutig definiert ist; da gäbe es vieles abzuwägen: flaches gegen tiefes Vergleichen, Zeiger- gegen Wertevergleich, wie umgehen mit rekursiven Typen, und so weiter. Kann sein, dass wir da nochmal drangehen — ein nachträgliches Implementieren von Gleichheit für Slices wird jedenfalls kein existierendes Programm ungültig machen. Aber ohne eine klare Vorstellung davon, was Gleichheit bei Slices bedeutet, war es einfacher das erst einmal wegzulassen.

In Go 1, anders als bei den vorherigen Auslieferungen, ist Gleichheit für Strukturen und Arrays definiert; sie können also als Schlüssel für Maps verwendet werden. Slices warten aber noch immer auf eine Definition von Gleichheit.

Warum sind Maps, Slices und Kanäle Referenzen, Arrays aber Werte?

Das ist eine lange Geschichte. Zu Anfang waren Maps und Kanäle auch syntaktisch Zeiger und es war nicht möglich Nicht-Zeiger-Ausprägungen zu deklarieren oder zu benutzen. Außerdem kämpften wir damit, wie Arrays funktionieren sollten. Schließlich erkannten wir, dass die Sprache mit dieser strikten Trennung von Zeigern und Werten auch schwerer zu benutzen war. Wir änderten diese Typen so, dass sie als Referenzen auf die mit ihnen verbundenen Datenstrukturen funktionierten; das löste die Probleme. Zwar erhöhte sich zu unserem Bedauern auch die Komplexität der Sprache, doch der Einfluss auf die Benutzbarkeit war erfreulich: Go wurde produktiver und komfortabler.

Mit Go arbeiten

Wie sind die Bibliotheken dokumentiert?

Es gibt ein Programm godoc, geschrieben in Go, das Paketdokumentationen aus dem Quellkode extrahiert. Es kann von der Kommandozeile oder vom Netz aus benutzt werden. Ein Exemplar läuft für golang.org/pkg/. Es ist sogar so, dass Godoc den kompletten Netzplatz golang.org bedient.

Eine godoc-Instanz kann man so einrichten, dass eine ausführliche interaktive statische Analyse der Programmsymbole bereitgestellt wird; mehr dazu steht hier.

Für den Zugriff von der Kommandozeile aus bietet das go-Kommando mit dem Subkommando doc die gleiche Information über eine textuelle Schnittstelle.

Gibt es Stil-Richtlinien für Go?

Hier und da gibt es ein paar wenige Regeln für Namensgebung, Layout und Dateiorganisation. Der Artikel "Effective Go" (de) enthält ein paar Stil-Ratschläge. Ein direkterer Weg führt über das Programm gofmt, einen Quelltextformatierer, dessen Aufgabe es ist, Layoutregeln durchzusetzen; er ersetzt die sonst übliche, interpretationsbedürftige Sammlung von Ge- und Verboten. Aller Go-Kode im Repositorium wurde mit gofmt behandelt.

Das Dokument mit dem Titel "Go Code Review Comments" ist eine Sammlung sehr kurzer Artikel über Details des Go-Stils, die von Programmierern gerne übersehen werden. Es ist ein praktischer Leitfaden für solche, die Kode sichten müssen.

Wie kann ich Patches zu den Go-Bibliotheken beisteuern?

Die Bibliotheksquellen befinden sich im src-Ordner des Repositoriums. Wenn Sie signifikant ändern wollen, konsultieren Sie vorher die golang-nuts.

Mehr über die Vorgehensweise erfahren Sie in "Contributing to the Go project".

Warum benutzt "go get" zum Klonen HTTPS?

Firmen erlauben das Senden von Daten oft nur über die Standard-TCP-Kanäle 80 (HTTP) und 443 (HTTPS); andere TCP-Kanäle wie 9418 (git) oder 22 (ssh) sind dagegen oft gesperrt. Wenn HTTPS statt HTTP benutzt wird, erzwingt git von Haus aus Zertifikatsvalidierung, um gegen Janus-, Lausch- und manipulative Angriffe zu schützen. [Janusangriff - englisch: man-in-the-middle attack, A.d.Ü.] Das Kommando go get benutzt HTTPS also wegen der Sicherheit.

Wenn Sie git benutzen, und es vorziehen, Änderungen mit Ihrem Schlüssel durch SSH zu schieben, so können Sie auch das leicht tun:

Wie kombiniere ich Paketversionen mit "go get"?

"Go get" kennt das Konzept "Paketversion" nicht. Versionierung ist Quelle ganz erheblicher Komplexität, besonders bei großen Projekten, und uns ist keine Methode bekannt, die auch im großen Maßstab in genügend vielen verschiedenen Situationen gut genug wäre, um sie allen Benutzern von Go aufzuzwingen. Was "go get" und der übrige Go-Werkzeugsatz tun: sie isolieren Pakete mit unterschiedlichen Importpfaden. Zum Beispiel existieren in der Standardbibliothek sowohl html/template als auch text/template, obwohl beide ein template-Paket sind. Daraus ergeben sich ein paar Ratschläge für Paketersteller und Paketnutzer:

Wenn Pakete für die Öffentlichkeit bestimmt sind, bemühen Sie sich im Lauf ihrer Entwicklung um Rückwärtskompatibilität. Hier sind die "Go 1 Kompatibilitätsrichtlinien" hilfreich: bewahren Sie exportierte Namen, ermutigen Sie zum Benutzen von Verbundliteralen mit Schlüsseln, und so weiter. Wird eine geänderte Funktionalität gebraucht, ergänzen Sie einen neuen Namen statt einen alten zu ändern. Ändert sich die API grundlegend, dann kreieren Sie ein neues Paket mit neuem Importpfad.

Wenn Sie ein fremdes Paket benutzen und Sorge haben, dass es sich unerwartet ändern könnte, so ist am einfachsten, Sie kopieren es in ihr lokales Repositorium. (So macht das Google intern auch.) Speichern Sie die Kopie in einem neuen Importpfad, der es als lokale Kopie erkennen lässt. Beispielsweise könnten Sie von "original.com/pkg" nach "mein.de/fremd/original.com/pkg" kopieren. Das Programm gomvpkg hilft bei so was.

Für das Release Go 1.5 wurde das go-Kommando so ausgestattet, dass externe Abhängigkeiten mittels "Vendoring" [gemeint ist: Zwischenspeichern in einem eigenen Repositorium, A.d.Ü.] in einem extra Verzeichnis nahe beim Paket gehandhabt werden können. Mehr dazu im Design-Dokument.

Ein experimentelles Paketverwaltungswerkzeug dep ist in Arbeit, um zu erkunden, wie Werkzeuge das Verwalten von Paketen unterstützen kann. Mehr Information dazu in den dep-FAQ.

Zeiger und Speicherzuteilung

Wann werden Funktionsparameter als Werte übergeben?

Wie in allen Sprachen der C-Familie, wird in Go alles als Wert (by value) übergeben. Das heißt, eine Funktion bekommt immer eine Kopie des Übergebenen, so als ob dort eine Anweisung kodiert wäre, die den Wert dem Parameter zuweist. Zum Beispiel macht die Übergabe eines int-Wertes an eine Funktion eine Kopie dieses int. Und die Übergabe eines Zeigers macht eine Kopie von diesem Zeiger ... aber keine Kopie der Daten, auf die der Zeiger zeigt! (Beachten Sie auch weiter unten die Diskussion darüber, wie sich das auf Empfänger von Methoden auswirkt.)

Map- und Slice-Werte verhalten sich wie Zeiger: es sind Deskriptoren, welche Zeiger auf die darunterliegenden Map- oder Slice-Daten enthalten. Kopieren von Map- oder Slice-Werten kopiert nicht die Daten, auf die sie zeigen. Dagegen kopiert das Kopieren von Interface-Werten das Objekt, das im Interface gespeichert ist. Enthält das Interface eine Struktur, so macht das Kopieren des Interfaces eine Kopie dieser Struktur. Wenn das Interface einen Zeiger enthält, so macht das Kopieren des Interfaces eine Kopie dieses Zeigers, aber wiederum nicht der Daten, auf die der Zeiger zeigt.

Achtung, wir reden hier von Semantik. Tatsächlich dürfen die Operationen auch mit Optimierungen implementiert werden, die das Kopieren vermeiden ... solange die Semantik dadurch nicht verändert wird.

Wann sollte ich einen Zeiger auf ein Interface verwenden?

So gut wie nie. Zeiger auf Interfaces gibt es nur in sehr seltenen Fällen, wenn trickreich der Typ des Interface-Werts für eine verzögerte Auswertung versteckt werden soll.

Jedenfalls ist es ein Fehler, und zwar ein häufiger, den Zeiger auf ein Interface einer Funktion zu übergeben, die ein Interface erwartet. Der Compiler meckert das an, was aber weiterhin verwirrt, weil manchmal ein Zeiger gebraucht wird, um einem Interface zu genügen. Die Einsicht lautet: obwohl ein Zeiger auf einen konkreten Typ einem Interface genügen kann, so gilt mit einer Ausnahme, dass ein Zeiger auf ein Interface niemals einem Interface genügen kann.

Nehmen wir folgende Variablendeklaration:

var w io.Writer

Die Druckfunktion fmt.Fprintf erwartet als erstes Argument einen Wert, dessen Typ dem io.Writer genügt; das ist etwas, das die Standard-Write-Methode implementiert. Deshalb können wir schreiben:

fmt.Fprintf(w, "Hallo Welt\n")

Wenn wir versuchen, die Adresse von w zu übergeben, wird das Programm nicht umgewandelt:

fmt.Fprintf(&w, "Hallo Welt\n") // Fehler bei der Umwandlung

Die erwähnte eine Ausnahme ist, dass ein Zeiger auf ein Interface einem leeren Interface (interface{}) zugewiesen werden kann. Aber auch das ist ziemlich sicher ein Fehler, das Ergebnis jedenfalls höchst verwirrend.

Soll ich Methoden auf Werte oder auf Zeiger definieren?

func (s *MyStruct) pointerMethod() { } // Methode auf einen Zeiger
func (s MyStruct)  valueMethod()   { } // Methode auf einen Wert

Für Programmierer, denen Zeiger nicht so geläufig sind, kann der Unterschied zwischen den beiden Beispielen verwirrend sein, aber eigentlich ist es ziemlich einfach. Wenn eine Methode für einem Typ definiert wird, dann verhält sich der Empfänger, also s in den Beispielen oben so, als wären er ein Argument für die Methode. Den Empfänger entweder als Wert oder als Zeiger zu definieren, ist die gleiche Abwägung wie für das Argument einer Funktion. Da gibt es verschiedene Überlegungen.

Erster und wichtigster Punkt: muss die Methode den Empfänger verändern? Wenn ja, dann muss der Empfänger ein Zeiger sein. (Slices und Maps funktionieren als Referenzen, also ist die Geschichte hier etwas subtiler, aber damit beispielsweise die Länge eines Slices geändert werden kann, muss der Empfänger immer noch ein Zeiger sein.) Wenn im obigen Beispiel die Methode pointerMethod Felder in s ändert, sieht auch der Rufer diese Änderung; aber die Methode valueMethod wird mit einer Kopie des Rufer-Arguments aufgerufen (das ist die Definition von "Aufruf mit Wertparametern"), also bleiben Änderungen für den Rufer unsichtbar.

Übrigens, Java arbeitet so wie hier die Zeiger-Empfänger, nur dass in Java die Zeiger versteckt bleiben; das Ungewohnte sind Go's Wert-Empfänger.

Ein zweiter Punkt: Effizienz. Wenn der Empfänger groß ist, eine "längliche" Struktur etwa, dann ist es wesentlich "billiger", mit Zeiger-Empfängern zu arbeiten.

Und noch ein Punkt: Konsistenz. Wenn einige Methoden für einem Typ Zeiger-Empfänger brauchen, sollte alle sie benutzen; dann ist die Methodenmenge konsistent, egal wie der Typ benutzt wird. Zu Details siehe unter "Methodenmenge".

Für Basistypen, Slices und kleine Strukturen sind Wert-Empfänger sehr "billig", also solange ein Zeiger nicht gebraucht wird, ist ein Wert-Empfänger effizient und verständlich.

Was unterscheidet new von make?

Kurz gesagt, new stellt Speicher bereit während make Slice-, Map- und Kanaltypen vorbereitet.

Näheres dazu im relevanten Abschnitt in "Effective Go" (de).

Wie groß ist ein int auf einer 64-Bit-Maschine?

Die Größen von int und uint hängen von der Implementierung ab; auf jeweils einer Plattform sind die beiden gleich groß. Um portierbar zu bleiben, sollte Kode, der sich auf eine bestimmte Größe verlässt, Typen mit expliziten Größenangaben benutzen, zum Beispiel int64. Vor Go 1.1 benutzten die 64-Bit-Compiler (sowohl gc als auch gccgo) 32-Bit lange ints, seit Go 1.1 benutzen sie eine 64-Bit-Darstellung.

Gleitkomma- und Komplextypen hingegen sind immer größenbehaftet — es gibt keine Basistypen float oder complex — damit Programmierer sich immer der jeweiligen Präzision der Gleitkommazahlen bewusst sein sollen. Vorgabe für typfreie Gleitkommakonstanten ist float64. Für eine float32-Variable also, die mit einer typfreien Konstanten vorbelegt wird, muss der Typ explizit Teil der Variablendeklaration sein:

var foo float32 = 3.0

Stattdessen ist auch möglich, der Konstanten mittel Konversion einen Typ zu geben: foo := float32(3.0).

Woher weiß ich, ob eine Variable auf dem "Heap" oder den "Stack" liegt?

Um es klar zu sagen: das müssen Sie nicht wissen. Jede Variable in Go existiert so lange, wie es Referenzen darauf gibt. Die Implementierung bestimmt den Speicherort; er ist irrelevant für die Semantik der Sprache.

In der Tat wirkt sich der Speicherort auf die Effizienz eines Programms aus. Wenn möglich, erzeugen die Go-Compiler funktionslokale Variablen im "Stack"-Speicherblock dieser Funktion. Wenn allerdings der Compiler nicht ausschließen kann, dass die Variable nach Verlassen der Funktion doch noch angesprochen wird, dann muss er die Variable auf dem "Heap" erzeugen, damit keine Zeiger ins Leere greifen; danach ist die Automatische Speicherbereinigung zuständig. Es kann auch sinnvoll sein, eine sehr große lokale Variable auf dem "Heap" statt auf dem "Stack" abzulegen.

Bei den aktuellen Compilern ist eine Variable dann Kandidat für den "Heap", wenn seine Adresse genommen wurde. Eine einfache Ausreißanalyse (escape analysis) erkennt einige Fälle, in denen die Variable nicht über das Funktionsende hinaus am Leben bleibt, also auf den "Stack" wandern kann.

Warum braucht mein Go-Prozess soviel virtuellen Speicher?

Der Go-Speicherzuteiler reserviert einen große Bereich im virtuellen Speicher als Arena für Speicherzuweisungen. Der virtuelle Speicher gehört zum jeweiligen Go-Prozess; die Reservierung behindert keine anderen Speicheroperationen.

Die Menge des tatsächlich dem Go-Prozess zugewiesenen Speichers sieht man beim Unix-top-Kommando in der Spalte RES (Linux) oder RSIZE (Mac OS X).

Nebenläufigkeit

Welche Operationen sind unteilbar? Was ist mit Zugriffssperren?

Wir haben noch nicht alles festgelegt. Ein paar Einzelheiten sind verfügbar unter "The Go Memory Model".

Was Zugriffssperren (mutexes) angeht, so werden sie vom Paket sync implementiert. Wir hoffen jedoch, dass der Go-typische Programmierstil dazu anregt, abstraktere Techniken zu benutzen. Erwägen Sie insbesondere, ihr Programm so zu strukturieren, das zu einem Zeitpunkt immer nur eine Goroutine für eine bestimmte Portion Daten zuständig ist.

Do not communicate by sharing memory. Instead, share memory by communicating.

Einen detaillierteren Einblick in dieses Konzept erhalten Sie wenn Sie den Kodespaziergangs "Share Memory By Communicating" machen und den zugehörigen Artikel lesen.

Wieso benutzt mein Multi-Goroutinen-Programm nicht auch mehrere CPUs?

Die Anzahl der für Goroutinen zur Verfügung stehenden CPUs wird über die Umgebungsvariable GOMAXPROCS gesteuert. Bei früheren Go-Releases war der Vorgabewert 1, doch ab Go 1.5 ist der Vorgabewert gleich der Anzahl der verfügbaren CPU-Kerne. Deshalb werden Programme, die mit Go 1.5 umgewandelt wurden, paralleles Verarbeiten der Goroutinen zeigen. Dieses Verhalten können Sie ändern, indem Sie die Umgebungsvariable explizit setzen, oder indem Sie die gleichnamige Funktion aus dem runtime-Paket benutzen, um so eine andere Zahl von "Threads" zu nutzen.

Programme, die parallele Berechnungen durchführen, können durch weiteres Hochsetzen von GOMAXPROCS profitieren. Trotzdem, machen Sie sich bewusst, dass nebenläufig nicht gleich parallel ist.

Warum macht GOMAXPROCS > 1 manchmal mein Programm langsamer?

Das hängt von Ihrem Programm ab. Probleme, die sequentieller Natur sind, können auch durch mehr Goroutinen nicht beschleunigt gelöst werden. Nebenläufigkeit wird erst dann zu Parallelismus, wenn das Problem paralleler Natur ist.

In der Praxis heißt das: Programme, die mehr Zeit fürs Kommunizieren über Kanäle brauchen als für Berechnungen, können durch das Benutzen mehrerer Verarbeitungsstränge langsamer werden. Das ist so, weil das Nachrichtensenden von Strang zu Strang jedes mal den Kontext wechselt, und das bedeutet Kosten. Zum Beispiel arbeitet das Primzahlensieb (de) in der Go-Sprachbeschreibung nicht parallel, obwohl es viele Goroutinen startet; erhöhen von GOMAXPROCS wird es eher verlangsamen als beschleunigen.

Der Ablaufmanager (scheduler) in Go ist noch nicht so gut, wie er sein müsste; immerhin er wurde zuletzt stark verbessert. In Zukunft soll er die Benutzung von Betriebssystem-Threads noch weiter optimieren. Bis dahin kann, wenn es Performanzprobleme gibt, das Setzen von GOMAXPROCS auf Anwendungsebene weiterhelfen.

Weitere Details dazu hier: "Concurrency is not parallelism".

Funktionen und Methoden

Warum haben T und *T verschiedene Methodenmengen?

Die Go-Sprachbeschreibung (de) sagt:

Die Methodenmenge jedes anderen namenbehafteten Typs T besteht aus allen mit Empfängertyp T deklarierten Methoden. Die Methodenmenge des korrespondierenden Zeigertyps *T besteht aus allen mit Empfängertyp *T oder T deklarierten Methoden. (Das heißt, sie enthält auch die Methodenmenge von T.)

Wenn der Empfänger ein Zeiger *T ist, so kann ein Methodenaufruf den Wert dazu durch Dereferenzieren des Zeigers ermitteln, doch wenn er ein Wert T ist, gibt es für die Methode keinen Weg, einen sinnvollen Zeiger dazu zu ermitteln.

Nur mal angenommen, der Compiler Könnte die Adresse des an die Methode übergebenen Wertes ermitteln, dann wären Änderungen des Werts an dieser Stelle für den Aufrufer verloren. Würde zu Beispiel die Write-Methode des bytes.Buffer einen Wert anstelle des Zeigers benutzen, so würde der folgende Kode:

var buf bytes.Buffer
io.Copy(buf, os.Stdin)

die Standardeingabe in eine Kopie von buf kopieren, und nicht in buf selbst. Das ist wohl kaum das erwartete Verhalten.

Was passiert mit Funktionsabschlüssen, die als Goroutinen laufen?

Funktionsabschlüsse (closures) kombiniert mit Nebenläufigkeit können Verwirrung stiften. Sehen wir uns folgendes Programm an:

func main() {
    done := make(chan bool)

    values := []string{"a", "b", "c"}
    for _, v := range values {
        go func() {
            fmt.Println(v)
            done <- true
        }()
    }

    // warten bis alle Goroutinen fertig sind
    for _ = range values {
        <-done
    }
}

Man könnte nun fälschlicherweise als Ausgabe a, b, c erwarten. Was man sehr wahrscheinlich stattdessen sieht ist c, c, c. Das ist deshalb so, weil jede Iteration der Schleife dieselbe Instanz der Variablen v benutzt, so dass alle Funktionsabschlüsse sich diese Variable teilen. Wenn ein Funktionsabschluss läuft, druckt er den Wert von v zum Zeitpunkt des Aufrufs von fmt.Println; v kann seit dem Start der Goroutine geändert worden sein. Um solche Probleme frühzeitig zu erkennen, benutzen Sie bitte go vet.

Um den jeweils aktuellen Wert von v beim Starten an die jeweilige Goroutine zu binden, muss man das Schleifeninnere so modifizieren, dass für jede Iteration eine neue Variable erzeugt wird. Eine Möglichkeit ist, die Variable als Argument dem Funktionsabschluss zu übergeben:

    for _, v := range values {
        go func(u string) {
            fmt.Println(u)
            done <- true
        }(v)
    }

Hier wird also der Wert von v als Argument an die anonyme Funktion übergeben. Dieser Wert ist dann innerhalb der Funktion als Variable u verfügbar.

Noch einfacher ist es, eine neue Variable zu erzeugen, und zwar in einem recht eigenartigen Deklarationsstil, der aber in Go prima funktioniert:

    for _, v := range values {
        v := v // Erzeuge ein neues 'v'.
        go func() {
            fmt.Println(v)
            done <- true
        }()
    }

Kontrollanweisungen

Hat Go den ?: Operator?

Es gibt keine ternäre Anweisung in Go. Schreiben Sie stattdessen so etwas:

if expr {
    n = trueVal
} else {
    n = falseVal
}

Paketierung und Testen

Wie baue ich ein Paket aus vielen Dateien?

Packen Sie alle Quelldateien für das Paket zusammen in einen Ordner. Quelldateien können sich beliebig auf Dinge aus anderen Quelldateien beziehen; es sind weder Vorwärtsdeklarationen noch Header-Dateien nötig.

Auch mit vielen Quelldateien, wird sich das Paket genauso wie ein Ein-Datei-Paket umwandeln und testen lassen.

Wie schreibe ich einen Komponententest?

Legen Sie im selben Ordner eine neue Datei an, deren Name mit _test.go endet. Importieren Sie "testing" und schreiben Sie Funktionen der Form:

func TestFoo(t *testing.T) {
    ...
}

Starten Sie dann im selben Ordner go test. Dieses Programm findet die Test-Funktionen, baut eine Binärdatei für den Test, und lässt sie laufen.

Lesen Sie "How to Write Go Code" (de), schauen Sie sich das Paket testing und das Subkommando go test an.

Wo ist mein liebste Hilfsfunktion fürs Testen?

Go's testing-Paket erleichtert das Schreiben der Komponententests; einiges, was es in anderen Sprachen gibt, fehlt hingegen, zum Beispiel "assert". Weiter oben wurde erklärt, warum Go keine Zusicherungen kennt; dasselbe gilt für assert in Tests. Korrekte Fehlerbehandlung bedeutet auch, dass nach einem gescheiterten Test die übrigen noch durchgeführt werden, so dass die Person, die die Tests durchführt, ein komplettes Bild davon erhält, was falsch ist und was nicht. Es ist nützlicher zu erfahren, dass isPrime falsche Antworten für 2, 3, 5 und 7 (oder für 2, 4, 8 und 16) liefert, als nur die Meldung, dass isPrime für 2 eine falsche Antwort gibt und deshalb keine weiteren Tests durchgeführt wurden. Derjenige, der den Test veranlasst, ist vielleicht nicht mit dem fehlerhaften Kode vertraut. Zeit, die jetzt für aussagekräftige Fehlermeldungen investiert wird, zahlt sich später aus, wenn ein Test scheitert.

Übrigens tendieren Testrahmen dazu, sich zu Mini-Sprachen auszuwachsen, inklusive Kontrollanweisungen und Druckfunktionen. Aber Go verfügt bereits über all dies; warum also neu erfinden? Schreiben wir doch lieber die Tests in Go; das heißt, eine Sprache weniger lernen, und die Tests bleiben schlicht, und einfach zu verstehen.

Wenn einem die Menge an zusätzlichem Kode für gute Fehlermeldung zu groß und zu monoton erscheint, können tabellengesteuerte Tests die bessere Wahl sein: sie iterieren über eine Liste von Eingabe- und erwarten Ausgabewerten einer Datenstruktur — Go bietet eine ausgezeichnete Unterstützung für Datenstrukturliterale. Die Arbeit, die man in gute Tests mit guten Fehlermeldungen steckt, macht sich über viele Testfälle hinweg bezahlt. Die Standardbibliothek ist voll von anschaulichen Beispielen, nehmen wir nur mal die Formatierungstests des Pakets fmt.

Warum ist X nicht in der Standardbibliothek?

Zweck der Standardbibliothek ist es, die Laufzeitumgebung zu unterstützen, zum Betriebssystem zu verbinden sowie grundlegende Funktionen bereitzustellen, die von vielen Go-Programmen gebraucht werden, wie zum Beispiel formatierte Ein- und Ausgabe und Netzwerkfunktionen. Sie enthält außerdem wichtige Bausteine für die Web-Entwicklung, inclusive Kryptographie und Unterstützung von Standards wie HTTP, JSON und XML.

Es gibt keine eindeutige Regel, die festlegt, was dazugehört und was nicht. Denn lange Zeit war dies die einzige Go-Bibliothek. Allerdings gibt es Kriterien dafür, was heute noch aufgenommen werden kann.

Ergänzungen der Standardbibliothek sind selten, für eine Neuaufnahme liegt die Latte hoch. Kode in der Standardbibliothek bringt langfristige Instandhaltungskosten mit sich (oft für andere als die Originalautoren). Kode in der Standardbibliothek unterliegt der Go-1-Kompatibilitätsgarantie (was auch das Reparieren von API-Schwachstellen unmöglich macht). Und Kode in der Standardbibliothek unterliegt dem Go-Freigabezyklus, wodurch verhindert wird, dass Fehlerkorrekturen schnell den Benutzer erreichen.

Der richtige Platz für neuen Kode liegt meist außerhalb der Standardbibliothek, erreichbar über go get, welches Teil des go-Kommandos ist. Solcher Kode hat seine eigenen Betreuer, seinen eigenen Freigabzyklus, seine eigene Kompatibilitätsgarantie. Pakete und ihre Dokumentation findet man auf godoc.org.

Trotzdem es Teile in der Standardbibliothek gibt, die eigentlich nicht hineingehören, wie zum Beispiel log/syslog, so pflegen wir auch weiterhin alles wegen der Go-1-Kompatibilitätsgarantie. Doch wir ermuntern dazu, neuen Kode woanders unterzubringen.

Implementierung

Welche Technik wurde für den Bau der Compiler benutzt?

Gccgo hat ein in C++ geschriebenes "Front-End" mit einem rekursiv absteigenden Parser, der an das Standard-"GCC-Back-End" gekoppelt ist. Gc wurde in Go geschrieben mit einem rekursiv absteigenden Parser und einem angepassten Lader, der ebenfalls in Go geschrieben ist und auf dem Plan-9-Lader basiert, um ELF/Mach-O/PE-Binärdateien zu erzeugen.

Wir hatten daran gedacht, die LLVM für gc zu benutzen, fanden sie aber zu groß und zu langsam für unsere Ansprüche.

Der ursprüngliche gc-Go-Compiler wurde in C geschrieben wegen der Schwierigkeiten beim Lösen des "Henne-Ei-Problems" beim Urladen (bootstrapping); man braucht einen Go-Compiler, um eine Go-Umgebung aufzubauen. Doch es hat Fortschritte gegeben, und seit Go 1.5 ist auch der Compiler in Go geschrieben. Er wurde mit einem maschinellen Übersetzungswerkzeug von C nach Go übertragen, das in diesem Design-Dokument beschrieben ist, und über das es neulich auch einen Vortrag gab. Der Compiler ist jetzt selbstbezüglich, was bedeutet, dass wir uns mit dem "Henne-Ei-Problem" beschäftigen müssen. Die Lösung lautet natürlich, dass wir bereits über eine funktionierende Go-Installation verfügen können müssen — wie man üblicherweise ja auch eine funktionierende C-Installation bereitstehen hat. Wie man eine neue Go-Installation aus den Quellen heraus zum Laufen bringt, ist hier separat beschrieben.

Go ist eine feine Sprache, um damit einen Go-Compiler zu implementieren. Wenn auch (noch) nicht vom Compiler benutzt, so gibt es bereits einen eigenen Lexer und einen eigenen Parser im Paket go und auch ein eigenes Typprüfpaket wurde auch schon geschrieben.

Wie ist die "Run-Time"-Unterstützung implementiert?

Ebenfalls wegen des "Henne-Ei-Problems" wurde der "Run-Time"-Kode ursprünglich vorwiegend in C geschrieben (mit ganz wenig Assembler), aber auch der wurde inzwischen nach Go übertragen (wenige Assembler-Teile ausgenommen). "Run-Time"-Unterstützung bei Gccgo benutzt die glibc-Bibliothek. Der gccgo-Compiler implementiert Goroutinen mit einer Technik, die sich "segmentierte Stacks" nennt; diese wiederum werden durch neueste Änderungen am Gold-Linker ermöglicht.

Wieso wird mein triviales Programm zu so einer großen Binärdatei?

Der Linker im Gc-Werkzeugsatz erzeugt standardmäßig statisch gebundene Binärdateien. Alle Go-Binärdateien enthalten darum das Go-Laufzeitsystem (runtime system) und zusätzlich alle Laufzeitinformationen, die nötig sind für dynamische Typprüfung, für Reflexion und sogar die für "Stacktraces" bei Panik zur Laufzeit.

Ein einfaches "Hallo Welt"-Programm in C, das mit GCC kompiliert und statisch gebunden wird, ist auf Linux ca. 750 KB groß, inklusive printf. Das entsprechende Go-Programm mit fmt.Printf ist 1,5 MB groß, bietet aber sehr viel mehr an Laufzeitunterstützung, inclusive Typinformation.

Kann ich diesen Klagen über unbenutzte Variablen/Imports ein Ende machen?

Die Existenz einer unbenutzten Variablen kann auf einen Fehler hinweisen. Unbenutzte Imports machen das Umwandeln langsam; Auswirkungen können umso spürbarer werden, je mehr der Kode anwächst und je mehr Programmierer beteiligt sind. Darum verweigert der Compiler die Umwandlung von Go-Kode mit unbenutzten Variablen oder Imports. Bequemlichkeit auf kurze Sicht wird zugunsten von Umwandlungstempo und Klarheit auf lange Sicht getauscht.

Unbenutzte Variablen/Imports sind dennoch üblich, temporär während der Programmentwicklung, und da kann es ziemlich nerven, jedes mal Kode auskommentieren zu müssen, nur damit die Umwandlung gelingt.

Wir sind um eine Compileroption gebeten worden, um das abschalten zu können oder zumindest um nur Warnungen zu bekommen. Wir haben es trotzdem nicht getan, weil Compileroptionen nicht die Semantik einer Sprache verändern sollten, und weil der Go-Compiler nicht warnt, sondern nur Fehler meldet, die eine Umwandlung verhindern.

Zwei Gründe sprechen gegen Warnungen. Erstens, wenn es der Klage wert ist, dann ist der Kode auch wert, korrigiert zu werden. (Und wenn nicht korrigiert werden muss, dann muss man auch keine Worte verlieren.) Zweitens, wenn der Compiler warnen kann, gibt es bald Warnungen bei jeder Kleinigkeit und die Umwandlung wird geschwätzig — das verdeckt die echten Fehler.

Die Situation lässt sich aber einfach retten. Benutzen Sie den Leeren Bezeichner, um Unbenutztes während der Entwicklung am Leben zu halten:

import "unbenutzt"

// Diese Deklaration benutzt - nur zum Schein - diesen Import,
// indem Ding aus unbenutzt angesprochen wird.
var _ = unbenutzt.Ding  // TODO: Vor dem Commit löschen!

func main() {
    debugData := debug.Profile()
    _ = debugData // wird nur fürs Entwanzen gebraucht
    ....
}

Heutzutage benutzen die meisten Go-Programmierer ein Werkzeug namens goimports, welches zum Sicherstellen korrekter Imports den Go-Quellkode automatisch überschreibt; damit entfällt schon mal das Unbenutzte-Imports-Problem. Das Programm kann leicht in die meisten Editoren eingebunden werden und damit automatisch beim Dateischreiben ablaufen.

Performanz

Warum schneidet Go beim Benchmarktest X so schlecht ab?

Ein Ziel beim Design von Go war, für vergleichbare Programme an die Performanz von C heranzukommen. Trotzdem schaut's bei manchen Benchmarktests schlecht aus, auch bei einigen unter golang.org/x/exp/shootout. Die langsamsten Tests hängen von Bibliotheken ab, von denen es für Go keine vergleichbar schnellen gibt. Zum Beispiel hängt pidigits.go von dem vielfach-genauen math-Paket ab, aber die C-Version nutzt GMP, geschrieben in optimiertem Assembler. Benchmarktests mit regulären Ausdrücken, zum Beispiel regex-dna.go, vergleichen das Go-eigene regexp-Paket mit ausgereiften, hochoptimierten Regexp-Bibliotheken wie PCRE.

Benchmarkwettbewerbe werden durch intensives Tuning gewonnen, und um die meisten Tests in Go müsste man sich noch kümmern. Wenn Sie C- und Go-Programme vergleichen, die auch vergleichbar sind, wie reverse-complement.go, dann bemerken Sie, dass die beiden sehr viel näher beieinander liegen, als der Rest der Testsammlung zeigt.

Doch es gibt noch Luft nach oben. Die Compiler sind gut, könnten aber noch besser sein, um viele Bibliotheken muss sich noch ernsthaft gekümmert werden, und die Müllabfuhr ist auch noch nicht schnell genug. (Selbst wenn sie's wäre: es wirkt sich enorm günstig aus, wenn man unnötigen Müll vermeidet.)

Jedenfalls kann Go oft genug mithalten. Die Performanz vieler Programme hat sich im Laufe der Sprach- und Bibliotheksentwicklung schon signifikant verbessert. Lesen Sie auch den Artikel "Profiling Go programs".

Unterschiede zu C

Warum ist die Syntax so anders als in C?

Abgesehen von den Deklarationen sind die Unterschiede nicht besonders groß. Sie sind Folge zweierlei Ansinnens. Erstens soll sich die Syntax leicht anfühlen, ohne viele obligatorische Schlüsselwörter, Wiederholungen oder gar Geheimnisse. Zweitens wurde die Sprache so gestaltet, dass sie einfach zu analysieren ist, dass man sie ohne Symboltabelle "parsen" kann. Das macht es viel einfacher, Werkzeuge wie Debugger, Abhängigkeitsprüfer, automatische Extraktoren für Dokumentation oder etwa Zusatzprogramme für Integrierte Etwicklungsumgebungen zu bauen. Die Probleme, die C und seine Nachkommen in dieser Hinsicht machen, sind berüchtigt.

Warum sind die Deklarationen anders herum?

Anders herum sind sie nur, wenn man C gewohnt ist. Der Gedanke bei der C-Variante ist, dass eine Variable wie ein Ausdruck deklariert wird, als Erklärung zum Typ. Das ist eine nette Idee, doch die Grammatik der Typen und Ausdrücke passen nicht gut zusammen, und das Ergebnis kann ausgesprochen verwirrend sein — denken Sie an Funktionszeiger. Go trennt (größtenteils) Ausdrucks- von Typsyntax, und damit wird's leichter; der Präfix * für Zeiger ist die Ausnahme zur Regel. In C deklariert:

    int* a, b;

a als Zeiger, b aber nicht; in Go deklariert:

    var a, b *int

beide als Zeiger. Das ist klarer und regulärer. Außerdem ist die Kurzdeklaration := noch ein Argument für diese Form, weil eine volle Variablendeklaration die gleiche Reihenfolge haben sollte wie :=, so dass:

    var a uint64 = 1

dasselbe besagt wie:

    a := uint64(1)

Auch das Parsen wird einfacher durch die unterscheidbaren Grammatiken für Typen und Ausdrücke. Für Sauberkeit sorgen außerdem Schlüsselwörter wie func und chan.

Weitere Einzelheiten hierzu finden Sie im Aufsatz "Go's Declaration Syntax".

Warum gibt es keine Zeigerarithmetik?

Weil's sicherer ist. Ohne Zeigerarithmetik kann man eine Sprache schaffen, die nie mit illegalen Adressen hantiert. Compiler- und Hardwaretechnik haben sich soweit entwickelt, dass eine Schleife mit Array-Indices genauso effizient sein kann wie eine mit Zeigerarithmetik. Der Verzicht auf Zeigerarithmetik erleichtert auch die Implementierung der Müllabfuhr.

Warum sind ++ und -- Anweisungen und nicht Ausdrücke? Und warum Postfix und nicht Präfix?

Ohne Zeigerarithmetik entfällt der Komfortgewinn durch Prä- und Postfix-Operatoren. Lässt man sie ganz weg, so vereinfacht sich die Ausdruckssyntax und das Durcheinander bei der Auswertungsreihenfolge — denken Sie nur an f(i++) und p[i] = q[++i] — entfällt ebenfalls. Das ist signifikant einfacher! Was Postfix versus Präfix angeht, so wäre beides in Ordnung. Postfix hat aber die ältere Geschichte; auf Präfix wurde nur im Zusammenhang mit der STL bestanden, einer Bibliothek für eine Sprache, die ironischerweise ein Postfix im Namen trägt.

Warum gibt es geschweifte Klammern aber keine Semikolons? Und warum darf ich die öffnende geschweifte Klammer nicht in die nächste Zeile schreiben?

Go benutzt geschweifte Klammern zum Gruppieren von Anweisungen, was jedem geläufig ist, der in Sprachen aus der C-Familie gearbeitet hat. Semikolons aber sind für den Parser gedacht, nicht für Programmierer; wir wollen soweit möglich auf sie verzichten. Um das zu erreichen, übernimmt Go einen Trick von BCPL: Semikolons, die Anweisungen trennen, sind Teil der formalen Grammatik, werden aber automatisch vom Lexer eingefügt (ohne Vorausschau), und zwar am Ende jeder Zeile, die Ende einer Anweisung sein kann. Das funktioniert gut in der Praxis, hat nur die Nebenwirkung, das ein bestimmter Geschweifter-Klammern-Stil erzwungen wird. Insbesondere darf die öffnende geschweifte Klammer einer Funktion nicht in einer eigenen Zeile stehen.

Manch einer hat argumentiert, dass der Lexer vorausschauen soll, damit das wieder erlaubt werden kann. Dem widersprechen wir. Da ja Go-Kode automatisch durch gofmt formatiert werden soll, muss ein Stil gewählt werden. Dieser Stil mag sich von Ihrem gewohnten aus C oder Java unterscheiden, aber Go ist eine neue Sprache, und gofmt's Stil ist so gut wie jeder andere. Wichtiger — viel wichtiger — ist: die Vorteile eines einheitlichen, programmatisch verfügten Formats für alle Go-Programme wiegen viel schwerer als jeder gefühlte Nachteil eines bestimmten Stils. Beachten Sie auch, dass mit Go's Stil eine interaktive Implementierung von Go jede Zeile einzeln betrachten könnte, ohne speziellere Regeln.

Warum Automatische Speicherbereinigung? Ist das nicht zu teuer?

Speicherverwaltung ist der größte Buchhaltungposten bei der Systementwicklung. Wir halten es für wesentlich, dass dieser Wasserkopf entfernt wird. Die Fortschritte in der Technik des Müllsammelns über die letzten Jahren machen uns zuversichtlich, dass wir automatische Speicherbereinigung mit genügend geringem Aufwand und ohne signifikante Latenzen implementieren können.

Noch ein Punkt: ein Großteil der Schwierigkeiten bei nebenläufiger oder Mehrstrang-Programmierung erwächst aus der Speicherverwaltung; wenn Objekte zwischen Verarbeitungssträngen (threads) ausgetauscht werden, wird's schwer, sie wieder garantiert und sicher freizugeben. Automatische Speicherbereinigung macht nebenläufigen Kode sehr viel leichter zu schreiben. Aber natürlich, den Müllsammler für einer nebenläufige Umgebung zu implementieren, ist eine Herausforderung für sich; diese einmal anzugehen, statt in jedem Programm wieder, hilft jedem.

Schließlich, Nebenläufigkeit mal beiseite, vereinfacht der Müllsammler die Schnittstellen, weil diese nicht mehr festlegen müssen, wie Speicher über die Schnittstelle hinweg gehandhabt werden muss.

Die derzeitige Implementierung ist ein paralleler Markier-und-Fege-Sammler (mark-and-sweep collector). Durch jüngste Neuerungen, die in diesem Design-Dokument beschrieben sind, wurden Wartezeiten beschränkt und Parallelverarbeitung verbessert. Zukünftige Version mögen neue Wege gehen.

Zum Thema Performanz sollten Sie bedenken, dass Go dem Programmierer beachtlich viel Kontrolle über Speicherlayout und -zuweisung gibt, viel mehr als in typischen Sprachen mit Müllsammler. Ein sorgsamer Programmierer kann den Verwaltungsaufwand für die Speicherbereinigung dramatisch verringern; ein Beispiel dazu finden Sie im Artikel "Profiling Go Programs", inklusive einer Demonstration von Go's Analysewerkzeugen.