Der Golang-Spicker
😈 Die Programmiersprache Go ("Golang")
Warum Go? Achtung - subjektiv!
ist eine von Grund auf klug durchdachte und in jeder Hinsicht ästhetische Programmiersprache.
Bei Betrachtung drei populärer Rankings für Programmiersprachen (PyPl-Index, Tiobe-Index, und Languish) fällt trotz der deutlichen Abweichungen bei Ermittlung der Rankings auf, daß langjährige Platzhirsche wie C/C++ und Java an Beliebtheit einbüßen. Eindeutiger Liebling der Entwickler und die neue Nummer 1 ist 2024 die Sprache Python. Das ist kein Wunder, denn das Programming mit dieser schönen Programmiersprache macht Spaß und geht schnell von der Hand. Aber Python ist eine Skriptsprache und hinsichtlich Performance begrenzt. So ist auch eine ganze Anzahl junger Compilersprachen nachgerückt, die nun wieder mehr auf Typsicherheit und Performanceoptimierung Wert legen, als Entwickler es in den letzten Jahren mit dynamischen Interpreter Sprachen wie Python, PHP und Perl gewohnt waren. Eine Ursache hierfür mag tatsächlich die verlangsamte Performancesteigerung bei Hardwareentwicklungen sein, die dazu führt, daß (wie schon vor 30 Jahren) Leistungssteigerungen durch Codeoptimierungen erzielt werden müssen, wie Edoardo Paolo Scalafiotti auf Hackernoon am 16.10.2016 beschrieben hat[1].
Interessant ist aber auch, daß es zwar die großen IT-Riesen sind, die sich die Entwicklung eigener Programmiersprachen leisten, doch glücklicherweise können sie es sich trotzdem nicht erlauben, diese Entwicklung nicht unter eine Open Source Lizenz zu stellen. Für besonders zukunftsträchtig halte ich die Sprachen Swift (von Apple), Go (von Google) und Rust (von Mozilla). Nachfolgend werden die Basis-Elemente der Programmiersprache Go kompakt zusammengefaßt. Für Go spricht meiner persönlichen Ansicht nach im Vergleich zu Swift und Rust der ressourcenschonende und zugleich sehr ästhetische Minimalismus, sowie die neu bzw. anders erdachte Objektorientierung, die dazu führt, daß Entwickler gewohnte Konstrukte wie die Klassen-Definition, aber auch try-catch-finally-Blöcke zum Abfangen von Exceptions nicht mehr benötigen.[2] Go vereint u.a. die Typsicherheit und das Package-Modell von Java, die pragmatische Syntax von Python und die Pointer von C. Auch die Dokumentation ist sagenhaft transparent. Und bei keiner anderen Programmiersprache wird die Nebenläufigkeit und die Kommunikation zwischen parallel laufenden Routinen so trivial wie bei Go.[3]
Aber da gerade kritisches Infragestellen oft die Vorstufe glühendster Begeisterung ist, sollen hier auch die "5 Dinge, die man an Go hassen kann (aber nicht muss)" unbedingt zur Lektüre empfohlen werden, die Christoph Engelbert am 10. April 2018 auf jaxenter.de[4] zur Programmierung mit Go zusammengetragen hat. So kann sich jeder einen eigenen Eindruck erarbeiten, ob er die Besonderheiten der Programmiersprache nun als Vorteil oder als Nachteil, als Fortschritt oder als Rückschritt betrachten möchte.
Inhaltsverzeichnis
- 1 😈 Die Programmiersprache Go ("Golang")
- 2 Ein Go-Spickzettel ("Cheat Sheet")
- 2.1 Compiler
- 2.1.1 Gc
- 2.1.2 Gccgo
- 2.1.3 Cross-Compiling
- 2.1.3.1 In Linux eine 32bit-.exe-Datei für Windows erzeugen
- 2.1.3.2 In Linux eine 64bit-.exe-Datei für Windows erzeugen
- 2.1.3.3 Eine ausführbare Datei für das Raspberry PI2 (ARMv6) erzeugen
- 2.1.3.4 Eine ausführbare Datei für das Raspberry PI3 (ARMv7) erzeugen
- 2.1.3.5 Eine ausführbare Datei für 64Bit-ARM-Prozessoren (ARMv8, z.B. Rock64 und Ubuntu-Touch auf Volla Phone) erzeugen
- 2.1.3.6 In Windows eine ausführbare 64bit Datei für Linux erzeugen
- 2.1.3.7 Statische Verlinkung der glibc-Libraries (Linux)
- 2.1.3.8 Cross-Compiling unter Linux für Windows mit MXE
- 2.2 Packages
- 2.3 Funktionen
- 2.3.1 Einfache Funktionen
- 2.3.2 Veränderliche Parameter (Variadic Functions)
- 2.3.3 Lambda-Funktionen oder Ternary-Operation? Funktions-Literale! (anonyme Funktionen als Variablen)
- 2.3.4 Der Datentyp einer Funktion
- 2.3.5 Funktionen mit Gedächtnis: Closures (wenn Funktionen Funktionen zurückgeben)
- 2.3.6 Rekursiver Funktionsaufruf
- 2.4 Speicherverwaltung
- 2.4.1 Built-in Types
- 2.4.2 Literale
- 2.4.2.1 Zeichenketten (string literals)
- 2.4.2.2 Einzelne Zeichen (rune literals)
- 2.4.2.3 Ganzzahlen (integer literals)
- 2.4.2.4 Fließkommazahlen (floating point literals)
- 2.4.2.5 Imaginäre Zahlen (imaginary literals)
- 2.4.2.6 Trennzeichen in Zahlen (digits separators) – Ohne weitere Funktion, nur Code-Kosmetik!
- 2.4.3 Deklarationen
- 2.4.4 Zeiger (Pointer)
- 2.4.5 Typumwandlung (type casting)
- 2.4.6 Arrays, Slices, Strings, Maps, Ranges
- 2.4.6.1 Arrays
- 2.4.6.2 Slices
- 2.4.6.2.1 Unterschiede zwischen new() and make()
- 2.4.6.2.2 Erzeuge ein Slice aus einem array
- 2.4.6.2.3 Kopiere Slice mit copy()
- 2.4.6.2.4 Kopiere einen Teil des Slice in einen Teil eines anderen Slices
- 2.4.6.2.5 Erweitere ein Slice mit append()
- 2.4.6.2.6 Reset eines Slices mit clear()
- 2.4.6.2.7 Slices sind eigentlich nichts anderes als Zeiger auf Arrays
- 2.4.6.2.8 Weswegen das Berücksichtigen der Kapazität so wichtig ist!
- 2.4.6.3 Strings
- 2.4.6.4 Maps
- 2.4.6.5 Ranges
- 2.4.7 "Generics" - Kombinierte Datentypen
- 2.4.8 Strukturen
- 2.5 Operatoren
- 2.6 Kontrollstrukturen
- 2.7 Objektorientierung
- 2.8 Interfaces
- 2.8.1 Ein Interface hat keinen speziellen (also jeden) Datentyp
- 2.8.2 Die Implementierung von Methoden ist nur für einen lokalen Datentyp möglich, auch beim Interface
- 2.8.3 Interface mit struct-Datentyp
- 2.8.4 Interface und Pointer-Receiver
- 2.8.5 Ein Interface kann auf unterschiedliche Weisen implementiert werden
- 2.8.5.1 Implementierung der geforderten Methoden über die Struktur; das "io.Writer"-Interface fordert einzig die Implementierung der Funktion "Write"
- 2.8.5.2 Implementierung eines Interfaces mit selbst definierten Eigenschaften
- 2.8.5.3 Implementierung eines Interfaces mit geerbten Eigenschaften
- 2.8.5.4 Beispiele für Implementierung durch Vererbung
- 2.8.6 Type assertions
- 2.8.7 Type switches
- 2.8.8 Polymorphie
- 2.9 Error-Handling
- 2.10 Goroutinen (Nebenläufigkeit)
- 2.11 go test
- 2.12 Kontrollieren der Speicherverwaltung
- 2.13 Reflections – Bezüge auf den eigenen Code
- 2.14 C code einbinden
- 2.15 GUI
- 2.16 Verteilte Systeme
- 2.17 IDE
- 2.18 Links
- 2.19 Benchmark
- 2.20 Kritik
- 2.21 Anmerkungen
- 2.1 Compiler
Ein Go-Spickzettel ("Cheat Sheet")
Compiler
Gc
Gc ist der Go-Compiler. Seit Version 1.5. wird er selber in Go geschrieben. Er erzeugt statische Binaries, ermöglicht aber auch die direkte Ausführung des Programmes – wie bei Scriptsprachen!
Erzeuge ein statisches Binary
$ go build byebyeJava.go
$ ./byebyeJava
Führe das "Script" sofort aus
$ go run byebyeJava.go
Kompiliere ein externes Package
$ go install [packages]
Lade externe Packages aus einem Repository
$ go get github.com/therecipe/qt
$ go get -u github.com/golang/lint/golint # -u = Lade update für bereits bestehende Packages
Einblick in den Assembler-Code
Siehe https://golang.org/doc/asm
$ go build -gcflags -S byebyeJava.go
Compilieren mit Makefile
# Basic Go makefile
GOCMD=/usr/local/bin/go
GOBUILD=$(GOCMD) build
GOCLEAN=$(GOCMD) clean
GOTEST=$(GOCMD) test
GOGET=$(GOCMD) get
all: build test clean
build:
$(GOBUILD) -v
dbg-build:
$(GOBUILD) -v -gcflags=all="-N -l" -tags debug
test:
$(GOTEST) -v ./...
clean:
$(GOCLEAN)
Auszuführen beispielsweise mit:
$ make
$ make all
$ make build
Gccgo
Gccgo ist ein Go-Frontend für die GNU Compiler Collection (GCC). Der Compiler wurde in C++ geschrieben. Gccgo hinkt dem Gc bei der Implementierung des Sprachumfangs hinterher[5] Der Gccgo ist zwar langsamer, erzeugt aber mit der Programmierung effizienteren Maschinencode und dynamisch gelinkte Binaries!
$ gccgo -Wall -O2 -o byebyeJava byebyeJava.go
$ ./byebyeJava
Cross-Compiling
(Siehe https://gist.github.com/asukakenji/f15ba7e588ac42795f421b48b8aede63 für eine umfassende Liste der GOOS- und GOARCH-Umgebungsvariablen!)
In Linux eine 32bit-.exe-Datei für Windows erzeugen
$ GOOS=windows GOARCH=386 go build byebyeJava.go
In Linux eine 64bit-.exe-Datei für Windows erzeugen
$ GOOS=windows GOARCH=amd64 go build byebyeJava.go
Eine ausführbare Datei für das Raspberry PI2 (ARMv6) erzeugen
$ GOOS=linux GOARCH=arm GOARM=6 go build byebyeJava.go
Eine ausführbare Datei für das Raspberry PI3 (ARMv7) erzeugen
$ GOOS=linux GOARCH=arm GOARM=7 go build byebyeJava.go
Eine ausführbare Datei für 64Bit-ARM-Prozessoren (ARMv8, z.B. Rock64 und Ubuntu-Touch auf Volla Phone) erzeugen
$ GOOS=linux GOARCH=arm64 go build byebyeJava.go # GOARM=8 wäre unzulässig!
Siehe https://github.com/golang/go/wiki/GoArm
In Windows eine ausführbare 64bit Datei für Linux erzeugen
$ GOOS=linux GOARCH=amd64 go build byebyeJava.go
Statische Verlinkung der glibc-Libraries (Linux)
$ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -ldflags '-w -extldflags "-static"' -o toolbox *.go
(Bitte GOARCH berücksichtigen, das Beispiel erzeugt ein Binary für 64Bit-ARM-Prozessoren.)
Zu den ldflags siehe "My favorite build options for Go" (https://opensource.com/article/22/4/go-build-options) und die Manual Page (man ld)
Cross-Compiling unter Linux für Windows mit MXE
MXE (M cross environment) erleichtert die Codegenerierung für sehr viele (freie) Windows-Libraries (z.B. SDL, Qt, GTK, ImageMagick, etc.) unter Linux ganz enorm, indem es die benötigte Infrastruktur aus Compiler und Libraries einfach über ein Makefile erzeugt:
- Installation von MXE
$ cd /mein/wunsch/pfad/zu/
$ git clone https://github.com/mxe/mxe.git
$ cd mxe
- Erzeuge z.B. alle Packages für alle Installationsziele
$ make MXE_TARGETS='i686-w64-mingw32.static i686-w64-mingw32.shared x86_64-w64-mingw32.static x86_64-w64-mingw32.shared'
- Aber es genügt zunächst die Erzeugung des GCC-Kompilers nur für das benötigte Installationsziel
$ make MXE_TARGETS='x86_64-w64-mingw32.static' gcc
- Anmerkung
- Die Umgebungsvariable MXE_TARGETS kann auch in Datei settings.mk definiert werden und muß dann nicht angegeben werden
- Weitere Pakete können einfach hinzuinstalliert werden. Z.B. qt5. (Auch Mehrere Jobs und Compiler-Prozesse sind möglich!)
$ make qt5 --jobs=4 JOBS=2
- Anmerkung
- Details über die Installationsziele und auswählbaren Packages: https://mxe.cc/#usage und https://mxe.cc/#packages
- Kompiliere go unter Linux für 32bit-Windows
$ cd /mein/projekt/pfad
$ CGO_ENABLED=1 CC=/mein/wunsch/pfad/zu/mxe/usr/bin/i686-w64-mingw32.static-gcc CXX=/mein/wunsch/pfad/zu/mxe/usr/bin/i686-w64-mingw32.static-g++ GOOS=windows GOARCH=386 go build -o meineApp.exe .
- Konkret
- CGO_ENABLED=1 CC=/usr/local/src/mxe/usr/bin/i686-w64-mingw32.shared-gcc CXX=/usr/local/src/mxe/usr/bin/i686-w64-mingw32.shared-g++ GOOS=windows GOARCH=386 go build -o meineApp.exe
Packages
Verzeichnisstruktur
Wichtige Umgebungsvariablen
Installation der Go-Systemdateien
$ export GOROOT='/usr/local/go'
Workspace für den eigenen Code
$ export GOPATH='/home/me/workspace/'
Eigene Git-Repositories für go modules[6]
$ export GOPRIVATE='*.schnulli.com,git.home.at/myZeugs'
Verzeichnisse, die Go im Workspace anlegt
- src
- Source Codes (Endung .go)
- Dieses Verzeichnis enthält in Unterverzeichnissen die Packages. Darin enthalten die eigenen Source Codes.
- bin
- Binärcode (Ohne Endung)
- Dieses Verzeichnis enthält die ausführbaren Programme mit beliebigem Namen.
- pkg
- Binärcode (Endung .a)
- Dieses Verzeichnis enthält die externen Package-Objekte in kompillierter Form.
Packages importieren
package main
import (
"fmt"
_ "image" // Geladen, aber nicht genutzt (keine Fehlermeldung)
"math"
"math/rand"
sc "strconv"
. "strings"
t "time"
)
/*
// Alternative Schreibweise
import "fmt"
import _ "image"
*/
/*
//Lokale Pfade werden als relative Pfade mit ./ begonnen
import ./meinPackage
*/
func main() {
fmt.Println(math.Sqrt(9))
rand.Seed(t.Now().UnixNano()) // Der Zufallsgenerator benötigt veränderlichen Input
fmt.Println(rand.Intn(100)) // Und nicht etwa: math/rand.Intn(100)
fmt.Println(ToLower("BYE BYE JAVA")) // Und nicht: fmt.Println(strings.ToLower("BYE BYE JAVA"))
fmt.Printf("%s ist jetzt ein String\n", sc.FormatFloat(0.12345, 'G', -1, 64))
}
Packages erzeugen
Benennung
package myPackageName // Die Deklaration des Packages erfolgt immer in der Kopfzeile eines
// Source-Dokuments (Leerzeichen und Kommentare davor sind erlaubt)
package main // Executables müssen immer zum Package *main* gehören
package rand // Der package name ist immer der letzte Name des Import-Pfades. (import path math/rand => package rand)
Streuung von Dateien
- Wenn innerhalb eines Verzeichnisses unterschiedliche Dateien mit der Endung .go dem gleichen package zugeordnet sind, werden sie vom Compiler als eine zusammenhängende Datei interpretiert. ⇒ Auf diese Weise lassen sich während der Programmierung z.B. Funktionen sehr einfach in getrennte Dateien auslagern.
go modules
- Seit go1.11 ist es möglich, und seit go1.13 ist es Standard, Projekte auch außerhalb des $GOPATH anzulegen. Die Pfadinformation steht dann in der Datei go.mod im Projektordner. Erzeugt wird die Datei go.mod durch:
$ go mod init mein/projekt/pfad
bzw.
$ go mod init
wenn ein import comment hinter der package-Anweisung im Code auf diesen Pfad hinweist:
// goModTest project main.go
package main // import "mein/projekt/pfad"
- Example
-
- Erzeuge den Go-Code ...
// goModTest project main.go package main // import "goModTest" import ( "fmt" "os" "github.com/hackebrot/turtle" ) func main() { emotion := "angry" emoji, ok := turtle.Emojis[emotion] if !ok { fmt.Fprintf(os.Stderr, "Das Zeichen \"%v\" konnte nicht gefunden werden\n", emotion) os.Exit(1) } fmt.Println(emoji.Char) //😠 }
- ... und generiere die Datei go.mod ...
$ go mod init
- Lade die Dependencies
$ go mod tidy
- Es entsteht folgende Datei go.mod:
module goModTest require ( github.com/google/go-cmp v0.2.0 // indirect github.com/hackebrot/go-repr v0.0.0-20170331152400-586d894a5734 // indirect github.com/hackebrot/turtle v0.1.0 )
- Der Code ist dann auch außerhalb des $GOPATH kompilierbar.
- Eigene Git-Repositories sind mit $GOPRIVATE zu öffnen.
Sichtbarkeit von Variablen und Funktionen
- Identifier, die mit Großbuchstaben beginnen, sind public, sind also in anderen packages sichtbar
- Identifier, die mit Kleinbuchstaben beginnen, sind private, bleiben also für andere packages unsichtbar
Standard-Funktionen
func main(){} // Ausführbare Programme müssen immer die Funktion main() besitzen
func init(){} // Die Funktion init() steht innerhalb eines Packages und wird beim Laden des Packages ausgeführt.
Es können auch mehrere init()-Funktionen hintereinander aufgerufen und es können globalen Variablen Werte zugewiesen werden. Zum defer-Statement siehe den Abschnitt Error Handling:
package main
import "fmt"
var zähler int
func init() {
fmt.Println("Die erste init-Funktion wurde aufgerufen") //1
zähler++
}
func init() {
defer fmt.Println("wurde aufgerufen") //3
fmt.Print("Die zweite init-Funktion ") //2
zähler++
}
func main() {
defer fmt.Println("(Und dann kann mit defer sogar *nach* der main-Funktion eine Funktion ausgeführt werden!)") //6
fmt.Println("Die main-Funktion wurde aufgerufen") //4
fmt.Printf("Die init-Funktion wurde zuvor %dmal aufgerufen\n", zähler) //5
}
/*
Die erste init-Funktion wurde aufgerufen
Die zweite init-Funktion wurde aufgerufen
Die main-Funktion wurde aufgerufen
Die init-Funktion wurde zuvor 2mal aufgerufen
(Und dann kann mit defer sogar *nach* der main-Funktion eine Funktion ausgeführt werden!)
*/
Funktionen
Einfache Funktionen
// Parameterübergabe ...
func functionName0si(param1 string, param2 int) {}
// ... kann bei gleichem Typ vereinfacht dargestellt werden.
func functionName0ii(param1, param2 int) {}
// Deklaration des Rückgabewertes erfolgt hinter der Funktion ...
func functionName1() int {
return 123
}
// ... und kann *mehrere* Parameter zurückgeben!
func functionName2() (int, string) {
return 123, "Blabla"
}
var x, y = functionName2()
Rückgabeparameter können auch benannt werden, dann erhält die return-Anweisung keine Argumente und Rückgabeparemater werden im Funktionskopf deklariert:
func functionName2() (i int, s string) {
i = 123
s = "Blabla"
return // zurückgegeben werden i und s
}
var x, y = functionName2()
Veränderliche Parameter (Variadic Functions)
- Drei Punkte ... unmittelbar vor dem letzten Typ bei der Funktionsdefinition bereiten die Funktion auf die Übernahme einer unbestimmten Anzahl von Parametern dieses Typs vor. Die Funktion wird danach wie jede andere Funktion aufgerufen, nur mit dem Unterschied, daß wir ihr beliebig viele Argumente übergeben können.
- Drei Punkte ... unmittelbar nach der Variable beim Funktionsaufruf übergeben die Elemente einer Liste einzeln als Argumente.
- Es können auch KEINE Parameter übergeben werden.
func main() {
fmt.Println(adder(1, 2, 3)) // 6
fmt.Println(adder(9, 9)) // 18
fmt.Println(adder()) // 0
nums := []int{10, 20, 30}
fmt.Println(adder(nums...)) // 60
}
func adder(args ...int) int {
total := 0
for _, v := range args { // Iteration durch sämtliche Argumente
total += v
}
return total
}
Lambda-Funktionen oder Ternary-Operation? Funktions-Literale! (anonyme Funktionen als Variablen)
//Functions as values
func main() {
// Bedenke, auch Funktionen sind auch ein Datentyp! Es möglich, Variablen
// als Funktion zu deklarieren und die Funktion über die Variable aufzurufen:
var add func(int, int) int
add = func(a, b int) int { return a + b }
fmt.Println(add(3, 4))
// Etwas kürzer mit Typinferenz:
min := func(a, b int) int {
if a < b {
return a
} else {
return b
}
}
fmt.Println(min(1,2)) // min(1,2) = 1
// Aber notwendig ist selbst das nicht. Anonymen Funktionen können
// die Parameter direkt übergeben werden:
fmt.Println(func(a, b int) int { if a > b { return a } else { return b }}(3, 4))// max(3,4) = 4
}
Sichtbarkeit von Variablen in anonymen Funktionen
package main
import (
"fmt"
)
func altera(a int) {
a = 2
fmt.Println(a)
}
func main() {
a := 1
fmt.Println(a) // Vorgabe 1
// Übergabe an benannte Funktion
altera(a) // geändert zu 2
fmt.Println(a) // wieder 1
// Anonyme Funktion ohne Übergabe
func() {
a = 3
fmt.Println(a) // geändert zu 3
}()
fmt.Println(a) // bleibt 3 !!!
// Übergabe an anonyme Funktion
func(a int) {
a = 4
fmt.Println(a) // geändert zu 4
}(a)
fmt.Println(a) // wieder 3
}
- Anonyme Funktionen sind der einzige Weg innerhalb einer anderen Funktion eine Funktion zu deklarieren!
Der Datentyp einer Funktion
- In Go hat eine Funktion auch einen für sie charakterischen Datentyp
package main
import "fmt"
func main() {
var p func(...interface{}) (int, error)
p = fmt.Print //Funktion OHNE Parameterübergabe = Datentyp der Funktion
p("Hallo")
pl := fmt.Println //In der Praxis geht das natürlich etwas kompakter
pl(" Welt")
} //Hallo Welt
Funktionen mit Gedächtnis: Closures (wenn Funktionen Funktionen zurückgeben)
- Closure-Funktionen machen es sich zunutze, auf Variablen des Eröffnungskontextes lesend und auf die eigenen Kopien dieser Variablen schreibend zugreifen zu können! Die Funktionsweise ist erst auf den zweiten Blick logisch: Eine eingeschlossene Funktion wird bei ihrem ersten Aufruf die Variable aus dem Eröffnungskontext lesen, eine Kopie anfertigen und bei allen weiteren Aufrufen mit ihrer eigenen Kopie arbeiten. Den Status ihrer eigenen Kopie wird sie sich merken!
- Der "Eröffnungskontext"
- ist der Teil der äußeren Funktion, der vor der zurückgegebenen Funktion ausgeführt wird. er wird nur ein einziges Mal, nämlich bei der Variablenzuweiseung ausgeführt.
package main
import (
"fmt"
)
func main() {
var cf func() int
cf = meineFunktion()
// gleichbedeutend mit:
// cf := meineFunktion()
fmt.Println(cf()) //1
fmt.Println(cf()) //2
fmt.Println(cf()) //3
/*
Äußere Funktion
Innere Funktion
1
Innere Funktion
2
Innere Funktion
3
*/
fmt.Println(meineFunktion()()) //1
fmt.Println(meineFunktion()()) //1
fmt.Println(meineFunktion()()) //1
/*
Äußere Funktion
Innere Funktion
1
Äußere Funktion
Innere Funktion
1
Äußere Funktion
Innere Funktion
1
*/
}
func meineFunktion() func() int { // Funktion meineFunktion gibt eine anonyme Funktion zurück, die den Typ int zurückgibt.
// Aber bedenke: Der Typ ist nicht "int", sondern "func() int", da "func() int" gemeinsam den Rückgabewert bilden!
// Eindeutigere Schreibweise: "func meineFunktion() (func() int){}"
fmt.Println("Äußere Funktion")
var outerVar = 0 // outerVar := 0
meinErgebnis := func() int {
fmt.Println("Innere Funktion")
outerVar++
return outerVar
}
return meinErgebnis
}
- Natürlich sind auch Übergabeparameter zulässig:
package main
import (
"fmt"
"strconv"
)
func main() {
cf := meineFunktion()
fmt.Println(cf("a")) //a1
fmt.Println(cf("b")) //b2
fmt.Println(cf("c")) //c3
}
func meineFunktion() func(string) string {
outerVar := 0
meinErgebnis := func(eingabestring string) string {
outerVar++
return eingabestring + strconv.Itoa(outerVar)
}
return meinErgebnis
}
- Beachte bei Closures, wann und wo die Veränderung stattfindet! Wie bei allen untergeordneten Sprach-Strukturen werden Variablen zwar außerhalb deklariert, aber innerhalb nicht verändert, sondern kopiert!
package main
import (
"fmt"
)
func main() {
a, b := outerFun()
c := outerFun2()
fmt.Println(a(), b) // Achtung, Klammer nicht vergessen!
fmt.Println(a, b) // ... liefert ansonsten die Adresse von a
// Entsprechend erfordert der direkte Aufruf der Funktion *zwei* Klammern!
fmt.Println(outerFun2()()) // => 1
fmt.Printf("%v %v %v\n", c(), c(), c()) // => 2 3 4
}
func outerFun() (func() int, int) {
outerVar := 4
innerFun := func() int {
outerVar += 10 // Änderung der Variable nur innerhalb der Funktion sichtbar
return outerVar
}
return innerFun, outerVar // => 14, 4
}
var i int // Bei Deklaration außerhalb der Funktion outerFun2 ist i für die anonyme Funktion innen ebenso sichtbar ...
func outerFun2() func() int {
// ... wie es bei Deklaration innerhalb der Funktion outerFun2 sichtbar wäre:
// var i int
return func() int {
i++
return i
}
}
- Variablen müssen aber immer zuvor deklariert werden
// Fehler:
func outerFun3() func() int {
// ??? Hier fehlt die Deklaration. z.B. "var i int" oder "i := 0"
return func() int {
i += 1
return i
}
}
Rekursiver Funktionsaufruf
... ist möglich:
package main
import "fmt"
func SeventhSonofaSeventhSon(age int8, probability float32) float32 {
age++
probability = probability * 1 / 7
fmt.Printf("Generation: %d; Probability: %.5f%%\n", age, probability)
if age < 7 {
SeventhSonofaSeventhSon(age, probability)
}
if age != 1 {
return probability
} else {
return 100
}
}
func main() {
fmt.Printf("%.0f%% Iron Maiden 👍😁\n", SeventhSonofaSeventhSon(0, 100))
}
Speicherverwaltung
- Anmerkung
- new(Typ) ist eine Build-in-Funktion zum Zuweisen (bzw. Reservieren) von Speicher. new(Typ) gibt einen Zeiger *Typ auf die Speicheradresse zurück.
a := new(int) *a = 23 fmt.Println(a,*a) //0xc42009c010 23
- make(Typ, Länge, Kapazität) ist eine Build-in-Funktion zum Zuweisen (bzw. Reservieren) von Speicher für Slices, Maps und Channels. make(Typ, Länge, Kapazität) gibt den Wert vom Typ Typ zurück.
Built-in Types
default: false
bool // (true oder false)
default: ""
string
default: 0
int8 // (-128 bis 127) int16 // (-32768 bis 32767) int32 // (-2147483648 bis 2147483647) int64 // (-9223372036854775808 bis 9223372036854775807) int // entspricht – von der Systemarchitektur abhängig – int32 oder int64
uint8 // (0 bis 255) uint16 // (0 bis 65535) uint32 // (0 bis 4294967295) uint64 // (0 bis 18446744073709551615) uint // uint entspricht – von der Systemarchitektur abhängig – uint32 oder uint64
byte // Alias für uint8 rune // Alias für int32, repräsentiert einen Unicode Code Point uintptr // unsigned integer, groß genug um das Bitmuster jedes anderen Pointers zu speichern. Wird für die Zeigerarithmetik benötigt.
default: 0.0
float32 // Gleitkommazahlen nach IEEE-754 mit Größe 32 bit (Exponent 8 bit, Mantisse 23 bit) float64 // Gleitkommazahlen nach IEEE-754 mit Größe 64 bit (Exponent 11 bit, Mantisse 52 bit)
default: (0+0i)
complex64 // Komplexe Zahlen mit float32-Realteil. Eingabe inklusive Imaginärteil i z.B.: (1+2i) complex128 // Komplexe Zahlen mit float64-Realteil.
default: nil
pointers functions interfaces arrays slices channels maps error // Typ für die Fehlerbehandlung
Literale
Zeichenketten (string literals)
fmt.Println("Strings benötigen einen Backslash als \"Escape-Code\" und kennen Unicode-Points wie \u039B bzw. \U0000039B und Escape-Characters wie Tab\toder Newline\n")
fmt.Println(`Raw-Strings enthalten auch "Anführungszeichen', kennen aber keine Unicode-Points wie \u039B und kein Newline\n`)
- Anmerkung zur Mehrzeiligkeit von Strings
- Raw-Strings können mehrzeilig sein! Einfache Strings können nur einzeilig sein, einzelne Zeilen müssen dann mit + aneinander gereiht werden.
Vergleiche `Zeile 1 Zeile 2 Zeile 3`
mit "Zeile 1" + "Zeile 2" + "Zeile 3"
Einzelne Zeichen (rune literals)
fmt.Println(string('H'), 'a', string('l'), 'l', 'o') //H 97 l 108 111
fmt.Println([]rune{'A', 'B', 'C'}) //[65 66 67]
fmt.Println([]byte{68, 69, 70}) //[68 69 70]
fmt.Println(string([]rune{'A', 'B', 'C'})) //ABC
fmt.Println(string([]byte{68, 69, 70})) //DEF
fmt.Println(rune('ŋ')) // 331
fmt.Println(string(rune('ŋ'))) // ŋ
fmt.Println(([]rune(string("ŋ")))[0]) // 331 // Wandele String in rune um
Ganzzahlen (integer literals)
fmt.Println(255, 0xff, 0Xff, 0o377, 0O377, 0377, 0b11111111, 0B11111111)
/* Jeweils die Zahl 255 in Dezimal-, Hexadezimal-, Oktal- und Binär-Schreibweise.
Beachte, daß bis go1.13 eine Oktalzahl ohne o/O geschrieben wurde; diese Schreibweise bleibt weiter gültig!*/
Fließkommazahlen (floating point literals)
fmt.Println(1e3, 1e-3, 1.234, 1.234e+0, 1.23e-4, .123, .123456E+5) //1000 0.001 1.234 1.234 0.000123 0.123 12345.6
//Auch Fließkommazahlen können seit go1.13 hexadezimal geschrieben werden. Hinter der Mantisse muß ein Exponent in Dezimalschreibweise stehen, getrennt durch p/P. Der Exponent potenziert die Basis 2 um den Wert des Exponenten:
fmt.Println(0x8.ap3, 0xf.7p-2) //69 (= 0x8,a * 2³ ) 3.859375 (= 0xf,7 * 2⁻²)
//Alternative Schreibweise:
fmt.Println((0x8.ap0)*math.Exp2(3), (0xf.7p0)*math.Exp2(-2)) //69 3.859375
Imaginäre Zahlen (imaginary literals)
fmt.Println(0i, 11i, 1.234i, 1.234e+0i, 1.23e-4i, 1E2i, .123i, .123E+4i) //(0+0i) (0+11i) (0+1.234i) (0+1.234i) (0+0.000123i) (0+100i) (0+0.123i) (0+1230i)
fmt.Println(0xai, 0b101i, 0o7i) //(0+10i) (0+5i) (0+7i); Auch imaginäre Zahlen können (seit go1.13) hexadezimal, oktal oder binär geschrieben werden.
Trennzeichen in Zahlen (digits separators) – Ohne weitere Funktion, nur Code-Kosmetik!
fmt.Println(1_234.56_78_90) //1234.56789
Deklarationen
Variablen
var a int // Deklaration ohne Initialisierung
var b int = 42 // Deklaration mit Initialisierung
var a, b int = 42, 1302 // Deklaration gleicher Typen und Initialisierung über Liste
var MaxUint = ^uint(0) // Auch die Ergebnisse von Operationen können zugewiesen werden
var MinUint = 0
var MaxInt = int(MaxUint >> 1)
var MinInt = -MaxInt - 1
var c = 42 // Deklaration und Initialisierung durch Typinferenz
c := 42 // dasselbe, alternative Schreibweise
var c = int(42) // dasselbe mit Typumwandlung zur Typbestimmung
Konstanten
package main
const (
constant = "Die Deklaration des Typs ist eigentlich unnötig ..."
constanter string = "... die Initialisierung erfolgt zwangsläufig durch Typinferenz, da der Wert bekannt ist."
)
const (
a = iota // a = 0 // Die erste iota-Zuweisung ist notwendig, ...
b = iota // b = 1 // ... alle weiteren sind optional, ...
c // c = 2 // ... es wird innerhalb der const()-Deklaration trotzdem weiter gezählt
d int = iota // d = 3
)
const e = iota // e = 0
const (
f = 1 << iota // 1
g = 1 << iota // 2
h = 1 << iota // 4
i = 1 << iota // 8
j = 1 << iota // 16
)
func main() {
println(a, b, c, d, e) //0 1 2 3 0
println(f, g, h, i, j) //1 2 4 8 16
}
- Anmerkung
- Automatische Aufzählungen (Enumerationen) lassen sich über die Zuweisung des Schlüsselwortes iota an die Konstante umsetzen.
Type-Declaration
- Ein deklarierter Typ ist mit dem ableitenden Typ nicht identisch!
package main
import "fmt"
type unsigned_short uint8
func main() {
var a uint8
var b byte
var c unsigned_short
a = 12
b = a
c = 12 // "c = a" oder "c = b" ist nicht erlaubt
fmt.Println(a + uint8(b) + (uint8)(c)) // type casting ist notwendig
}
Tutorial für die praktische Anwendung: Ersatz für den enum-Datentyp (C++, Java u.a.)
package main
import "fmt"
type (
sheepType string // To ensure, that the set of constants is limited (a kind of enumeration)
dogType string
)
const (
SHEEP1 sheepType = "Shaun"
SHEEP2 sheepType = "Charly"
SHEEP3 sheepType = "Timmy"
DOG dogType = "Bitzer"
PIG string = "Oink"
)
func doSomethingWithSheepsOnly(s sheepType) {
fmt.Println(s)
}
func main() {
doSomethingWithSheepsOnly(SHEEP1)
doSomethingWithSheepsOnly(SHEEP2)
doSomethingWithSheepsOnly(SHEEP3)
//doSomethingWithSheepsOnly(DOG) // ... cannot use DOG (type dogType) as type sheepType in argument to doSomethingWithSheepsOnly
//doSomethingWithSheepsOnly(PIG) // ... cannot use PIG (type string) as type sheepType in argument to doSomethingWithSheepsOnly
}
Type-Alias
- Ein Alias-Typ ist mit dem ableitenden Typ identisch!
- Ein Type-Alias erleichtert das Refactoring, da die Änderung der Definition sämtliche Vorkommen in Code einschließt. Sehr bequem kann somit z.B. aus int64 int32 oder aus int64 float64 gemacht werden, ohne diese Änderung manuell im gesamten Code durchführen zu müssen.
package main
import "fmt"
type unsigned_short = uint8
func main() {
var a uint8
var b byte
var c unsigned_short
a = 12
b = a
c = b
fmt.Println(a + b + c) // valide, denn Alias-Typen benötigen kein type casting
}
Zeiger (Pointer)
Speicher-Allozierung durch Pointer auf Variable
- Merke
- & liefert die Speicher-Adresse einer Variablen
- * liefert den Wert, der in einer Speicher-Adresse gespeichert ist
package main
import "fmt"
func main() {
// Pointer auf einen Datentyp
var a int
var b *int // Pointer auf int, aber natürlich können Pointer auch für alle anderen Datentypen deklariert werden
a = 10
b = &a // b ist ein Pointer (Datentyp *int) und bekommt die Adresse von a
fmt.Println(a) //10
fmt.Println(b) //0xc42000e310
fmt.Println(*b) //10
fmt.Println(&*b) //0xc42000e310
fmt.Println(*&*b) //10
c := &b // c hat den Datentyp **int
fmt.Println(c) //0xc42000c028
fmt.Println(&c) //0xc42000c038
fmt.Println(**c) //10
fmt.Println(*&c) //0xc42000c028
fmt.Println(&*&c) //0xc42000c038
fmt.Println(***&c) //10
**c = 11 // Der Wert an der Adresse, auf die d direkt bzw. c indirekt zeigen, bekommt einen Wert zugewiesen. (Somit ist nun auch a = 11)
fmt.Println(*b) //11
fmt.Println(a) //11
// Pointer auf ein Array eines Datentyps
var e [4]byte
var f *[4]byte
var g *byte
e = [4]byte{22, 33, 44, 55}
f = &e //Bedenke: Wenn die Startadresse eines Arrays benötigt wird …
g = &e[0] // … dann muß auf das erste Element des Arrays gezeigt werden! (z.B. bei CGO sehr wichtig!)
fmt.Println(&e, e, &e[0]) //&[22 33 44 55] [22 33 44 55] 0xc0000141e0
fmt.Println(f, &(f[0]), g) //&[22 33 44 55] 0xc0000141e0 0xc0000141e0
}
Speicher-Allozierung durch Zuweisung von Speicher an Pointer
// Möglichkeit 1:
// var c *float64 = new(float64)
// Möglichkeit 2:
// var c *float64; c = new(float64)
// Möglichkeit 3:
c := new(float64)
*c = 123.456
fmt.Println(*c)
Wertzuweisung an Pointer kann alternativ auch indirekt erfolgen
// z.B. über anonyme Funktion …
var c *float64
c = func(f float64)*float64 {return &f}(123.456)
// …oder Variable
var c *float64
f := 123.456
c = &f
Zeigerarithmetik (wie in C) gibt es in Go eigentlich nicht …
… es sei denn, man verwendet den Datentyp uintptr und das Package unsafe:
package main
import (
"fmt"
"unsafe"
)
func main() {
i := [3]int16{123, 456, 789}
var pi *[3]int16
pi = &i
fmt.Println(pi, *pi) //&[123 456 789] [123 456 789]
upi := unsafe.Pointer(pi) // Wandele normalen (typisierten) Pointer in unsafe.Pointer um
fmt.Println(upi) //0xc42000e2f2
//fmt.Println(*upi) // Fehler: invalid indirect of upj (type unsafe.Pointer)
pi2 := (*int16)(upi) // Wandele unsafe.Pointer in normalen (typisierten) Pointer um
fmt.Println(pi2, *pi2) //0xc42000e2f2 123
// Mit dem Datentyp "uintptr" kann man dann auch rechnen:
uipi := uintptr(upi) // Wandele unsafe.Pointer in uintptr um
uipi += 2 // Rechne mit dem Pointer (Bei int16: +2 Bytes)
upi2 := unsafe.Pointer(uipi) // Wandele uintptr in unsafe.Pointer um ...
pi3 := (*int16)(upi2) // ... und danach in einen normalen (typisierten) Pointer
fmt.Println(pi3, *pi3) //0xc42000e2f4 456
fmt.Println(unsafe.Pointer(uipi+4), *(*int16)(unsafe.Pointer(uipi + 2))) //0xc42000e2f8 789
println(unsafe.Sizeof(i)) //6 (Ermittle die Größe der gesamten Variable in Bytes)
println(unsafe.Alignof(i)) //2 (Pointer Alignment/Speicherausrichtung in Abhängigkeit vom Typ, hier sozusagen die "Breite" eines einzelnen Array-Elements in Bytes; z.B. 2 bei int16)
type struktur struct {
a int8
b string
}
j := struktur{123, "abc"}
println(unsafe.Offsetof(j.a)) //0 (Nur bei Strukturen: Offset eines Elements in Relation zur Basis
println(unsafe.Offsetof(j.b)) //8
println(unsafe.Alignof(j.a)) //1 (1 Byte bei int8)
println(unsafe.Alignof(j.b)) //8
println(unsafe.Alignof(j)) //8
// Beachte, daß der tatsächliche Platzbedarf von der Speicherausrichtung abweichen kann!
println(unsafe.Sizeof(j.a)) //1
println(unsafe.Sizeof(j.b)) //16
println(unsafe.Sizeof(j)) //24
}
Typumwandlung (type casting)
var i int = 42
var f float64 = float64(i)
var u uint = uint(f)
// Dasselbe mit Typinferenz
j := 42
g := float64(i)
v := uint(f)
// Ebenfalls gültige Schreibweise
w := (int)(u)
x := (float64)(1234)
Arrays, Slices, Strings, Maps, Ranges
Arrays
Deklaration und Initialisierung
var a [10]int // Deklaration eines int-Arrays der Länge 10
a[3] = 42 // Wertezuweisung an das vierte(!) Element des Arrays
i := a[3] // Werte aus Array auslesen
var a [3]int = [3]int{11, 22, 33} // Deklaration und Zuweisung. Wir zählen die Länge des Arrays selber.
var a = [3]int{11, 22, 33} // Gleichbedeutend und etwas kürzer
a := [3]int{11, 22, 33} // Dasselbe mit Typinferenz
a := [...]int{11, 22, 33} // Und diesmal zählt der Compiler die Arraylänge für uns - funktioniert aber nur mit Typinferenz!
Multidimensionale Arrays
// Mit Deklaration
var multia [2][2]string
multia = [2][2]string{{"α", "β"}, {"γ", "δ"}}
// Mit Typinferenz
multib:=[2][2]string{{"α", "β"}, {"γ", "δ"}}
Pointer auf Arrays
var a *[2]int // Pointer auf das Array aus int
var b [2]*int // Array eines Pointers auf int
a = new([2]int) // Notwendig zur Speicherallozierung
a[0] = 11
c := 12
b[0] = &c
fmt.Println(a, *a, (*a)[0]) // &[11 0] [11 0] 11
fmt.Println(b, b[0], *b[0]) // [0xc420012168 <nil>] 0xc420012168 12
Slices
Slices sind Arrays sehr ähnlich, ihre Länge ist jedoch nicht festgelegt. Arrays sind unveränderlich, Slices zeigen einen Ausschnitt aus Arrays, besitzen also keinen eigenen Speicherbereich. Deswegen unterscheiden Slices zusätzlich zwischen Länge und Kapazität. Die Länge entspricht der Anzahl der Elemente eines Slices, die Kapazität beschreibt die Anzahl der im Speicher zu reservierenden Elemente des Arrays, auf welches das Slice zeigt.
var a []int // Deklaration eines slice.
var a = []int {1, 2, 3, 4} // Deklaration und Initialisierung ...
a := []int{1, 2, 3, 4} // ... mit Typinferenz
chars := []string{0:"a", 2:"c", 1: "b"} // ["a", "b", "c"]
var b = a[1:4] // Slice von Index 1 bis 3 (Achtung, kein Fehler! 4-1)
var b = a[:3] // Fehlender low-Index = 0
var b = a[3:] // Fehlender high-Index = len(a)
var b = a[lo:hi] // Slicegrenzen durch Variablen angezeigt. (Achte auf hi-1!)
Unterschiede zwischen new() and make()
package main
import "fmt"
func main() {
var a []int
// Erzeugung eines Slices mit new
a = new([10]int)[0:5]
// Erzeugung desselben Slices mit make
a = make([]int, 5, 10) // Erstes Argument = Länge, zweites Argument = Kapazität
fmt.Println(a, cap(a), len(a)) // [0 0 0 0 0] 10 5
a = make([]int, 5) // Kapazität ist optional
fmt.Println(a, cap(a), len(a)) // [0 0 0 0 0] 5 5
var v []int = make([]int, 10) // Der Slice referenziert auf ein Array aus 10 ints mit 0 als Inhalt
fmt.Println(v) // [0 0 0 0 0 0 0 0 0 0]
var m [][]string = make([][]string, 10) // Auch multidimensionale Slices können alloziiert werden
m[0] = make([]string, 2)
m[0] = []string{"123", "abc"}
fmt.Println(m) // [[123 abc] [] [] [] [] [] [] [] [] []]
// Erzeugung eines Pointers auf ein Slice mit new
var p *[]int = new([]int) // Alloziiert einen Slice mit nil als Inhalt und gibt einen Pointer zurück (*p == nil)
fmt.Println(*p) // []
*p = make([]int, 2, 2) // make wird weiterhin benötigt, um den Speicher zu alloziieren
(*p)[0] = 10
(*p)[1] = 20
fmt.Println(*p, cap(*p), len(*p)) // [10 20] 2 2
}
Erzeuge ein Slice aus einem array
x := [3]string{"α","β","γ"}
s := x[:] // Ein slice referenziert auf den Speicherbereich des Arrays x
Kopiere Slice mit copy()
array := [3]string{"a","b","c"}
slice := []string{"x", "y", "z"}
copy(array[:], slice[:]) // Hier wird ein Slice in ein Array kopiert
fmt.Println(array) //[x y z]
Kopiere einen Teil des Slice in einen Teil eines anderen Slices
package main
import "fmt"
var a = []string{"J", "a", "v", "a"}
var b = []string{"G", "o", "p", "h", "e", "r"}
func main() {
fmt.Println(a) //[J a v a]
fmt.Println(b) //[G o p h e r]
copy(a[2:4], b[0:2])
fmt.Println(a) //[J a G o]
}
Erweitere ein Slice mit append()
slice := []string{"a", "b", "c"}
slice = append(slice, "d") // Das Slice erhält ein zusätzliches Element
fmt.Println(slice) //[a b c d]
anotherSlice := []string{"e", "f", "g"}
slice = append(slice, anotherSlice...) // Die drei Punkte ... der "Variadic Functions"
// expandieren einen Slice in seine einzelnen Elemente und dienen so zur
// Aneinanderreihung aller Elemente
fmt.Println(slice) //[a b c d e f g]
Reset eines Slices mit clear()
var a = []int {1, 2, 3, 4}
var b = []string {"1", "2", "3", "4"}
fmt.Println(a) //[1 2 3 4]
fmt.Println(b) //[1 2 3 4]
clear(a) //Setzt alle Werte eines Slices auf den default-Wert
clear(b)
fmt.Println(a) //[0 0 0 0]
fmt.Println(b) //[ ]
Slices sind eigentlich nichts anderes als Zeiger auf Arrays
- Ein Array, das nicht existiert, wird im Hintergrund erzeugt.
- Vergleiche Array ...
a := [4]byte{'J', 'a', 'v', 'a'} // Ein Array s := a[1:] // Ein Slice s[0] = 'e' s[1] = 'r' s[2] = 'k' fmt.Printf("%s\n", a)
- ... mit Slice
var a = []byte{'J', 'a', 'v', 'a'} // Ein Slice, es erzeugt ein anonymes Array im Hintergrund s := a[1:] // Ein Slice s[0] = 'e' s[1] = 'r' s[2] = 'k' fmt.Printf("%s\n", a)
Weswegen das Berücksichtigen der Kapazität so wichtig ist!
Ein Slice kann beliebig erweitert werden. Sind jedoch die Grenzen der Kapazität (= die Grenzen des Arrays im Hintergrund) erreicht, wird der Slice im Hintergrund in ein neues Array mit doppelter Kapazität kopiert. (Und kopieren ist "teuer", d.h. Kopieren verbraucht Speicherplatz und Rechenleistung!)
package main
import "fmt"
func main() {
s := make([]int, 3, 5) // Erstes Argument = Länge, zweites Argument = Kapazität
fmt.Printf("Länge: %v; Kapazität: %v; Inhalt: %v\n", len(s), cap(s), s)
s[0] = 10
s[1] = 21
s[2] = 32
fmt.Printf("Länge: %v; Kapazität: %v; Inhalt: %v\n", len(s), cap(s), s)
s = append(s, 43)
fmt.Printf("Länge: %v; Kapazität: %v; Inhalt: %v\n", len(s), cap(s), s)
s = append(s, 54)
fmt.Printf("Länge: %v; Kapazität: %v; Inhalt: %v\n", len(s), cap(s), s)
s = append(s, 65) // Hier wird die Kapazität verdoppelt
fmt.Printf("Länge: %v; Kapazität: %v; Inhalt: %v\n", len(s), cap(s), s)
s = append(s, 76)
s = append(s, 87)
s = append(s, 98)
s = append(s, 109)
fmt.Printf("Länge: %v; Kapazität: %v; Inhalt: %v\n", len(s), cap(s), s)
s = append(s, 120) // Hier wird die Kapazität verdoppelt
fmt.Printf("Länge: %v; Kapazität: %v; Inhalt: %v\n", len(s), cap(s), s)
s = append(s, 131)
fmt.Printf("Länge: %v; Kapazität: %v; Inhalt: %v\n", len(s), cap(s), s)
}
führt zu folgendem Ergebnis:
Länge: 3; Kapazität: 5; Inhalt: [0 0 0] Länge: 3; Kapazität: 5; Inhalt: [10 21 32] Länge: 4; Kapazität: 5; Inhalt: [10 21 32 43] Länge: 5; Kapazität: 5; Inhalt: [10 21 32 43 54] Länge: 6; Kapazität: 10; Inhalt: [10 21 32 43 54 65] Länge: 10; Kapazität: 10; Inhalt: [10 21 32 43 54 65 76 87 98 109] Länge: 11; Kapazität: 20; Inhalt: [10 21 32 43 54 65 76 87 98 109 120] Länge: 12; Kapazität: 20; Inhalt: [10 21 32 43 54 65 76 87 98 109 120 131]
Weitere Details https://blog.golang.org/go-slices-usage-and-internals
Strings
Strings ähneln Arrays, indem ein Slice einen Ausschnitt aus einem String zeigen kann. Allerdings ist zu bedenken, daß Sonderzeichen (außerhalb der ASCII-Tabelle) in einem String mehr als ein Byte beanspruchen können!
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
fmt.Println("Bye bye Java"[8:10]) // "Ja"
fmt.Println("Bye bye Java"[8]) // 74 bzw. 'J'
fmt.Println(string("Bye bye Java"[8])) // "J"
fmt.Println([]byte("Bye bye Java"[8:12])) // [[74 97 118 97] bzw. []byte{'J', 'a', 'v', 'a'})
fmt.Println(string("ABC"[0:3])) //ABC
fmt.Println(string("ÄÖÜ"[0:3])) //Ä�
fmt.Println(string("ÄÖÜ"[0:4])) //ÄÖ
fmt.Println(string("ÄÖ語"[0:6])) //ÄÖ��
fmt.Println(string("ÄÖ語"[0:7])) //ÄÖ語
fmt.Println(len("ÄÖ語"),utf8.RuneCountInString("ÄÖ語")) //7 3
for _, c := range "ÄÖ語" {
fmt.Println(string(c), rune(c)) // Ä 196, Ö 214, 語 35486
}
}
Maps
- Sämtliche Types können einer Map zugeordnet werden – auch verschachtelt ...
package main
import "fmt"
func main() {
var m map[string]int
m = make(map[string]int)
// m := make(map[string]int,10) // Optional mit Kapazität
m["Answer"] = 42 // Value zuweisen
fmt.Println("The value:", m["Answer"])
delete(m, "Answer") //Element einer Map löschen
fmt.Println("The value:", m["Answer"])
v, ok := m["Answer"] // Enthält den Value, ok ist true oder false, falls Element nicht vorhanden
fmt.Println("The value:", v, "Present?", ok)
clear(m) //Löscht alle Elemente einer Map
fmt.Println(m)
mi := map[string]int{
"a": 1,
"b": 2,
"c": 3,
}
fmt.Println(mi)
pm := &m // Erzeuge einen Pointer auf die Map
fmt.Println((*pm)["Answer"]) // Achtung! Klammern notwendig …
// … *pm["Answer"] wäre unzulässig bzw. wäre gleichbedeutend mit *(pm["Answer"])
mdm := map[string]map[string]map[int]int{ // Multidimensionale Maps sind möglich, wenn auch etwas sperriger, als Dictionaries in Python oder Arrays in PHP
"color": {"green": {1: 0x00ff00, 2: 0x00ee00}}}
fmt.Println(mdm)
//var mdm2 map[string]map[string]map[int]int
//mdm2 = make(map[string]map[string]map[int]int)
// oder
mdm2 := make(map[string]map[string]map[int]int)
mdm2["color"] = make(map[string]map[int]int)
mdm2["color"]["red"] = make(map[int]int)
mdm2["color"]["red"][1] = 0xff0000
fmt.Println(mdm2["color"]["red"][1])
}
- … auch Funktionen!
package main
import "fmt"
func main() {
mf := make(map[int]func(float64, float64) float64)
mf[1] = add
mf[2] = mult
mf[3] = sub
mf[4] = div
for i := 1; i <= 4; i++ {
fmt.Println(mf[i](2, 3)) //5 ⇒ 6 ⇒ -1 ⇒ 0.6666666666666666
}
}
func add(a float64, b float64) float64 {
return a + b
}
func mult(a float64, b float64) float64 {
return a * b
}
func sub(a float64, b float64) float64 {
return a - b
}
func div(a float64, b float64) float64 {
return a / b
}
Ranges
Iteration durch die Argumente von Array, Slice oder Map
var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}
for i, v := range pow {
fmt.Printf("2**%d = %d\n", i, v)
}
for j := range pow { // Identical result, may omit value
fmt.Printf("2**%d = %d\n", j, pow[j])
}
"Generics" - Kombinierte Datentypen
Ab go1.18 ist es möglich, Funktionen generische Datentypen zu übergeben, womit ein lange beklagter Nachteil von Go gegenüber anderen Sprachen wie Java oder C++ behoben ist.
package main
import "fmt"
//Die Definition der Type-Argumente erfolgt in eckigen Klammern vor der Variablendeklaration
func dividiereAlleZahlen[num int8 | int16 | int32 | int64 | int | float32 | float64](a,b num) (c num){
c = a/b
return c
}
// Vereinfachung komplexer Datentype-Kombinationen erfolgt durch Type-Constraints.
// Type-Constraints sind eigentlich nur Platzhalter für die Regeln, die eine
// Kombination anderer Daten beschreiben.
// Das Kombinieren verschiedner Datentypen geschieht mit dem |-Operator
type NumberConstraint interface { // Erzeugung eines Type-Constraints
int8 | int16 | int32 | int64 | int | float32 | float64
}
// Beachte! Type-Constraints sind jedoch KEINE eigenständigen Datentypen
// "var Zahl NumberConstraint" wäre unzulässig!
//TypArgumente werden in der Reihenfolge der Variablen-Deklartion in den Funktionsparametern angegeben.
// Zur Vereinfachung können Datentyp ketten durch Type-Constraints umschrieben werden
func eineFunktionMitMehrenTypArgumenten[idx string|[]byte, str string, num NumberConstraint](i idx, m map[str]num) (z num) {
z = m[str(i)] //Typumwandlung in den generischen Typ beachten!
return z
}
func main() {
d := dividiereAlleZahlen(8.0,2.0) // Wenn möglich, werden Datentypen per Typinferenz übergeben.
fmt.Printf("%v, %T\n",d,d) //4, float64
//e := dividiereAlleZahlen(8,2.0) // Wenn nicht möglich, wird ein Fehler geworfen:
//fmt.Printf("%v, %T\n",e,e) //default type float64 of 2.0 does not match inferred type int for num
f := dividiereAlleZahlen(float32(8),2.0) // Entscheidend ist, daß die Datentypen im Sinne der Funktionsdefinition miteinander operieren können.111111111
fmt.Printf("%v, %T\n",f,f) //4, float32
g := dividiereAlleZahlen[int8](8,2.0) // Ist Typinferenz nicht möglich, müssen zusätzlich Type-Argumente (in eckigen Klammern) übergeben werden!
fmt.Printf("%v, %T\n",g,g) //4, int8
h := eineFunktionMitMehrenTypArgumenten("indexI", map[string]float64{"indexI":12.345})
fmt.Printf("%v, %T\n",h,h) //12.345, float64
i := eineFunktionMitMehrenTypArgumenten[[]byte,string,int8]([]byte("indexII"), map[string]int8{"indexII":127})//Die Type-Argumente dienen nur der Veranschaulichung; sie wären hier nicht notwendig, da Typinferenz aufgrund der eindeutigen Datentypen möglich wäre. Die Reihenfolge der Type-Argumente orientiert sich an der Reihenfolge der Variablendeklaration in den Funktionsparametern.
fmt.Printf("%v, %T\n",i,i) //127, int8
}
Vordefinierte Type-Constraints
comparable Alle Datentypen, die mit == und != verglichen werden können. any Alle Datentypen.[7] (any ist ein Type-Alias zu interface{}) ~ s.g. underlying types, d.h. alle Basis-Datentypen (keine Interfaces!),
denen dieser Typ zugrunde liegt. (Z.B. ~int, ~float32 etc.)
Strukturen
Definition und Zugriff
package main
import "fmt"
type programmingLanguages struct {
name string
rating int
future bool
float64 //Anonymes Feld
string //Anonymes Feld
}
func main() {
// Unbenannte Zuweisung in Reihenfolge der Typen
fmt.Println(programmingLanguages{"php", 4, true, 0.815, "interpreter"}) //{php 4 true 0.815 interpreter}
// Benannte Zuweisung in beliebiger Reihenfolge
fmt.Println(programmingLanguages{rating: 2, future: true, name: "Python"}) //{Python 2 true}
// Pointer auf eine Struktur
fmt.Println(&programmingLanguages{name: "Java", rating: 3, future: false}) //&{Java 3 false}
// Unvollständige Zuweisung ist gültig, Lücken erhalten die Default/Null-Werte
fmt.Println(programmingLanguages{name: "C/C++"}) //{C/C++ 0 false}
// Zugriff auf ein Element der Struktur
var a programmingLanguages
a = programmingLanguages{"Go", 1, true, 1.23, "compiler"}
fmt.Println(a.name) //Go
// Zeiger …
b := &a
//… werden automatisch zurückverfolgt, weswegen hier "b.rating" steht und nicht "*b.rating":
fmt.Println(b.rating) //1
// Strukturen sind veränderlich
b.name = "Golang"
fmt.Println(b.name) //Golang
fmt.Println(a) //{php 4 true 0.815 interpreter} (Zeiger auf b!)
// Leere Strukturen prüfen
if a != (programmingLanguages{}) { // Beachte die zusätzlichen Klammern ()
fmt.Println("Die Struktur a ist nicht leer")
}
}
Der Funktionsdatentyp in Strukturen
package main
import "fmt"
type dataType struct {
s string
i int
b bool
f func(string) string
}
func main() {
var data dataType
data = dataType{s: "Hallo", i: 12345, b: true, f: func(a string) string { return data.s + " " + a }}
fmt.Println(data.f("Welt"), data.i, data.b) //Hallo Welt 12345 true
}
Verschachtelte (nested) Strukturen
package main
import (
"fmt"
)
func main() {
// Verschachtelte Strukturen können entweder einzeln definiert werden ...
type c struct {
ca string
cb bool
}
type nested struct {
a int
b float64
c
}
var sn nested
sn = nested{a: 1, b: 1.23, c: c{ca: "xyz", cb: true}}
fmt.Println(sn) //{1 1.23 {xyz true}}
// ... oder "anonym" gemeinsam mit der obergeordneten Struktur definiert werden
type nestedanonymous struct {
a int
b float64
d struct {
da string
db bool
}
e []struct {
ea string
eb bool
}
}
var sa, sb nestedanonymous
sa.a = 2
sa.b = 3.45
sa.d.da = "abc"
sa.d.db = true
sa.e = make([]struct {
ea string
eb bool
}, 2)
sa.e[0].ea = "def"
sa.e[0].eb = true
sa.e[1].ea = "ghi"
sa.e[1].eb = false
fmt.Println(sa) //{2 3.45 {abc true} [{def true} {ghi false}]}
sb = nestedanonymous{a: 3, b: 4.56, d: struct {
da string
db bool
}{da: "αβγ", db: true}, e: []struct {
ea string
eb bool
}{{ea: "δεζ", eb: true}, {ea: "ηθι", eb: false}}}
fmt.Println(sb) //{3 4.56 {αβγ true} [{δεζ true} {ηθι false}]}
}
Tags
- Struktur-Elemente können zusätzliche Tags erhalten und über das Package reflect genutzt werden. (Sinnvoll einzusetzen z.B. beim OR-Mapping)
package main
import (
"fmt"
"reflect"
)
type programmingLanguages struct {
name string `json:"fieldName"`
rating int `xml:"rating"`
future bool `abc:""`
}
func main() {
a := programmingLanguages{name: "Go", future: true, rating: 1}
ra := reflect.TypeOf(a)
fmt.Println(ra) //main.programmingLanguages
fmt.Println(ra.Field(0)) //{name main string json:"fieldName" 0 [0] false}
fmt.Println(ra.Field(0).Tag.Get("json")) //fieldName
fmt.Println(ra.Field(0).Tag.Lookup("json")) //fieldName true
fmt.Println(ra.Field(2).Tag.Lookup("abc")) // true
}
Operatoren
Arithmetik
+ Addition - Subtraktion * Multiplikation / Division % Rest einer Division ("Modulo") & bitweise UND (6 & 3 = 2, da 00000110 AND 00000011 = 00000010) | bitweise ODER (6 | 3 = 7, da 00000110 OR 00000011} = 00000111) ^ bitweise EXKLUSIVES ODER (6 ^ 3 = 5, da 000000110 XOR 00000011 = 00000101) &^ bitweise UND NICHT (6 &^ 3 = 4, da 00000110 AND NOT 00000011 = 000000100) ^() bitweise NEGATION (^byte(1) = 254, da 00000001 zu 11111110 wird) << bitweise Verschiebung nach links (left shift) (6<<3 = 48, da 00000110 um drei Stellen nach links verschoben = 00110000) >> bitweise Verschiebung nach rechts (right shift) (6>>2 = 1, da 00000110 um zwei Stellen nach rechts verschoben = 00000001)
- Anmerkung
- Arithmetische Operatoren können mit dem Zuweisungsoperator kombiniert werden
x += y entspricht x = x + y x |= y entspricht x = x | y …
Vergleiche
== gleich != ungleich < kleiner <= kleiner oder gleich > größer >= größer oder gleich
Logische Verknüpfungen
&& logisches UND || logisches ODER ! logisches Negation
Besonderheiten
& Speicheradresse von * Zeiger auf Speicheradresse <- Schreibe in/Lese aus Channel _ Platzhalter, leer
Klammern
() begrenzen den Gültigkeitsbereich eines Ausdrucks. Z.B. entscheidet bei Strukturen die Klammer über die Adresszuordnung eines Pointers. (Ohne Klammern zeigt der Pointer immer auf das letzte Element!) Vergleiche (*struktur).element oder *struktur.element
Kontrollstrukturen
Einflußnahme auf den Ablauf der Kontrollstrukturen von innen erfolgt durch die Statements:
- return
- Verlasse die Funktion sofort
- break
- Breche die Ausführung der Kontrollstruktur an dieser Stelle ab (in for, switch, select innerhalb der selben Funktion). Sprünge über verschiedene Stufen können mit Labels realisiert werden:
- continue
- Springe zur nächsten Iteration (in "for" innerhalb der selben Funktion). Sprünge über verschiedene Stufen können mit Labels (Sprungmarken) realisiert werden:
… OuterLoop: for i = 0; i < n; i++ { InnerLoop: for j = 0; j < m; j++ { switch a[i][j] { case nil: break OuterLoop default: continue InnerLoop …
- fallthrough
- Springe zur nächsten case-Bedingung (in switch-Blöcken) und führe diese aus. Es findet keine erneute Prüfung statt!
Bedingungen
func main() {
if x > 0 {
return x
} else {
return -x
}
// Vor die Bedingung kann ein Deklarationsstatement gesetzt werden. Im Example bleibt die Variable a nur innerhalb des if-else-Blockes gültig.
if a := b + c; a < 42 {
return a
} else {
return a - 42
}
}
Schleifen
- Es gibt nur ein sehr flexibles for, so daß keine Notwendigkeit für ein while mehr besteht.
for i := 1; i < 10; i++ {
}
var i int
for ; i < 10; { // while - loop
}
var i int
for i < 10 { // die Semikolons können weggelassen werden
}
for { // ohne Bedingung – entspricht einem while (true)
}
Switch
Switch mit Überprüfung einer Expression
switch m {
case 0, 2, 4, 6:
tuWas()
default:
tuNix() // Möglicherweise ungewohnt: Die Position der default-Bedingung ist egal
case 1, 3, 5, 7:
tuWasAnderes()
}
... oder auch mit Rückgabewert einer Funktion als Expression
switch liefereM() {
case 0, 2, 4, 6:
tuWas()
default:
tuNix() // Möglicherweise ungewohnt: Die Position der default-Bedingung ist egal
case 1, 3, 5, 7:
tuWasAnderes()
}
Switch ohne Expression (bzw. Standardannahme "true"), stattdessen erhält die jeweilige case-Bedingung eine Expression
switch {
case a < b:
tuWas()
fallthrough // Führe die nachfolgende case-Bedingung auch noch aus!
case a <= b:
tuNochWas()
case a > b:
tuWasAnderes()
case b == d:
tuNix()
}
Wie alle Kontrollstrukturen kennt auch switch optionale Variablendeklarationen mit exklusiver Sichtbarkeit
switch os := getMyOS(); os {
case "windows": fmt.Println("😱")
case "mac": fmt.Println("😚")
case "linux": fmt.Println("😊")
case "bsd": fmt.Println("😎")
default: fmt.Println("👻")
}
Auch einem Switch ohne Expression können Variablendeklarationen vorneweg gestellt werden (entscheidend ist das Semikolon ;)
switch os := getMyOS(); {
case os == "windows": fmt.Println("😱")
case os == "mac": fmt.Println("😚")
case os == "linux": fmt.Println("😊")
case os == "bsd": fmt.Println("😎")
default: fmt.Println("👻")
}
Sichtbarkeit von Variablen in Kontrollstrukturen
Alle Kontrollstrukturen kennen die optionale Deklaration von Variablen, die nur innerhalb der Kontrollstruktur sichtbar sind
switch i := 123; i { ... }
Ebenfalls gültig:
package main
import (
"fmt"
)
var i int = 123
func main() {
switch i := i * 2; i {
default:
fmt.Println(i) // Hier ist i gleich 246
}
fmt.Println(i) // Ab hier ist i wieder 123
}
Geächtet, aber möglich: goto
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
Label:
r := rand.New(rand.NewSource(time.Now().UnixNano()))
if i := r.Intn(100); i <= 98 {
fmt.Print(i)
goto Label
} else {
fmt.Println("Endlich geschafft")
}
}
Objektorientierung
Go kennt keine Klassen, keine Zugriffsmodifikatoren (public, private, protected) und auch keine explizite Vererbungssyntax.
- Dafür aber lassen sich die Objekteigenschaften über drei Basis-Komponenten darstellen:
- Datentypen: In Go kann jeder selbstdefinierte Datentyp zum Objekt werden, also die Eigenschaften übernehmen, die in anderen OOP-Sprachen in der Regel eine Klasse hat.
- Methoden: Anders als aus anderen OOP-Sprachen gewöhnt, sind Methoden in Go keine innerhalb einer Klasse definierten Funktionen. Vielmehr werden Funktionen in Go außerhalb eines Datentyps definiert und durch Angabe eines Empfänger-Datentyps (für die "Receiver-Variable") als Methode dieses Datentyps deklariert.
- Interfaces: Mehrere Methoden können in Go zu Interfaces zusammengebunden werden.
- Die Objektsichtbarkeit wird über die Schreibweise der Identifikatoren auf Modulebene definiert.
- Und die Vererbung von Eigenschaften erfolgt über die Bildung neuer Verbundtypen aus bestehenden Verbundtypen.
Vererbung
package main
import "fmt"
// Der Verbundtyp übernimmt die Klassendefinition
type noClass struct {
a string
b int
c bool
}
//
func (self noClass) method() int {
return self.b + self.b // "self" ist kein Schlüsselwort in go, die Empfänger-Variable wurde hier nur zur Veranschaulichung so genannt
}
// Pointer sind effizienter und sie gestatten Veränderungen der Variable durch Nennung des Speicherbereichs einer Variable anstelle sie zu kopieren
func (this *noClass) pointerMethod() int {
return this.b * this.b // "this" ist kein Schlüsselwort in go, die Empfänger-Variable wurde hier nur zur Veranschaulichung so genannt
}
type noClass2 struct {
noClass // Durch die Einbindung des Typs noClass, "erbt" noClass2 dessen Eigenschaften!
d float64
}
type noClass3 struct {
noClass2 // Durch die Einbindung des Typs noClass2, "erbt" noClass3 dessen Eigenschaften – und natürlich auch die Eigenschaften von noClass!
e rune
}
func main() {
// Das Setzen von Parametern ähnelt der Übergabe von Parametern an einen OOP-Konstruktor
entity := noClass{a: "Blabla", c: true, b: 123}
entity2 := noClass2{noClass: entity, d: 4.56789}
entity3 := noClass3{noClass2: entity2, e: '🐧'}
// Beachte die vielfältigen Möglichkeiten auf eigene und geerbte Elemente zuzugreifen:
fmt.Println(entity.a, entity.b, entity.c) // Blabla 123 true
fmt.Println(entity2.a, entity2.b, entity2.c, entity2.d, entity2.noClass.a, entity2.noClass.b, entity2.noClass.c, entity.a, entity.b, entity.c) // Blabla 123 true 4.56789 Blabla 123 true Blabla 123 true
fmt.Println(entity3.a, entity3.b, entity3.c, entity3.d, entity3.e, entity3.noClass.a, entity3.noClass.b, entity3.noClass.c, entity3.noClass2.a, entity3.noClass2.b, entity3.noClass2.c, entity3.noClass2.d, entity3.noClass2.noClass.a, entity3.noClass2.noClass.b, entity3.noClass2.noClass.c, entity2.a, entity2.b, entity2.c, entity2.d, entity2.noClass.a, entity2.noClass.b, entity2.noClass.c, entity.a, entity.b, entity.c) // Blabla 123 true 4.56789 128039 Blabla 123 true Blabla 123 true 4.56789 Blabla 123 true Blabla 123 true 4.56789 Blabla 123 true Blabla 123 true
// Es ist zu beachten, daß Go die Konversion zwischen Werten und Pointern bei Methodenaufrufen automatisch vollzieht und immer Werte zurückliefert!
entityp := &entity
entity2p := &entity2
entity3p := &noClass3{noClass2: *entity2p, e: '🐧'}
fmt.Println(entity.method(), "==", entityp.method()) //246 == 246
fmt.Println(entity.method(), "==", entity2p.noClass.method()) //246 == 246
fmt.Println(entity3p.noClass2.noClass.pointerMethod(), "==", entity2p.pointerMethod()) //15129 == 15129
// Das Überschreiben von Paramtern hat nur innerhalb der eigenen Instanz Auswirkungen, ...
entity2.a = "Laber"
entity2.b = 246
fmt.Println(entity3.a, entity3.b, entity3.c, entity3.d, entity3.e, entity3.noClass.a, entity3.noClass.b, entity3.noClass.c, entity3.noClass2.a, entity3.noClass2.b, entity3.noClass2.c, entity3.noClass2.d, entity3.noClass2.noClass.a, entity3.noClass2.noClass.b, entity3.noClass2.noClass.c, entity2.a, entity2.b, entity2.c, entity2.d, entity2.noClass.a, entity2.noClass.b, entity2.noClass.c, entity.a, entity.b, entity.c) // Blabla 123 true 4.56789 128039 Blabla 123 true Blabla 123 true 4.56789 Blabla 123 true *Laber* *246* true 4.56789 *Laber* *246* true Blabla 123 true
// ... auch bei Zeigern auf Instanzen
entityp.a = "Sülz"
entityp.b = 369
fmt.Println(entity3p.a, entity3p.b, entity3p.c, entity3p.d, entity3p.e, entity3p.noClass.a, entity3p.noClass.b, entity3p.noClass.c, entity3p.noClass2.a, entity3p.noClass2.b, entity3p.noClass2.c, entity3p.noClass2.d, entity3p.noClass2.noClass.a, entity3p.noClass2.noClass.b, entity3p.noClass2.noClass.c, entity2p.a, entity2p.b, entity2p.c, entity2p.d, entity2p.noClass.a, entity2p.noClass.b, entity2p.noClass.c, entityp.a, entityp.b, entityp.c) //Blabla 123 true 4.56789 128039 Blabla 123 true Blabla 123 true 4.56789 Blabla 123 true Laber 246 true 4.56789 Laber 246 true *Sülz* *369* true
}
Wo Pointer-Receiver notwendig sind
- Da Variablen in untergeordneten Kontrollstrukturen und Funktionen/Methoden zwar global lesbar sind aber nur innerhalb des eigenen Gültigkeitsbereichs beschreibbar, ist eine Methode über den Zeiger auf einen Strukturtyp zu implementieren, wenn innerhalb der Methode eine Veränderung durchgeführt wird, die auch für nachfolgende Methoden-Zugriffe auf den Strukturtyp erhalten bleiben soll.[8]
package main
import "fmt"
type noClass struct {
X int
}
func (this noClass) setter1() {
this.X = 1
}
func (this *noClass) point_setter1() {
this.X = 2
}
func (this noClass) getter1() int {
return this.X
}
func (this *noClass) point_getter1() int {
return this.X
}
func main() {
var a noClass = noClass{}
a.setter1()
fmt.Println(a.getter1()) // 0 und nicht etwa 1
fmt.Println(a.point_getter1()) // 0 und nicht etwa 1
a.point_setter1()
fmt.Println(a.getter1()) // 2
fmt.Println(a.point_getter1()) // 2
// или
var b *noClass = &noClass{}
b.setter1()
fmt.Println(b.getter1()) // 0
fmt.Println(b.point_getter1()) // 0
b.point_setter1()
fmt.Println(b.getter1()) // 2
fmt.Println(b.point_getter1()) // 2
}
Statische Methoden bzw. statischer Methodenaufruf
package main
import "fmt"
type myStructType struct {
X int
}
func (s myStructType) doSomething(v int) {
fmt.Println("I got", v)
s.X = v //X wird eigentlich nicht benötigt
}
func (s *myStructType) setValue(v int) {
fmt.Println("I set", v)
s.X = v
}
func (s *myStructType) getValue() int {
return s.X
}
func main() {
var a myStructType = myStructType{123}
//Statischer Methodenaufruf, das erste zusätzliche Argument ist das Object!
fmt.Println("I read", (*myStructType).getValue(&a)) // 123
myStructType.doSomething(a, 1) // 1
(*myStructType).setValue(&a, 2) // 2
fmt.Println("I read", (*myStructType).getValue(&a)) // 2
}
Jeder selbstdefinierte Datentyp wird zum Objekt und kann Methoden erhalten
- Methoden können auf alle Datentypen angewendet werden, die innerhalb des selben Source-Files deklariert wurden.
package main
import "fmt"
type keineKlasse int // Deklaration eines neuen Datentyps übernimmt die Aufgaben einer "Klasse", hier mit den Eigenschaften des Datentyps "int"
func (self keineKlasse) rechenMethode() int { // "self" zur Bezeichnung der Empfänger-Variable ist ebenfalls schlechter Stil, hilft hier aber beim Verständnis!
return int(self * self)
}
func (self keineKlasse) rechenMethodeMitArgument(argument int) int {
return int(int(self) * argument)
}
func main() {
var instanz keineKlasse
instanz = 2
//oder ebenfalls möglich:
//instanz := keineKlasse(2) // Zuweiseung bei Implementierung ähnelt einem "Konstruktor"
fmt.Println(instanz.rechenMethode()) // 4
fmt.Println(instanz.rechenMethodeMitArgument(3)) // 6
}
Interfaces
- Ein Interface ist ein Datentyp.
- Ein Interface ist ein Datentyp mit methodischen Eigenschaften.
- Ein Interface ist ein Datentyp, der zusammen mit allen seinen Methoden implementiert werden muß. [9]
- Die Instanz eines Interfaces kennt nur Methoden, keine typabhängigen Felder![10]
- Die Methoden eines Interfaces können unterschiedlichen Empfänger-Datentypen zugeordnet sein. Das hat zur Folge, daß ein Interface, das durch gar keine Methoden eingeschränkt wird, grundsätzlich die Werte aller Datentypen speichern kann!
- Ein Interface wird benötigt, wenn nicht Werte als Parameter übergeben werden sollen, sondern Methoden, wie mit Werten umzugehen ist.
- Ein Interface kann auch verwendet werden, um Polymorphie zu ermöglichen – gleichnamige Funktionen (bzw. Methoden) mit identischen Übergabe-Parametern können dann unterschiedliche Aufgaben übernehmen.[11]
Ein Interface hat keinen speziellen (also jeden) Datentyp
package main
import "fmt"
type datentyper interface {}
func main() {
var a datentyper
a = "abc"
fmt.Println(a) // abc
a = 123
fmt.Println(a) // 123
}
Die Implementierung von Methoden ist nur für einen lokalen Datentyp möglich, auch beim Interface
package main
import (
"fmt"
"strconv"
"time"
)
type datentyper interface {
dosomething() string
}
type number int
func (receiver number) dosomething() string {
return strconv.Itoa(int(receiver) + 456)
}
type text string
func (receiver text) dosomething() string {
return string(receiver + "def")
}
/* Resultiert in Fehler:
./main.go:27:6: cannot define new methods on non-local type time.Time
func (receiver time.Time) dosomething() string {
return string(receiver.String())
}
Deklariere stattdessen einen local type:
*/
type mytime time.Time
func (receiver mytime) dosomething() string {
return string(time.Time(receiver).String())
}
func main() {
var a datentyper
a = text("abc")
fmt.Println(a.dosomething()) //abcdef
a = number(123)
fmt.Println(a.dosomething()) // 579
a = mytime(time.Now())
fmt.Println(a.dosomething()) // 2020-04-14 16:17:52.159587904 +0200 CEST m=+0.000060994
}
Interface mit struct-Datentyp
package main
import (
"fmt"
"strconv"
)
type datentyper interface {
dosomething() string
}
type datentyp struct {
number int
text string
}
func (receiver datentyp) dosomething() string {
return strconv.Itoa(int(receiver.number)) + string(receiver.text)
}
func main() {
var a datentyper
a = datentyp{number: 123, text: "abc"}
fmt.Println(a.dosomething()) //123abc
}
Warum Interfaces mit struct-Datentyp dennoch keine Klassen sind
Anders als Klassen in Objektorientierten Programmiersprachen wie C++, Java oder Python, erlauben Go-Interfaces nur Zugriff auf ihre Methoden, nicht jedoch den Zugriff auf Ihre Attribute!
Folgendes Example entstammt der Phantasie und ist nicht lauffähig:
package main
import (
"fmt"
"strconv"
)
type datentyper interface {
dosomething() string
number int // UNGÜLTIG
text string // UNGÜLTIG
}
type datentyp struct {
number int
text string
}
func (receiver datentyp) dosomething() string {
return strconv.Itoa(int(receiver.number)) + string(receiver.text)
}
func main() {
var a datentyper
a = datentyp{number: 123, text: "abc"}
fmt.Println(a.dosomething()) //123abc
fmt.Println(a.number) //123 ? UNGÜLTIG!
fmt.Println(a.text) //abc ? UNGÜLTIG!
}
Interface und Pointer-Receiver
- Über Adresszuweisung, nicht Wertzuweisung (call by reference)[12]
package main
import "fmt"
type datentyper interface {
set(int)
pset(int)
get() int
pget() int
}
type datentyp struct {
X int
}
func (receiver datentyp) set(i int) {
receiver.X = i
}
func (receiver *datentyp) pset(i int) {
receiver.X = i
}
func (receiver datentyp) get() int {
return receiver.X
}
func (receiver *datentyp) pget() int {
return receiver.X
}
func main() {
var b datentyper
//b = datentyp{} // falsch
b = &datentyp{} // richtig
b.set(1)
fmt.Println(b.get()) // 0 und nicht etwa 1
fmt.Println(b.pget()) // 0 und nicht etwa 1
b.pset(2)
fmt.Println(b.get()) // 2
fmt.Println(b.pget()) // 2
}
Ein Interface kann auf unterschiedliche Weisen implementiert werden
Drei fast gleiche Beispiele:
Implementierung der geforderten Methoden über die Struktur; das "io.Writer"-Interface fordert einzig die Implementierung der Funktion "Write"
package main
import (
"errors"
"fmt"
)
type myStruct struct{}
func (ms myStruct) Write(p []byte) (int, error) {
fmt.Println(p, string(p)) // [71 111 32 114 111 99 107 115] Go rocks
return 1, errors.New("Irgendein Fehler")
}
func main() {
var w myStruct
fmt.Fprintf(w, "Go rocks")
}
Implementierung eines Interfaces mit selbst definierten Eigenschaften
package main
import (
"errors"
"fmt"
)
type myInterface interface {
Write(p []byte) (int, error)
}
type myStruct struct{}
func (ms myStruct) Write(p []byte) (int, error) {
fmt.Println(p, string(p)) // [71 111 32 114 111 99 107 115] Go rocks
return 1, errors.New("Irgendein Fehler")
}
func main() {
var w myInterface
w = myStruct{}
fmt.Fprintf(w, "Go rocks")
}
Implementierung eines Interfaces mit geerbten Eigenschaften
package main
import (
"errors"
"fmt"
"io"
)
type myInterface interface {
io.Writer
}
type myStruct struct{}
func (ms myStruct) Write(p []byte) (int, error) {
fmt.Println(p, string(p)) // [71 111 32 114 111 99 107 115] Go rocks
return 1, errors.New("Irgendein Fehler")
}
func main() {
var w myInterface
w = myStruct{}
fmt.Fprintf(w, "Go rocks")
}
Beispiele für Implementierung durch Vererbung
Nachfolgend implementiert die Methode os.Stdout das Interface "ReadWriter":
package main
import (
"fmt"
"os"
)
type Reader interface {
Read([]byte) (int, error)
}
type Writer interface {
Write([]byte) (int, error)
}
type ReadWriter interface {
Reader
Writer
}
func main() {
var w ReadWriter
w = os.Stdout // Hier erbt und implementiert das ReadWriter-Interface die Eigenschaften des Writer-Interfaces,
// da "Stdout" (Variable des Packages "os") ein Zeiger auf Datentyp "File" ist, der in
// Package "os" deklariert ist. Read() und Write() sind Methoden des Datentyps
// "os.File" und erfüllen somit die Vorgaben der Interfaces.
// Erwartet wird das io.Writer-Interface von
// func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error)
// (Siehe https://golang.org/pkg/fmt/#Fprintf und https://golang.org/pkg/io/#Writer)
fmt.Fprintf(w, "Cooler als eine Java ProblemFactory!\n")
}
Interface und Implementierung sind voneinander unabhängig. Zum Vergleich – nachfolgender Code würde zu exakt demselben Ergebnis führen, da die Interfaces io.Reader und io.Writer identisch sind mit den Interfaces Reader und Writer des vorherigen Beispiels:
package main
import (
"fmt"
"io"
"os"
)
type ReadWriter interface {
io.Reader
io.Writer
}
func main() {
var w ReadWriter
w = os.Stdout
fmt.Fprintf(w, "Cooler als eine Java ProblemFactory!\n")
}
Type assertions
package main
import "fmt"
func main() {
var t interface{}
t = 123
a := t.(int)
fmt.Println(a) // 123
/*Type assertions liefern im Erfolgsfall als zweiten Parameter "true" zurück, ansonsten "false"*/
ai, ok := t.(int)
fmt.Println(ai, ok) // 123 true
af, ok := t.(float64)
fmt.Println(af, ok) // 0 false
// Expemple: Type assertion innerhalb eines if-else-Blockes als Type-Prüfung
var b interface{}
// Probiere beide Zuweisungen aus:
//b = "Blabla"
b = 123
if str, ok := b.(string); ok {
fmt.Printf("Der String b enthält den Wert: %s\n", str)
} else {
fmt.Println("b ist kein String")
}
}
Type switches
package main
import (
"fmt"
)
var t interface{}
func main() {
a := 123 // Experimentiere auch mal mit 123.456 oder "123" ...
t = &a // ... und lasse das & weg
//switch t.(type) {
switch u := t.(type) { // dasselbe mit Wertzuweisung
default:
fmt.Printf("unexpected type %T\n", u)
case bool:
fmt.Printf("boolean %t\n", u)
case int:
fmt.Printf("integer %d\n", u)
case float64:
fmt.Printf("float64 %g\n", u)
case *bool:
fmt.Printf("pointer to boolean %t\n", *u)
case *int:
fmt.Printf("pointer to integer %d\n", *u)
case *float64:
fmt.Printf("pointer to float64 %g\n", *u)
case string:
fmt.Printf("string %s\n", u)
}
}
- Anmerkung
- In type switches ist das Statement fallthrough unzulässig
Anwendung: Composite Types mittels Interface und Struct
package main
import (
"fmt"
"strconv"
)
type universaltype interface{} // Ein leeres Interface als Universaltyp
type Number interface {
String() string // Eine Methode des Interfaces fmt.Stringer wird nun auch zur Methode des Interfaces Number
latin(bool) string // Eine Methode des Interfaces Number
}
type Numb struct {
universal universaltype
}
type NumbOhne struct {
universal universaltype
}
//Mit dieser Methode implementiert Number das Interface fmt.Stringer, benötigt von fmt.Println, fmt.Printf etc.
func (n Numb) String() string {
switch t := n.universal.(type) {
default:
return fmt.Sprintf("Unzulässiger Typ: %T", t)
case int:
return strconv.Itoa(t) + n.latin(false)
case string:
return t + n.latin(true)
}
}
func (n NumbOhne) String() string {
switch t := n.universal.(type) {
default:
return fmt.Sprintf("Unzulässiger Typ: %T", t)
case int:
return strconv.Itoa(t)
case string:
return t
}
}
func (n Numb) latin(romanOnOff bool) string {
if romanOnOff == true {
return " (Römische Zahl)"
} else {
return " (Arabische Zahl)"
}
}
func main() {
var nr1a, nr1b Number // nr1a und nr1b imeplementieren das Interface.
nr1a = Numb{"LXXXVIII"}
nr1b = Numb{88}
fmt.Printf("%s = %s\n", nr1a, nr1b) // LXXXVIII (Römische Zahl) = 88 (Arabische Zahl)
// Alle übrigen Aufrufe der Methode erfolgen direkt bzw. ohne die Notwendigkeit, das Interface zu implementieren!
var nr2a = Numb{"LXXXIX"}
var nr2b = NumbOhne{89}
fmt.Printf("%s = %s\n", nr2a, nr2b) // LXXXIX (Römische Zahl) = 89
fmt.Printf("%s = %s\n", NumbOhne{"XC"}, NumbOhne{90}) // XC = 90
fmt.Println(Numb{123.456}) // Unzulässiger Typ: float64
}
Anwendung: Interface Type als veränderlicher Parameter
package main
import (
"fmt"
)
type universaltype interface{}
func main() {
fmt.Println(adder(1, 2, 3)) // 6
fmt.Println(adder("A", "B", "C")) // "ABC"
}
func adder(args ...universaltype) universaltype {
defer func() {
if err := recover(); err != nil {
fmt.Printf("Error: %v", err)
}
}()
var (
i_total int
s_total string
total universaltype
)
for _, v := range args { // Iteration durch sämtliche Argumente
switch t := v.(type) {
default:
panic(fmt.Sprintf("unexpected type %T\n", t))
case int:
i_total += v.(int)
total = i_total
case string:
s_total += v.(string)
total = s_total
}
}
return total
}
Polymorphie
... wird mit Interfaces möglich, da Interfaces zwar die "Methoden" (Funktionen) vorschreiben, die Gestalt der "Attribute" (Variablen) aber völlig offen lassen.
package main
import (
"fmt"
"math/rand"
"strconv"
"strings"
"time"
)
type confuser interface {
shuffle(time.Time) string
}
type numerals struct {
n int
}
type words struct {
w string
}
func (r numerals) shuffle(t time.Time) string {
defer fmt.Println(t)
numerallist := []rune(strconv.Itoa(r.n))
rand.Seed(time.Now().Unix())
rand.Shuffle(len(numerallist), func(i, j int) { numerallist[i], numerallist[j] = numerallist[j], numerallist[i] })
return string(numerallist)
}
func (r words) shuffle(t time.Time) string {
defer fmt.Println(t)
wordlist := strings.Split(r.w, " ")
rand.Seed(time.Now().Unix())
rand.Shuffle(len(wordlist), func(i, j int) { wordlist[i], wordlist[j] = wordlist[j], wordlist[i] })
return strings.Join(wordlist, " ")
}
func letsrock(c confuser) {
fmt.Println(c)
fmt.Println(c.shuffle(time.Now()))
}
func main() {
inputn := numerals{1234567890}
inputw := words{"penguin Tux eats fish"}
letsrock(inputn) /*
{1234567890}
2020-09-26 20:59:38.419783812 +0200 CEST m=+0.000065019
3265847091
*/
letsrock(inputw) /*
{penguin Tux eats fish}
2020-09-26 21:18:38.019358421 +0200 CEST m=+0.000169138
Tux eats penguin fish
*/
}
Error-Handling
Datentyp error
- Go ist beim Werfen von Fehlern nicht auf try-catch-finally Blöcke und Exceptions angewiesen, sondern nutzt hierzu den Datentyp error.
- Nachfolgend zwei Beispiele für das Werfen von Fehlern, die die Funktion Error() des errors-Interfaces (type error interface { Error() string }) implementieren:
package main
import (
"fmt"
"time"
)
type ErrorMessage struct {
Zeitpunkt time.Time
Meldung string
}
func (e *ErrorMessage) Error() string { // Implementiere die Funktion Error() des errors-Interfaces als Methode der Struktur ErrorMessage
return fmt.Sprintf("Um %v, %s", e.Zeitpunkt, e.Meldung)
}
func reportError() error {
return &ErrorMessage{time.Now(), "Ach manno!"}
}
func main() {
if err := reportError(); err != nil {
fmt.Println(reportError()) // Um 2016-12-31 23:59:59.999999999 +0100 CET, Ach manno!
}
}
package main
import (
"fmt"
)
type ErrorMessage string
func (e ErrorMessage) Error() string {
return string(e)
}
func reportError() error {
return ErrorMessage("Na sowas!")
}
func main() {
if err := reportError(); err != nil {
fmt.Println(err) // Na sowas!
}
}
- Anstelle einer eigenen Funktion zur Implementierung des errors-Interfaces kann alternativ auch die Build-In-Funktion errors.New() zum Werfen eines Fehlers genutzt werden:
package main
import (
"errors"
"fmt"
)
func reportError() error {
return errors.New("Grrr!")
}
func main() {
if err := reportError(); err != nil {
fmt.Println(err) // Grrr!
}
}
Abfangen von Fehlern
- defer func()
- Das defer-Statement weist eine Funktion an, nach der return-Anweisung (also zum Zeitpunkt der Beendigung der umgebenden Funktion) ausgeführt zu werden. Existieren mehrere defer-Funktionsaufrufe, werden diese nacheinander in umgekehrter Reihenfolge (Last In First Out) ausgeführt, nachdem die Funktion endete.
- panic(interface{})
- Eine Build-In-Funktion zur sofortigen Unterbrechung der Funktionsauführung mit Fehlermeldung.
- recover() interface{}
- Fängt den panic-State einer Funktion ab zur Wiedererlangung der Kontrolle. Nur innerhalb von mit defer ausgelösten Funktionen sinnvoll.
package main
import "fmt"
func fehlerhafteFunktion() {
defer func() { // 7.
if err := recover(); err != nil { // Da der error zuvor schon abgefangen wurde und nun nicht mehr besteht, ...
fmt.Println("Wird nicht ausgegeben, da der Fehler zuvor schon abgefangen worden war!") // ... wird die Ausführung dieser Code-Zeile verhindert.
}
}()
defer fmt.Println("Hier steht auch noch was!") // 6.
defer func() { // 4.
fmt.Printf("Es gab einen Fehler: \"%v\" Der Fehler wurde abgefangen!\n", recover()) // 5.
}()
fmt.Println("Bevor der Fehler ausgelöst wird …") // 2.
panic("Jetzt schlägts dreizehn!") // 3.
fmt.Println("… und wenn kein Fehler ausgelöst worden wäre")
}
func main() {
fehlerhafteFunktion() // 1.
fmt.Println("Und hier gehts dann weiter!") // 8.
}
Goroutinen (Nebenläufigkeit)
Asynchrone Laufzeit
Goroutinen werden im selben Adressraum ausgeführt, der Zugriff auf gemeinsamen Speicher muss also synchronisiert werden. Es ist im Hauptprogramm zu beachten, auf nebenläufige Unterprogramme zu warten.
package main
import (
"fmt"
"math/rand"
"runtime"
"time"
)
func say(s string, proc int) {
r := rand.New(rand.NewSource(time.Now().UnixNano()))
for i := 0; i < 5; i++ {
time.Sleep(time.Duration(r.Intn(10000)) * time.Millisecond) // Der Zufallsgenerator macht die unterschiedliche Ausführungsdauer der einzelnen Unterprogramme noch besser sichtbar.
fmt.Printf("Prozess=%v, Schleife=%v, Wert=%v\n", proc, i, s)
}
fmt.Printf("\nProzess %v ist fertig!\n\n", proc)
}
func main() {
runtime.GOMAXPROCS(runtime.NumCPU()) // Normalerweise laufen alle Prozesse im gleichen Thread. Sage dem Compiler, wieviele Threads er öffnen darf.
go say("Bye", 1) // Wird mit dem Schlüsselwort go eine Routine ausgeführt, fährt das Hauptprogramm ohne zu warten fort!
go say(" ", 2)
go say("bye", 3)
go say(", ", 4)
go say("Java", 5)
go say("!", 6)
// Die Funktion ist nun 6 mal gestartet worden. Und unser Programm ist fertig - fertig, ohne daß auch nur eine einzige say-Funktion hätte ausgeführt werden können. Deswegen verordnen wir unserem Hauptprogramm etwas Wartezeit:
time.Sleep(time.Second * 60)
fmt.Println("Puuuh!")
}
Synchronisation über WaitGroups
Eine fest vordefinierte Wartezeit ist natürlich nicht sinnvoll. Besser ist es, auf die Ausführung der einzelnen Subroutinen zu warten.
package main
import (
"fmt"
"math/rand"
"runtime"
"sync"
"time"
)
var wg sync.WaitGroup
func say(s string, proc int) {
defer wg.Done()
r := rand.New(rand.NewSource(time.Now().UnixNano()))
for i := 0; i < 5; i++ {
time.Sleep(time.Duration(r.Intn(10000)) * time.Millisecond) // Der Zufallsgenerator macht die unterschiedliche Ausführungsdauer der einzelnen Unterprogramme noch besser sichtbar.
fmt.Printf("Prozess=%v, Schleife=%v, Wert=%v\n", proc, i, s)
}
fmt.Printf("\nProzess %v ist fertig!\n\n", proc)
}
func main() {
runtime.GOMAXPROCS(runtime.NumCPU()) // Normalerweise laufen alle Prozesse im gleichen Thread. Sage dem Compiler, wieviele Threads er öffnen darf.
wg.Add(6)
go say("Bye", 1) // Wird mit dem Schlüsselwort go eine Routine ausgeführt, fährt das Hauptprogramm ohne zu warten fort!
go say(" ", 2)
go say("bye", 3)
go say(", ", 4)
go say("Java", 5)
go say("!", 6)
wg.Wait() // Anstelle der Wartezeit, warten wir besser auf das Ende der letzten Funktion
fmt.Println("Puuuh!")
}
Synchronisation über den wechselseitigen Ausschluß ("Mutex")
Ein Mutex (Abk. für mutual exclusion) erlaubt das Setzen und Freigeben von Sperren
package main
import (
"fmt"
"sync"
"time"
)
var mtx sync.Mutex
func doSomething() {
mtx.Lock()
for i := 0; i <= 1000; i++ {
fmt.Println("DoSomething", i)
}
mtx.Unlock()
}
func doSomethingElse() {
defer mtx.Unlock()
mtx.Lock()
for i := 0; i <= 1000; i++ {
fmt.Println("DoSomethingElse", i)
}
}
func main() {
// Das Lock verhindert die Abarbeitung der zweiten Funktion, bevor die erste
// ihre Aufgabe beendet hat.
// Ohne das Lock würden beide Funktionen ihre Textausgaben zeitgleich vermischen.
go doSomething()
go doSomethingElse()
time.Sleep(3 * time.Second)
}
Kommunikation zwischen Routinen
- Die Kommunikation zwischen den Routinen läuft über Channels. Die Channel sind (wie Variablen) typisiert. Ein Channel kann als Pipe oder Semaphore verstanden werden.
Channel-Typen
chan // bidirektional
chan<- // nur senden
<-chan // nur empfangen
Channel-Deklaration
var chA chan int
chA = make(chan int) // Ungepufferter Channel. Der Channel wird in der Regel im Hauptprogramm definiert und wird wie eine Variable an die Unterprogramme übergeben.
chB := make(chan int, 10) // Gepufferter Channel, der 10 Werte zwischenspeichern kann.
- Anmerkung zu Channel-Puffern
- Ist der Channel ungepuffert oder die maximale Anzahl der zu puffernden Variablen erreicht, dann
- * wird der Schreiber solange blockiert, bis der Channel ausgelesen wurde und wieder leer ist!
- * wird der Leser solange blockiert, bis der Channel nicht mehr leer ist und wieder einen Inhalt hat!
- * Die Kapazität eines Channels ist auf maximal 100 Elemente beschränkt!
Channel I/O
ch <- v // Sende v über Kanal ch.
v := <-ch // Empfange von ch und weise den empfangenen Wert v zu.
- Anmerkung
- Wenn in einen Channel geschrieben wird, bleibt der Programmzeiger solange an dieser Stelle stehen, bis aus dem Channel wieder gelesen wurde. Dies kann umgangen werden, wenn das Schreiben nebenläufig von einer anonymen Funktion übernommen wird (go func(){mychannel <- 123}()).
package main
import (
"fmt"
"math/rand"
"runtime"
"time"
)
func think(s string, proc int, c chan string) {
var timestamp [2]int64
timestamp[0] = time.Now().Unix()
for i := 0; i < 5; i++ {
rand.Seed(time.Now().UnixNano())
time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond) // Der Zufallsgenerator macht die unterschiedliche Ausführungsdauer der einzelnen Unterprogramme noch besser sichtbar.
c <- fmt.Sprintf("Prozess=%v, Schleife=%v, Wert=%v\n", proc, i, s) // Schreibe in Channel
}
timestamp[1] = time.Now().Unix()
c <- fmt.Sprintf("\nProzess %v ist fertig und hat %v Sekunden gedauert!\n\n", proc, (timestamp[1] - timestamp[0])) // Schreibe in Channel
}
func say(l int, c chan string) {
for i := 1; i <= l; i++ {
cvalue, cstatus := <-c
if cstatus {
fmt.Println(cvalue) // Lese aus Channel
} else {
break
}
}
}
func main() {
runtime.GOMAXPROCS(runtime.NumCPU()) // Normalerweise laufen alle Prozesse im gleichen Thread. Sage dem Compiler, wieviele Threads er öffnen darf.
ChanelNo5 := make(chan string, 1) // Gepufferte Ausgabe wäre hier nicht nötig, ist aber möglich
go think("Bye", 1, ChanelNo5) // Die Channel werden wie Variablen an die Unterprogramme übergeben
go think(" ", 2, ChanelNo5)
go think("bye", 3, ChanelNo5)
go think(", ", 4, ChanelNo5)
go think("Java", 5, ChanelNo5)
go think("!", 6, ChanelNo5)
say((6*5 + 6), ChanelNo5)
close(ChanelNo5) // Channels können wieder geschlossen werden, wenn sie nicht mehr benötigt werden
fmt.Println("Puuuh!")
}
Das selektive Warten auf bestimmte Channel-Aktivitäten
Ein der switch-Kontrollstruktur sehr ähnlicher select-Block ermöglicht die Bedienung mehrerer Channels. Priorität hat der Channel, der (zum Lesen/Schreiben gleichermaßen) bereit ist. Warten mehrere Channels, erfolgt die Priorisierung zufällig. Es gibt auch eine default-Bedingung, die dann ausgeführt wird, wenn kein Channel bereit ist. Beim Weglassen der default-Bedingung, blockiert select solange, bis ein Channel bereit ist.
package main
import (
"fmt"
"math/rand"
"runtime"
"time"
)
func think(s string, proc int, c chan string, stop chan bool) {
var timestamp [2]int64
timestamp[0] = time.Now().Unix()
for i := 0; i < 5; i++ {
rand.Seed(time.Now().UnixNano())
time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
c <- fmt.Sprintf("Prozess=%v, Schleife=%v, Wert=%v\n", proc, i, s) // Schreibe in Channel
}
timestamp[1] = time.Now().Unix()
c <- fmt.Sprintf("\nProzess %v ist fertig und hat %v Sekunden gedauert!\n\n", proc, (timestamp[1] - timestamp[0])) // Schreibe in Channel, ...
stop <- true // ... und beende auch alle anderen Routinen
}
func say(c chan string, stop chan bool) {
for {
select {
case cvalue, cstatus := <-c:
if cstatus {
fmt.Print(cvalue) // Lese aus Channel
}
case <-stop:
return
default:
time.Sleep(50 * time.Millisecond)
}
}
}
func main() {
runtime.GOMAXPROCS(runtime.NumCPU())
ChanelNo5 := make(chan string, 1)
ChanelNo19 := make(chan bool, 6) // Sobald dieser Channel true enthält, sollen alle Prozesse beendet werden.
go think("Bye", 1, ChanelNo5, ChanelNo19)
go think(" ", 2, ChanelNo5, ChanelNo19)
go think("bye", 3, ChanelNo5, ChanelNo19)
go think(", ", 4, ChanelNo5, ChanelNo19)
go think("Java", 5, ChanelNo5, ChanelNo19)
go think("!", 6, ChanelNo5, ChanelNo19)
say(ChanelNo5, ChanelNo19)
fmt.Println("Puuuh!")
close(ChanelNo5) // Channels können wieder geschlossen werden, wenn sie nicht mehr benötigt werden
close(ChanelNo19)
}
Channel-Puffer prüfen
package main
import "fmt"
var pipe chan int
func main() {
pipe = make(chan int, 3)
pipe <- 123
pipe <- 456
fmt.Println(cap(pipe)) // 3
fmt.Println(len(pipe)) // 2
fmt.Println(<-pipe) // 123
fmt.Println(len(pipe)) // 1
fmt.Println(<-pipe) // 456
fmt.Println(len(pipe)) // 0
fmt.Println(cap(pipe)) // 3
}
Deadlock verhindern:
select {
case ch <- true:
default:
fmt.Println("Habe den Channel voll!")
}
oder:
if len(ch) == cap(ch) {
fmt.Println("Habe den Channel gestrichen voll!")
} else {
fmt.Println("Alles gut!")
}
Der context-Datentyp
Der Datentyp context kann Zeitpunkte/Fristen, Cancel-Signale oder auch Vergleichswerte auf mehrere Routinen verteilen. Er ist mit geringerem Aufwand als Signal-Bus einzusetzen, als gepufferte Channels.
package main
import (
"context"
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
const (
rightString = "Oh Yeah"
wrongString = "D'oh"
)
func doSth1(ctx context.Context) {
defer wg.Done()
select {
case <-time.After(1 * time.Second):
fmt.Println("just in time")
case <-ctx.Done():
fmt.Println(ctx.Err())
}
}
func doSth2(ctx context.Context, k string) {
defer wg.Done()
if v := ctx.Value(k); v != nil {
fmt.Println("found value:", v)
return
}
fmt.Println("key not found:", k)
}
func main() {
fmt.Println("Context test 1 - Deadline:")
d := time.Now().Add(time.Duration(3000 * time.Millisecond))
ctx, cancel := context.WithDeadline(context.Background(), d)
defer cancel()
wg.Add(6)
go doSth1(ctx)
go doSth1(ctx)
go doSth1(ctx)
time.Sleep(3 * time.Second)
go doSth1(ctx)
go doSth1(ctx)
go doSth1(ctx)
wg.Wait()
// Context test 1 - Deadline:
// just in time
// just in time
// just in time
// context deadline exceeded
// context deadline exceeded
// context deadline exceeded
fmt.Println("\nContext test 2 - Value:")
ctx = context.WithValue(context.Background(), "Oh Yeah", rightString)
wg.Add(8)
go doSth2(ctx, rightString)
go doSth2(ctx, rightString)
go doSth2(ctx, rightString)
go doSth2(ctx, rightString)
go doSth2(ctx, wrongString)
go doSth2(ctx, wrongString)
go doSth2(ctx, wrongString)
go doSth2(ctx, wrongString)
wg.Wait()
// Context test 2 - Value:
// key not found: D'oh
// found value: Oh Yeah
// found value: Oh Yeah
// found value: Oh Yeah
// key not found: D'oh
// key not found: D'oh
// key not found: D'oh
// found value: Oh Yeah
}
go test
Konvention bei Datei- und Funktionsnamen
Referenz-Code
- z.B. in Datei main.go
package main
import (
"fmt"
"math"
)
var input float64
func main() {
fmt.Println("Enter number:")
fmt.Scan(&input)
fmt.Printf("math: √%f=%.10f\n", input, math.Sqrt(input))
fmt.Printf("homemade: √%f=%.10f\n", input, Sqrt(input))
}
func Sqrt(radikand float64) float64 {
ergebnis := float64(2)
ergebnisAlt := float64(0)
for {
ergebnis = ergebnis - (ergebnis*ergebnis-radikand)/(2*ergebnis)
// Iteriere solange, bis vorherigeZahl und aktuelleZahl bis auf 10 Stellen hinter dem Komma gleich sind
if int(ergebnis*1E10) == int(ergebnisAlt*1E10) {
break
}
ergebnisAlt = ergebnis
}
return ergebnis
}
Testcode
- z.B. in Datei main_test.go oder mein_test.go oder Dein_test.go; (Nicht jedoch maintest.go!)
package main
import (
"math"
"testing"
)
//Namenskonvention: Test…
func TestMyTestingFunction(t *testing.T) {
var testzahl float64 = 12361423
ergebnisIst := Sqrt(testzahl)
ergebnisSoll := math.Sqrt(testzahl)
if ergebnisIst != ergebnisSoll {
t.Error("Erwartung:", ergebnisSoll, "Ergebnis:", ergebnisIst)
}
}
//Namenskonvention: Benchmark…
func BenchmarkMyBenchmarkingFunctionHomemade(b *testing.B) {
var testzahl float64 = 12361423
for i := 0; i < b.N; i++ {
_ = Sqrt(testzahl)
}
}
//Namenskonvention: Benchmark…
func BenchmarkMyBenchmarkingFunctionMath(b *testing.B) {
var testzahl float64 = 12361423
for i := 0; i < b.N; i++ {
_ = math.Sqrt(testzahl)
}
}
Ausführung
Unittests
$ go test
=== RUN TestMyTestingFunction
--- PASS: TestMyTestingFunction (0.00s)
PASS
ok mySqrt 0.008s
Benchmarks
$ go test -test.bench=.*
BenchmarkMyBenchmarkingFunctionHomemade-4 10000000 121 ns/op
BenchmarkMyBenchmarkingFunctionMath-4 2000000000 0.31 ns/op
PASS
ok mySqrt 1.996s
Tipp: Unsystematische Laufzeit-Messung von Codesegmenten
package main import ( "log" "math" "time" ) func main() { zeitpunkt := time.Now() a := math.Sqrt(123456) log.Println(time.Since(zeitpunkt)) //2018/11/07 20:30:56 152ns _ = a }
Profiling
$ go test -cpuprofile cpu.prof -memprofile mem.prof -bench .
$ go tool pprof cpu.prof
Entering interactive mode (type "help" for commands)
(pprof) png > Schaubild.png
(pprof) weblist
(pprof) help
…
Kontrollieren der Speicherverwaltung
- GC findet nur im dynamischen Speicher (Heap) statt.
- Arrays (nicht Slices!) stehen im Stapelspeicher (Stack) und können zur Vermeidung von GCs genutzt werden.
- Die Zuweisung von nil (a = nil) gibt den Speicherbereich für die GC frei; durch die "Weiternutzung" des Speicherbereichs (a = a[:0]) kann die GC überlistet werden.
- Die Garbage Collection wird ausgelöst, sobald der Anteil frisch zugewiesener Daten im Verhältnis zu den nach der vorhergehenden GC verbliebenden Daten einen bestimmten Prozentsatz erreicht hat:
…
import (
"runtime"
"runtime/debug"
)
func main() {
…
debug.SetGCPercent(50) // Sobald der Anteil neuer Daten im Heap (seit der letzten GC) 50% des Anteils der Altdaten erreicht hat, wird die GC ausgelöst
debug.SetGCPercent(100) // Der Default-Wert liegt bei 100%
debug.SetGCPercent(-1) // Die GC wird vollständig abgeschaltet!
runtime.GC() // Die GC wird manuell ausgelöst
…
}
Siehe http://stackoverflow.com/questions/12277426/how-to-minimize-the-garbage-collection-in-go
Reflections – Bezüge auf den eigenen Code
package main
import (
"fmt"
"reflect"
)
type einObjektTyp struct {
Maja int
willi float32
flip bool
Max string
}
var motz int
var Rempel int16
func main() {
var einObjekt einObjektTyp = einObjektTyp{123, 456.789, true, "einszweidrei"}
fmt.Println(reflect.ValueOf(einObjekt)) // {123 456.789 true einszweidrei}
fmt.Println(reflect.TypeOf(einObjekt)) // main.einObjektTyp
fmt.Println(reflect.ValueOf(einObjekt).Field(1)) // 456.789
fmt.Println(reflect.TypeOf(einObjekt).FieldByName("willi")) // {willi main float32 8 [1] false} true
fmt.Println(reflect.TypeOf(einObjekt).Field(2).Type) // bool
fmt.Println(reflect.TypeOf(einObjekt).Field(2).Index) // [2]
fmt.Println(reflect.TypeOf(einObjekt).Field(2).Name) // flip
fmt.Println(reflect.TypeOf(einObjekt).Field(2).Offset) // 12, weil 8 (int) + 4 (float32) = 12
fmt.Println(reflect.TypeOf(einObjekt).Field(3).Offset) // 16, weil 8 (int) + 4 (float32) + 4 (bool) = 16
fmt.Println(reflect.ValueOf(&einObjekt).Elem().Field(3)) // einszweidrei
// Variablen können mit Hilfe eines Pointers beschreiben werden! Felder einer Struktur müssen jedoch exportiert werden.
fmt.Println(reflect.ValueOf(&einObjekt).Elem().Field(3).CanSet()) // true, weil "Max" exportiert wird (public)
fmt.Println(reflect.ValueOf(&einObjekt).Elem().Field(1).CanSet()) // false, weil "willi" nicht exportiert wird (private)
reflect.ValueOf(&einObjekt).Elem().Field(3).SetString("eintausendundzwei")
//reflect.ValueOf(&einObjekt).Elem().Field(1).SetFloat(1.2345) wäre deshalb unzulässig!
fmt.Println(reflect.ValueOf(&motz).Elem().CanSet()) // true, da "motz" eine Variable ist und nicht Feld einer Struktur!
fmt.Println(reflect.ValueOf(&Rempel).Elem().CanSet()) // true
// Bei Verwendung des typunabhängigen Set(x reflect.Value) muß auch das aus der Struktur gelesene Feld exportierbar sein!
reflect.ValueOf(&motz).Elem().Set(reflect.ValueOf(einObjekt).Field(0))
}
Iteration durch Strukturen
- Da nicht mit range durch Strukturen iteriert werden kann, hilft hier das reflect-Package:
package main
import (
"fmt"
"reflect"
)
type einObjektTyp struct {
Maja int
willi float32
flip bool
Max string
}
func main() {
var einObjekt einObjektTyp = einObjektTyp{123, 456.789, true, "einszweidrei"}
for i, rType, rValue := 0, reflect.TypeOf(&einObjekt).Elem(), reflect.ValueOf(&einObjekt).Elem(); i < rType.NumField(); i++ {
fmt.Println(rType.Field(i), rValue.Field(i)) /*
{Maja int 0 [0] false} 123
{willi main float32 8 [1] false} 456.789
{flip main bool 12 [2] false} true
{Max string 16 [3] false} einszweidrei
*/
}
}
C code einbinden
Benutzung von cgo über das Pseudo-Package "C"
- Compiler- und linker-flags (CFLAGS, CPPFLAGS, CXXFLAGS, FFLAGS, DFLAGS, …)
- sind notwendig und werden als Kommentar-Annotationen mit #cgo-Direktive direkt über dem Import von "C" bekannt gemacht.
package main
import (
"fmt"
Go "math"
"time"
"unsafe"
)
// #cgo CFLAGS: -O2 -march=native
// #cgo LDFLAGS: -lm
// #include <math.h>
// #include <stdio.h>
// #include <stdlib.h>
// #include <ctype.h>
/*
double mySqrt(int mydata)
{
return sqrt(mydata);
}
struct
{
int tag;
char monat[10];
int jahr;
char *anlass;
} termin1;
struct anonym
{
int tag;
char monat[10];
int jahr;
char *anlass;
};
*/
import "C"
const steps int64 = 100000000
var (
a float64
b C.double
pg *float64 // Zeiger in Go
pc *C.double // Zeiger in C
sg string // String in Go
sc *C.char // String in C
bg []byte // Slice of Bytes in Go
ag [5]int32 // Int-Array in Go
ac [5]C.int // Int-Array in C
)
func main() {
// Verwende math aus den Go-Packages
t0 := time.Now()
for i := (int64)(1); i <= steps; i++ {
a = Go.Sqrt(float64(i))
}
fmt.Println(time.Since(t0)) // Ca. 170 ms Ausführungszeit der nativen Funktion aus dem Go-math-Package ist signifikant schneller ...
// Verwende math aus den C-Packages
t1 := time.Now()
for i := (C.int)(1); i <= C.int(steps); i++ {
b = C.sqrt(C.double(i))
}
fmt.Println(time.Since(t1)) // ... als bei Verwendung der math.h; ca. 5s
// Schreibe eine eigene C-Funktion
t2 := time.Now()
for i := (C.int)(1); i <= C.int(steps); i++ {
b = C.mySqrt(i)
}
fmt.Println(time.Since(t2)) // Ca. 5s
// Zeiger funktioneren in CGO auf dieselbe Weise
pc = &b // Adresse aus dem C-Adressraum
pg = (*float64)(&b) // Umwandlung eines C-Pointers in einen Go-Pointer
fmt.Println(pc, pg) // Beide Adressen sind identisch (z.B. 0x77f3c8 0x77f3c8)
pc = (*C.double)(pg) // Umwandlung eines Go-Pointers in einen C-Pointer
fmt.Println(pg) // z.B. 0x77f3c8
// String-Verarbeitung
sg = "abcdefghijklmnopqrstuvwxyz"
sc = C.CString(sg) // Wandele Go-String in C-String
fmt.Println(C.GoString(sc)) // Wandele C-String in Go-String (abcdefghijklmnopqrstuvwxyz)
fmt.Println(C.GoStringN(sc, 5)) // Wandele C-String mit definierter Länge in Go-String (abcde)
// Byte-Verarbeitung
bg = C.GoBytes(unsafe.Pointer(sc), 26) // Die Umwandlung eines Byte-Arrays von C nach Go erfordert einen unsafe.Pointer
fmt.Println(bg) // [97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122]
bc := *(*[26]byte)(C.CBytes(bg)) // bg ist ein unsafe.Pointer auf den Beginn der Bytefolge. Wandele ihn in einen sicheren Pointer des Datentyps []byte um und gebe dessen Inhalt an bc aus. Wichtig ist hier die Längenangabe des Arrays, da C keine Slices kennt!
fmt.Println(bc) // [97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122]
// Memory-Management
defer C.free(unsafe.Pointer(sc)) // In C gibt es keinen Garbage-Collector, wir müssen uns selber darum kümmern, den (mit malloc(),
// calloc() oder realloc()) alloziierten Speicherbereich wieder freizugeben, so wie es bei char pointern auf Zeichenketten
// unbestimmter Länge durch cgo beim kompillieren geschieht
// Arrays/Slices - werden über Zeiger auf ihre Startadressen ausgetauscht. Grundsätzlich ist zu beachten, daß der Zeiger auf das erste Element eines Arrays/Slices zeigt: "&slice[0]" und nicht "&slice" !
ag = [5]int32{1, 2, 3, 4, 5} // Achtung! Auf die richtige Datenlänge muß geachtet werden C.int entspricht int32 in Go!
ac = *(*[5]C.int)(unsafe.Pointer(&ag))
fmt.Println(ag, ac) // [1 2 3 4 5] [1 2 3 4 5] Bei int64 wäre das Ergebnis der Umwandlung [1 2 3 4 5] [1 0 2 0 3]
// Zugriff auf benannte C-Struktur
C.termin1.tag = 24
C.termin1.monat = *(*[10]C.char)(unsafe.Pointer(&[10]byte{'D', 'e', 'z', 'e', 'm', 'b', 'e', 'r'}))
C.termin1.jahr = 2018
C.termin1.anlass = C.CString("Weihnachten")
fmt.Println(C.termin1) // {24 [68 101 122 101 109 98 101 114 0 0] [0 0] 2018 [0 0 0 0] 0x14d9840}
// Zugriff auf unbenannte C-Struktur über das optionale structure tag
var termin2 C.struct_anonym //Beachte den Prefix "C.struct_" vor dem structure tag, ohne den die anonyme Struktur nicht adressierbar wäre!
termin2.tag = 24
termin2.monat = *(*[10]C.char)(unsafe.Pointer(&[10]byte{'D', 'e', 'z', 'e', 'm', 'b', 'e', 'r'}))
termin2.jahr = 2018
termin2.anlass = C.CString("Weihnachten")
fmt.Println(termin2) // {24 [68 101 122 101 109 98 101 114 0 0] [0 0] 2018 [0 0 0 0] 0x14d9860}
}
Referenzierung von C zu Go
In der Regel werden zwei Go-Code-Files benötigt, da CGO zwei C-Output-Files der exportierten Funktionen erzeugt und dort ansonsten alle C-Definitionen unzulässig doppelt existierten.
//File 1 von 2
package main
/*
#cgo CFLAGS: -O2 -march=native
extern void ccallsme(char *);
void callback2go(char *text)
{
ccallsme(text);
}
*/
import "C"
func main() {
C.callback2go(C.CString("Ich bin eine Go-Funktion wurde von einer C-Funktion aufgerufen"))
}
Der zur Annotation erweiterte Kommentar //export ist notwendig, damit CGO die Go-Funktion im C-Code referenzieren kann:
//File 2 von 2
package main
//#cgo CFLAGS: -O2 -march=native
import "C"
import "fmt"
//export ccallsme
func ccallsme(text *C.char) {
fmt.Println(C.GoString(text))
}
Siehe https://golang.org/cmd/cgo/[13]
- Tipp
- Sehr hilfreich bei der Arbeit mit cgo ist die Verwendung von go tool cgo main.go. Es erzeugt im Projektverzeichnis einen Order _obj mit den von Compiler generierten Objekt-Codes. Beispielsweise die Datei _obj/_cgo_gotypes.go hilft beim nicht ganz einfachen Type-Casting zwischen C und Go.
- Noch ein Tipp
- Nebenläufigkeit mit cgo verwendenden Routinen kann insbesondere bei der Verwendung externer C-Libraries zu unvorhergesehenen Speicherzugriffsfehlern führen, die im einzeln ausgeführten Prozess nicht auffallen. In diesem Fall kann die Flankierung von cgo-Codes mit (global definierten!) Mutexes (sync.Mutex.Lock() vorher und sync.Mutex.Unlock() hinterher) für Stabilität sorgen.
GUI
Verteilte Systeme
IDE
- Die universelle Entwicklungsumgebung Visual Studio Code von Microsoft ist so gut, daß ich sie selbst als eingefleischter Linux-Freak empfehle! Sie ist derzeit meine erste Wahl für Projekte mit unterschiedlichen Code-Formaten.
- Mein absoluter Favorit speziell für Go ist jedoch die LiteIDE. Sie ist wirklich "leicht", jedoch erstaunlich mächtig und ausgereift. Alles ist auf kurzem Wege intuitiv erreichbar und geht fühlbar zügiger als bei VSCode, grobe Bugs sucht man vergebens. Das Layout ist sehr übersichtlich und durchdacht. Lediglich eine GIT-Sourcecode-Verwaltung vermisse ich.
Links
- Die offiziellen Seiten:
- golang.org (Insbesondere Specs und Packages) isr die offizielle Seite mit Dokumentation, Tutorial und Blog
- godoc.org beherbergt die Code-Dokumentationen für Go-Packages aus Bitbucket, GitHub, Google Project Hosting & Launchpad
- goissues.org Hier werden Bugs aus den Paketen des Go-Ökosystems gemeldet, gesammelt und besprochen.
- gochanges.org zeigt Veränderungen in Go packages.
- Tutorien & Lehrbücher:
- Offizielles Tutorial
- Das sehr ausführliche und mit anschaulichen Beispielen versehene Go-Lehrbuch von Big Yuuta auf https://go-book.appspot.com
- Matt Aimonetti: Go Bootcamp – Everything you need to know to get started with Go. (Onlineausgabe frei verfügbar)
- Die leicht verständliche deutschsprachige Einführung von Kai Spichale, Teil 1 - Grundlagen und Teil 2 - Objektorientierung
- Effektiv Go programmieren
- SliceTricks – Hilfestellungen beim Umgang mit Slices/Arrays
- Blogs:
- Hervorragende deutsche GO-Beschreibung von Harald Weidner (Sehr anschaulich, bietet sehr viele Handreichungen zum Erlernen der Programmiersprache! Der immer aktuelle News-Blog ist zum täglichen Nachschauen sehr zu empfehlen!)
- appliedgo.net – Applied Go - Go beyond the Go tutorials (Ein SUPER-Blog um noch tiefer in die Go-Welt einzutauchen!)
- A Gopher's Reading List
- Libraries & Frameworks:
- Code-Beispiele und Vergleiche mit anderen Sprachen:
- gobyexample.com (Ein Tutorial für jede Gelegenheit, spartanisch, übersichtlich, das Wesentliche auf einen Blick)
- https://yourbasic.org/ (Exzellente Go-Fundgrube)
- golangbyexample.com (Noch ein Tutorial für jede Gelegenheit, schöner, manchmal detaillierter, das Wesentliche findet man beim zweiten Blick 😉)
- gowebexamples.com (Go für Web-Entwickler)
- govspy.peterbe.com (Go für Python-Entwickler)
- godbolt.org godbolts Compiler-Explorer ist genial! Das perfekte Werkzeug um Compilerversionen zu testen, Compilersprachen zu vergleichen und Code zu optimieren.
- go-vs-python
- benchmark-go-vs-python-pypy
- Foren:
Benchmark
Siehe Benchmark Go vs. Java vs. Python[14]
Kritik
Der Streitpunkt schlechthin: Go kennt keine Generics. Das Thema wird wunderbar erörtert in: Who needs generics? Use ... instead!, vom 14.7.2016 (https://appliedgo.net/generics/)Siehe Erweiterung ab go1.18- ???!
Anmerkungen
- ↑ Vgl. Sakshi Sharma: "Programming With Go (GOlang) And It’s Benefits", December 18, 2017
- ↑ Dieser und andere Aspekte werden sehr anschaulich erklärt von Peter Verhas: "Comparing Golang with Java", JavaDeep, April 27, 2016 und Christian Helmbold: "Pro und contra Go", 22. September 2013.
Unbedingt lesenswert ist hierzu auch die sehr objektive kritische Analyse von Daniel Lemire: "Best programming language for high performance (January 2017)?", Daniel Lemire's blog, January 16, 2017
Die objektiven Gegenüberstellungen "Rust vs Go in 2023" von John Arundel, "Go vs Rust? Choose Go." von Matthias Endler (15.9.2017) und "Should I Rust, or Should I Go" von Andrew Lader (26.10.2017) sind sehr empfehlenswert und machen neugierig auch auf Rust. - ↑ Einen bemerkenswerten Trend zu Go erkennt auch das Java-Infoportal Jaxenter in: Dominik Mohilo: "Google Go: Darum ist die Programmiersprache so beliebt", 9. März 2017.
Das derzeit möglicherweise stärkste deutschsprachige Plädoyer für Go findet sich auf entwickler.de. Dort stellt Sebastian Mancke die Frage: "Ist Golang das neue Java?" (29.5.2017)
Über weitere Vorzüge der Programmiersprache Go schreiben Kirill Rogovoy: "Here are some amazing advantages of Go that you don’t hear much about", February 1, 2018 und Sugandha Lahoti: "Why Golang is the fastest growing language on GitHub", August 9, 2018 - ↑ https://jaxenter.de/5-dinge-go-golang-hass-69789
- ↑ https://golang.org/doc/install/gccgo: "[...] The GCC 4.9 releases include a complete Go 1.2 implementation. The GCC 5 releases include a complete implementation of the Go 1.4 user libraries. The Go 1.4 runtime is not fully merged, but that should not be visible to Go programs. [...]"
- ↑ Siehe hierzu und auch zu GOPROXY https://golang.org/cmd/go/#hdr-Module_configuration_for_non_public_modules
- ↑ Siehe https://blog.boot.dev/golang/how-to-use-golangs-generics/
- ↑ Vergleiche Interface und Pointer-Receiver
- ↑ Neuere Implementierungen überschreiben vorherige!
- ↑ Adressiere die Felder stattdessen über "Getter"- und "Setter"-Methoden.
Die Implementierung von Go-Interfaces im Vergleich zu Java-Interfaces wurde am 27. April 2016 von Peter Verhas sehr anschaulich beschrieben: "Interfaces are very simple in Go, and the same time very complex, or at least different from what they are in Java. Interfaces declare a bunch of functions that structs should implement if they want to be compliant with the interface. The inheritance is done the same way as in case of structs. The strange thing is that you need not specify in case of a struct that it implements an interface if it does. After all it is really not the struct that implements the interface, but rather the set of functions that use the struct or a pointer to the struct as receiver. If all the functions are implemented then the struct does implement the interface. If some of them are missing then the implementation is not complete.
Why do we need the 'implements' keyword in Java and not in Go? Go does not need it because it is fully compiled and there is nothing like a classloader that loads separately compiled code during run-time. If a struct is supposed to implement an interface but it does not then this will be discovered during compile time without explicitly classifying that the struct does implement the interface. You can overcome this and cause a run-time error if you use reflection (that Go has) but the 'implements' declaration would not help that anyway." (Siehe https://javax0.wordpress.com/2016/04/27/comparing-golang-with-java/) - ↑ Siehe auch Abschnitt Composite Types mittels Interface und Struct und ausführlichen Post zum Thema: http://jordanorelli.com/post/32665860244/how-to-use-interfaces-in-go
- ↑ Vergleiche Wo Pointer-Receiver notwendig sind
- ↑ Zur weiterern Vertiefung: https://dave.cheney.net/2016/01/18/cgo-is-not-go
- ↑ Auf Basis von BenchmarkPzf.go, BenchmarkPzf.py und BenchmarkPzf.java