Golang für die Profis: Networking, Multithreading, Datenstrukturen und maschinelles Lernen mit Go

Bild Hallo Bewohner!



Kennen Sie sich bereits mit den Grundlagen der Go-Sprache aus? Dann ist dieses Buch für Sie. Michalis Tsukalos wird die Fähigkeiten der Sprache demonstrieren, klare und einfache Erklärungen geben, Beispiele geben und effektive Programmiermuster vorschlagen. Während Sie die Nuancen von Go erkunden, beherrschen Sie die Datentypen und -strukturen der Sprache sowie Verpackung, Parallelität, Netzwerkprogrammierung, Compilerdesign, Optimierung und mehr. Die Materialien und Übungen am Ende jedes Kapitels helfen Ihnen, Ihr neues Wissen zu vertiefen. Ein einzigartiges Material wird das Kapitel über maschinelles Lernen in Go sein, das Sie von grundlegenden statistischen Techniken zu Regression und Clustering führt. Sie lernen Klassifikation, neuronale Netze und Anomalieerkennungstechniken. In den angewandten Abschnitten erfahren Sie, wie Sie Go mit Docker und Kubernetes, Git, WebAssembly, JSON und mehr verwenden.



Worum geht es in diesem Buch
1 «Go » Go , godoc , Go-. , . , Go .



2 «Go » Go . unsafe, , Go- C, C- — Go.



, defer, strace(1) dtrace(1). , Go, Go WebAssembly.



3 « Go» , Go: , -, , , , . !



4 « » Go struct, , , , . , switch, strings math/big, Go « — » XML JSON.



5 « Go » , Go . , , -, , . Go container, , Go .



6 « Go» , init(), Go- syscall, text/template html/template. , , go/scanner, go/parser go/token. Go!



7 « » Go: , . , - Go Go- Delve.



8 « UNIX-, » Go. , flag , UNIX, , bytes, io.Reader io.Writer, Viper Cobra Go. : Go, Go Systems Programming!



9 « Go: , » , — , Go.



, , , sync Go.



10 « Go: » . , ! Go, select, Go, , , sync.Mutex sync.RWMutex. context, , (race conditions).



11 «, » , , - , , Go-, Go-.



12 « Go» net/http , - - Go. http.Response, http.Request http.Transport, http.NewServeMux. , Go -! , Go DNS-, Go gRPC.



13 « : » HTTPS- Go UDP TCP net. , RPC, Go TCP- «» .



14 « Go» Go , , , , , TensorFlow, Go Apache Kafka.



. Go, , Go-, Go-, Go C WebAssembly, Go. 5 6 7. Go- , Go- Go.



Go. 8–11 Go, Go, , . Go.

Go WebAssembly, Docker Go, Viper Cobra, JSON YAML, , , go/scanner go/token, git(1) GitHub, atomic, Go gRPC HTTPS.



, Go-, . : -, , , -, .



Und wieder über Go-Kanäle



Sobald das Schlüsselwort select verwendet wird, gibt es verschiedene Möglichkeiten, Go-Kanäle zu verwenden, die viel mehr leisten als in Kapitel 9 beschrieben. In diesem Abschnitt erfahren Sie mehr über die verschiedenen Verwendungszwecke von Go-Kanälen.



Ich möchte Sie daran erinnern, dass der Nullwert für Kanäle Null ist. Wenn Sie eine Nachricht an einen geschlossenen Kanal senden, wechselt das Programm in den Panikmodus. Wenn Sie jedoch versuchen, Daten von einem geschlossenen Kanal zu lesen, erhalten Sie für diesen Kanaltyp einen Wert von Null. Nach dem Schließen des Kanals können Sie also keine Daten mehr darauf schreiben, aber dennoch lesen.



Damit ein Kanal geschlossen werden kann, muss er nicht nur für den Empfang von Daten ausgelegt sein. Außerdem ist Kanal Null immer blockiert, dh ein Versuch, von Kanal Null zu lesen oder zu schreiben, blockiert den Kanal. Diese Eigenschaft von Kanälen ist sehr nützlich, wenn Sie einen Zweig einer select-Anweisung deaktivieren möchten, indem Sie die Kanalvariable auf nil setzen.



Wenn Sie schließlich versuchen, den Nullkanal zu schließen, löst das Programm eine Panik aus. Schauen wir uns das Beispiel closeNilChannel.go an:



package main

func main() {
      var c chan string
      close(c)
}


Das Ausführen von closeNilChannel.go führt zu folgendem Ergebnis:



$ go run closeNilChannel.go
panic: close of nil channel
goroutine 1 [running]:
main.main()
       /Users/mtsouk/closeNilChannel.go:5 +0x2a
exit status 2


Signalkanäle



Ein Signalisierungskanal ist ein Kanal, der nur zur Signalisierung verwendet wird. Einfach ausgedrückt kann der Signalkanal verwendet werden, wenn Sie ein anderes Programm über etwas informieren möchten. Signalisierungskanäle müssen nicht zur Datenübertragung verwendet werden.



Signalisierungskanäle sollten nicht mit der in Kapitel 8 beschriebenen UNIX-Signalbehandlung verwechselt werden, es handelt sich um völlig andere Dinge.


Ein Beispiel für Code, der Signalisierungskanäle verwendet, wird später in diesem Kapitel erläutert.



Gepufferte Kanäle



Das Thema dieses Unterabschnitts sind gepufferte Pipes. Dies sind Kanäle, mit denen der Go-Scheduler Jobs schnell in die Warteschlange stellen kann, um mehr Anforderungen zu verarbeiten. Darüber hinaus können sie als Semaphore verwendet werden, um die Bandbreite einer Anwendung zu begrenzen.



Die hier vorgestellte Methode funktioniert folgendermaßen: Alle eingehenden Anforderungen werden an einen Kanal umgeleitet, der sie wiederum verarbeitet. Wenn der Kanal die Verarbeitung der Anforderung beendet hat, sendet er eine Nachricht an den ursprünglichen Anrufer, dass der Kanal bereit ist, eine neue Anforderung zu verarbeiten. Somit begrenzt die Pufferkapazität eines Kanals die Anzahl gleichzeitiger Anforderungen, die der Kanal speichern kann.



Wir werden uns diese Methode am Beispiel des Programmcodes bufChannel.go ansehen. Teilen wir es in vier Teile.



Der erste Teil des Codes bufChannel.go sieht folgendermaßen aus:



package main

import (
       "fmt"
)


Der zweite Teil der Datei bufChannel.go enthält den folgenden Go-Code:



func main() {
      numbers := make(chan int, 5)
      counter := 10


Mit der hier vorgestellten Zahlendefinition können Sie bis zu fünf Ganzzahlen in dieser Pipe speichern.



Der dritte Teil von bufChannel.go enthält den folgenden Go-Code:



for i := 0; i < counter; i++ {
     select {
     case numbers <- i:
     default:
            fmt.Println("Not enough space for", i)
     }
}


In diesem Code haben wir versucht, zehn Nummern in den Nummernkanal einzufügen. Da Zahlen jedoch nur Platz für fünf Ganzzahlen haben, können wir nicht alle zehn Ganzzahlen darin speichern.



Der Rest des Go-Codes von bufChannel.go sieht folgendermaßen aus:



   for i := 0; i < counter+5; i++ {
        select {
              case num := <-numbers:
                    fmt.Println(num)
              default:
                    fmt.Println("Nothing more to be done!")
              break
        }
   }
}


In diesem Go-Code haben wir versucht, den Inhalt des Nummernkanals mit einer for-Schleife und einer select-Anweisung zu lesen. Solange im Nummernkanal etwas zu lesen ist, wird der erste Zweig der select-Anweisung ausgeführt. Wenn der Nummernkanal leer ist, wird der Standardzweig ausgeführt.



Das Ausführen von bufChannel.go führt zu folgendem Ergebnis:



$ go run bufChannel.go
Not enough space for 5
Not enough space for 6
Not enough space for 7
Not enough space for 8
Not enough space for 9
0
1
2
3
4
Nothing more to be done!
Nothing more to be done!
Nothing more to be done!
Nothing more to be done!
Nothing more to be done!
Nothing more to be done!
Nothing more to be done!
Nothing more to be done!
Nothing more to be done!
Nothing more to be done!


Keine Kanäle



In diesem Abschnitt erfahren Sie mehr über Kanal Null. Dies ist eine spezielle Art von Kanal, der immer blockiert ist. Wir werden uns diese Kanäle am Beispiel des Programms nilChannel.go ansehen. Teilen wir es in vier Codeteile auf.



Der erste Teil von nilChannel.go sieht folgendermaßen aus:



package main

import (
       "fmt"
       "math/rand"
       "time"
)


Der zweite Teil von nilChannel.go enthält den folgenden Go-Code:



func add(c chan int) {
      sum := 0
      t := time.NewTimer(time.Second)

      for {
           select {
           case input := <-c:
                 sum = sum + input
           case <-t.C:
                 c = nil
                 fmt.Println(sum)
           }
      }
}


Hier wird am Beispiel der Funktion add () gezeigt, wie der Nullkanal verwendet wird. Der Operator <-tC blockiert den C-Kanal des Timers t für die im Aufruf von time.NewTimer () angegebene Zeit. Verwechseln Sie Kanal c, der ein Funktionsargument ist, nicht mit Kanal tC, der zum Timer t gehört. Nach Ablauf der Zeit sendet der Timer einen Wert an den tC-Kanal, der die Ausführung des entsprechenden Zweigs der select-Anweisung initiiert. Er setzt Kanal c auf null und zeigt den Wert der Summenvariablen an.



Das dritte nilChannel.go-Code-Snippet sieht folgendermaßen aus:



func send(c chan int) {
      for {
           c <- rand.Intn(10)
      }
}


Der Zweck der Funktion send () besteht darin, Zufallszahlen zu generieren und diese an den Kanal zu senden, solange der Kanal geöffnet ist.



Der Rest des Go-Codes in nilChannel.go sieht folgendermaßen aus:



func main() {
      c := make(chan int)
      go add(c)
      go send(c)
      time.Sleep(3 * time.Second)
}


Die Funktion time.Sleep () wird benötigt, damit die beiden Goroutinen genügend Zeit zur Ausführung haben.



Wenn Sie nilChannel.go ausführen, werden die folgenden Ergebnisse erzielt:



$ go run nilChannel.go
13167523
$ go run nilChannel.go
12988362


Da die Häufigkeit, mit der der erste Zweig der select-Anweisung in add () ausgeführt wird, nicht festgelegt ist, führt das mehrmalige Ausführen von nilChannel.go zu unterschiedlichen Ergebnissen.



Kanal Kanäle



Ein Kanalkanal ist eine spezielle Art von Kanalvariable, die mit anderen Kanälen anstelle der üblichen Variablentypen funktioniert. Sie müssen jedoch noch einen Datentyp für einen Kanal von Kanälen deklarieren. Verwenden Sie zum Definieren des Kanals von Kanälen das Schlüsselwort chan zweimal hintereinander, wie in der folgenden Anweisung gezeigt:



c1 := make(chan chan int)


Andere in diesem Kapitel vorgestellte Kanaltypen sind beliebter und nützlicher als Kanalkanäle.


Wir werden die Verwendung von Kanalkanälen anhand des Beispielcodes in der Datei chSquare.go erläutern. Teilen wir es in vier Teile.



Der erste Teil von chSquare.go sieht folgendermaßen aus:



package main

import (
       "fmt"
       "os"
       "strconv"
       "time"
)

var times int


Der zweite Teil von chSquare.go enthält den folgenden Go-Code:



func f1(cc chan chan int, f chan bool) {
      c := make(chan int)
      cc <- c
      defer close(c)

      sum := 0
      select {
      case x := <-c:
            for i := 0; i <= x; i++ {
                 sum = sum + i
            }
            c <- sum
      case <-f:
            return
      }
}


Nachdem wir einen regulären Kanal vom Typ int deklariert haben, übergeben wir ihn an die Kanalkanalvariable. Mit der select-Anweisung erhalten wir dann die Möglichkeit, Daten von einem regulären int-Kanal zu lesen oder die Funktion über den Signalkanal f zu verlassen.



Nachdem wir einen Wert aus Kanal c gelesen haben, führen wir eine for-Schleife aus, die die Summe aller Ganzzahlen von 0 bis zu dem gerade gelesenen Ganzzahlwert berechnet. Dann senden wir den berechneten Wert an den int-Kanal c, und das war's.



Der dritte Teil von chSquare.go enthält den folgenden Go-Code:



func main() {
      arguments := os.Args
      if len(arguments) != 2 {
          fmt.Println("Need just one integer argument!")
          return
      }
      times, err := strconv.Atoi(arguments[1])
      if err != nil {
           fmt.Println(err)
           return
      }

      cc := make(chan chan int)


In der letzten Zeile dieses Codeausschnitts deklarieren wir eine Kanalvariable mit dem Namen cc. Diese Variable ist der Star dieses Programms, weil alles davon abhängt. Die Variable cc wird an f1 () übergeben und in der nächsten for-Schleife verwendet.



Der Rest des chSquare.go Go-Codes sieht folgendermaßen aus:



   for i := 1; i < times+1; i++ {
        f := make(chan bool)
        go f1(cc, f)
        ch := <-cc
        ch <- i
        for sum := range ch {
             fmt.Print("Sum(", i, ")=", sum)
        }
        fmt.Println()
        time.Sleep(time.Second)
        close(f)
    }
}


Kanal f ist der Signalkanal für das Ende der Goroutine, wenn alle Arbeiten erledigt sind. Mit dem Befehl ch: = <-cc können Sie einen regulären Kanal aus einer Kanalvariablen abrufen, um dort mit dem Operator ch <- i einen int-Wert zu übergeben. Danach lesen wir Daten aus der Pipe mit einer for-Schleife. Die Funktion f1 () ist so programmiert, dass sie einen Wert zurückgibt, aber wir können auch mehrere Werte lesen. Beachten Sie, dass jeder i-Wert von einer eigenen Goroutine bedient wird.



Der Signalkanaltyp kann beliebig sein, einschließlich des im vorherigen Code verwendeten Bools und der Struktur {}, die im nächsten Abschnitt für den Signalkanal verwendet wird. Der Hauptvorteil eines Signalisierungskanals vom Typ struct {} besteht darin, dass keine Daten an einen solchen Kanal gesendet werden können, wodurch das Auftreten von Fehlern verhindert wird.



Wenn Sie chSquare.go ausführen, erhalten Sie folgende Ergebnisse:



$ go run chSquare.go 4
Sum(1)=1
Sum(2)=3
Sum(3)=6
Sum(4)=10
$ go run chSquare.go 7
Sum(1)=1
Sum(2)=3
Sum(3)=6
Sum(4)=10
Sum(5)=15
Sum(6)=21
Sum(7)=28


Auswahl der Reihenfolge der Ausführung von Goroutinen



Sie müssen keine Annahmen über die Reihenfolge der Ausführung der Goroutinen treffen. Es gibt jedoch Zeiten, in denen es erforderlich ist, diese Reihenfolge zu kontrollieren. In diesem Unterabschnitt erfahren Sie, wie Sie dies mithilfe von Signalisierungskanälen tun.



Sie fragen sich vielleicht: "Warum Goroutinen erstellen und dann in einer bestimmten Reihenfolge ausführen, wenn es mit regulären Funktionen viel einfacher ist, dasselbe zu tun?" Die Antwort ist einfach: Goroutinen können gleichzeitig ausgeführt werden und auf den Abschluss anderer Goroutinen warten, während Funktionen dies nicht können, da sie nacheinander ausgeführt werden.


In diesem Unterabschnitt sehen wir uns ein Go-Programm namens defineOrder.go an. Teilen wir es in fünf Teile. Der erste Teil von defineOrder.go sieht folgendermaßen aus:



package main

import (
       "fmt"
       "time"
)

func A(a, b chan struct{}) {
      <-a
      fmt.Println("A()!")
      time.Sleep(time.Second)
      close(b)
}


Die Funktion A () wird durch den in Parameter a gespeicherten Kanal gesperrt. Sobald dieser Kanal in main () entsperrt ist, beginnt die Funktion A () zu arbeiten. Schließlich wird Kanal b geschlossen, wodurch eine andere Funktion - in diesem Fall B () - entsperrt wird.



Der zweite Teil von defineOrder.go enthält den folgenden Go-Code:



func B(a, b chan struct{}) {
      <-a
      fmt.Println("B()!")
      close(b)
}


Die Logik der Funktion B () ist dieselbe wie die von A (). Diese Funktion ist gesperrt, bis Kanal a geschlossen wird. Dann macht es seine Arbeit und schließt Kanal b. Beachten Sie, dass sich die Kanäle a und b auf die Funktionsparameternamen beziehen.



Der dritte Code für defineOrder.go sieht folgendermaßen aus:



func C(a chan struct{}) {
      <-a
      fmt.Println("C()!")
}


Die C () - Funktion ist blockiert und wartet vor dem Start darauf, dass Kanal a geschlossen wird.



Der vierte Teil von defineOrder.go enthält den folgenden Code:



func main() {
      x := make(chan struct{})
      y := make(chan struct{})
      z := make(chan struct{})


Diese drei Kanäle werden zu Parametern für drei Funktionen.



Das letzte Snippet von defineOrder.go enthält den folgenden Go-Code:



     go C(z)
     go A(x, y)
     go C(z)
     go B(y, z)
     go C(z)

     close(x)
     time.Sleep(3 * time.Second)
}


Hier führt das Programm alle notwendigen Funktionen aus, schließt dann Kanal x und schläft drei Sekunden lang.



Wenn Sie defineOrder.go ausführen, wird das gewünschte Ergebnis erzielt, obwohl die C () - Funktion mehrmals aufgerufen wird:



$ go run defineOrder.go
A()!
B()!
C()!
C()!
C()!


Das mehrmalige Aufrufen von C () als Goroutine verursacht keine Probleme, da C () keine Kanäle schließt. Wenn Sie A () oder B () jedoch mehrmals aufrufen, wird höchstwahrscheinlich eine Fehlermeldung angezeigt, zum Beispiel:



$ go run defineOrder.go
A()!
A()!
B()!
C()!
C()!
C()!
panic: close of closed channel
goroutine 7 [running]:
main.A(0xc420072060, 0xc4200720c0)
       /Users/mtsouk/Desktop/defineOrder.go:12 +0x9d
created by main.main
       /Users/mtsouk/Desktop/defineOrder.go:33 +0xfa
exit status 2


Wie Sie sehen, wurde hier die Funktion A () zweimal aufgerufen. Wenn A () jedoch einen Kanal schließt, erkennt eine seiner Goroutinen, dass der Kanal bereits geschlossen ist, und erzeugt eine Paniksituation, wenn versucht wird, diesen Kanal erneut zu schließen. Wenn wir versuchen, die B () - Funktion mehr als einmal aufzurufen, kommt es zu einer ähnlichen Paniksituation.



Wie man Goroutinen nicht benutzt



In diesem Abschnitt lernen Sie eine naive Methode zum Sortieren natürlicher Zahlen mithilfe von Goroutinen. Das Programm, das wir uns ansehen werden, heißt albernSort.go. Teilen wir es in zwei Teile. Der erste Teil von albernSort.go sieht folgendermaßen aus:



package main

import (
       "fmt"
       "os"
       "strconv"
       "sync"
       "time"
)

func main() {
      arguments := os.Args

      if len(arguments) == 1 {
          fmt.Println(os.Args[0], "n1, n2, [n]")
          return
      }

      var wg sync.WaitGroup
      for _, arg := range arguments[1:] {
           n, err := strconv.Atoi(arg)
           if err != nil || n < 0 {
                fmt.Print(". ")
                continue
           }


Der zweite Teil von albernSort.go enthält den folgenden Go-Code:



           wg.Add(1)
           go func(n int) {
                defer wg.Done()
                time.Sleep(time.Duration(n) * time.Second)
                fmt.Print(n, " ")
           }(n)
      }

      wg.Wait()
      fmt.Println()
}


Die Sortierung erfolgt durch Aufrufen der Funktion time.Sleep () - je größer die natürliche Zahl, desto länger dauert es, bis der Operator fmt.Print () ausgeführt wird!



Das Ausführen von albernSort.go führt zu folgenden Ergebnissen:



$ go run sillySort.go a -1 1 2 3 5 0 100 20 60
. . 0 1 2 3 5 20 60 100
$ go run sillySort.go a -1 1 2 3 5 0 100 -1 a 20 hello 60
. . . . . 0 1 2 3 5 20 60 100
$ go run sillySort.go 0 0 10 2 30 3 4 30
0 0 2 3 4 10 30 30




Über den Autor



Mihalis Tsoukalos ist UNIX-Administrator, Programmierer, Datenbankadministrator und Mathematiker. Schreibt gerne technische Bücher und Artikel, lernt etwas Neues. Zusätzlich zu diesem Buch hat Michalis Go Systems Programming sowie über 250 technische Artikel für viele Magazine verfasst, darunter Sys Admin, MacTech, Linux-Benutzer und -Entwickler, Usenix, Login :, Linux-Format und Linux Journal. Michalis 'Forschungsinteressen sind Datenbanken, Visualisierung, Statistik und maschinelles Lernen.



Über den wissenschaftlichen Herausgeber



Mat Ryer schreibt seit seinem sechsten Lebensjahr Computerprogramme: zuerst auf BASIC für das ZX Spectrum und dann mit seinem Vater auf AmigaBASIC und AMOS für den Commodore Amiga. Er verbrachte viel Zeit damit, Code manuell aus dem Amiga-Format-Protokoll zu kopieren und die Werte von Variablen oder GOTO-Anweisungsreferenzen zu ändern, um zu sehen, was daraus wurde. Der gleiche Geist der Erforschung und Besessenheit mit dem Programmieren veranlasste den 18-jährigen Matt, für eine lokale Organisation in Mansfield, Großbritannien, zu arbeiten, wo er mit dem Aufbau von Websites und anderen Online-Diensten begann.



Nachdem Mat mehrere Jahre mit verschiedenen Technologien in verschiedenen Bereichen gearbeitet hatte, nicht nur in London, sondern auf der ganzen Welt, wandte er sich einer neuen Systemprogrammiersprache namens Go zu, die erstmals bei Google verwendet wurde. Da Go sehr relevante und hochmoderne technische Probleme löste, begann Mat, die Sprache zur Problemlösung zu verwenden, als Go noch in der Beta war, und programmiert sie seitdem weiter. Mat hat an verschiedenen Open Source-Projekten gearbeitet und mehrere Go-Pakete erstellt, darunter Testify, Moq, Silk und Is sowie das MacOS-Entwickler-Toolkit BitBar.



Seit 2018 ist Mat Mitbegründer von Machine Box, nimmt aber weiterhin an Konferenzen teil, schreibt in seinem Blog über Go und ist aktives Mitglied der Go-Community.



»Weitere Details zum Buch finden Sie auf der Website des Herausgebers.

» Inhaltsverzeichnis

» Auszug



für Einwohner 25% Rabatt auf den Gutschein - Golang



Nach Zahlung der Papierversion des Buches wird ein E-Book per E-Mail verschickt.



All Articles