Skip to content

select

Select ist eine Struktur, die gleichzeitig den Status mehrerer Channels überwachen kann. Die Syntax ähnelt switch:

go
import (
  "context"
  "log/slog"
  "os"
  "os/signal"
  "time"
)

func main() {
  finished := make(chan struct{})
  ctx, stop := signal.NotifyContext(context.Background(), os.Kill, os.Interrupt)
  defer stop()
  slog.Info("running")

  go func() {
    time.Sleep(time.Second * 2)
    finished <- struct{}{}
  }()

  select {
  case <-ctx.Done():
    slog.Info("shutting down")
  case <-finished:
    slog.Info("finished")
  }
}

Dieser Code implementiert eine einfache Logik für das sanfte Beenden des Programms durch die Kombination von Context, Channel und Select. Der Select-Block überwacht gleichzeitig die beiden Channels ctx.Done und finished. Es gibt zwei Bedingungen zum Beenden: Entweder sendet das Betriebssystem ein Beendigungssignal, oder der finished-Channel hat eine Nachricht zum Lesen, was bedeutet, dass die Benutzercode-Aufgabe abgeschlossen ist. Auf diese Weise können wir beim Beenden des Programms Aufräumarbeiten durchführen.

Select hat zwei sehr wichtige Eigenschaften: Erstens ist es nicht-blockierend. In den Quellcode für das Senden und Empfangen von Channels ist zu sehen, dass für Select spezielle Behandlungen implementiert wurden, die es ermöglichen, ohne Blockierung zu prüfen, ob ein Channel verfügbar ist. Zweitens erfolgt die Auswahl zufällig. Wenn mehrere Channels verfügbar sind, wird zufällig einer zur Ausführung ausgewählt. Das Nicht-Befolgen einer festgelegten Reihenfolge ermöglicht jedem Channel eine relativ faire Ausführung, da in extremen Fällen einige Channels sonst niemals verarbeitet werden könnten. Da die gesamte Arbeit mit Channels zu tun hat, wird empfohlen, zuerst den Artikel über chan zu lesen. Nachdem man Channels verstanden hat, wird das Verständnis von Select viel einfacher.

Struktur

Zur Laufzeit gibt es nur eine Struktur runtime.scase, die einen Select-Zweig repräsentiert. Jeder case wird zur Laufzeit durch ein scase repräsentiert.

go
type scase struct {
  c    *hchan         // chan
  elem unsafe.Pointer // data element
}

Dabei ist c der Channel und elem ist der Zeiger auf das zu empfangende oder zu sendende Element. Tatsächlich wird das Select-Schlüsselwort durch die Funktion runtime.selectgo repräsentiert.

Prinzip

Die Verwendung von Select wird von Go in vier Fälle unterteilt, die optimiert werden. Dies ist in der Funktion cmd/compile/internal/walk.walkSelectCases zu sehen, die die Behandlungslogik für diese vier Fälle enthält.

go
func walkSelectCases(cases []*ir.CommClause) []ir.Node {
  ncas := len(cases)
  sellineno := base.Pos

  // optimization: zero-case select
  if ncas == 0 {
    return []ir.Node{mkcallstmt("block")}
  }

  // optimization: one-case select: single op.
  if ncas == 1 {
    ...
  }

  // optimization: two-case select but one is default: single non-blocking op.
  if ncas == 2 && dflt != nil {
    ...
  }

  ...
  return init
}

Optimierung

Der Compiler optimiert die ersten drei Fälle. Der erste Fall ist, wenn die Anzahl der Cases 0 ist, also ein leeres Select. Es ist bekannt, dass ein leeres Select-Statement die aktuelle Goroutine dauerhaft blockiert.

go
select{}

Die Blockierung tritt auf, weil der Compiler sie in einen direkten Aufruf der Funktion runtime.block übersetzt:

go
func block() {
  gopark(nil, nil, waitReasonSelectNoCases, traceBlockForever, 1) // forever
}

Die block-Funktion ruft wiederum die Funktion runtime.gopark auf, wodurch die aktuelle Goroutine in den Zustand _Gwaitting versetzt wird und dauerhaft blockiert, ohne jemals wieder eingeplant zu werden.

Der zweite Fall: Es gibt nur einen case und dieser ist nicht default. In diesem Fall übersetzt der Compiler ihn direkt in eine Sende- oder Empfangsoperation für den Channel, und diese ist blockierend. Beispiel:

go
func main() {
  ch := make(chan int)
  select {
  case <-ch:
        // do something
  }
}
go
func main() {
  ch := make(chan int)
  select {
  case <-ch:
        // do something
  }
}

Es wird in einen direkten Aufruf der Funktion runtime.chanrecv1 übersetzt, was aus dem Assembler-Code ersichtlich ist:

go
TEXT  main.main(SB), ABIInternal, $2
...
LEAQ  type:chan int(SB), AX
XORL  BX, BX
PCDATA  $1, $0
CALL  runtime.makechan(SB)
XORL  BX, BX
NOP
CALL  runtime.chanrecv1(SB)
ADDQ  $16, SP
POPQ  BP
...

Auch beim Senden von Daten an einen Channel mit nur einem case gilt dasselbe Prinzip: Es wird in einen direkten Aufruf der Funktion runtime.chansend1 übersetzt, der ebenfalls blockierend ist.

Der dritte Fall: Es gibt zwei cases und einer davon ist default:

go
func main() {
  ch := make(chan int)
  select {
  case ch <- 1:
        // do something
  default:
        // do something
  }
}

In diesem Fall wird es in eine if-Anweisung übersetzt, die runtime.selectnbsend aufruft:

go
if selectnbsend(ch, 1) {
  // do something
} else {
  // do something
}

Wenn es sich um den Empfang von Channel-Daten handelt, wird es in einen Aufruf von runtime.selectnbrecv übersetzt:

go
ch := make(chan int)
select {
  case x, ok := <-ch:
      // do something
  default:
      // do something
}
go
if selected, ok = selectnbrecv(&v, c); selected {
  // do something
} else {
  // do something
}

Es ist erwähnenswert, dass in diesem Fall das Senden oder Empfangen am Channel nicht-blockierend ist. Dies ist deutlich daran zu erkennen, dass der block-Parameter false ist.

go
func selectnbsend(c *hchan, elem unsafe.Pointer) (selected bool) {
  return chansend(c, elem, false, getcallerpc())
}

func selectnbrecv(elem unsafe.Pointer, c *hchan) (selected, received bool) {
  return chanrecv(c, elem, false)
}

Egal ob beim Senden oder Empfangen von Channel-Daten, wenn block auf false gesetzt ist, gibt es einen schnellen Pfad, um ohne Sperren zu prüfen, ob gesendet oder empfangen werden kann, wie unten gezeigt:

go
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
  if !block && empty(c) {
        if atomic.Load(&c.closed) == 0 {
      return
    }
    return true, false
  }
  ...
}

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
    if !block && c.closed == 0 && full(c) {
    return false
  }
    ...
}

Beim Lesen aus einem Channel wird direkt zurückgekehrt, wenn der Channel leer ist. Beim Schreiben in einen Channel wird direkt zurückgekehrt, wenn der Channel nicht geschlossen ist und bereits voll ist. Normalerweise würden diese Operationen die Goroutine blockieren, aber in Kombination mit Select geschieht dies nicht.

Verarbeitung

Die oben genannten drei Fälle sind nur Optimierungen für Sonderfälle. Ein normal verwendetes Select-Schlüsselwort wird in einen Aufruf der Funktion runtime.selectgo übersetzt, deren Verarbeitungslogik über 400 Zeilen umfasst.

go
func selectgo(cas0 *scase, order0 *uint16, pc0 *uintptr, nsends, nrecvs int, block bool) (int, bool)

Der Compiler sammelt alle case-Anweisungen in einem scase-Array und übergibt dieses an die selectgo-Funktion. Nach Abschluss der Verarbeitung werden zwei Rückgabewerte zurückgegeben:

  1. Der erste ist der zufällig ausgewählte Channel-Index, der angibt, welcher Channel verarbeitet wurde. Wenn keiner, wird -1 zurückgegeben.
  2. Der zweite gibt an, ob der Lesevorgang am Channel erfolgreich war.

Hier eine kurze Erklärung der Parameter:

  • cas0: Der Kopfzeiger des scase-Arrays. Die erste Hälfte speichert die Schreib-Channel-Cases, die zweite Hälfte die Lese-Channel-Cases, getrennt durch nsends.
  • order0: Seine Länge ist doppelt so groß wie das scase-Array. Die erste Hälfte wird dem pollorder-Array zugewiesen, die zweite Hälfte dem lockorder-Array.
  • nsends und nrecvs geben die Anzahl der Lese-/Schreib-Channel-Cases an. Ihre Summe ist die Gesamtzahl der Cases.
  • block gibt an, ob blockiert werden soll. Wenn ein default-Case vorhanden ist, bedeutet dies nicht-blockierend, der Wert ist false, andernfalls true.
  • pc0: Zeigt auf den Kopf eines [ncases]uintptr-Arrays, verwendet für Race-Analysen. Kann ignoriert werden, da es für das Verständnis von Select nicht hilfreich ist.

Angenommen, wir haben folgenden Code:

go
func main() {
  ch := make(chan int)
  select {
  case ch <- 1:
    println(1)
  case ch <- 2:
    println(2)
  case ch <- 3:
    println(3)
  case ch <- 4:
    println(4)
  default:
    println("default")
  }
}

Betrachten wir die Assembler-Form, hier wurde ein Teil des Codes weggelassen, um das Verständnis zu erleichtern:

go
0x0000 00000 TEXT  main.main(SB), ABIInterna
...
0x0023 00035 CALL  runtime.makechan(SB)
0x0028 00040 MOVQ  $1, main..autotmp_2+72(SP) // 1 2 3 4 mehrere temporäre Variablen
0x0031 00049 MOVQ  $2, main..autotmp_4+64(SP)
0x003a 00058 MOVQ  $3, main..autotmp_6+56(SP)
0x0043 00067 MOVQ  $4, main..autotmp_8+48(SP)
...
0x00c8 00200 CALL  runtime.selectgo(SB) // Aufruf der runtime.selectgo-Funktion
0x00cd 00205 TESTQ  AX, AX
0x00d0 00208 JLT  352 // Sprung zum default-Zweig
0x00d6 00214 PCDATA  $1, $-1
0x00d6 00214 JEQ  320 // Sprung zum Zweig 4
0x00d8 00216 CMPQ  AX, $1
0x00dc 00220 JEQ  288 // Sprung zum Zweig 3
0x00de 00222 NOP
0x00e0 00224 CMPQ  AX, $2
0x00e4 00228 JNE  258 // Sprung zum Zweig 2
0x00e6 00230 PCDATA  $1, $0
0x00e6 00230 CALL  runtime.printlock(SB)
0x00eb 00235 MOVL  $3, AX
0x00f0 00240 CALL  runtime.printint(SB)
0x00f5 00245 CALL  runtime.printnl(SB)
0x00fa 00250 CALL  runtime.printunlock(SB)
0x00ff 00255 NOP
0x0100 00256 JMP  379
0x0102 00258 CALL  runtime.printlock(SB)
0x0107 00263 MOVL  $4, AX
0x010c 00268 CALL  runtime.printint(SB)
0x0111 00273 CALL  runtime.printnl(SB)
0x0116 00278 CALL  runtime.printunlock(SB)
0x011b 00283 JMP  379
0x011d 00285 NOP
0x0120 00288 CALL  runtime.printlock(SB)
0x0125 00293 MOVL  $2, AX
0x012a 00298 CALL  runtime.printint(SB)
0x012f 00303 CALL  runtime.printnl(SB)
0x0134 00308 CALL  runtime.printunlock(SB)
0x0139 00313 JMP  379
0x013b 00315 NOP
0x0140 00320 CALL  runtime.printlock(SB)
0x0145 00325 MOVL  $1, AX
0x014a 00330 CALL  runtime.printint(SB)
0x014f 00335 CALL  runtime.printnl(SB)
0x0154 00340 CALL  runtime.printunlock(SB)
0x0159 00345 JMP  379
0x015b 00347 NOP
0x0160 00352 CALL  runtime.printlock(SB)
0x0165 00357 LEAQ  go:string."default\n"(SB)
0x016c 00364 MOVL  $8, BX
0x0171 00369 CALL  runtime.printstring(SB)
0x0176 00374 CALL  runtime.printunlock(SB)
0x017b 00379 PCDATA  $1, $-1
0x017b 00379 ADDQ  $160, SP
0x0182 00386 POPQ  BP
0x0183 00387 RET

Man kann sehen, dass nach dem Aufruf der selectgo-Funktion eine Logik für Bedingung und Sprung existiert. Anhand dieser können wir leicht die ursprüngliche Form rekonstruieren:

go
casei, ok := runtime.selectgo()
if casei == -1 {
    println("default")
} else if casei == 3 {
    println(4)
} else if casei == 2 {
    println(3)
} else if casei == 1 {
    println(2)
} else {
    println(1)
}

Der vom Compiler generierte tatsächliche Code kann hiervon abweichen, aber die Grundbedeutung ist ähnlich. Der Compiler verwendet also nach dem Aufruf der selectgo-Funktion gleichzeitig eine if-Anweisung, um zu bestimmen, welcher Channel ausgeführt werden soll. Vor dem Aufruf generiert der Compiler außerdem eine for-Schleife, um das scase-Array zu sammeln, was hier jedoch weggelassen wurde.

Nachdem wir nun verstanden haben, wie die selectgo-Funktion extern verwendet wird, wollen wir uns ansehen, wie sie intern arbeitet. Zuerst initialisiert sie mehrere Arrays. nsends+nrecvs gibt die Gesamtzahl der Cases an. Aus dem folgenden Code ist auch ersichtlich, dass die maximale Anzahl an Cases 1 << 16 beträgt. pollorder bestimmt die Ausführungsreihenfolge der Channels und lockorder bestimmt die Sperrreihenfolge der Channels.

go
cas1 := (*[1 << 16]scase)(unsafe.Pointer(cas0))
// Seine Länge ist doppelt so groß wie das scase-Array. Die erste Hälfte wird dem pollorder-Array zugewiesen, die zweite Hälfte dem lockorder-Array.
order1 := (*[1 << 17]uint16)(unsafe.Pointer(order0))

ncases := nsends + nrecvs
scases := cas1[:ncases:ncases]
pollorder := order1[:ncases:ncases]
lockorder := order1[ncases:][:ncases:ncases]

Als Nächstes wird das pollorder-Array initialisiert. Es speichert die Indizes des scases-Arrays für die auszuführenden Channels:

go
norder := 0
for i := range scases {
    cas := &scases[i]

    // Omit cases without channels from the poll and lock orders.
    if cas.c == nil {
       cas.elem = nil // allow GC
       continue
    }

    j := fastrandn(uint32(norder + 1))
    pollorder[norder] = pollorder[j]
    pollorder[j] = uint16(i)
    norder++
}
pollorder = pollorder[:norder]
lockorder = lockorder[:norder]

Es durchläuft das gesamte scases-Array und generiert dann mit runtime.fastrandn eine Zufallszahl zwischen [0, i], die dann mit i getauscht wird. Dabei werden Cases mit nil-Channel übersprungen. Nach Abschluss des Durchlaufs erhalten wir ein pollorder-Array mit gemischten Elementen, wie in der folgenden Abbildung gezeigt:

Danach wird das pollorder-Array mittels Heapsort nach der Adresse der Channels sortiert, um das lockorder-Array zu erhalten. Anschließend wird runtime.sellock aufgerufen, um die Channels in der entsprechenden Reihenfolge zu sperren:

go
func sellock(scases []scase, lockorder []uint16) {
  var c *hchan
  for _, o := range lockorder {
    c0 := scases[o].c
    if c0 != c {
      c = c0
      lock(&c.lock)
    }
  }
}

Es ist erwähnenswert, dass die Sortierung der Channels nach Adresse erfolgt, um Deadlocks zu vermeiden, da Select-Operationen selbst keine Sperren für Nebenläufigkeit benötigen. Angenommen, wir würden die Sperren in zufälliger Reihenfolge gemäß pollorder setzen, dann betrachten wir folgende Codesituation:

go
ch1 := make(chan int)
ch2 := make(chan int)
ch3 := make(chan int)
ch4 := make(chan int)

poll := func() {
    select {
    case ch1 <- 1:
        println(1)
    case ch2 <- 2:
        println(2)
    case ch3 <- 3:
        println(3)
    case ch4 <- 4:
        println(4)
    default:
        println("default")
    }
}

// A
go poll()
// B
go poll()
// C
go poll()

Die drei Goroutinen A, B und C haben alle den Schritt des Sperrens erreicht, und ihre Sperrenreihenfolgen sind zufällig und voneinander verschieden. Dies kann zu einer Situation wie in der folgenden Abbildung führen:

Angenommen, die Sperrenreihenfolge von A, B und C entspricht der obigen Abbildung, dann ist die Wahrscheinlichkeit eines Deadlocks sehr groß. Zum Beispiel hält A zuerst die Sperre von ch2 und versucht dann, die Sperre von ch1 zu erhalten. Aber angenommen, ch1 wurde bereits von Goroutine B gesperrt, und Goroutine B versucht wiederum, die Sperre von ch2 zu erhalten, dann entsteht ein Deadlock.

Wenn alle Goroutinen in derselben Reihenfolge sperren, tritt kein Deadlock-Problem auf. Das ist der grundlegende Grund, warum lockorder nach der Adresse sortiert wird.

Nachdem die Sperren gesetzt wurden, beginnt die eigentliche Verarbeitungsphase. Zuerst wird das pollorder-Array durchlaufen, um in der zuvor gemischten Reihenfolge auf die Channels zuzugreifen und nacheinander einen verfügbaren Channel zu finden:

go
for _, casei := range pollorder {
    casi = int(casei)
    cas = &scases[casi]
    c = cas.c

    if casi >= nsends { // 读管道
        sg = c.sendq.dequeue()
        if sg != nil {
            goto recv
        }
        if c.qcount > 0 {
            goto bufrecv
        }
        if c.closed != 0 {
            goto rclose
        }
    } else { // 写管道
        if c.closed != 0 {
            goto sclose
        }
        sg = c.recvq.dequeue()
        if sg != nil {
            goto send
        }
        if c.qcount < c.dataqsiz {
            goto bufsend
        }
    }
}

Man sieht, dass hier 6 Fälle für das Lesen/Schreiben von Channels behandelt werden. Diese werden im Folgenden einzeln erklärt. Der erste Fall: Lesen aus einem Channel, wobei ein Sender darauf wartet zu senden. Hier wird die Funktion runtime.recv aufgerufen, deren Funktion bereits erläutert wurde. Sie weckt schließlich die Sender-Goroutine auf, und vor dem Aufwecken entsperrt die Rückruffunktion alle Channels.

go
recv:
  // can receive from sleeping sender (sg)
  recv(c, sg, cas.elem, func() { selunlock(scases, lockorder) }, 2)
  recvOK = true
  goto retc

Der zweite Fall: Lesen aus einem Channel, kein Sender wartet, und die Anzahl der Pufferelemente ist größer als 0. Hier werden die Daten direkt aus dem Puffer gelesen. Die Logik ist identisch mit runtime.chanrecv, anschließend wird entsperrt:

go
bufrecv:
  recvOK = true
  qp = chanbuf(c, c.recvx)
  if cas.elem != nil {
    typedmemmove(c.elemtype, cas.elem, qp)
  }
  typedmemclr(c.elemtype, qp)
  c.recvx++
  if c.recvx == c.dataqsiz {
    c.recvx = 0
  }
  c.qcount--
  selunlock(scases, lockorder)
  goto retc

Der dritte Fall: Lesen aus einem Channel, aber der Channel ist bereits geschlossen und es befinden sich keine Elemente mehr im Puffer. Hier wird zuerst entsperrt und dann direkt zurückgekehrt:

go
rclose:
  selunlock(scases, lockorder)
  recvOK = false
  if cas.elem != nil {
    typedmemclr(c.elemtype, cas.elem)
  }
  goto retc

Der vierte Fall: Senden von Daten an einen bereits geschlossenen Channel. Hier wird zuerst entsperrt und dann panic ausgelöst:

go
sclose:
  selunlock(scases, lockorder)
  panic(plainError("send on closed channel"))

Der fünfte Fall: Es gibt einen Empfänger, der blockierend wartet. Hier wird die Funktion runtime.send aufgerufen, die schließlich die Empfänger-Goroutine aufweckt. Vor dem Aufwecken entsperrt die Rückruffunktion alle Channels:

go
send:
  send(c, sg, cas.elem, func() { selunlock(scases, lockorder) }, 2)
  goto retc

Der sechste Fall: Es wartet keine Empfänger-Goroutine, die zu sendenden Daten werden in den Puffer gelegt, dann wird entsperrt:

go
bufsend:
  typedmemmove(c.elemtype, chanbuf(c, c.sendx), cas.elem)
  c.sendx++
  if c.sendx == c.dataqsiz {
    c.sendx = 0
  }
  c.qcount++
  selunlock(scases, lockorder)
  goto retc

Alle oben genannten Fälle führen schließlich zum retc-Zweig, der nur den ausgewählten Channel-Index casi und den recvOK-Wert zurückgibt, der angibt, ob das Lesen erfolgreich war.

go
retc:
    return casi, recvOK

Der siebte Fall: Es wurde kein verfügbarer Channel gefunden und der Code enthält einen default-Zweig. Dann werden die Channels entsperrt und direkt zurückgekehrt. Der zurückgegebene casi-Wert ist -1, was anzeigt, dass kein verfügbarer Channel existiert.

go
if !block {
    selunlock(scases, lockorder)
    casi = -1
    goto retc
}

Der letzte Fall: Es wurde kein verfügbarer Channel gefunden und der Code enthält keinen default-Zweig. Dann geht die aktuelle Goroutine in den Blockierungszustand über. Zuvor fügt selectgo die aktuelle Goroutine in die recvq/sendq-Warteschlangen aller überwachten Channels ein:

go
gp = getg()
nextp = &gp.waiting
for _, casei := range lockorder {
    casi = int(casei)
    cas = &scases[casi]
    c = cas.c
    sg := acquireSudog()
    sg.g = gp
    sg.isSelect = true
    sg.elem = cas.elem
    sg.releasetime = 0
    sg.c = c
    *nextp = sg
    nextp = &sg.waitlink

    if casi < nsends {
        c.sendq.enqueue(sg)
    } else {
        c.recvq.enqueue(sg)
    }
}

Hier werden mehrere sudog erstellt und mit den entsprechenden Channels verknüpft, wie in der folgenden Abbildung gezeigt:

Anschließend wird durch runtime.gopark blockiert. Vor der Blockierung werden die Channels entsperrt. Die Entsperrung erfolgt durch die Funktion runtime.selparkcommit, die als Rückruffunktion an gopark übergeben wird:

go
gp.param = nil
// Signal to anyone trying to shrink our stack that we're about
// to park on a channel. The window between when this G's status
// changes and when we set gp.activeStackChans is not safe for
// stack shrinking.
gp.parkingOnChan.Store(true)
gopark(selparkcommit, nil, waitReasonSelect, traceBlockSelect, 1)
gp.activeStackChans = false

Das Erste, was nach dem Aufwecken getan wird, ist die Verknüpfung zwischen sudog und dem Channel zu lösen:

go
sellock(scases, lockorder)

gp.selectDone.Store(0)
sg = (*sudog)(gp.param)
gp.param = nil

casi = -1
cas = nil
caseSuccess = false
sglist = gp.waiting
// Clear all elem before unlinking from gp.waiting.
for sg1 := gp.waiting; sg1 != nil; sg1 = sg1.waitlink {
    sg1.isSelect = false
    sg1.elem = nil
    sg1.c = nil
}
gp.waiting = nil

Anschließend wird sudog aus den Warteschlangen der vorherigen Channels entfernt:

go
for _, casei := range lockorder {
    k = &scases[casei]
    if sg == sglist {
        // sg has already been dequeued by the G that woke us up.
        casi = int(casei)
        cas = k
        caseSuccess = sglist.success
        if sglist.releasetime > 0 {
            caseReleaseTime = sglist.releasetime
        }
    } else {
        c = k.c
        if int(casei) < nsends {
            c.sendq.dequeueSudoG(sglist)
        } else {
            c.recvq.dequeueSudoG(sglist)
        }
    }
    sgnext = sglist.waitlink
    sglist.waitlink = nil
    releaseSudog(sglist)
    sglist = sgnext
}

Im oben genannten Prozess wird mit Sicherheit ein Channel gefunden, der von der aufweckenden Goroutine verarbeitet wurde. Anschließend wird basierend auf caseSuccess die endgültige Verarbeitung durchgeführt. Für Schreiboperationen bedeutet sg.success gleich false, dass der Channel bereits geschlossen wurde. In der gesamten Go-Laufzeitumgebung setzt nur die close-Funktion dieses Feld aktiv auf false, was darauf hinweist, dass die aktuelle Goroutine durch die close-Funktion aufgeweckt wurde. Für Leseoperationen, wenn sie durch einen Sender aufgeweckt wurden, wurde der Datenlesevorgang bereits vor dem Aufwecken vom Sender durch die Funktion runtime.send abgeschlossen, und der Wert ist true. Wenn sie durch die close-Funktion aufgeweckt wurden, wird wie zuvor direkt zurückgekehrt.

go
c = cas.c

if casi < nsends {
    if !caseSuccess {
       goto sclose
    }
} else {
    recvOK = caseSuccess
}

selunlock(scases, lockorder)
goto retc

Damit ist die gesamte Logik von Select im Wesentlichen geklärt. Wie oben an den verschiedenen Fällen zu sehen ist, ist die Verarbeitung von Select recht komplex.

Golang by www.golangdev.cn edit