Frequently Asked Questions (FAQ) — Deutsche Übersetzung
- Das Original:
-
https://golang.org/doc/faq
Version of February 25, 2021 - Diese Übersetzung:
-
https://bitloeffel.de/DOC/golang/go_faq_20210817_de.html
Stand: 04.08.2021
© 2014-21 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. Stottern
(wie in: 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.
Lassen sich Go-Programme mit C/C++-Programmen binden?
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?
Ein Implementierungsvorschlag für eine Art generischer Typen zur Ergänzung der Sprache wurde angenommen. Wenn alles gut geht, werden generische Typen ab Go 1.18 zur Verfügung stehen.
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.
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"?
Der Go-Werkzeugsatz enthält ein standardisiertes System zum Verwalten von versionierten Mengen von Paketen, die als Module bekannt sind. Module wurden in Go 1.11 eingeführt und sind für den Produktionsbetrieb einsatzbereit seit Go 1.14.
Ein Projekt, das Module nutzen soll, erzeugt man mit
go mod init
Dieses Kommando legt eine go.mod
-Datei an, die
die Versionsabhängigkeiten beobachtet.
go mod init example.com/project
Um eine Abhängigkeit zu ergänzen, oder hoch- oder herunterzustufen
ruft man go get
:
go get golang.org/x/text@v0.3.5
"Tutorial: Create a module" liefert weitere Infos, wie man Module ans Laufen bringt.
"Developing modules" gibt Ratschläge zum Verwalten von Abhängigkeiten mithilfe von Modulen.
Pakete, die Teil von Modulen sind, sollten, während sie sich entwickeln, rückwärtskompatibel bleiben — und zwar gemäß der Import-Kompatibilitätsregel:
Haben ein altes und ein neues Paket denselben Importpfad,
so muss das neue Paket zu dem alten Paket rückwärtskompatibel sein.
Hier sind die "Go 1 Kompatibilitätsrichtlinien" hilfreich: Erhalten Sie einmal 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 anstatt das Verhalten des alten zu ändern.
Module können sowas festklopfen mithilfe der
semantischer Versionierung
zusammen mit semantischer Importversionierung.
Wird eine inkompatible Änderung nötig, so gibt man eine neue
Hauptversion (major version) des Moduls frei.
Module mit einer Hauptversion von 2 und höher benötigen ein
Hauptversionssuffix als Teil ihres Pfades, etwa
/v2
. Dies erfüllt die Import-Kompatibilitätsregel:
Pakete in verschiedenen Hauptversionen eines Moduls haben
verschiedene Pfade.
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.