Aus Linux-Magazin 06/2010

Google erfindet neue Programmiersprache

© Helmut-Wegmann, Pixelio.de

Von Google sind mehr als nur Suchergebnisse zu erwarten. Nun stellt sich der Suchmaschinenriese sogar bei den Programmiersprachen in die Startblöcke – glückt der schnelle Antritt ?

Es gibt systemnahe Programmiersprachen wie C und C++ sowie Sprachen, die unabhängig vom Betriebssystem funktionieren und in einer virtuellen Umgebung laufen wie Java. Zu unterscheiden sind interpretierte Sprachen wie Python und Ruby gegenüber solchen, die von Quell- in Binärcode zu übersetzen sind. Diese Liste lässt sich ziemlich lang fortführen und jeder Entwickler hat sich wohl auch schon mal seine Gedanken über eine von ihm zu erfindende Sprache gemacht.

Neustart

Auf den ersten Blick sieht es daher so aus, als ob der Platzhirsch Google sich nur aus Prinzip in einem weiteren Sektor platzieren wollte. Bei genauerem Hinsehen eröffnen sich jedoch einige interessante Konzepte, die es verdienen, sich näher mit der neuen Programmiersprache Go auseinanderzusetzen.

Der bei Google angestellte C- und Unix-Veteran Rob Pike erklärt in seinem Vortrag [1] das Jahr 2007 zum Geburtsjahr der Idee zu Go. Gemeinsam diskutierte er mit Ken Thompson, mit dem er schon am Betriebssystem Plan 9 arbeitete [2], und anderen Entwicklern, wie sie den Spaß ins Programmieren zurückbringen könnten. Ihr Eindruck war, dass sich an der Art der Programmiersprachen in den letzten Jahren nicht viel geändert hat, die Anforderungen an jene sich hingegen aber sehr wohl verschieben. So begreifen die Entwickler heute das Netzwerk als zentrales Element, setzen Cluster ein und entwickeln in verschiedenen Schichten und Abstraktionsebenen. Besonderes Augenmerk richten sie auf Mehrkernprozessoren und die daraus resultierende Nebenläufigkeit.

Google erklärt daher eine leichtere Kommunikation zwischen Komponenten und eine intuitiv zu verwendende Form der Nebenläufigkeit, in Englisch Concurrency genannt, zum Ziel der Sprache. Außerdem soll ein Garbage Collector und nicht der Programmierer das Aufräumen des Speichers übernehmen. Weitere Anforderung war das schnelle Übersetzen des Quellcode in den durch das System ausführbaren Binärcode [3].

Umgebungsgestaltung

Die Go-Programmierwerkzeuge gibt es im Quelltext aus einem Repository auf der Projekt-Homepage [4]. Nachdem der Anwender die verschiedenen Go-Compiler übersetzt hat (siehe Kasten “Installation”), baut er die Bibliotheken. Da die in Go selbst vorliegen, profitiert der Entwickler bereits von der hohen Geschwindigkeit des Google-Babys – die gesamte Bibliothek lässt sich in wenigen Sekunden übersetzen.

Installation

Blockiert während der Installation eine Firewall den Zugriff auf das Internet, so muss sich der Entwickler zurzeit noch eines Workaround bedienen. Er deaktiviert im Makefile die Tests für die Subsysteme »http« und »net«, sodass deren Ausführungsergebnisse keine Voraussetzung für den Erfolg der Gesamtinstallation sind [5]. Dazu erweitert er den Wert der Variablen »NOTEST« in der Datei »$GOROOT/src/pkg/Makefile« um die Einträge »http« und »net«.

Zum Kompilieren stehen zurzeit zwei Werkzeuge zur Verfügung, der »gc«-Compiler sowie »gccgo«. Letzterer interagiert mit dem C-Compiler von GNU, ist jedoch nicht so weit fortgeschritten wie die »gc«-Werkzeuge, deren Namensschema von Plan 9 herrührt. Die Zahl kennzeichnet die Plattform, wobei »5« für ARM, »6« für 64-Bit- und »8« für 32-Bit-x86-Systeme stehen. Der Buchstabe kennzeichnet das Tool selbst (siehe Tabelle 1).

Tabelle 1:
Ausführbare 64-Bit-Go-Tools

 

Werkzeugname

Kurzbeschreibung

6a

Go-Assembler

6c

Go-C-Compiler (»cgo« benötigt das Tool)

6g

Go-Compiler

6l

Go-Linker

cgo

Erzeugt Pakete und Programme, die C-Code aufrufen

Den Kompilier- und Linkvorgang demonstriert Abbildung 1, das obligatorische Hello-World-Programm zeigt Listing 1. Zeile 3 enthält ein von Python und Java bekanntes »import«-Statement, das dort die Bibliothek »fmt« einbindet. Go fordert, dass das Programm wie bei C oder C++ eine Funktion »main()« als Einsprungspunkt besitzt. Die Funktion »Println()« von »fmt« gibt schließlich den Text aus. Dass die Funktion von den Entwicklern der Bibliothek einen großen Anfangsbuchstaben erhalten hat, ist kein Zufall. Eine wichtige Eigenschaft von Go ist es nämlich, dass Variablen und Funktionen automatisch global sichtbar werden, sofern deren Name einen Großbuchstaben an erster Stelle besitzt.

Abbildung 1: Die Kommandos zum Kompilieren und Linken eines Go-Programms lehnen sich an die zunächst ungewohnte Schreibweise von Plan 9 an.

Abbildung 1: Die Kommandos zum Kompilieren und Linken eines Go-Programms lehnen sich an die zunächst ungewohnte Schreibweise von Plan 9 an.

Listing 1:
»hello.go«

01 package main
02 
03 import "fmt"
04 
05 func main () {
06   fmt.Println("Hallo, Welt!")
07 }

Einfacher Einstieg

Zusätzliche Syntax benötigt Go im Gegensatz zu anderen Hochsprachen nicht. Der Strichpunkt dient außerdem nicht als Abschluss einer Anweisung, sondern als Trennzeichen, vergleichbar mit einer Liste oder seiner Bedeutung in Pascal. Da in diesem Beispiel nur eine Anweisung erfolgt, ist auch das Semikolon überflüssig. Go erbt stark von diversen Sprachen, darunter C, C++ und Python. Dennoch unterscheidet sich die Syntax an einigen Stellen. Google bietet sowohl eine allgemeine Einführung in die Sprachsyntax [6] wie auch eine Übersicht weiterführender Themen an [7].

Die Client-Server-Fähigkeiten demonstriert ein kleines Programmierprojekt, das den Grundstock für einen Mini-Chat realisiert: Der Serverprozess lauscht auf einem bestimmten Port auf TCP-Verbindungen, Clients schicken nach dem Verbindungsaufbau eine Nachricht in einem definierten Format an den Server ab und beenden sich danach wieder.

Serveraufgaben

Der Server, den Listing 2 implementiert, importiert ab Zeile 3 Pakete, die er später als Bibliotheksaufrufe nutzt. Der Code ab Zeile 15 unterstreicht, dass die Sprachentwickler die Reihenfolge der Schlüsselwörter bei der Variablendeklaration umgedreht haben: Dies bringt laut Projektdokumentation klarere Syntax und Ersparnis beim Tippen. Außerdem erkennt Go den Typ automatisch, wenn Deklaration und Initialisierung in einem Schritt erfolgen. Das macht zusätzliche Typbezeichnung überflüssig. Der Code legt zunächst zwei Konstanten vom Standardtyp »int« sowie eine globale Variable vom Typ »int *« an. Die Variable »listenPort« ist damit ein Zeiger. Diese Funktionalität übernahmen Pike & Co. von C/C++.

Listing 2:
»server.go«

001 package main
002 
003 import (
004         "bytes";
005         "encoding/binary";
006         "flag";
007         "fmt";
008         "net";
009         "os";
010         "os/signal";
011         "syscall";
012         "./build/msg/msg"
013 )
014 
015 const (
016         defPort = 7777;
017         bufSize = 1024
018 )
019 
020 var listenPort *int = flag.Int("p", defPort, "port to listen for connections")
021 
022 // wait for incoming tcp connections
023 func acceptor(listener *net.TCPListener, quit chan bool) {
024         var buf [bufSize]byte
025 
026         for {
027                 conn, e := listener.AcceptTCP()
028                 if e != nil {
029                         fmt.Fprintf(os.Stderr, "Error: %vn", e)
030                         continue
031                 }
032 
033                 num, e := conn.Read(&buf)
034                 if num < 0 {
035                         fmt.Fprintf(os.Stderr, "Error: %vn", e)
036                         conn.Close()
037                         continue
038                 }
039 
040                 go handleClient(conn, buf[0:num])
041         }
042 }
043 
044 // handle a single client connection
045 func handleClient(conn *net.TCPConn, bytebuf []byte) {
046         message := new(msg.Message)
047         buf := bytes.NewBuffer(bytebuf)
048 
049         binary.Read(buf, binary.LittleEndian, &message.SenderLen)
050         s := make([]byte, message.SenderLen)
051         buf.Read(s)
052         message.SetSender(string(s))
053         binary.Read(buf, binary.LittleEndian, &message.DataLen)
054         d := make([]byte, message.DataLen)
055         buf.Read(d)
056         message.SetData(string(d))
057 
058         fmt.Printf("%s connectedn > %snn",
059                 message.GetSender(), message.GetData())
060 
061         conn.Close()
062 }
063 
064 // read from signal.Incoming channel and
065 // return SIGINT is received
066 func signalHandler(quit chan bool) {
067         for {
068                 select {
069                 case sig := <-signal.Incoming:
070                         fmt.Printf("Received signal %dn", sig)
071                         if sig.(signal.UnixSignal) != syscall.SIGINT {
072                                 continue
073                         }
074                         quit<- true
075                         return
076                 }
077         }
078 }
079 
080 func main() {
081         flag.Parse()
082         address := fmt.Sprintf("%s:%d", "127.0.0.1", *listenPort)
083         quit := make(chan bool)
084 
085         socket, e := net.ResolveTCPAddr(address)
086         if e != nil {
087                 fmt.Fprintf(os.Stderr, "Error: %vn", e)
088                 os.Exit(1)
089         }
090         listener, e := net.ListenTCP("tcp4", socket)
091         if e != nil {
092                 fmt.Fprintf(os.Stderr, "Error: %vn", e)
093                 os.Exit(1)
094         }
095 
096         go signalHandler(quit)
097 
098         fmt.Printf("Listening on %s:%dnn", socket.IP.String(), socket.Port)
099         go acceptor(listener, quit)
100 
101         for {
102                 select {
103                 case <-quit:
104                         fmt.Printf("Shutting downn")
105                         listener.Close()
106                         return
107                 }
108         }
109 }

Nützliche Module

Listing 3:
»msg.go«

01 package msg
02 
03 type Message struct {
04         SenderLen uint32;
05         sender []byte;
06         DataLen uint32;
07         data []byte
08 }
09 
10 func (m *Message) GetSender() string {
11         return string(m.sender)
12 }
13 
14 func (m *Message) SetSender(s string) {
15         m.sender = stringToBytes(s)
16         m.SenderLen = uint32(len(s))
17 }
18 
19 func (m *Message) GetData() string {
20         return string(m.data)
21 }
22 
23 func (m *Message) SetData(s string) {
24         m.data = stringToBytes(s)
25         m.DataLen = uint32(len(s))
26 }
27 
28 // helper function to convert a given
29 // string to a byte slice
30 func stringToBytes(s string) []byte {
31         slice := make([]byte, len(s))
32 
33         for i := 0; i < len(s); i++ {
34                 slice[i] = s[i]
35         }
36         return slice
37 }

Die Funktion »main()« verarbeitet ab Zeile 80 zuerst die Kommandozeilen-Parameter. Dazu nutzt sie die Funktionen des Pakets »flag«. Eine Eigenheit von Go sind Channels, die die Sprache zur Kommunikation zwischen Goroutines, dem Pendant zu Threads, einsetzt (siehe Abbildung 2). Zeile 83 legt einen neuen Channel an, über den die einzelnen Threads Daten vom Typ »bool« austauschen dürfen. Der Kasten “Make vs. New” beschreibt das zum Anlegen neuer Instanzen verwendete Schlüsselwort »make«.

Abbildung 2: Goroutines kommunizieren über einen Channel. Zum Zeitpunkt t1 liest A aus dem Channel, blockiert daher bis Zeitpunkt t2, zu dem B Daten in den Channel schreibt.

Abbildung 2: Goroutines kommunizieren über einen Channel. Zum Zeitpunkt t1 liest A aus dem Channel, blockiert daher bis Zeitpunkt t2, zu dem B Daten in den Channel schreibt.

Make vs. New

Go kennt zwei Schlüsselwörter für einen ähnlichen Zweck: Programmierer fordern ebenso mit »make« wie mit »new« neuen Speicherplatz an. Das Schlüsselwort »new« – bekannt aus objektorientierten Sprachen wie C++ und Java – fordert Speicherplatz für ein neues Objekt an und liefert als Rückgabewert einen Zeiger auf die erstellte Instanz des angeforderten Objekttyps. Der Parameter von »new« gibt den Typ an, für den Go den Speicherplatz allozieren soll.

»make« verwenden Entwickler hingegen, um Slices, Maps oder Channels anzulegen. Für andere Typen eignet sich dieses Schlüsselwort nicht. Der Rückgabewert ist hierbei nicht ein Zeiger auf eine neue Instanz des übergebenen Datentyps, sondern der Wert selbst. Außerdem wird das resultierende Objekt zuvor intern initialisiert, um es sofort verwenden zu können. Käme für die drei genannten Typen »new« zum Einsatz, würde Go einen »nil«-Pointer liefern, da niemand die zugrunde liegenden Datenstrukturen initialisiert [7].

Zudem erlaubt »make« weitere Argumente. Bei Slices sind das die Länge sowie die Kapazität des betreffenden Feldes. Erstere bezeichnet die aktuelle Länge des Slice beim Erstellen, letztere kennzeichnet die Länge des zugrunde liegenden Feldes, also jene Länge, auf die der Slice anwachsen kann. So erzeugt zum Beispiel Zeile 50 von Listing 2 einen Byte-Slice, dem ein Array der Länge »message.SenderLen« zugrunde liegt.

Auch wenn die Unterschiede schlüssig scheinen, bleibt doch die Frage, warum die Google-Entwickler zwei Schlüsselwörter für so ähnliche Aufgaben implementieren. Vor allem Anfängern fällt die genaue Unterscheidung schwer.

Die von Sprachen wie Pascal übernommene Syntax »:=« bietet eine kürzere Variante der Variablendeklaration mit gleichzeitiger Initialisierung. Das Programm verwendet dieses Idiom, um in den Zeilen 85 und 90 einen Socket sowie einen TCP-Handler mit Hilfe der »net«-Bibliothek anzulegen. Funktionen können in Go mehrere Rückgabewerte liefern, daher die Liste von Variablen auf der linken Seite der Zuweisung in Zeile 85.

Das Schlüsselwort »go« startet eine Goroutine. Das Hauptprogramm erzeugt in Zeile 96 einen parallelen Thread mit der Funktion »signalHandler()«, läuft selbst jedoch normal weiter. So führt das Programm zwei Goroutines aus. Eine ab Zeile 66 ist für die Signalbehandlung zuständig, die andere horcht ab Zeile 23 auf dem TCP-Port. Am Ende von »main()« steht eine Endlosschleife, die mit Hilfe von »select« auf eine ankommende Nachricht am Kanal »quit« wartet.

Warten und horchen

Empfängt die Schleife eine Nachricht, beendet sich das Programm. Der Entwickler darf mit der Anweisung »select« auch auf mehreren Kanälen warten. Kommen auf unterschiedlichen Kanälen Daten an, wählt die Laufzeitumgebung der Sprache Go willkürlich eine Nachricht zur weiteren Verarbeitung aus.

Funktionen leitet Go mit dem Schlüsselwort »func« ein. Im Falle von »acceptor()« wartet das Programm in Zeile 23 in einer Endlosschleife auf TCP-Verbindungen und liest von jeder Verbindung immer maximal 1024 Bytes ein. Die tatsächliche Anzahl an gelesenen Bytes übergibt die Funktion anschließend an die Goroutine »handleClient()«.

Eine Besonderheit ist das Übergabeargument mit der Syntax »buf[0:num]« in Zeile 41: Das ist der Weg von Go, so genannte Slices zu erzeugen, die in der Sprache eine wichtige Rolle beim Arbeiten mit Feldern spielen. Slices lassen sich mit Feldern in anderen Programmiersprachen wie C und C++ vergleichen, sind also Pointer auf einen Speicherbereich inklusive einiger Meta-Informationen, zum Beispiel der Länge.

Um einen Slice zu erzeugen, muss entweder bereits ein Feld existieren (wie »buf«, das Zeile 24 anlegt) oder Go legt es wie in Zeile 50 beim Erzeugen eines neuen Slice mit »make« automatisch an.

Die für jede Clientverbindung aufgerufene Funktion »handleClient()« bekommt demnach einen Byte-Slice, also jene Bytes, die sie las. Über den Umweg der Pakete »bytes« und »binary« gießen die Zeilen 49 bis 56 den Bytestrom in eine eigens für die Nachrichtenübertragung definierte Struktur, die Listing 3 festlegt.

Nachrichtenformat

Die Datei »msg.go« legt in Zeile 1 ein Paket namens »msg« an, das ab Zeile 3 eine Struktur mit vier Elementen definiert. Der Code demonstriert durch große Anfangsbuchstaben, wie Go Elemente öffentlich verfügbar macht. Die anderen beiden Attribute, also »sender« und »data«, manipuliert der Programmierer über Zugriffsmethoden.

Diese Methoden werden wie Funktionen mit dem Schlüsselwort »func« eingeleitet. Sie erwarten jedoch nach dem Schlüsselwort einen Empfänger, in diesem Fall »m«, auf welchen sie aufgerufen werden können. Dadurch unterscheidet sich das Klassenkonzept von jenen der Sprachen C++ und Java, die Methoden innerhalb der Typdefinition anführen.

Prinzipiell darf jeder Typ als Empfänger auftreten, es sind also auch primitive Datentypen und Typen erlaubt, die keine Zeiger sind. Auf diese Weise lässt sich die Methode »String()« für beliebige Datentypen definieren. So passen Entwickler beispielsweise die Ausgabe von »Println()« eigenen Zwecken an. Die Methode

func (m *Message) String() string {
  return fmt.Sprintf("Sender=%s, Data=%s",
                     m.sender, m.data)
}

legt eine Ausgabe nach Maß für die Struktur »Message« fest, die beim Aufruf von »fmt.Println(m)« zum Tragen kommt.

Lebenszeichen

Das Clientprogramm verhält sich ähnlich zum Server, es lässt sich per FTP beim Linux-Magazin mit den anderen Beispielen herunterladen [8]. Nachdem der Client einen Socket initialisiert hat, baut er eine TCP-Verbindung zum Server auf. Er baut seinerseits eine »Message«-Struktur mit Informationen aus den Kommandozeilen-Parametern zusammen und versendet sie Byte-weise über den Socket. Danach beendet sich der Client.

Experimentierfreudige Leser entwickeln aus diesen Grundlagen einen vollständigen Chat, dessen Server beispielsweise eine Liste aller angemeldeten Clients verwaltet. Dazu bietet sich eine »map« an, das Go-Äquivalent eines Hash in Perl. Eingehende Nachrichten versendet der Server dann an alle in der Map gespeicherten Clients.

Die Beispiele zeigten einige interessante Konstrukte von Go. Auch kommt die Programmiersprache mit einer umfangreichen Bibliothek mit Paketen für die verschiedensten Zwecke. Man darf hier noch nicht den Umfang der Java Enterprise Edition (JEE) erwarten, für das Alter von Go ist die Paketliste jedoch schon recht umfangreich, zumal Google für ihre Erweiterung sorgt. Eine Übersicht liefert das Verzeichnis »src/pkg« in Gos Installationsverzeichnis.

Andere Interfaces

Interfaces, eine Spracheneigenheit, die Java-Programmierer kennen, bedürfen einer besonderen Betrachtung: Obwohl sie in Go eine ähnliche Bedeutung haben, unterscheidet sich ihre Implementierung. Schlüsselwörter wie das von Java bekannte »implements« fehlen in Go. Der Code unterstützt ein Interface, sobald er alle vom Interface definierten Methoden implementiert.

Ein bekanntes Beispiel ist das Reader-Interface des Pakets »io« [9]. Es definiert eine öffentliche Methode »Read()«, die einen Byte-Slice erhält und zwei Rückgabewerte liefert. Jede Klasse, die eine Methode »Read()« mit genau dieser Signatur besitzt, implementiert automatisch das Reader-Interface. So dürfen Entwickler nun Variablen eigener Typen an Funktionen übergeben, die einen »io.Reader« erwarten. Auf diese Weise sparen sie zusätzliche Schreibarbeit und erleichtern den flexiblen Austausch von Implementierungen.

Auch das Chat-Beispiel wendet die Interface-Technik bereits an. Da Listing 3 für den Typ »Message« eine »String()«-Methode enthält, implementiert dieser Typ ab sofort das Stringer-Interface [10]. Künftig formatieren Ausgabefunktionen solche Objekte mittels dieser selbst definierten Methode.

Eine Alternative zum nativen Go-Compiler »gc« bietet sein Pendant mit GCC-Backend an. Das Go-Projekt liefert dazu Hinweise für die Installation [11]. Die stellt für den erfahrenen Programmierer keine besondere Herausforderung dar, zieht aber mehr als 65 000 Dateien aus dem Source-Repository, die sich auf der lokalen Festplatte samt Versionierung auf etwa 1,3 GByte breitmachen.

GCC Go!

Die Mühe lohnt sich jedoch für den, der die mit dem alternativen Compiler generierte Codegröße betrachtet. Haben die mit dem nativen Compiler generierten Binaries selbst bei einfachsten Programmen mehrere Hundert Kilobytes, so ähnelt die ausführbare Programmgröße bei »gccgo« und dynamisch gelinkten Bibliotheken in etwa normalen C-Programmen. Dafür sind die Programme aber unter anderem von der Go-Laufzeitbibliothek abhängig (siehe Abbildung 3).

Abbildung 3: Einen Tod muss der Entwickler sterben: Entweder erhält er bei Go ein kleines Binary von 33 KByte mit vielen Laufzeitbibliotheken oder ein statisch gelinktes Programm von stattlichen 3 MByte.

Abbildung 3: Einen Tod muss der Entwickler sterben: Entweder erhält er bei Go ein kleines Binary von 33 KByte mit vielen Laufzeitbibliotheken oder ein statisch gelinktes Programm von stattlichen 3 MByte.

Letztere schlägt mit rund 16 MByte auf AMD-64-kompatiblen Plattformen und mit rund 11 MByte auf x86-kompatiblen Plattformen zu Buche. Statisch gelinkte Binaries beeindrucken auch bei diesem Compiler wieder durch ihre Größe. Das Hello-World-Beispiel aus Listing 1 beansprucht nun 3 MByte. Wer die mit »gccgo« erzeugten und statisch gelinkten Binaries mit dem Universal Packer for Executables (UPX) strippt und packt, reduziert die Dateien auf Größen, die an den nativen Compiler erinnern [12]. UPX weist den Benutzer allerdings mit einer Fehlermeldung ab, wenn er das Tool auf Binaries anwendet, die er mit dem nativen Go-Compiler erzeugt hat.

Ab geht’s

Die jeweiligen Binaries unterscheiden sich noch deutlich in ihrer Ausführungsgeschwindigkeit, die jedoch stark von verwendeten Optimierungen und Einstellungen abhängt. Um möglichst vergleichbare Aussagen über die Geschwindigkeit zu erzielen, vermeidet das Testprogramm aus Listing 4 unkalkulierbare Wartezeiten, die durch Ein- und Ausgabeoperationen anfallen.

Listing 4:
»perf.go«

01 package main
02 
03 const (
04     SIZE = 200000;
05     MULT = 1103515245;
06     MASK = 4096;
07     INC = 12345;
08 )
09 
10 func bubbleSort(numbers []uint32) {
11     for i := (len(numbers) - 1); i >= 0; i-- {
12         for j := 1; j <= i; j++ {
13             if numbers[j-1] > numbers[j] {
14                 numbers[j-1], numbers[j] =
15                 numbers[j], numbers[j-1]
16             }
17         }
18     }
19 }
20 
21 func main () {
22     var arrayOfInt [SIZE]uint32
23     var lNext uint32 = 1;
24 
25     for i := 0; i < len(arrayOfInt); i++ {
26         lNext = lNext * MULT + INC;
27         arrayOfInt[i] = (uint32)(lNext/MASK) % SIZE;
29     }
31     bubbleSort(&arrayOfInt)
33 }

Der einfache Benchmark implementiert den Bubblesort-Algorithmus [13] und befüllt dazu ein Array mit einer reproduzierbaren Folge aus 32-Bit-Pseudo-Zufallswerten. Anschließend ordnet das Programm die Werte wieder in aufsteigender Reihenfolge an. Auf Eigenarten der Sprachen verzichtetet das Beispiel weitgehend. Die Arrays implementiert das Beispiel in C mit Zeigern (siehe Listing 5), in Go mit Slices. In beiden Fällen erzeugten die Tester statische Binaries, die über keine Debugging-Symbole verfügen. Der Quelltext der präsentierten Programme liegt auf dem FTP-Server des Linux-Magazins bereit [8]. Abbildung 4 zeigt, dass reines C immer noch ein gutes Stück schneller ist.

Abbildung 4: Fix, aber noch steigerungsfähig: Die Bubblesort-Implementierung in Go benötigt eine noch mehr als doppelt so lange Ausführungszeit wie ein vergleichbares C-Programm für die gleiche Aufgabe.

Abbildung 4: Fix, aber noch steigerungsfähig: Die Bubblesort-Implementierung in Go benötigt eine noch mehr als doppelt so lange Ausführungszeit wie ein vergleichbares C-Programm für die gleiche Aufgabe.

Listing 5:
»perf.c«

01 #include <stdint.h>
02 #include <stdlib.h>
03 
04 #define SIZE 200000
05 #define SEED 1
06 #define MULT 1103515245L
07 #define MASK 4096
08 #define INCR 12345
09 
10 void bubbleSort(int numbers[], int array_size)
11 {
12     int i, j, temp;
13 
14     for (i = (array_size - 1); i >= 0; i--)
15     {
16         for (j = 1; j <= i; j++)
17         {
18             if (numbers[j-1] > numbers[j])
19             {
20                 temp           = numbers[j - 1];
21                 numbers[j - 1] = numbers[j];
22                 numbers[j]     = temp;
23             }
24         }
25     }
26 }
27 
28 int main(void)
29 {
30     uint32_t *lArray = NULL;
31     uint32_t lCnt = 0;
32     uint32_t lNext = SEED;
33 
34     lArray = (uint32_t*)malloc(sizeof(uint32_t) * SIZE);
35 
36     for (lCnt = 0; lCnt < ARRAY_SIZE; lCnt++)
37     {
38         lNext = lNext * MULT + INCR;
39         lArray[lCnt] = (uint32_t)(lNext / MASK) % SIZE;
40     }
41     bubbleSort(lArray, ARRAY_SIZE);
42     return free(lArray);
43 }

Ziel in Bewegung

Künftig sollen die Go-Utils mit einem Debugger aufwarten, der den Programmierer bei seiner Arbeit unterstützt. Google will zudem die Zusammenarbeit zwischen Go und C vereinfachen. Die Sprachdesigner philosophieren noch, ob sie weitere objektorientierte Sprachkonzepte wie Exceptions und Generics einfließen lassen sollen. Sie erwägen kommende Versionen des Go-Compilers selbst in Go zu schreiben, da mittlerweile sowohl Lexer als auch Parser als Go-Bibliothek vorliegen. Eine Roadmap verrät weitere Details [14].

Viele Konzepte von Go klingen vielversprechend, in der Praxis muss sich die Programmiersprache aber erst bei größeren Projekten bewähren. Die einfache, aber mächtige Syntax zwischen Java und C dürfte beide Lager interessieren. Die pragmatisch entworfenen Bibliotheken verhelfen zu schnellen Ergebnissen. Manche Details der Sprache wirken zwar etwas akademisch, haben aber einen unbestreitbaren Nutzen. Die Build- und Ausführungsumgebung wie auch ihre Kernparameter wie Ausführungsgeschwindigkeit und Codegröße brauchen noch Feinschliff, der aber bereits abzusehen ist. Bleibt der Akzeptanztest bei den Entwicklern. (mg)

Infos

[1] “The Go Programming Language”,Google Tech Talk: [http://www.youtube.com/watch?v=rKnDgT73v8s]

[2] Plan 9 from Bell Labs:[http://plan9.bell-labs.com/plan9/]

[3] Xkcd “Compiling”: [http://xkcd.com/303/]

[4] Installationsanleitung des Projekts: [http://golang.org/doc/install.html]

[5] “HTTP client & server tests fail”: [http://code.google.com/p/go/issues/detail?id=5]

[6] Go-Tutorial:[http://golang.org/doc/go_tutorial.html]

[7] Effective Go:[http://golang.org/doc/effective_go.html]

[8] Quelltexte zu diesem Artikel:[ftp://ftp.linux-magazin.de/pub/listings/magazin/2010/05/go]

[9] Reader-Interface »io.go«:[http://golang.org/src/pkg/io/io.go]

[10] Stringer-Interface »print.go«:[http://golang.org/src/pkg/fmt/print.go]

[11] Setting up and Using Gccgo: [http://golang.org/doc/gccgo_install.html]

[12] Ultimate Packer for Executables (UPX): [http://upx.sourceforge.net/]

[13] Bubblesort: [http://www.sorting-algorithms.com/bubble-sort]

[14] Go-Roadmap: [http://go.googlecode.com/hg/doc/devel/roadmap.html]

Die Autoren

Marcus Nutzinger und Rainer Poisel arbeiten als wissenschaftliche Projektmitarbeiter am Institut für IT Sicherheitsforschung an der Fachhochschule St. Pölten in Österreich. Sie erforschen im Rahmen des Projekts “StegIT-2”, wie das Einbetten geheimer Nachrichten in Voice-over-IP-Telefonaten zu verhindern ist. Zum Studiengang IT Security halten die Autoren Lehrveranstaltungen in den Wahlfächern ab.

LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben