Go Home Page
Die Programmiersprache Go

Frequently Asked Questions (FAQ) — Deutsche Übersetzung

Das Original:
https://golang.org/doc/go_faq.html
Version of June 23, 2020 (go1.15)
Diese Übersetzung:
https://bitloeffel.de/DOC/golang/go_faq_20200814_de.html
Stand: 27.07.2020
© 2014-20 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?

Beim Start von Go vor zehn Jahren war die Programmierwelt noch eine andere. Die in Produktion eingesetzte Software war üblicherweise in C++ oder Java geschrieben. Es gab noch kein GitHub, die wenigsten Computer hatten Mehrfachprozessoren, und abgesehen von Visual Studio und Eclipse standen nur wenige Integrierte Entwicklungsumgebungen (IDEs) oder andere hochentwickelte Werkzeuge zur Verfügung, erst recht nicht kostenlos im Internet.

Wir waren damals ziemlich frustriert von der unangemessenen komplexen Handhabung dieser Sprachen bei der Entwicklung von Server-Software. Zwar waren die Computer seit dem Auftreten der Sprachen C, C++ und Java ernorm viel schneller geworden, doch das Programmieren selbst hatte keine solchen Fortschritte gemacht. Zudem war klar, dass Mehrfachprozessoren die Norm werden würden, nur dass die meisten Sprachen wenig Hilfe anboten, diese auch effizient und sicher zu programmieren.

Wir beschlossen, einen Schritt zurückzutreten und darüber nachzudenken, welche Probleme in den kommenden Jahren mit Weiterentwicklung der Technik die Softwareentwicklung dominieren würde, und wie eine neue Sprache bei ihrer Lösung helfen könnte. Zum Beispiel sollte die Sprache mit dem Aufstieg der Mehrkernprozessoren auch erstklassige Unterstützung für irgendeine Art von Nebenläufigkeit oder Parallelverarbeitung bieten. Und um die Ressourcenverwaltung in großen nebenläufigen Programmen handhabbar zu halten, brauchte es eine Müllabfuhr (garbage collection), oder zumindestens so etwas wie eine sichere automatische Speicherverwaltung.

Diese Überlegungen führten zu einer Reihe von Diskussionen, aus denen Go hervorging, zunächst als eine Ansammlung von Ideen, schließlich als Programmiersprache. Ein übergeordnetes Ziel von Go war, die Arbeit der Programmierer zu unterstützen, indem das Werkzeugmmachen unterstützt wurde, indem schlichte Aufgaben wie das Kodeformatieren automatisiert und indem Hindernisse für die Arbeit mit großen Quellarchiven aus dem Weg geräumt wurden.

Sehr viel ausführlichere sind die Ziele von Go, und wie sie erreicht wurden oder zumindest wie sich ihnen genähert wurde, in dem Aufsatz "Go at Google: Language Design in the Service of Software Engineering" beschrieben.

Wie ist das Projekt verlaufen?

Am 21. September 2007 begannen Robert Griesemer, Rob Pike und Ken Thompson, die Ziele der 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 hatte mit der Arbeit an einem Compiler begonnen, um damit die Ideen auszuloten; 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 ging am 10. November 2009 als "Open Source"-Projekt an die Öffentlichkeit. Menschen ohne Zahl haben seitdem ihre Ideen, Kommentare und Kode beigetragen.

Heute gibt es Millionen von Go-Programmierern — "Gopher" — auf der ganzen Welt. Der Erfolg von Go hat unsere Erwartungen weit übertroffen.

Woher stammt das Gopher-Maskottchen?

Maskottchen und Logo wurden gestaltet von Renée French, von der auch Glenda, das Häschen von Plan 9 stammt. Der 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.

Für den Gopher gibt es ein Modellblatt, das die Charakteristika zeigt, und wie man sie richtig darstellt. Dieses Blatt wurde erstmals auf der Gophercon 2016 bei einem Vortrag von Renée gezeigt. Er ist einzigartig; er ist der Go-Gopher und nicht irgendein Wald-und-Wiesen-Gopher.

Heißt die Sprache nun Go oder Golang?

Die Sprache heißt Go. Der Spitzname "golang" kam auf, weil die Internetseite golang.org und nicht go.org heißt; go.org stand uns nicht zur Verfügung. Dessen ungeachtet benutzen viele den Namen golang, und der ist als Markenzeichen ganz praktisch. Zum Beispiel lautet das Kürzel auf Twitter "#golang". Trotz allem lautet der Name der Sprache schlicht Go.

Nebenbei bemerkt: Auch wenn das offizielle Logo aus zwei Großbuchstaben besteht, wird der Name der Sprache als Go und nicht als GO geschrieben.

Warum eine neue Sprache?

Go entstand aus Frust über die existierenden Sprachen und Umgebungen, die wir für unsere Arbeit bei Google zur Verfügung hatten. Programmieren war zu schwierig geworden, und das lag zum Teil an der Wahl der Sprachen. Man konnte nur zwischen effizientem Kompilieren, effizienter Ausführung oder effizientem Entwickeln wählen; alles zusammen waren bei einer der Mainstream-Sprachen nicht 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.

Wir waren nicht allein mit unseren Sorgen. Nachdem es jahrelang ruhig geblieben war im Land der Programmiersprachen, gehörte Go zu dem ersten vom mehreren neuen Sprachen — Rust, Elixir, Swift und weiteren — die die Sprachentwicklung wieder zu einem aktiven, beinahe Mainstream-Bereich machten.

Go begenete diesen Problemen mit dem Versuch, die Bequemlichkeit einer interpretierten Sprache mit dynamischen Typen zu verbinden mit der Effizienz und Sicherheit einer kompilierten Sprache mit statischen Typen. Sie sollte auch modern sein, also mit Unterstützung für Netzwerk- und Mehrkernrechnen. Schließlich sollte 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), und einiger Konzepte 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?

Zu der Zeit als Go entworfen wurde, wurden zum Schreiben von Servern gewöhnlich Java und C++ verwendet, zumindest war das bei Google so. Wir empfanden, dass die Arbeit mit diesen Sprachen nach zu viel Buchhaltung und Wiederholung verlangte. Einige Programmierer reagierten, indem sie sich dynamischeren und flexibleren Sprachen wie Python zuwandten, zu Lasten von Effizienz und Typsicherheit. Wir meinten, dass es möglich sein sollte, Effizienz, Sicherheit und Flexibilität in einer einzigen Sprache zu vereinen.

Go versucht den Aufwand fürs Tippen ebenso zu verringern wie den fürs Typisieren. In der Entwurfsphase 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 weiteres 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. In Googles Produktionssystem wird Go ausgiebig genutzt. Ein einfaches Beispiel ist der Server hinter golang.org. Er ist nichts anderes als der godoc-Dokumentenserver, der konfiguriert für die Produktion auf der Google App Engine läuft.

Bedeutender ist der Einsatz in Googles Download-Server, dl.google.com, welcher Binärdateien für Chrome ausliefert, sowie andere große installierbare Dateien wie etwa apt-get-Pakete.

Go ist bei Weitem nicht die einzige Sprache bei Google, doch sie ist eine der Haupt-Sprachen in mehreren Einsatzbereichen, inklusive dem "site reliability engineering (SRE)" sowie der Verarbeitung von Massendaten.

Welche anderen Firmen setzen Go ein?

Weltweit wächst der Einsatz von Go, insbesondere — aber nicht nur — im Bereich Cloud-Computing. Zwei wichtige Cloud-Infrastrukturprojekte sind Docker und Kubernetes, doch es gibt noch viele weitere.

Und es ist nicht nur die Cloud. Im Go-Wiki gibt es eine regelmäßig aktualisierte Seite, die eine Liste von Firmen führt, die Go nutzen.

Im Wiki gibt es außerdem eine Seite mit Links zu Erfolgsgeschichten über Firmen und Projekte, in denen unsere Sprache eingesetzt wird.

Man kann C und Go im selben Adressraum zusammen verwenden, nur passen sie nicht "natürlich" zusammen und es kann spezielle Schnittstellen-Software vonnöten sein. Außerdem, wenn man C und Go zusammenbindet, verzichtet man damit auf die Speichersicherheit und die Stapelverwaltung (stack management), die Go bietet. Die Verwendung von C-Bibliotheken kann absolut notwendig sein, um ein Problem zu lösen, doch wenn man das tut, ist damit immer ein Risiko verbunden, das es bei reinem Go-Kode nicht gäbe — tun Sie es nur mit Sorgfalt!

Wenn Sie C und Go zusammen verwenden müssen, hängt die Vorgehensweise von der Compiler-Implementierung ab. Es gibt drei Implementierungen des Go-Compilers, die vom Go-Team unterstützt werden. Dies sind gc, der Standard-Compiler, gccgo, welcher das GCC-Backend benutzt, sowie der etwas weniger ausgereifte gollvm, der die Infrastruktur von LLVM nutzt.

Gc arbeitet mit anderen Aufrufkonventionen und mit einem anderen Binder als C und kann deshalb nicht direkt von C-Programmen gerufen werden ... und umgekehrt. Das Kommando cgo stellt den Mechanismus für eine Fremdfunktionsschnittestelle (FFI) zur Verfügung, die sicheres Rufen aus Go heraus in C-Bibliotheken ermöglicht. SWIG dehnt diese Fähigkeit auf C++-Bibliotheken aus.

Cgo und SWIG können Sie auch zusammen mit Gccgo und gollvm benutzen. Weil diese eine traditionelle Schnittstelle benutzen, ist es — mit größter Sorgfalt — auch möglich, Kode dieser Compiler direkt mit von GCC oder LLVM kompilierten C- oder C++-Programmen zu binden. Jedenfalls muss man, um so etwas sicher tun zu können, die Aufrufkonventionen aller beteiligten Sprachen verstehen und sich der Stapelbegrenzung beim Aufruf von C oder C++ von Go aus bewusst sein.

Welche IDEs werden von Go unterstützt?

Das Go-Projekt hat keie eigene angepasste IDE (Integrierte Entwicklungsumgebung), aber Sprache und Bibliotheken wurden so gestaltet, dass Quellkode einfach zu analysieren ist. Als Konsequenz unterstützen die meisten bekannten Editoren und IDEs Go, entweder direkt oder über ein Plugin.

Zu den bekannten IDEs und Editoren, die Go gut unterstützen, zählen Emacs, Vim, VSCode, Atom, Eclipse, Sublime, IntelliJ (über eine angepasset Variante namens Goland) und viele weitere. Die Chancen stehen nicht schlecht, dass auch Ihre bevorzugte Umgebung sich produktiv fürs Go-Programmieren nutzen lässt.

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 (oder bei einigen Spezialimplementierungen zu JavaScript oder WebAssembly). 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?

Als wir Go entwarfen, war es uns wichtig, dass es nicht allzu ASCII-zentrisch wurde; der Raum für Bezeichner sollte von den 7 Bit der ASCII-Beschränkung befreit werden. 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 es gibt auch Grenzen. Zum Beispiel werden zusammengesetzte Zeichen bewusst ausgeschlossen, was Sprachen wie zum Beispiel Devanagari außen vor lässt.

Diese Regel hat auch eine andere bedauerliche Folge. Weil exportierte Bezeichner mit einem Großbuchstaben beginnen müssen, können Bezeichner aus Buchstaben bestimmter Sprachen definitionsgemäß nicht exportiert werden. Bis auf weiteres ist die einzige Lösung etwas in der Art von X日本語, was ganz klar unbefriedigend ist.

Schon von der ersten Sprachversion an haben wir ernsthaft darüber nachgedacht, wie wir am besten den Zeichenraum für Bezeichner erweitern könnten, um auch Programmierern anderer Muttersprache entgegenzukommen. Was jetzt noch zu tun ist, bleibt aktives Diskussionsthema; zukünftige Versionen der Sprache könnten die Regeln für die Definition von Bezeichnern weiter liberalisieren. Zum Beispiel könnte wir uns ein paar der Empfehlungen der Unicode-Organisation für Bezeichner zueigen machen. Egal, was getan wird, es muss kompatibel geschehen und gleichzeitig die Art, wie Groß-Klein-Schreibung die Sichtbarkeit von Bezeichnern festlegt, entweder erhalten oder eventuell erweitern; denn die bleibt für uns eine der vier wichtigsten Eigenschaften von Go.

Bis es soweit ist, 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.

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.

Go war als eine Sprache für Server-Programme gedacht, die über lange Zeit einfach zu warten sein sollten. (Dieser Aufsatz enthält Hintergrundinformationen.) Das Sprachdesign hatte vor allem Skalierbarkeit, Lesbarkeit und Nebenläufigkeit im Blick. Programmieren mit Polymorphien schienen uns für die Ziele der Sprache nicht wichtig, und so wurde das zugunsten von Einfachheit weggelassen.

Die Sprache ist inzwischen reifer geworden und es gibt Spielraum, über die eine oder andere Form von generischem Programmieren nachzudenken. Wie dem auch sei, es gibt noch immer Vorbehalte.

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. Weiterhin beschreibt der Blog-Beitrag "Errors are values" eine Herangehensweise zum sauberen Behandeln von Fehlern in Go, die zeigt, wie Go's ganze Stärke bei der Fehlerbehandlung zum Tragen kommt, weil Fehler ja einfach nur Werte sind.

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 haben sich einen Ruf als schwierig erworben. Wir glauben, dass das teilweise am komplizierten Aufbau liegt, wie bei "pthreads", und der 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.Ü.]

Zugriffe auf Maps sind nur dann unsicher, wenn gleichzeitig aktualisiert wird. Solange alle Goroutinen nur Elemente der Map lesen ‐ dazu gehört auch das Iterieren mit einer for range-Schleife, ohne dass Elemente geschrieben oder gelöscht werden ‐ solange sind konkurrierende Map-Zugriffe auch ohne Synchronisation sicher.

Einige Sprachimplementierungen enthalten, als Hilfestellung für korrektes Benutzen von Maps, eine spezielle Prüfung, die zur Laufzeit automatisch benachrichtigt, wenn eine Map nebenläufig unsicher geändert wird.

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, zumindest nicht auf der Ebene des Quellkodes. (Es kann sein, dass Programme hin und wieder neu kompiliert werden müssen, um aktuell zu bleiben.) Ein zukünftiges neues Haupt-Release von Go mag mit Go 1 inkompatibel werden, doch die Diskussion zu diesem Thema hat gerade erst begonnen. Eins ist sicher: Es wird nur sehr wenige solcher Inkompatibilitäten geben. Darüberhinaus drängt uns das Kompatibilitätsversprechen, einen automatischen Mechanismus für alte Programme sicherzustellen, sollte die Situation es erfordern.

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 go doc 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. Die Sprachbeschreibung verbietet das, weil die beiden Typen im Speicher unterschiedlich aufgebaut sind. 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 als zwei Elemente implementiert, einen Typ T und einen Wert V. V ist ein konkreter Wert wie etwa ein int, eine Struktur (struct) oder ein Zeiger, niemals aber selbst ein Interface, und es ist vom Typ T. Wenn wir zum Beispiel den int-Wert 3 in ein Interface stecken, sieht das resultierende Interface schematisch so aus: T=int, V=3. 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 — (T=nil und V nicht gesetzt). Ein nil-Interface enthält also immer nil für den Typ. Wenn wir einen nil-Zeiger vom Typ *int in einem Interface-Wert speichern, so wird der Typ darin T=*int sein, ganz unabhängig vom Wert des Zeigers: (*int, V=nil). So ein Interface-Wert ist also auch dann non-nil, wenn der Zeigerwert V 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 (T=*MyError, V=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.

Wie funktionieren Konstanten in Go?

Wenn auch Go bei der Konversion zwischen Variablen unterschiedlicher numerischer Typen sehr streng vorgeht, so ist es, was Konstanten angeht, sehr viel flexibler. Konstantenliterale wie 23, 3.14159 und math.Pi bewohnen eine Art idealen Zahlenraum mit beliebiger Genauigkeit und ohne Über- oder Unterlauf. Zum Beispiel ist der Wert von math.Pi im Quellkode auf 63 Stellen genau angegeben, und konstante Ausdrücke, die diesen Wert enthalten, konservieren die Genauigkeit weit über das Maß, das ein float64 abbilden kann. Erst wenn eine Konstante oder ein konstanter Ausdruck einer solchen Variablen — einem Speicherort im Programm — zugewiesen wird, wird sie zu einer "Computerzahl" mit den üblichen Gleitkomma-Eigenschaften und der üblichen Genauigkeit.

Und weil sie einfach nur Zahlen sind, und keine typbehafteten Werte, kann man Konstanten in Go flexibler einsatzen als Variablen, und dabei etwas von der Schwerfälligkeit der strikten Konversionsregeln abmildern. Man kann Ausdrücke wie

sqrt2 := math.Sqrt(2)

schreiben, ohne dass der Compiler meckert, weil die ideale Zahl 2 für den Aufruf von math.Sqrt sicher und genau zu einem float64 konvertiert werden kann.

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 und sie als Web-Seite bereitstellt, komplett mit Links zu Deklarationen, Dateien und so weiter. 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?

Ein Richtliniendokument zum Stil gibt es keins, doch es gibt sicherlich einen erkennbaren "Go-Stil".

Go hat eine Reihe von Konventionen begründet, die Richtschnur sind für Namensgebung, Layout und Dateiorgnisation. Der Artikel "Effective Go" (de) enthält Ratschläge zu diesen Themen. 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 und auch das Allermeiste druaßen in der Open-Source-Welt wurde mit gofmt behandelt.

Das Dokument mit dem Titel "Go Code Review Comments" ist eine Sammlung sehr kurzer Aufsätze über Einzelheiten des Go-Stils, die von Programmierern gerne übersehen werden. Es ist ein praktischer Leitfaden für Leute, 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.

Man kann git so konfigurieren, dass es sich über HTTPS authentifiziert oder SSH anstelle von HTTPS benutzt. Für die Authentifizierung über HTTPS können Sie der von Git benutzte Datei $HOME/.netrc eine Zeile hinzufügen:

machine github.com login USERNAME password APIKEY

Für GitHub-Konten kann das Kennwort ein "personal access token" sein.

Git kann auch so konfiguriert werden, dass es für URLs mit einem bestimmten Präfix SSH anstelle von HTTPS verwendet. Um zum Beispiel SSH für alle GitHub-Zugriffe zu benutzen, fügen Sie Ihrem ~/.gitconfig folgende Zeilen hinzu:

[url "ssh://git@github.com/"]
	insteadOf = https://github.com/

Wie kombiniere ich Paketversionen mit "go get"?

Seit Beginn des Projekts gab es für Go kein explizites Konzept für Paketversionen, doch das ändert sich gerade. Versionierung ist Quelle ganz erheblicher Komplexität, besonders, wenn viel Kode beteiligt ist, und es hat eine ganze Weile gedauert, bis wir eine Vorgehensweise entwickelt hatten, die auch im großen Rahmen für genügend viele verschiedene Situationen passt; sie wird jetzt allen Benutzern von Go zur Verfügung gestellt.

Im Release 1.11 bringt Go neue, experimentelle Unterstützung für Paketversionierung durch das Kommando go mit dem Konzept der Go-Module. Weitere Information finden Sie in den "Go 1.11 release notes" und in der Dokumentation des go-Kommandos.

Unabhängig von der aktuellen Paketverwaltungstechnik behandeln "go get" und der darüber hinausgehende Go-Werkzeugsatz Pakete mit verschiedenen Importpfade jeweils für sich. Zum Beispiel existieren in der Standardbibliothek html/template und text/template nebeneinander obwohl beide ein "package template" sind. Daraus ergeben sich verschiedene Ratschläge für Paketautoren und Paketbenutzer.

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, und wenn Sie noch keine Go-Module benutzen, so ist am einfachsten, Sie kopieren es in ihr lokales Repositorium. So macht das Google intern auch und das wird vom go-Kommando unterstützt durch eine Technik, die "Vendoring" genannt wird. Dazu gehört, dass Sie eine Kopie der fremden Abhängigkeit in einem neuen Importpfad speichern, der es als lokale Kopie erkennen lässt. Einzelheiten finden Sie in diesem "design document.

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.

Es ist ein verbreiteter Fehler, 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 sind in Java die Methodenempfänger immer Zeiger, nur dass ihre Zeigernatur verschleiert ist (und es gibt der Vorschlag, die Sprache um Wert-Empfänger zu erweitern). Ungewöhnlich sind also 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. Auf 32-Bit-Maschinen benutzen die Compiler als Standard 32-Bit-Ganzzahlen, während es auf 54-Bit-Maschinen 64-Bit-Ganzzahlen sind. (Das war allerdings nicht immer so.)

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 (macOS).

Nebenläufigkeit

Welche Operationen sind unteilbar? Was ist mit Zugriffssperren?

Eine Beschreibung zur Atomizität von Operationen in Go findet man im "The Go Memory Model".

Kleinteilige Synchronisation sowie atomische Grundbausteine stehen mit den Paketen sync und sync/atomic zur Verfügung. Diese Pakete eignen sich für einfache Aufgaben, wie etwa das Erhöhen von Referenzzählern, oder um wechselseitige Sperren (Mutexe) im kleinen Maßstab zu gewährleisten.

Für Operationen auf höherer Ebene, wie etwa das Koordinieren nebenläufiger Server, führen meist höher entwickelte Techniken zu hübscheren Programmen, und Go unterstützt auch das durch seine Goroutinen und Kanäle. Zum Beispiel können Sie Ihr Programm so strukturieren, dass immer nur eine Goroutine zu einem Zeitpunkt für eine Portion der Daten zuständig ist. Diese Herangehensweise wird komprimiert beschrieben durch das Original-Go-Sprichwort:

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.

Große nebenläufige Programme werden sich wahrscheinlich beider Werkzeugkisten bedienen.

Why doesn't my program run faster with more CPUs?

Ob ein Programm auf mehreren CPUs schneller läuft, hängt von dem zu lösenden Problem ab. Go bietet Grundfunktionen für Nebenläufigkeit, wie Goroutinen und Kanäle, aber Nebenläufigkeit macht Parallelverarbeitung nur dann möglich, wenn das zu lösende Problem an sich schon parallel ist. Probleme sequentieller Natur, können durch mehr CPUs nicht schneller gelöst werden; nur solche Probleme, die man in parallel bearbeitbare Teilprobleme aufteilen kann, können schneller, manchmal dramatisch viel schneller beabeitet werden.

Manchmal wird ein Programm sogar langsamer, wenn man ihm mehr CPUs zuweist. Konkret kann die Performanz eines Programms, das mehrere Betriebssystem-Threads nutzt, dann leiden, wenn es mehr Zeit für's Synchronisieren braucht als für nützliche Rechenarbeit. Grund ist, dass beim Übergeben von Daten von Thread zu Thread jeweils die Umgebung ausgetauscht werden muss (context switch), was merkliche Kosten verursacht. Und diese Kosten steigen mit mehr CPUs. Beispielsweise ist das Beispielprogramm aus der Sprachbeschreibung, das Primzahlensieb von seiner Natur nur wenig parallel, startet aber eine Vielzahl von Goroutinen; erhöht man die Anzahl der Threads (CPUs), so läuft das Programm eher langsamer als schneller.

Einzelheiten dazu erfährt man in diesem Vortrag mit dem Titel Concurrency is not Parallelism.

Wie kann ich die Anzahl der CPUs steuern?

Die Anzahl der CPUs, die gleichzeitig für das Abarbeiten von Goroutinen zur Verfügung steht, wird durch die Umgebungsvaiable GOMAXPROCS gesteuert; Voreinstellung ist die Anzahl der verfügbaren CPU-Kerne. Programme mit einem Potential für parallele Ausführung müssten es eigentlich durch die Voreinstellung schon ausschöpfen. Um die Anzahl der zu nutzenden CPUs zu ändern, setzen Sie die Umgebungsvariable entsprechend, oder benutzen die Funktion ähnlichen Namens aus dem Paket runtime, um die Laufzeitunterstützung zum Nutzen unterschiedlicher Thread-Anzahlen einzurichten. Setzt man sie auf 1, so verhindert man die Möglichkeit echten Parallelismus' und zwingt die Goroutinen, immer nur abwechselnd zu laufen.

Die Laufzeitsystem kann mehr Threads allozieren, als der Wert von GOMAXPROCS angibt, um mehr offene Ein-/Ausgabeanforderungen bedienen zu können. GOMAXPROCS beeinflusst nur, wie viele Goroutinen tatsächlich gleichzeitig ausgeführt werden können; beliebig viele andere können gerade durch Systemaufrufe blockiert sein.

Go's Ablaufmanager ist noch nicht so gut, wie er sein sollte, doch er ist im Laufe der Zeit schon besser geworden. Zukünftig sollte er Betriebssystem-Threads noch besser nutzen. Bis dahin, und wenn es Performanzprobleme gibt, sollte das Setzen von GOMAXPROCS auf Programmebene weiterhelfen.

Warum gibt es keine Goroutinen-ID?

Goroutnen haben keine Namen; sie sind anonyme Arbeitseinheiten. Sie offenbaren den Programmierern weder einen eindeutigen Bezeichner noch einen Eigennamen und auch keine eigene Datenstruktur. Das mag überraschen, denn man erwartet vielleicht von der Anweisung go, dass sie irgendetwas zurückgibt, das in Folge für Zugriff auf und Kontrolle über die Goroutine benutzt werden könnte.

Der tiefere Grund für die Anonymität der Goroutinen lautet, dass man so auch in nebenläufigem Kode die Sprachmittel von Go komplett nutzen kann. Denn im anderen Fall, wenn also Threads und Goroutinen Namen hätten, entstünden Programmiermuster, die Bibliotheken in ihren Möglichkeiten einschränken würden.

Hier ein Beispiel für solche Schwierigkeiten: Hat man einer Goroutine erst mal einen Namen gegeben und um diesen Namen herum ein Modell konstruiert, so ist diese Goroutine etwas Besonderes geworden, und man ist versucht, alle Berechnung dieser Goroutine zuzuordnen. Und dabei vernachlässigt man die Chance, mehrere, eventuell sogar zusammenarbeitende Goroutinen für die Verarbeitung einzusetzen. Würde das Paket net/http den Zustand je Anfrage an eine Goroutine binden, so könnten Klienten dieses Pakets zum Bearbeiten der Anfrage keine zusätzlichen Goroutinen mehr einsetzen.

Des Weiteren hat die Erfahrung mit Bibliotheken, die wie solche für grafische Systeme verlangen, dass alle Verarbeitung im "Haupt-Thread" geschehen muss, gezeigt, wie unbeholfen und beschränkt diese Herangehensweise in einer nebenläufigen Sprache ist. Allein die Existenz eines besonderen Threads oder einer besonderen Goroutine zwingt Programmierer, ihre Programme zu verunstalten, nur um zu Abbrüche und andere Probleme zu vermeiden, die auftreten, wenn man versehentlich im falschen Thread arbeitet.

Für Fälle, in denen eine Goroutine tatsächlich etwas Besonderes ist, bietet die Sprache zum Beispiel Kanäle, die ganz flexibel für die Zusammenarbeit mit dieser besonderen Goroutine eingesetzt werden können.

Funktionen und Methoden

Warum haben T und *T verschiedene Methodenmengen?

Wie dDie Go-Sprachbeschreibung (de) sagt, besteht die Methodenmenge eines Typs T aus allen mit Empfängertyp T deklarierten Methoden, während die Methodenmenge des korrespondierenden Zeigertyps *T aus allen mit Empfängertyp *T oder T deklarierten Methoden besteht. Das heißt, die Methodenmenge von *T enthält auch die Methodenmenge von T, aber nicht umgekehrt.

Der Unterschied entspringt folgendem Grund: Wenn ein Schnittstellenwert einen Zeiger *T enthält, so kann ein Methodenaufruf den Wert dazu durch Dereferenzieren des Zeigers ermitteln, doch wenn er einen Wert T enthält, gibt es für den Methodenaufruf keinen sicheren Weg, einen Zeiger dazu zu ermitteln. (Wäre das möglich, so könnte die Methode den Inhalt des Werts in der Schnittstelle verändern, was die Sprachbeschreibung nicht erlaubt.)

Selbst wenn der Compiler die Adresse des an die Methode übergebenen Wertes ermitteln könnte, 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
        }()
    }

Dieses Verhalten der Sprache, also nicht für jede Iteration eine neue Variable zu definieren, scheint im Rückblick eine Fehlentscheidung gewesen zu sein. Das wird vielleicht in einer späteren Version behoben werden, kann sich aber wegen des Kompatibilitätsversprechens für Go 1 nicht mehr ändern.

Kontrollanweisungen

Warum hat Go nicht den ?: Operator?

Es gibt keine ternäre Anweisung in Go. Mit dem Folgenden erhalten Sie dasselbe Ergebnis:

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

?: gibt es in Go nicht, weil die Entwickler der Sprache es zu oft in undurchdringlich komplexen Ausdrücken gesehen haben. Die Form if-else ist länger, aber fraglos klarer. Eine Programmiersprache benötigt nur ein Konstrukt für eine bedingte Kontrollanweisung.

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?

Für Go gibt es für verschiedene Platformen mehrere produktive Compiler und weitere sind in Entwicklung .

Der Standardcompiler gc gehört zur Go-Distribution, um dort das go-Kommando zu unterstützen. Gc war ursprünglich in C geschrieben wegen der Schwierigkeiten beim Lösen des "Henne-Ei-Problems" beim Urladen (bootstrapping) — man braucht einen Go-Compiler bereitsn fürs Einrichten der Go-Umgebung. Inzwischen sind wir weiter und seit dem Release Go 1.5 ist der Compiler ein Go-Programm. Die Konversion von C zu Go wurde mit Übersetzungswerkzeugen erreicht, die in diesem Design-Dokument beschrieben und über die in diesem Vortrag gesprochen wird. Der Compiler ist jetzt "selbstbezüglich" (self-hosting), was bedeutet, dass wir uns mit dem Henne-Ei-Problem auseinandersetzen mussten. Die Lösung ist, dass eine arbeitsfähige Go-Installation bereits vorhanden sein muss, genauso, wie man normalerweise eine arbeitsfähige C-Installation zur Verfügung hat. Wie man eine neue Go-Umgebung ausgehend vom Quellkode einrichtet, ist hier und hier beschrieben.

Das in Go geschriebene gc hat einen rekursiv absteigenden Parser und benutzt einen angepassten Lader, ebenfalls in Go geschrieben, der auf dem Lader aus Pan 9 basiert, und ELF/Mach-O/PE-Binärdateien erzeugt.

Zu Beginn des Projekts dachten wir daran, LLVM für gc zu nutzen, entschieden dann aber, dass es zu groß und zu langsam für unsere Performanzziele sei. Im Rückblick erscheint allerdings wichtiger, dass eine Entscheidung für LLVM uns die von Go benötigte Systemschnittstelle (ABI) und damit zusammenhängende Änderungen wie die Stapelverwaltung schwer gemacht hätte; das alles geht über den üblichen C-Standard hinaus. Inzwischen allerdings kommt eine neue LLVM-Implementierung dem näher.

Der gccgo-Compiler ist ein in C++ geschriebenes "Front-End" mit einem rekursiv absteigenden Parser, das an das Standard-"GCC-Back-End" gekoppelt ist.

Go erwies sich schließlich doch als feine Sprache, um damit einen Go-Compiler zu implementieren, auch wenn das keines der ursprünglichen Ziele war. Dass Go zu Beginn nicht selbstbezüglich geplant war, erlaubte, das Design am ursprünglichen Anwendungsfall auszurichten, also an Netzwerk-Servern. Hätten wir anders entschieden, wären wir vielleicht bei einer Sprache gelandet, die besser für die Compiler-Konstruktion geeignet wäre — ein ehrenwertes Ziel, aber nicht unser ursprüngliches.

Wenn auch (noch?) nicht vom gc-Compiler benutzt, so gibt es bereits einen eigenen Lexer und einen eigenen Parser im Paket go und außerdem ein eigenes Typprüfpaket.

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 Stapel" nennt; diese wiederum werden durch neueste Änderungen am Gold-Linker ermöglicht. Ähnlich ist gollvm auf die entsprechende LLVM-Infrastruktur aufgebaut.

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 wurd, ist unter Linux ca. 750 KB groß, inklusive printf. Ein entsprechendes Go-Programm mit fmt.Printf wiegt etliche Megabyte, bietet aber sehr viel mehr Laufzeitunterstützung sowie Typ- und Debug-Informationen.

Ein Go-Programm, dass mit gc kompiliert wurde, kann man mit dem Schalter -ldflags=-w binden, um das Generieren von DWARF auszuschalten; damit werden Debugging-Informationen aus der Binärdatei entfernt ohne dabei Funktionalität zu verlieren. Das kann die Größe der Binärdatei deutlich reduzieren.

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.

Warum glaubt mein Virenerkennungsprogramm, meine Go-Destribution oder meine mit Go erzeugte Binärdatei sei virenverseucht?

Das passiert häufig, besonders auf Windows-Maschinen, ist aber meistens blinder Alarm. Kommerzielle Virenerkennungsprogramme werden oft verwirrt von der Struktur von Go-Binärdateien, die sie nicht so häufig zu Gesicht bekommen, wie die von anderen Programmiersprachen.

Wenn Sie Go gerade erst installiert haben und Ihr System meldet eine Infektion, dann ist das mit Sicherheit ein Irrtum. Um ganz sicher zu gehen, können Sie Ihr Go-Exemplar verifizieren, indem Sie die Prüfsumme mit der auf unserer Download-Seite vergleichen.

Wenn Sie glauben, dass die Meldung blinder Alarm war, schicken Sie bitte einen Fehlerbericht an den Hersteller ihres Virenscanners. Vielleicht hilft das den Virenscannern, mit der Zeit auch Go-Programme zu verstehen.

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 andere 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?

Einer der größten Buchhaltungposten der Systementwicklung ist die Kontrolle der Lebensdauer von Objekten, für die Speicherplatz alloziert wurde. In einer Sprache wie C, wo das händisch getan wird, kann das einen bedeutenden Teil der Arbeitszeit der Programmierer fressen und ist oft die Quelle bösartiger Fehler. Und sogar in Sprachen wie C++ oder Rust, die hilfreiche Mechanismen zur Verfügung stellen, haben gerade diese Mechanismen Auswirkungen auf das Design der Software, wobei oft zusätzlicher Programmieraufwand entsteht. Wir hielten es für wichtig, dass dieser Wasserkopf entfernt würde. Die Fortschritte in der Technik des Müllsammelns über die letzten Jahre machten uns zuversichtlich, dass wir eine automatische Speicherbereinigung mit genügend geringem Aufwand und ohne signifikante Latenzen implementieren könnten, so dass sie für Netzwerksysteme praktikabel sein würde.

Ein Großteil der Schwierigkeiten nebenläufiger oder Mehrstrang-Programmierung erwächst aus dem Problem der Lebensdauer von Objekten; wenn Objekte zwischen Verarbeitungssträngen (threads) ausgetauscht werden, wird es mühsam, 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 nur einmal anzugehen, statt in jedem Programm wieder, hilft jedem.

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

Das soll jetzt nicht heißen, dass aktuelle Arbeiten an Sprachen wie Rust, welche mit neuen Ideen das Problem der Ressourcenverwaltung angehen, fehlgeleitet wären; wir möchten zu dieser Arbeit ermuntern und freuen uns darauf, die Entwicklung zu beobachten. Go allerdings nimmt den traditionelleren Weg und geht das Lebensdauerproblem mit einer automatischen Speicherbereinigung an, und nur damit.

Die derzeitige Implementierung ist ein Markier-und-Fege-Sammler (mark-and-sweep collector). Hat die Maschine einen Mehrkernprozessor, dann läuft der Müllsammler in einem separaten CPU-Kern parallel zum eigentlichen Programm. Größere Arbeiten am Sammler hat in den letzten Jahren die Pausenzeiten deutlich reduziert, oft in den Bereich unter einer Millisekunde, selbst für große Stapelspeicherbereiche; damit ist eine der Hauptbedenken gegen Müllsammler in Netzwerksystemen so gut wie eliminiert. Und die Arbeit geht weiter, um den Algorithmus weiter zu verfeinern, um Mehraufwand und Latenzzeiten weiter zu reduzieren und um neue Verfahren zu erforschen. Dieser ISMM-Eröffnungsvortrag von Rick Hudson vom Go-Team beschreibt den bisherigen Weg und schlägt ein paar neue Ansätze für die Zukunft vor.

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.