Über die Vor- und Nachteile von Go

    In diesem Artikel möchte ich die Erfahrungen teilen, die beim Umschreiben eines Projekts von Perl to Go gemacht wurden. Es wird mehr um die Minuspunkte als um die Pluspunkte gehen, da viel über die Tugenden von Go gesprochen wurde, aber Sie können sich oft über die Fallstricke informieren, die auf neue Entwickler warten, außer von Ihren eigenen Kegeln. Das Fasten ist in keiner Weise ein Schrei der Go-Sprache, obwohl ich, um zuzugeben, froh wäre, einige Dinge nicht zu schreiben. Es deckt auch einen relativ kleinen Teil der gesamten Plattform ab, insbesondere wird es nichts über Vorlagen, Regexp, Auspacken / Packen von Daten und dergleichen, die häufig bei der Webprogrammierung verwendet werden, geben.

    Da sich der Beitrag nicht im Hub „I PR“ befindet, werde ich die Funktionen des Projekts nur kurz umreißen. Dies ist eine hochgeladene Webanwendung, die derzeit ca. 600 Millionen Zugriffe pro Tag verarbeitet (Spitzenlast mehr als 10.000 Zugriffe pro Sekunde). Etwa 80% der Anforderungen können aus dem Cache gesendet werden, der Rest muss vollständig verarbeitet werden. Die Arbeitsdaten basieren hauptsächlich auf PostgreSQL, teilweise in Binärdateien mit einer flachen Struktur (d. H. Tatsächlich einem Array, aber nicht im Speicher, sondern in der Datei). Der Perl-Cluster bestand aus acht 24 Nuklearmaschinen mit praktisch erschöpften Leistungsspannen, der Go-Cluster wird bereits aus sechs bestehen und mehr als dreifache Reserven aufweisen. Darüber hinaus ist der Engpass weniger der Prozessor als das Betriebssystem und der Rest der Hardware und Software - es ist physikalisch nicht einfach, 10.000 nicht-triviale Anforderungen in einer Sekunde auf einem Computer zu verarbeiten.

    Entwicklungsgeschwindigkeit

    Meine Erfahrung mit Go vor dem Refactoring war minimal. Mehr als ein Jahr lang habe ich mir die Sprache angeschaut, es geschafft, die Spezifikationen von Anfang bis Ende zu studieren , nützliche Materialien auf der offiziellen Website und darüber hinaus zu studieren und fühlte mich bereit, meine Ärmel hochzukrempeln und mich an die Arbeit zu machen. Die anfängliche Einschätzung der Fristen für die Arbeiten betrug 3-6 Wochen. Die funktionierende Beta war pünktlich zum Ende der 6. Woche fertig, obwohl ich gegen Ende bereits zu denken begonnen hatte, dass ich keine Zeit mehr haben würde. Die Bereinigung von Fehlern und die Optimierung der Leistung dauerte einen weiteren Monat.

    Anfangs war es besonders schwierig, aber im Laufe der Zeit musste die Spezifikation immer weniger überprüft werden, und der Code erwies sich als sauberer. Wenn ich zuerst die Funktionalität nutzen musste, die ich in einer Stunde in Perl programmieren und den ganzen Tag auf Go verwenden konnte, wurde diese Lücke erheblich geschlossen. Die Programmierung in Go ist jedoch erheblich länger als in Perl - Sie müssen die Strukturen, Datentypen und Schnittstellen überdenken, die Sie für die Arbeit benötigen, alles in Code schreiben, für die Initialisierung von Slices, Maps und Channels sorgen und Prüfungen auf Null schreiben. Damit ist alles viel einfacher: Sie müssen Hashes für Strukturen verwenden, müssen dort vorher keine Felder deklarieren, und es gibt viel mehr syntaktischen Zucker für Programmierer. Vergleichen Sie zumindest die Sortierung - in Go gibt es keine Möglichkeit, einen Abschluss zum Vergleichen von Daten anzugeben. Sie müssen separate Funktionen registrieren, um die Länge zu ermitteln. Zusätzlich zur Indexvergleichsfunktion müssen Sie auch eine separate Funktion zum Austauschen von Elementen an Stellen im Array schreiben. Und warum? Da es keine Generika gibt und die Sortierfunktion einfacher ist, den speziell deklarierten Swap (i, j) aufzurufen, als herauszufinden, in was er gerutscht ist und in welchen Offsets dieser Werteaustausch erfolgen soll.

    Neben dem Sortieren hat mich auch das Fehlen des Perl-Konstrukts für / while () {...} continue {...} beeindruckt (der continue- Block wird auch dann ausgeführt, wenn die aktuelle Iteration durch den nächsten Operator vorzeitig unterbrochen wird ). In Go müssen Sie das nicht-koschere goto verwenden , das Sie auch dazu zwingt, alle Variablendeklarationen davor zu schreiben, auch diejenigen, die nicht nach der Sprungmarke verwendet werden:
    var cnt int
    for ;; {
            goto NEXT
            a := int(0) // ./main.go:16: goto NEXT jumps over declaration of a at ./main.go:17
            cnt += a
    NEXT:
            cnt ++
    }
    

    Auch das Paradigma der Syntaxvereinigung für Zeiger und Nichtzeiger funktioniert nicht - im Falle der Verwendung von Strukturen bietet der Compiler die Möglichkeit, dieselbe Syntax zu verwenden, und für die Zuordnung müssen wir bereits dereferenzieren und eckige Klammern verwenden, obwohl der Compiler alles bestimmen kann:
    type T struct { cnt int }
    s := T{}
    p := new(T)
    s.cnt ++
    p.cnt ++
    
    aber
    m := make(map[int]T)
    p := new(map[int]T)
    *p = make(map[int]T)
    m[1] = T{}
    (*p)[1] = T{}
    p[1] = T{}  // ./main.go:13: invalid operation: p[1] (type *map[int]T does not support indexing)
    

    Bereits am Ende der Arbeit musste ich den Teil der Funktionalität, der zu Beginn implementiert wurde, aufgrund einer inkorrekten anfänglichen Architektur umschreiben. Die gesammelten Erfahrungen bieten neue architektonische Paradigmen, aber diese Erfahrung muss noch erworben werden.))

    Übrigens, die Gesamtmenge des Codes in Zeichen stimmte fast überein (nur für die Ausrichtung der Leerzeichen in Perl wurden zwei Leerzeichen und in Go ein Tab verwendet), aber die Zeilen in Go erwiesen sich als richtig 20% mehr. Zwar unterscheidet sich die Funktionalität geringfügig, in Go wird beispielsweise die Arbeit mit GC hinzugefügt, in Perl wird jedoch weiterhin eine separate Bibliothek für das Zwischenspeichern von SQL-Abfragen in einem externen Dateicache berücksichtigt (mit Zugriff über mmap ()). Im Allgemeinen ist die Codemenge fast gleich, aber Perl ist noch ein bisschen kompakter. Go hat jedoch weniger Klammern und Semikolons - der Code sieht übersichtlicher und einfacher zu lesen aus.

    Im Allgemeinen wird Go-Code ziemlich schnell und genau geschrieben, viel schneller als beispielsweise in C / C ++, aber für einfache Aufgaben ohne besondere Leistungsanforderungen werde ich weiterhin Perl verwenden.

    Leistung

    Seien wir ehrlich, ich habe keine besonderen Beschwerden über Go in Bezug auf die Leistung, aber ich habe mehr erwartet. Der Unterschied zu Perl (er hängt stark von der Art der Berechnung ab, in der Arithmetik beispielsweise leuchtet Perl überhaupt nicht) beträgt etwa das 5- bis 10-fache. Ich hatte keine Gelegenheit, gccgo, as zu testen es ist nicht schwer für FreeBSD, aber es ist schade. Aber jetzt ist die Backend-Software kein Engpass mehr, der CPU-Verbrauch liegt bei etwa 50% eines Kerns, und mit zunehmender Auslastung beginnen die Probleme zuerst mit Nginx, PostgreSQL und dem Betriebssystem.

    Bei der Optimierung der Leistung hat der Profiler gezeigt, dass die Laufzeit neben meinem Code den aktiven Teil der CPU beansprucht (es geht nicht nur um das Laufzeitpaket).
    Hier ist ein Beispiel für top10 --cum:
    Total: 1945 samples
           0   0.0%   0.0%     1309  67.3% runtime.gosched0
           1   0.1%   0.1%     1152  59.2% bitbucket.org/mjl/scgi.func·002
           1   0.1%   0.1%     1151  59.2% bitbucket.org/mjl/scgi.serve
           0   0.0%   0.1%      953  49.0% net/http.HandlerFunc.ServeHTTP
           3   0.2%   0.3%      952  48.9% main.ProcessHttpRequest
           1   0.1%   0.3%      535  27.5% main.ProcessHttpRequestFromCache
           0   0.0%   0.3%      418  21.5% main.ProcessHttpRequestFromDb
          16   0.8%   1.1%      387  19.9% main.(*RequestRecord).SelectServerInDc
           0   0.0%   1.1%      367  18.9% System
           0   0.0%   1.1%      268  13.8% GC
    

    Wie Sie sehen, werden nur 49% der verbrauchten CPU für die Verarbeitung der tatsächlichen SCGI-Anforderung durch den Handler ausgegeben, und bis zu 33% werden für System + GC ausgegeben.

    Hier jedoch einfach top20 aus demselben Profil:
    Total: 1945 samples
         179   9.2%   9.2%      186   9.6% syscall.Syscall
         117   6.0%  15.2%      117   6.0% runtime.MSpan_Sweep
         114   5.9%  21.1%      114   5.9% runtime.kevent
          93   4.8%  25.9%       96   4.9% runtime.cgocall
          93   4.8%  30.6%       93   4.8% runtime.sys_umtx_op
          67   3.4%  34.1%      152   7.8% runtime.mallocgc
          63   3.2%  37.3%       63   3.2% runtime.duffcopy
          56   2.9%  40.2%       99   5.1% hash_insert
          56   2.9%  43.1%       56   2.9% scanblock
          53   2.7%  45.8%       53   2.7% runtime.usleep
          39   2.0%  47.8%       39   2.0% markonly
          36   1.9%  49.7%       41   2.1% runtime.mapaccess2_fast32
          28   1.4%  51.1%       28   1.4% runtime.casp
          25   1.3%  52.4%       34   1.7% hash_init
          23   1.2%  53.6%       23   1.2% hash_next
          22   1.1%  54.7%       22   1.1% flushptrbuf
          22   1.1%  55.8%       22   1.1% runtime.xchg
          21   1.1%  56.9%       29   1.5% runtime.mapaccess1_fast32
          21   1.1%  58.0%       21   1.1% settype
          20   1.0%  59.0%       31   1.6% runtime.mapaccess1_faststr
    

    Die Berechnungen meines Codes gehen einfach vor dem Hintergrund der Aufgaben verloren, mit denen sich die Laufzeit befasst (sollte es aber sein, ich habe keine harte Mathematik).

    IMHO gibt es immer noch eine riesige Reserve für die Optimierung des Compilers und der Bibliotheken. Zum Beispiel habe ich kein Inlining bemerkt - alle meine Mutexe sind in den Sweeps des Goroutin-Stapels perfekt sichtbar. Der Compiler-Optimierungsprozess steht nicht still (vor nicht allzu langer Zeit hat Dmitry Vyukov beispielsweise eine deutlich beschleunigte Implementierung von Kanälen vorgestellt), aber Kardinalverschiebungen sind bislang oft nicht zu bemerken. Beispielsweise konnte ich nach dem Wechsel von Go 1.2 zu Go 1.3 fast keinen Unterschied in der Leistung feststellen.

    Sogar während der Optimierung musste ich das math / rand-Paket aufgeben. Tatsache ist, dass während der Abfrageverarbeitung häufig Pseudozufallszahlen benötigt wurden, jedoch mit Datenbindung, und rand.Seed () zu viel CPU verwendete (der Profiler zeigte 13% der Gesamtzahl an). Jeder, der es braucht, wird die Methode zum Erzeugen von Pseudozufallszahlen mit Fast Seed () googeln, aber dennoch - für kryptografische Zwecke gibt es ein Crypto / Rand-Paket, und in Mathe / Rand stören sie sich möglicherweise nicht so sehr an qualitativ hochwertigen Bitmischungen während der Initialisierung.
    Übrigens habe ich mich auf den folgenden Algorithmus konzentriert:
    func RandFloat64(seed uint64) float64 {
            seed ^= seed >> 12
            seed ^= seed << 25
            seed ^= seed >> 27
            return float64((seed*2685821657736338717)&0x7fffffffffffffff) / (1 << 63)
    }
    


    Es ist sehr praktisch, dass alle Berechnungen in einem Prozess durchgeführt werden. In Perl wurden separate Worker-Prozesse verwendet, und ich musste einen gemeinsamen Cache organisieren - etwas durch Speichern in einem Speicher, etwas durch eine Datei. On Go ist dies viel einfacher und natürlicher. Aber jetzt, wo kein externer Cache vorhanden ist, entsteht das Problem eines Kaltstarts. Hier musste ich ein bisschen basteln. Zuerst habe ich versucht, mich auf nginx zu beschränken (um zu verhindern, dass einhunderttausend Goroutinen gleichzeitig starten und das Ganze nicht hochkommen würde), die Anzahl der gleichzeitigen Anfragen an den Upstream über das https: // Modul github.com/cfsego/nginx-limit-upstream, aber etwas, das nicht sehr stabil funktionierte (als der Verbindungspool verstopft war, war es für ihn aus irgendeinem Grund nicht einfach, in den normalen Modus zurückzukehren, selbst nach dem Entladen). Infolgedessen habe ich das scgi-Modul ein wenig gepatcht und der Anzahl der gleichzeitig ausgeführten Anforderungen einen Begrenzer hinzugefügt - bis einige der aktuellen Anforderungen verarbeitet wurden -, wird die neue Anforderung von Accept () nicht akzeptiert.
    func ServeLimited(l net.Listener, handler http.Handler, limit int) error {
            if limit <= 0 {
                    Serve(l, handler)
            }
            if l == nil {
                    var err error
                    l, err = net.FileListener(os.Stdin)
                    if err != nil {
                            return err
                    }
                    defer l.Close()
            }
            if handler == nil {
                    handler = http.DefaultServeMux
            }
            sem := make(chan struct{}, limit)
            for {
                    sem <- struct{}{}
                    rw, err := l.Accept()
                    if err != nil {
                            return err
                    }
                    go func(rw net.Conn) {
                            serve(rw, handler)
                            <-sem
                    }(rw)
            }
    }
    

    Das scgi- Modul wurde auch aus Performance-Gründen gewählt - aus irgendeinem Grund war net / http / fcgi langsamer als nur net / http (und unterstützt keine dauerhafte Verbindung), und net / http lud das Betriebssystem zusätzlich mit der Erzeugung von TCP-Paketen und der Unterstützung für interne TCP-Verbindungen (obwohl es technisch möglich ist, es über einen Unix-Socket anzuhören) - und da es möglich war, es loszuwerden, warum nicht loswerden? Die Verwendung von nginx als Front-End bietet die folgenden Vorteile: Timeout-Kontrolle, Protokollierung, Weiterleitung fehlgeschlagener Anforderungen an andere Server aus dem Cluster - und das bei minimaler zusätzlicher Serverlast. Ein weiteres Plus dieses Ansatzes: Laut netstat-Lan können Sie sehen, wann die Accept-Warteschlange auf dem SCGI-Socket wächst, was bedeutet, dass wir irgendwo eine Überlastung haben und etwas unternehmen müssen.

    Codequalität und Debugging

    Das Paket net / http / pprof ist eine magische Sache! Dies ist so etwas wie das Apache-Serverstatusmodul, aber für den Go-Dämon. Übrigens würde ich nicht empfehlen, es in die Produktion aufzunehmen, wenn Sie DefaultServeMux anstelle des dedizierten http-Handlers verwenden - da das Paket über den Link / debug / pprof / für alle verfügbar ist. Ich habe kein solches Problem. Um über http auf die Paketfunktionen zuzugreifen, muss auf localhost ein separater Miniserver ausgeführt werden:
    go func() {
            log.Println(http.ListenAndServe("127.0.0.1:8081", nil))
    }()
    

    Zusätzlich zum Abrufen eines Profils für den Prozessor und den Speicher ermöglicht dieses Modul die Anzeige einer Liste aller aktuell ausgeführten Goroutinen auf dem Stapel, der gesamten Kette von Funktionen, die derzeit in ihnen ausgeführt werden, und in welchem ​​Zustand: / debug / pprof / goroutine \? Debug = 1 ergibt Eine Liste der verschiedenen Goroutinen und ihrer Zustände, und / debug / pprof / goroutine \? debug = 2 gibt eine Liste aller laufenden Goroutinen an, einschließlich und duplizieren (d. h. in völlig identischen Zuständen). Hier ist ein Beispiel von einem von ihnen:
    goroutine 85 [IO wait]:
    net.runtime_pollWait(0x800c71b38, 0x72, 0x0)
            /usr/local/go/src/pkg/runtime/netpoll.goc:146 +0x66
    net.(*pollDesc).Wait(0xc20848daa0, 0x72, 0x0, 0x0)
            /usr/local/go/src/pkg/net/fd_poll_runtime.go:84 +0x46
    net.(*pollDesc).WaitRead(0xc20848daa0, 0x0, 0x0)
            /usr/local/go/src/pkg/net/fd_poll_runtime.go:89 +0x42
    net.(*netFD).accept(0xc20848da40, 0x8df378, 0x0, 0x800c6c518, 0x23)
            /usr/local/go/src/pkg/net/fd_unix.go:409 +0x343
    net.(*UnixListener).AcceptUnix(0xc208273880, 0x8019acea8, 0x0, 0x0)
            /usr/local/go/src/pkg/net/unixsock_posix.go:293 +0x73
    net.(*UnixListener).Accept(0xc208273880, 0x0, 0x0, 0x0, 0x0)
            /usr/local/go/src/pkg/net/unixsock_posix.go:304 +0x4b
    bitbucket.org/mjl/scgi.ServeLimited(0x800c7ec58, 0xc208273880, 0x800c6c898, 0x8df178, 0x1f4, 0x0, 0x0)
            /home/user/go/src/bitbucket.org/mjl/scgi/scgi.go:177 +0x20d
    main.func008()
            /home/user/repo/main.go:264 +0x90
    created by main.main
            /home/user/repo/main.go:265 +0x1f5c
    

    Dies hat mir geholfen, einen Fehler mit Sperren zu identifizieren (RUnlock () wurde unter bestimmten Bedingungen zweimal aufgerufen, aber Sie können dies nicht tun). Im Dump des Stapels sah ich eine ganze Reihe gesperrter Goroutinen und Zeilennummern, in denen RUnlock () aufgerufen wurde.

    Das CPU-Profil ist auch nicht schlecht. Ich empfehle, gv (Ghostview) zu installieren und das Xorg-Diagramm der Übergänge zwischen Funktionen mit Zählern zu betrachten. Sie können sehen, worauf Sie achten und optimieren sollten.

    go vet ist zwar ein nützliches Dienstprogramm, aber mein Hauptvorteil bestand in Warnungen über fehlende Formatbezeichner in allen Arten von printf () - der Compiler kann dies nicht erkennen. An offensichtlich schlechtem Code
    if UintValue < 0 {
            DoSomething()
    }
    
    Der Tierarzt reagiert nicht.

    Die Hauptarbeit der Codeüberprüfung wird vom Compiler ausgeführt. Er schwört regelmäßig auf nicht verwendete Variablen und Pakete, aber weder der Compiler noch der Tierarzt reagieren auf nicht verwendete Felder in Strukturen (zumindest mit einer Warnung), obwohl es auch etwas zu beachten gibt.

    Es sollte vorsichtig mit dem Bediener sein : = . Ich hatte einen Fall, in dem es notwendig war, die Differenz zwischen zwei uint zu berechnen, inkl. Betrachten Sie den negativen Unterschied als negativ und den Code
      var a, b uint
     ...
      diff := a - b
    
    zählt nicht, was Sie erwarten - Sie müssen die Konvertierung in den vorzeichenbehafteten Typ verwenden (oder nicht vorzeichenbehafteten verwenden).

    Es empfiehlt sich auch, dieselben Datentypen für unterschiedliche Zwecke mit unterschiedlichen Namen zu benennen. Zum Beispiel so:
    type ServerIdType uint32
    type CustomerIdType uint32
    var ServerId ServerIdType
    var CustomerId CustomerIdType
    
    Und jetzt für die Variable CustomerId, der Compiler wird nicht nur den Wert von ServerId (ohne Typkonvertierung) schreiben lassen, obwohl dort und dort in uint32. Es hilft bei allen Arten von Tippfehlern, obwohl es jetzt oft notwendig ist, das Typumwandeln zu verwenden, insbesondere beim Initialisieren von Variablen.

    Pakete, Bibliotheken und eine Reihe von C

    Eine wichtige Rolle für die Popularität von Go spielte ein effektiver Mechanismus für die Interaktion mit C-Bibliotheken (leider gibt es in Bezug auf die Leistung noch einige Probleme). Im Großen und Ganzen ist ein erheblicher Teil der Go-Bibliotheken nur ein Wrapper über ihren C-Gegenstücken. Beispielsweise werden die Pakete github.com/abh/geoip und github.com/jbarham/gopgsqldriver mit -lGeoIP bzw. -lpq kompiliert (in Wahrheit verwende ich den nativen Go PostgreSQL-Treiber - github.com/lib/pq).

    Betrachten Sie zum Beispiel die fast standardmäßige crypt () -Funktion von unistd.h - diese Funktion ist in vielen Sprachen standardmäßig verfügbar. Beispielsweise kann sie im Nginx Perl-Modul verwendet werden, ohne dass zusätzliche Module geladen werden müssen, was nützlich ist. Aber nicht in Go, hier müssen Sie es selbst an C weiterleiten. Dies geschieht auf elementare Weise (im Beispiel wird Salz aus dem Ergebnis herausgeschnitten):
    // #cgo LDFLAGS: -lcrypt
    // #include 
    // #include 
    import "C"
    import (
            "sync"
            "unsafe"
    )
    var cryptMutex sync.Mutex
    func Crypt(str, salt string) string {
            cryptStr := C.CString(str)
            cryptSalt := C.CString(salt)
            cryptMutex.Lock()
            key := C.GoString(C.crypt(cryptStr, cryptSalt))[len(salt):]
            cryptMutex.Unlock()
            C.free(unsafe.Pointer(cryptStr))
            C.free(unsafe.Pointer(cryptSalt))
            return key
    }
    
    Sperre wird benötigt, weil crypt () gibt das gleiche Zeichen * in den internen Zustand zurück, der empfangene String muss kopiert werden, andernfalls wird er beim nächsten Aufruf überschrieben, d. h. Die Funktion ist nicht threadsicher.

    Datenbank / SQL

    Für jeden verwendeten Db-Handler empfehle ich den Aufruf, um die maximale Anzahl von Verbindungen zu registrieren und eine von Null verschiedene Anzahl von Leerlaufverbindungen anzugeben:
    db.SetMaxOpenConns(30)
    db.SetMaxIdleConns(8)
    
    Die erste Methode vermeidet eine Überlastung der Datenbank und verwendet sie im Modus für maximale Leistung (mit zunehmender Anzahl gleichzeitiger Verbindungen sinkt die Datenbankleistung ab einem bestimmten Punkt, es gibt einen optimalen Wert für die Anzahl gleichzeitiger Anforderungen). Die zweite Methode beseitigt die Notwendigkeit, eine neue Verbindung zu eröffnen Dies ist bei jeder Abfrage für PostgreSQL mit dem fork () -Modus besonders wichtig. Natürlich können Sie für PostgreSQL immer noch pgpool oder pgbouncer verwenden, aber dies ist der zusätzliche Aufwand für das Senden von Daten durch den Kernel und zusätzliche Verzögerungen - daher ist es besser, die Kontinuität der Verbindungen direkt auf Anwendungsebene sicherzustellen.

    Um einen Mehraufwand für das Parsen einer Anforderung und das Erstellen eines Plans auszuschließen, sollten Sie vorbereitete Anweisungen anstelle von direkten Anforderungen verwenden. Beachten Sie jedoch Folgendes: In einigen Fällen verwendet der Scheduler für die Abfrageausführung möglicherweise nicht den optimalsten Plan, da er in der Phase des Parsens der Anforderung (und nicht der Ausführung) erstellt wird und der Scheduler nicht immer über genügend Daten verfügt, um zu wissen, welcher Index zu verwenden ist. Übrigens verwenden Platzhalter für Variablen im PostgreSQL Go-Treiber '$ 1', '$ 2' usw. anstelle von '?', Wie in Perl.

    sql. (Rows) .Scan () hat eine Funktion: Es versteht nicht umbenannte Zeichenfolgentypen, z. B. den Typ DomainNameType-Zeichenfolge . Ich muss eine temporäre Variable vom Typ string startenLaden Sie Daten aus der Datenbank in die Datenbank und weisen Sie sie anschließend mit der Typkonvertierung zu. Aus irgendeinem Grund gibt es bei umbenannten numerischen Typen kein solches Problem.

    Kanäle und Synchronisation

    Es ist eine etwas falsche Meinung, dass es sich lohnt, diese und nur diese Kanäle zu verwenden, da wir über Go-Kanäle verfügen. Dies ist nicht ganz richtig - jede Aufgabe hat ihr eigenes Werkzeug. Kanäle eignen sich hervorragend zum Senden verschiedener Arten von Nachrichten, aber für die Arbeit mit gemeinsam genutzten Ressourcen, wie z. B. SQL-Cache, ist die Verwendung von Mutexen rechtmäßig. Um mit dem Cache durch die Kanäle zu arbeiten, müssen wir einen Abfragemanager schreiben, der die Leistung des Cache-Zugriffs auf einen Kern begrenzt, dem Sheduler-Goroutin noch mehr Arbeit hinzufügt und dem Channel einen Overhead zum Kopieren und Lesen von Daten hinzufügt. Außerdem müssen wir jedes Mal einen temporären Channel für die Datenübertragung erstellen Rückruffunktion. Code, der Kanäle verwendet, wird häufig auch um ein Vielfaches komplizierter als Code mit Mutexen (seltsamerweise). Aber bei den Mutexen muss man sehr vorsichtig sein, um nicht in eine Sackgasse zu geraten.

    Go hat eine knifflige Funktion wie struct {} . Das heißt völlig leere Struktur, grenzenlos. Es belegt keinen Platz, ein Array beliebiger Größe solcher Strukturen belegt ebenfalls keinen Platz, und der gepufferte Kanal leerer Strukturen belegt ebenfalls keinen Platz (und natürlich auch interne Daten). Tatsächlich ist dieser gepufferte Kanal mit leeren Strukturen ein Semaphor, für den im Compiler sogar ein separater Handler erstellt wird. Wenn Sie ein Semaphor mit Go-Syntax benötigen, können Sie chan struct {} verwenden .

    Die Traurigkeit des Synchronisationspakets ist ein bisschen traurig. Zum Beispiel gibt es keine Spinlocks, obwohl sie sehr nützlich sind, da sie schnell sind (obwohl die Verwendung von GC die Verwendung von Spinlocks zu einem riskanten Geschäft macht). Darüber hinaus sind Operationen mit Mutexen selbst nicht inline (soweit ich das beurteilen kann). Noch frustrierender ist die Unfähigkeit, die RWMutex-Sperre zu aktualisieren - wenn die Sperre im RLock-Status ist und festgestellt wurde, dass Sie Änderungen vornehmen müssen -, führen Sie zuerst RUnlock (), dann Lock () aus und prüfen Sie erneut, ob diese Änderungen noch erforderlich sind, oder ob eine Art von Goroutine vorhanden ist alles geschafft. Es gibt auch keine nicht blockierende TryLock () -Funktion, auch hier ist nicht klar, warum - in einigen Fällen ist dies äußerst notwendig. Hier sind die Sprachentwickler mit ihrem "Wir wissen besser, wie Sie programmieren müssen", IMHO, bereits zu weit gegangen.

    In einigen Fällen hilft das Sync / Atomic-Paket mit seinen Atomic-Operationen, die Verwendung von Mutexen zu vermeiden. Beispielsweise verwende ich häufig den aktuellen uint32-Zeitstempel in meinem Code - ich behalte ihn in einer globalen Variablen und speichere zu Beginn jeder Anforderung einfach den aktuellen Wert atomar darin. Ein bisschen schmutzig, ich weiß, es war möglich, eine Hilfsfunktion zu schreiben, aber manchmal muss man solche Opfer im Kampf um die Leistung bringen - ich kann diese Variable jetzt ohne besondere Einschränkungen in arithmetischen Ausdrücken verwenden.

    Es gibt eine andere gute Optimierungsmethode für den Fall, dass einige allgemeine Daten nur an einer Stelle (z. B. in regelmäßigen Abständen) aktualisiert werden und in anderen Fällen im Nur-Lese-Modus verwendet werden. Die Quintessenz ist, dass es nicht erforderlich ist, RLock () / RUnlock () für Leseoperationen (und Lock () / Unlock () für Aktualisierungen) auszuführen - die Aktualisierungsfunktion kann Daten in einen neuen Speicherbereich laden und dann den Zeiger auf die alten Daten atomar durch einen Zeiger ersetzen zu neuen. Richtig, in Go erfordert die Funktion des atomaren Zeigerschreibens den Typ unsafe.Pointer, und Sie müssen dieses Design blockieren:
    atomic.StorePointer((*unsafe.Pointer)(unsafe.Pointer(&Data)), unsafe.Pointer(&newData))
    
    Sie können diese Daten jedoch in beliebigen Ausdrücken verwenden, ohne sich Gedanken über Sperren machen zu müssen. Dies ist von besonderer Bedeutung für Go, as scheinbar kurze Sperren können tatsächlich sehr lang sein - und das alles wegen der GC.

    GC (Müllsammler)

    Er hat ziemlich viel auf meinem Dach getrunken; (. Stellen Sie sich eine Situation vor - führen Sie einen Belastungstest durch - alles ist in Ordnung. Lassen Sie einen Live-Verkehr zu - alles ist in Ordnung. Und dann, bam - und alles wird schlecht oder sehr, sehr schlecht - alte Anfragen hängen, neue kommen und kommen an (mehrere Tausend pro Sekunde) müssen Sie die Anwendung neu starten, danach stirbt alles wieder, da der Cache wieder aufgefüllt werden muss, aber zumindest funktioniert er und nach einiger Zeit kehrt er zum Normalzustand zurück Bearbeitungsschritte anfordern - und vi Ich wünsche mir, dass die Ausführungszeit aller Phasen in regelmäßigen Abständen auf drei Sekunden oder mehr springt, selbst wenn keine Sperren verwendet werden, wird kein Zugriff auf die Datenbank und die Dateien verwendet, sondern es werden nur lokale Berechnungen durchgeführt, die normalerweise in Mikrosekunden passen. Es gab keinen externen Faktor und die Plattform selbst. Genauer gesagt, ein Müllmann.

    Es ist gut, dass Sie in Go die GC-Leistungsstatistik über Runtime / Debug sehen können. ReadGCStats () - es gibt etwas, das Sie überraschen sollte. In meinem Fall funktionierte der GC auf dem Server mit der höchsten Auslastung im folgenden Modus:
    0,06
    0,30
    2,00
    0,06
    0,30
    2,00
    ...
    Die Reihenfolge der Werte wurde beibehalten, obwohl die Zahlen selbst geringfügig variierten. Dies ist die Dauer des Einschlafens der Anwendung, während der GC ausgeführt wird. Die zuletzt ausgeführten befinden sich oben. Alle Arbeiten für 2 Sekunden anhalten - was? Ich habe sogar Angst davor, mir vorzustellen, was auf den am stärksten ausgelasteten Servern passiert, habe sie jedoch nicht angerührt, um zusätzliche Ausfallzeiten zu vermeiden.

    Die Lösung besteht darin, GC () häufiger auszuführen. Aus Gründen der Zuverlässigkeit ist es besser, vom Programm unabhängig zu sein. Sie können sogar nur in regelmäßigen Abständen, ich habe ein wenig verwirrt und machte einen Anforderungszähler sowie erzwungenen Start von GC () nach größeren Bereinigungen von veralteten Daten. Infolgedessen wurde GC () alle zehn bis zwanzig Sekunden anstatt alle paar Minuten ausgeführt, aber jeder Durchgang dauert ungefähr 0,1 Sekunden - eine völlig andere Angelegenheit! Gleichzeitig sank der Speicherverbrauch des Dämons um 20 Prozent. Es besteht die Möglichkeit, den Garbage Collector vollständig zu deaktivieren. Dies ist jedoch nur für kurzlebige Programme und nicht für Dämonen geeignet. Die Sprachentwickler sollten die Einstellung dem GC hinzufügen, damit die Anwendung nicht länger als angegeben angehalten wird, sondern häufiger ausgeführt wird. Dies würde viele Benutzer vor Problemen unter hoher Last bewahren.

    Karten

    Niemand wird argumentieren, dass Maps (Hashes in Bezug auf Perl) äußerst nützliche Dinge sind. Ich habe jedoch ernsthafte Beschwerden gegen die Sprachentwickler bezüglich der Art und Weise, wie sie implementiert und verwendet werden. Grob gesagt, verwendet die Arbeit mit Map Compiler die folgenden drei Funktionen:
    valueType, ok := map_fetch(keyType)
    map_store(keyType, valueType)
    map_delete(keyType)
    
    Und dies bringt eine Reihe von signifikanten Einschränkungen mit sich. Während die Karten aus Grundtypen bestehen - alles ist in Ordnung, aber Probleme mit der Karte von Strukturen oder Typen mit Referenzmethoden (dh Methoden, die durch Verknüpfen mit Daten und nicht durch Kopieren von Daten funktionieren) beginnen bereits - können wir zum Beispiel nicht schreiben
    type T struct { cnt int }
    m := make(map[int]T)
    m[0] = T{}
    m[0].cnt++  // ./main.go:9: cannot assign to m[0].cnt
    
    da der Compiler die Adresse des Wertes m [0] nicht erhalten kann, um um cnt offset zu inkrementieren.

    Sie können entweder eine Kartenverknüpfung mit der Struktur herstellen
    m := make(map[int]*T)
    m[0] = new(T)
    m[0].cnt++
    
    entweder entladen und speichern Sie die gesamte Struktur
    m := make(map[int]T)
    tmp := m[0]
    tmp.cnt++
    m[0] = tmp
    
    Die erste Option fügt dem Garbage Collector viel zusätzliche Arbeit hinzu und die zweite dem Prozessor (insbesondere wenn die Struktur ziemlich groß ist).

    Meiner Meinung nach kann die Frage gelöst werden, ob der Compiler bei der Arbeit mit Map die Funktion anstelle von map_store verwendet
    *valueType = map_allocate(keyType)
    
    und fügen Sie eine zusätzliche Einschränkung hinzu, die bewirkt, dass der der Karte hinzugefügte Wert nicht mehr im Speicher verschoben wird.

    Die Funktion map_allocate sollte verwendet werden, um Zeiger nicht nur auf neu erstellte, sondern auch auf vorhandene Elemente abzurufen, wenn diese geändert werden. Dieser Zeiger kann für die Ausgabe an den Programmierer, zum Aktualisieren des Werts und zum Aufrufen der Referenzmethode verwendet werden - und solange der Wert vorhanden ist, funktioniert alles einwandfrei.

    Einige argumentieren möglicherweise, dass die Möglichkeit, Verweise auf einen Wert in einer Karte abzurufen, die gesamte gepriesene Sprachensicherheit sofort verletzt. Für die Karte ist die Sicherheit ohnehin nicht garantiert - sie kann nicht von verschiedenen Goroutinen aus ohne Blockierung verwendet werden, da sonst die Gefahr besteht, dass interne Daten beim Einfügen von Elementen beschädigt werden. Darüber hinaus sagt niemand, dass es notwendig ist, dem Programmierer die Möglichkeit zu geben, die Adresse des Map-Elements abzurufen, ohne das unsichere Paket zu verwenden. Im obigen Beispiel könnte der Compiler die Adresse übernehmen, den Zähler erhöhen und diese Adresse vergessen Operationen können nicht beeinflusst werden.

    Probleme können nur auftreten, wenn Sie das Element löschen und den Link zu nicht verwendetem Speicher weiterhin verwenden. Dies ist aus dem gleichen Bereich wie die gleichzeitige Verwendung von Karten aus verschiedenen Goroutinen ohne zu blockieren - wenn der Programmierer Pinocchio selbst ist, wer ist dann der Arzt für ihn? Und wenn es möglich sein wird, den Garbage Collector an diesen Fall anzupassen, so dass nach dem Löschen kein Speicher freigegeben wird, solange das Programm einen Live-Link zum gelöschten Element hat, ist alles in Ordnung und es gibt keine Sicherheitsprobleme.

    Zusammenfassung

    Leider gibt es keine Perfektion auf der Welt. Aber es wäre naiv zu erwarten, dass die neue Sprache sofort ideal geboren wird. Ja, Go hat einige Nachteile, aber sie sind im Prinzip alle behebbar, es würde einen Wunsch geben. Go treibt die Entwicklung von Programmiersprachen auf die nächste Ebene, indem es sich an die modernen Gegebenheiten von Mehrkern-Computerarchitekturen anpasst und geeignete Paradigmen vorschlägt.

    Ich habe sehr lange keine neuen Programmiersprachen gelernt. Einmal habe ich C ein bisschen gemeistert (auf der Ebene von ein bisschen, um den FreeBSD-Kernel zu patchen), Perl und Shell-Scripting (für allgemeine Aufgaben). Ich hatte weder Zeit noch Lust, mich in das Lernen von Python, Ruby oder JS zu vertiefen - diese Sprachen konnten mir nichts grundlegend Neues bieten, und es gab keinen Wunsch, die Idee zu ändern. Go konnte meinen Werkzeugsatz erheblich ergänzen, worüber ich mich nur freue. Trotz all seiner Mängel bereue ich keinen Tropfen der Zeit, die ich damit verbracht habe, es zu studieren - es lohnt sich wirklich.

    Jetzt auch beliebt: