Understanding the Go runtime: the "select" statement

valyala2 pts0 comments

The select Statement | Internals for InternsπŸ“š<br>Understanding the Go Runtime (8 of 8)<br>β–Ό1.<br>The Bootstrap<br>2.<br>The Memory Allocator<br>3.<br>The Scheduler<br>4.<br>The Garbage Collector<br>5.<br>The System Monitor<br>6.<br>The Network Poller<br>7.<br>Slices, Maps, and Channels<br>8.<br>The select Statement<br>You are here

In the previous article<br>we walked through slices, maps, and channels, and how each of them is structured under the hood. Out of those three, channels are probably the most involved one in terms of how you actually use them β€” and there&rsquo;s one language construct that really stands out when working with channels: the select statement. That&rsquo;s what we&rsquo;re going to be talking about in this post. And maybe I&rsquo;m cheating a little here, because this article isn&rsquo;t strictly about the runtime β€” select falls in between the runtime and the compiler β€” but that&rsquo;s exactly what makes it an interesting corner of Go: it looks like a switch, behaves like a switch, and yet underneath it&rsquo;s a coordinated dance between the compiler and the runtime .<br>That coordination is the key idea I want you to take away from this post: select is not one feature, it&rsquo;s two. Half of it lives in the compiler β€” specifically in walkSelectCases<br>β€” which looks at the shape of your select and, in most common cases, rewrites it into something much simpler β€” often into code that has nothing to do with select at all. The other half lives in the runtime, in a single function called selectgo<br>, which only runs for the cases the compiler couldn&rsquo;t shortcut.<br>So let&rsquo;s split the article the same way: first we&rsquo;ll see all the rewrites the compiler does (and what they turn into), and then we&rsquo;ll dive into selectgo for the cases that survive.<br>Half One: What the Compiler Rewrites<br>Before we get into the rewrites, there&rsquo;s one thing worth pointing out: if you look at the language spec (or just at real code), a select case can only ever contain one of two things β€” a send to a channel (ch ) or a receive from a channel (v := , or just ). That&rsquo;s it. There&rsquo;s no &ldquo;case x == 5&rdquo; or &ldquo;case some arbitrary expression.&rdquo; The compiler enforces this at compile time, which is what makes all the rewrites below possible β€” it knows, statically, that every case is a channel op, and it knows exactly which kind.<br>With that in mind: when the compiler sees a select statement, the first thing it does is look at how many cases you have and what kind they are. Based on that, it picks one of four strategies. Three of them avoid the runtime&rsquo;s selectgo entirely. Only the fourth β€” the &ldquo;general case&rdquo; β€” actually calls into it.<br>Let&rsquo;s go through them in order, from &ldquo;barely a select&rdquo; to &ldquo;the real thing.&rdquo; For each, I&rsquo;ll show you what you wrote and what the compiler effectively turns it into.<br>Case 1: The Empty Select<br>The simplest select statement you can write is an empty select, with no cases at all. It&rsquo;s the &ldquo;park this goroutine forever&rdquo; idiom: there&rsquo;s nothing to wait on, so the goroutine just blocks until the program ends. The compiler doesn&rsquo;t waste any cycles here β€” it throws the select away<br>and replaces it with a single call into the runtime:

That block<br>function just calls gopark with a waitReasonSelectNoCases reason and never returns. No channels, no cases, no algorithm. Just &ldquo;go to sleep forever.&rdquo;<br>One step up the ladder, things still don&rsquo;t look much like a select at all.<br>Case 2: The Single-Case Select<br>The next step up is a select with exactly one case and no default. If you think about it, this is just a slow way of writing a normal channel operation β€” there&rsquo;s no choice to make, no fairness question, no parking on multiple things. The compiler sees right through it and strips the select away entirely<br>, leaving you with the plain channel op:

The same idea applies to a single send case (ch becomes a normal send) and to a single default case (which becomes&mldr; nothing β€” just the body). The compiler is essentially saying: &ldquo;you didn&rsquo;t really need a select here, so I&rsquo;m going to pretend you didn&rsquo;t write one.&rdquo;<br>Once we add a default into the mix, though, the rewrite stops being a pure no-op and starts to do something more useful.<br>Case 3: The &ldquo;One Case + Default&rdquo; Select<br>The pattern here is &ldquo;try to send (or receive), and give up if it would block&rdquo; β€” and it&rsquo;s so common that the compiler has a specialized rewrite<br>for it. Instead of building the full select machinery, it turns it into a plain if/else around a dedicated non-blocking runtime helper:

selectnbsend<br>lives in runtime/chan.go and is a one-liner:<br>func selectnbsend(c *hchan, elem unsafe.Pointer) bool {<br>return chansend(c, elem, false, sys.GetCallerPC())

It&rsquo;s just a regular channel send with an argument that says &ldquo;do not block&rdquo; (the third argument to chansend, set to false here). If the send...

rsquo select compiler case ldquo rdquo

Related Articles