Go

Go #

Typen #

Structs #

// Deklaration
type Vertex struct {
	X int
	Y int
}

// Initialisierung
v := Vertex{1, 2}

// Zugriff
fmt.Println(v.X)

// Zuweisung
v.X = 4

// Pointer
p := &Vertex{1, 2}
p.X = 1e9 // Pointer wird implizit dereferenziert

Arrays #

var a [2]string
a[0] = "Hello"
a[1] = "world"
	
// Oder:
a := [2]string{"Hello", "world"} // = "Array literals"

Slices #

Slices sind Referenzen zu Array(teilen)

a := [2]string{"Hello", "world"} // Array wird initiiert
var s []string = a[0:2]
s1 := a[1:2] // Alternativ
	
// Arrays und Slices können auch in einem Statement initiiert werden ("Slice literals")
s2 := []int{1, 2, 3}
s3 := []struct {
	i int
	j bool
}{
	{1, true},
	{6, false}
}
	
a := [5]int{1, 2, 3, 4, 5}
a[0:5] == a[0:] == a[:5] == a[:]
	
// Erstellen von Arrays mit dynamischer Grösse
s4 := make([]int, 0, 5) // Slice auf ein Array mit Länge=0 und Kapazität=5
	
// Slices können Slices enthalten
board := [][]string{
	[]string{"_", "_", "_"},
	[]string{"_", "_", "_"},
	[]string{"_", "_", "_"},
}
	
// Elemente zu Slice hinzufügen
var s5 []int
s = append(s, 7)
  • Slices haben eine Länge (Funktion len(s)) und eine Kapazität (Funktion cap(s))
  • Der zero value von Slices ist nil, Länge und Kapazität sind dann jeweils 0

Maps #

m := make(map[string]int)
	
// Map literal
type Vertex struct {
	Lat, Long float64
}
	
var m = map[string]Vertex{
	"Bell Labs": Vertex{
		40.68433, -74.39967,
	},
	"Google": Vertex{
		37.42202, -122.08408,
	},
}

// Oder ohne Typname
var m = map[string]Vertex{
	"Bell Labs": {40.68433, -74.39967},
	"Google":    {37.42202, -122.08408},
}
	
m["test"] = Vertex{1., 2.} // Einfügen / aktualisieren
delete(m, "test") // Löschen
v, ok := m["test"] // Zugriff (ok = ist vorhanden)
v2 := m["inexistent"] // v2 == 0 (!)	

Funktionenwerte #

func compute(fn func(float64, float64) float64) float64 {
	return fn(3, 4)
}
	
func donothing(fn func(float64, float64) float64) func(float64, float64) float64 {
	return fn
}
	
func main() {
	hypot := func(x, y float64) float64 {
		return math.Sqrt(x*x + y*y)
	}
	fmt.Println(hypot(5, 12))
	
	fmt.Println(compute(hypot))
	fmt.Println(compute(math.Pow))
	fmt.Println(donothing(hypot))
}

Closures:

func adder() func(int) int {
	sum := 0
	return func(x int) int {
		sum += x
		return sum
	}
}
	
func main() {
	pos, neg := adder(), adder()
	for i := 0; i < 10; i++ {
		fmt.Println(
			pos(i),
			neg(-2*i),
		)
	}
}

Methoden #

Methoden sind Funktionen mit einem Receiver-Argument:

func (v Vertex) Abs() float64 { // Funktion hat einen Receiver namens v vom Typ Vertex
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

Methoden können nur für Typen definiert werden, welche im gleichen Paket definiert werden. Häufiger als value receiver sind pointer receiver, da nur solche die innere Modifikation von Typwerten erlauben. Sie werden folgendermassen definiert:

type Vertex struct {
	X, Y float64
}
	
func (v *Vertex) Scale(f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

Daneben gibt es noch einen weiteren Grund, pointer receiver zu verwenden: So wird vermieden, dass der Wert nicht bei jedem Methodenaufruf kopiert werden muss.

Generell sollten alle Methoden für einen Typ entweder *pointer receivers *oder value receivers verwenden, nicht eine Mischung aus beiden.

Interfaces #

Ein interface type ist definiert als ein Set an Methodensignaturen:

type Abser interface {
	Abs() float64
}

Ein Wert eines interface type kann jeden Wert eines Typs umfassen, welche die definierten Signaturen implementiert. Diese Implementierung geschieht implizit (kein impl Schlüsselwort o.ä.). Ein interface value kann als auch Tuple eines Wertes und eines konkreten Typs gesehen werden: (value, type)

Type assertion: t, ok := i.(T). Falls i vom Typ T ist, ist ok == true, andernfalls false. Wenn ok nicht vorhanden ist, und die Assertion fehlschlägt, führt das zu einer panic. Um einen Wert auf mehrere Typen zu prüfen, ist eine type switch möglich:

switch v := i.(type) {  // Zu beachten das spezifische Schlüsselwort type, welches für eine einfache type assertion nicht verwendet wird
	case int:
		fmt.Printf("Twice %v is %v\n", v, v*2)
	case string:
		fmt.Printf("%q is %v bytes long\n", v, len(v))
	default:
		fmt.Printf("I don't know about type %T!\n", v)
	}
}

Ein empty interface interface{} kann irgendeinen Typ enthalten, denn jeder Typ implementiert mindestens null Methoden. interface{} kann für Code verwendet werden, welcher beliebige Typen prozessieren kann.

Goroutinen und Channels #

Goroutinen sind Funktionen und Methoden, welche nebenläufig zu anderen Funktionen/Methoden sind. Sie sind vergleichbar mit leichtgewichtigen Threads.

  • Goroutines are extremely cheap when compared to threads. They are only a few kb in stack size and the stack can grow and shrink according to needs of the application whereas in the case of threads the stack size has to be specified and is fixed.
  • The Goroutines are multiplexed to fewer number of OS threads. There might be only one thread in a program with thousands of Goroutines. If any Goroutine in that thread blocks say waiting for user input, then another OS thread is created and the remaining Goroutines are moved to the new OS thread. All these are taken care by the runtime and we as programmers are abstracted from these intricate details and are given a clean API to work with concurrency.
  • Goroutines communicate using channels. Channels by design prevent race conditions from happening when accessing shared memory using Goroutines. Channels can be thought of as a pipe using which Goroutines communicate.

Definition:

func hello() {
	fmt. Println("Hello world!")
}

func main() {
	go hello()
}

Channels sind vergleichbar mit Pipes, welche Goroutinen zur Kommunikation benutzen.

Erstellt werden Channels mit dem make-Befehl:

a := make(chan int)

Channels sind immer typisiert (im Beispiel als int).

Kommunikation:

data := <- a // Daten werden aus Channel gelesen
a <- data // Daten werden in Channel geschrieben

Sowohl schreibender wie lesender Zugriff auf (ungebufferte) Channels sind blockierend. Im Fall eines schreibenden Zugriffs ist die Funktion so lange blockiert, bis eine Funktion aus dem Channel liest. Umgekehrt ist bei einem lesenden Zugriff die Funktion blockiert, bis in den Channel geschrieben wird. Daher ist beispielweise folgendes möglich, um einen frühzeitigen Abbruch des Programms zu verhindern:

func hello(done chan bool) {  
	fmt.Println("Hello world goroutine")
	done <- true
}

func main() {  
	done := make(chan bool)
	go hello(done)
	<-done // Blockt, bis die Funktion hello in den Channel geschrieben hat
	fmt.Println("main function")
}

Es ist auch möglich, unidirektionale Channels zu erstellen. In diesen ist nur lesender (<-chan) oder schreibender (chan<-) Zugriff möglich. Da bidirektionale Channels zu unidirektionalen Channels gecastet werden können (aber nicht umgekehrt), ist ihr Zweck v.a. die Verengung ihres Scopes in einer bestimmten Funktion. Bspw.:

func counter(out chan<- int) {
	for x := 0; x < 100; x++ {
		out <- x
	}
	close(out)
	}

func squarer(out chan<- int, in <-chan int) {
	for v := range in {
		out <- v * v
	}
	close(out)
}

func printer(in <-chan int) {
	for v := range in {
		fmt.Println(v)
	}
}

func main() {
	naturals := make(chan int)
	squares := make(chan int)
	go counter(naturals)
	go squarer(squares, naturals)
	printer(squares)
}

Channels werden mit dem close(chan)-Befehl geschlossen. Um zu überprüfen, ob ein Channel bereits geschlossen wurde, kann bei der Erstellung des Channels ein entsprechender Wert einer zusätzlichen Variablen zugewiesen werden:

v, ok := <- ch

ok nimmt jeweils den Null-Wert des Typs des channels an, wenn der Channel geschlossen wurde.

Mithilfe einer for range kann ein channel so lange abgefragt werden, bis der Channel geschlossen wird:

func producer(chnl chan int) {  
	for i := 0; i < 10; i++ {
	    chnl <- i
	}
	close(chnl)
}

func main() {  
	ch := make(chan int)
	go producer(ch)
	for v := range ch {
	    fmt.Println("Received ",v)
	}
}