CINXE.COM
Go is not an easy language
<!DOCTYPE html> <html lang="en"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Go is not an easy language</title> <link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAMFBMVEUAAAAdISMbIy4jJSMvMjI9P0BLS0RLTUpLUFVWW2FdXFptb26VlJKwoJXCuazn18w7jQUuAAAAP0lEQVQI12NgwAWYYfQMKMMyAEJLTAQSHDMPce0UADJau7/u3wgSCNB+dRYswKC6LBGkroFB2VgAokWQgVgAAK99DG7/4O8zAAAAAElFTkSuQmCC"> <link rel="alternate" type="application/rss+xml" title="arp242.net" href="/feed.xml"> <link rel="canonical" href="https://www.arp242.net/go-easy.html"> <meta name="author" content="Martin Tournoij"> <style> @font-face { font-family: 'Libre Baskerville'; font-display: fallback; src: local('LibreBaskerville-Bold'), url(/fonts/libre-baskerville-bold.woff2) format('woff2'); } html { text-size-adjust: none; -webkit-text-size-adjust: none; font: 16px/180% sans-serif; } @media (max-width: 54em) { html { font-size: 14px; line-height: 160%; } } @media (max-width: 26em) { html { font-size: 14px; line-height: 150%; } } pre, code { font-family: 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Consolas', monospace; } pre { font-size: 14px; line-height: 130%; } table { border-collapse: collapse; width: 100%; } table + table { margin-top: 2em; } @media (min-width: 54em) { table.full { margin-left: -3.5rem; width: calc(100% + 7rem); } } caption { font-weight: bold; } tr { border-bottom: 1px solid #ddd; } table > tr:last-child, tbody tr:last-child, tfoot tr:last-child { border-bottom: none; } tfoot tr:first-child { border-top: 1px solid #ddd; } td, th { padding: .2em .5em; hyphens: none; } td.left, th.left { text-align: left; } td.right, th.right { text-align: right; } table td.right { font-feature-settings: 'tnum' on; font-variant-numeric: tabular-nums; font-family: sans-serif; } * { box-sizing: border-box; } *:target { background-color: #ff6; } html { background-color: #eee; color: #252525; tab-size: 4; -moz-tab-size: 4; } html, body { margin: 0; } .center, .page { max-width: 54rem; position: relative; } .center { margin: .5rem auto; } .center-flex { display: flex; justify-content: center; margin: .5rem 0; } @media (max-width: 46em) { .center-flex { display: block; } } .page { padding: 1rem 4rem; background-color: #fff; box-shadow: 0 0 6px rgba(0,0,0,.2); } @media (max-width: 65em) { .page, .center { padding: .5rem 1rem; max-width: 100%; } } @media (max-width: 54em) { .page, .center { max-width: 100%; padding: .5rem 1rem; } } @page { margin: 0; } @media print { body { background-color: #fff; } .page, .center { box-shadow: none; padding: 0 1cm; margin: 0; max-width: none; } .page:first-child, .center:first-child { padding-top: 5mm; } } .page-a4, .page.page-a4 { width: 21cm; height: 29.7cm; } a, a code { color: #00f; text-decoration: none; transition: color .2s; } a:hover { text-decoration: underline; color: #6491ff; } a:hover code { color: #6491ff; } h1, h2, h3, h4, h5, h6 { font: 16px/180% 'Libre Baskerville', 'DejaVu Serif', 'Bitstream Vera Serif', 'Georgia', serif; font-variant-ligatures: common-ligatures discretionary-ligatures; font-feature-settings: 'liga' on, 'dlig' on; } h1 { text-align: center; padding: .5em 0; font-size: 1.7em; } h2 { border-bottom: 1px solid #252525; padding-bottom: .2em; font-size: 1.5em; } h3 { font-size: 1.3em; } h4 { font-size: 1.1em; } h5, h6 { font-size: 1em; } blockquote { font-style: italic; } img { max-width: 100%; } figure { text-align: center; margin: 1rem 0; } figure.border { border: 1px solid #bbb; padding: 5px; margin: -5px; } code, pre { background-color: #f5f5f5; color: #000; } pre { overflow: auto; max-height: 500px; padding: .5em 1em; border-radius: 2px; background-color: #fff; border: 2px solid #d5d5d5; box-shadow: inset 0px 0px 2px rgba(0, 0, 0, .2); } code { padding: 1px 2px; } pre > code { padding: 0; box-shadow: none; border: none; } pre code { background-color: inherit; text-decoration: none; } @media (min-width: 54em) { pre.full { margin-left: -4rem; width: calc(100% + 8rem); border-left: 0; border-right: 0; border-radius: 0; } } sup, sub { height: 0; line-height: 1; vertical-align: baseline; position: relative; } sup { bottom: 1ex; } sub { top: .5ex; } hr { border: none; text-align: center; font-size: 60px; color: #252525; opacity: .9; } hr:before { content: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMC4yIDkuMiIgaGVpZ2h0PSIzNC44IiB3aWR0aD0iMTE0LjMiPjxwYXRoIGQ9Ik0xMy4yIDBxMS4xIDAgMS4zIDEuMiAwIDEtLjcgMS4yLTEuOC40LTIgLjggMi42LjMgMy4yIDMuMWguMmwuMi44LS40LjFxLS4yIDEuOC0xLjggMS45LTEgMC0xLjMtMSAwLS44LjItLjguMi40LjguNC44IDAgMS0uOFExMSA2LjEgMTEgMy43cS0xLjQgMC0yLjEgMSAxLjQuNyAxLjQgMS44LS4xIDIuNS0zLjYgMi43US41IDkuMiAwIDUuNXEwLS42LjctLjcuNSAwIC42LjV2LjJsLS4xLjRxMCAxIDEuMSAxIDEgMCAxLS43UTQgMS4yIDcgMXEyIC4xIDIgMS43di4xcTAgLjQtLjQgMS41IDEtLjggMi0xIC42IDAgLjYtLjQuOC0xIDEuNi0xLjMuNi0uNC42LS44bC0uMi0uNHEtLjItLjMgMC0uNHptLTEuNiAzLjd2LjFxMCAxLjQgMi4zIDIuMi0uMy0yLTIuMy0yLjN6Ii8+PHBhdGggZD0iTTE3IDBxLTEgMC0xLjMgMS4yIDAgMSAuOCAxLjIgMS44LjQgMiAuOC0yLjcuMy0zLjIgMy4xSDE1bC0uMi44LjUuMXEuMiAxLjggMS44IDEuOSAxIDAgMS4yLTEgMC0uOC0uMi0uOC0uMS40LS43LjQtLjkgMC0xLS44IDIuNy0uOCAyLjgtMy4yIDEuNCAwIDIgMS0xLjMuNy0xLjMgMS44IDAgMi41IDMuNSAyLjcgNi40IDAgNi44LTMuNyAwLS42LS42LS43LS42IDAtLjcuNXYuMmwuMi40cTAgMS0xLjIgMS0uOSAwLTEtLjctLjYtNS0zLjctNS4yLTIgLjEtMiAxLjd2LjFxMCAuNC40IDEuNS0xLS44LTItMS0uNiAwLS42LS40LS43LTEtMS41LTEuMy0uNi0uNC0uNi0uOGwuMi0uNHEuMi0uMyAwLS40em0xLjcgMy43di4xcTAgMS40LTIuNCAyLjIuMy0yIDIuNC0yLjN6Ii8+PC9zdmc+); } blockquote { font-style: italic; position: relative; } blockquote:before { position: absolute; color: #dbdbdb; font-size: 5em; content: "“"; left: -.6em; top: .25em; } h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { font-size: .7em; line-height: 0; color: #999; } h1 a:after, h2 a:after, h3 a:after, h4 a:after, h5 a:after, h6 a:after { content: "¶"; } .postscript { margin-top: 3em; border-top: 1px solid #bbb; font-size: .9em; position: relative; } .postscript + .postscript { margin-top: 2em; } .postscript > strong { font-style: normal; position: absolute; top: -1em; left: 1em; background-color: #fff; padding: 0 .5em; } .postscript > p { margin: 1em 2em !important; } @media (min-width: 54rem) { .postscript { margin-left: -4em; width: calc(100% + 4em); padding: 0 4em; } .postscript > strong { left: 6em; } } #header { text-align:center; margin-bottom:2em; } /* Post title, date */ #header h1, #header p { margin:0; } .hatnote { font-style:italic; margin-bottom:14px; } /* Hatnote */ nav a { font-weight:bold; } #disclaimer { font-size:14px; color:#666; padding-left:1em; } ul.posts, ul.code { list-style:none; padding-left:0; } /* HOME PAGE */ #home ul li a:first-child { display:inline-block; min-width:11em; vertical-align:top; } .posts li > span { font-family:monospace; color:#757575; margin-right:.5em; text-align:right; } /* date */ @media (max-width:46rem) { #home ul li { margin-bottom:0.5rem; } .posts li { padding-left:7.3em; text-indent:-7.3em; } #home ul li a:first-child { min-width:auto; } } a[href^="/draft"] { color:red; } #home h1, #home h2 { text-align:left; padding:0; border-bottom:none; } #home h1 a { color:#00f; font-size:1em; } #home h1 a:after { content:""; } #home h1 span, h1 sup { font-size:14px; font-weight:normal; } #home h2 { font-size:1.1em; margin-bottom:.2em; } #home h2 + ul { margin-top:0; padding-top:0; } .page + .page { margin-top:2em; } .related-posts .posts { margin-top:0; } /* Prevent long <pre> from breaking layout in between mobile and desktop. */ @media (max-width:68rem) { pre { white-space:pre-wrap; } } @media (max-width:46em) { pre { white-space:pre; } } /* SYNTAX */ .pre-wrap { position:relative; } .pre-copy { position:absolute; top:2px; padding:0em .6em; background-color:#f7f7f7; border-bottom:1px solid #d5d5d5; border-left:1px solid #d5d5d5; border-radius:2px; font:14px/26px sans-serif; color:#000; display:none; box-shadow:-1px 1px 6px rgba(0, 0, 0, .2); } .pre-copy:hover { color:inherit; text-decoration:none; background-color:#f9f9f9; } .pre-wrap:hover .pre-copy { display:block; } .Statement { color:#af5f00; } .Comment { color:#0000ee; } .Constant { color:#cd0000; } .Type { color:#009d00; } .Special { color:#cd00cd; } .PreProc { color:#cd00cd; } .Identifier { color:#008787; } .output { color:#666666; } .ModeMsg { color:#666666; } .Question { color:#000; } .page aside { background-color:#fffde8; position:relative; } /* INLINE ASIDE */ .page aside strong { font-size:2em; float:left; margin-right:.2em; } @media (min-width:65em) { .page aside strong { position:absolute; top:1.1em; left:.5em; top:.55em; } .page aside { padding:.1em 4em; margin-left:-4rem; width:calc(100% + 8rem); } } @media (max-width:54em) { .page aside { padding:.1em 1em; } .page p { margin-top:0; } } .page aside.warn strong:first-child { color:red; } .page aside.note strong:first-child { color:blue; } .page aside + aside { margin-top:1em; } /* Top nav */ .top-wrap { width:100%; overflow-x:clip; } /* Hack to make sure we don't get a horizontal scroll bar */ .top { max-width:54rem; position:relative; margin:.5rem auto; padding:.5rem 1rem; padding-bottom:1.5rem; } .top p { margin:0; } .top >* { z-index:2; } nav { display:inline; position:absolute; top:0rem; left:0; } #top1 { position:relative; top:.2rem; left:-1.5rem; max-width:30rem; transform:rotate(-5deg); } #top2 { position:absolute; top:-.4em; right:5em; max-width:18rem; transform:rotate(1deg); } #top3 { position:absolute; top:3.5em; right:5em; max-width:24rem; transform:rotate(-2deg); } #mugshot { position:absolute; right:-2rem; top:-.5rem; width:100px; transform:rotate(14deg); margin:0; padding:0; line-height:0; } #mugshot img { margin:0; padding:0; } .shadow { box-shadow:inset 0 0 10px rgba(0,0,0,.1); position:absolute; top:0; left:0; right:0; bottom:0; } @media (max-width:57em) { #top1 { left:.5rem; } #top3 { right:6rem; } } @media (max-width:48em) { .top { padding-bottom:2.5rem; hyphens:auto; } #top1 { top:.6rem; max-width:50%; } #top2 { top:.3rem; max-width:35%; } #top3 { top:5rem; max-width:60%; } } @media (max-width:38em) { .top { padding-bottom:3rem; font-size:.9rem; } #top2 { right:0rem; max-width:45%; z-index:4; } #top3 { top:4rem; right:4rem; max-width:35%; transform:rotate(-1.5deg); } #mugshot { width:80px; top:2rem; opacity:.7; } } @media (max-width:28em) { #top3 { top:6rem; right:1rem; max-width:50%; } } /* Main page */ article .page { clip-path:polygon(3rem 0px, 35% 1rem, 40% 0px, 90% 0.8rem, 100% 0px, calc(100% - 0.5rem) 10rem, calc(100% - 1rem) 20rem, 100% 100%, 20% calc(100% - 1rem), 0px 100%, 0.4rem calc(100% - 10rem), 0px 4.25rem, 0.31rem 1.25rem); box-shadow:none; } @media (max-width:65rem) { article .page { clip-path:polygon(3rem 0px, 35% 1rem, 40% 0px, 90% 0.8rem, 100% 0px, 100% 100%, 20% calc(100% - 1rem), 0px 100%, 0.4rem calc(100% - 10rem), 0px 4.25rem, 0.31rem 1.25rem); } } </style> </head> <body> <div class="top-wrap"><div class="top"> <nav><a href="/">← Home</a></nav> <p id="top1">Personal website of <strong>Martin Tournoij</strong> (“<strong>arp242</strong>”); writing about programming (<a href="/cv/">CV</a>) and various other things.</p> <p id="top2">Working on <a href="https://www.goatcounter.com" data-goatcounter-click="click-gc">GoatCounter</a> and <a href="/#code">more</a> – <a href="https://github.com/sponsors/arp242" target="_blank" data-goatcounter-click="click-gh-sponsor">GitHub Sponsors</a>. </p> <p id="top3">Contact at <a rel="me" href="mailto:martin@arp242.net">martin@arp242.net</a> or <a href="https://github.com/arp242/arp242.net/issues/new">GitHub</a>.</span> </p> <figure id="mugshot"><img alt="This page's author" src="data:image/webp;base64,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"><div class="shadow"></div></figure> </div></div> <div class="center-flex"> <article> <div class="page"> <header id="header"> <h1>Go is not an easy language</h1> <p>Written on 22 Feb 2021</p> </header> <div class="hatnote"><p>Discussions: <a href="https://lobste.rs/s/ee6nsc/go_is_not_easy_language">Lobsters</a>, <a href="https://news.ycombinator.com/item?id=26220693">Hacker News</a>, <a href="https://www.reddit.com/r/golang/comments/lpeafy/go_is_not_an_easy_language/">/r/golang</a>, <a href="https://www.reddit.com/r/golang/comments/lpo6zh/go_is_not_an_easy_language/">/r/programming</a></p> </div> <p>Go is not an easy programming language. It <em>is</em> simple in many ways: the syntax is simple, most of the semantics are simple. But a language is more than just syntax; it’s about doing useful <em>stuff</em>. And doing useful stuff is not always easy in Go.</p> <p>Turns out that combining all those simple features in a way to do something useful can be tricky. How do you remove an item from an array in Ruby? <code>list.delete_at(i)</code>. And remove entries by value? <code>list.delete(value)</code>. Pretty easy, yeah?</p> <p>In Go it’s … less easy; to remove the index <code>i</code> you need to do:</p> <div class="pre-wrap"> <pre class='hl ft-go'><code>list = <span class="Statement">append</span>(list[:i], list[i+<span class="Constant">1</span>:]...) </code></pre> </div> <p>And to remove the value <code>v</code> you’ll need to use a loop:</p> <div class="pre-wrap"> <pre class='hl ft-go'><code>n := <span class="Constant">0</span> <span class="Statement">for</span> _, l := <span class="Statement">range</span> list { <span class="Statement">if</span> l != v { list[n] = l n++ } } list = list[:n] </code></pre> </div> <p>Is this unacceptably hard? Not really; I think most programmers can figure out what the above does even without prior Go experience. But it’s not exactly <em>easy</em> either. I’m usually lazy and copy these kind of things from the <a href="https://github.com/golang/go/wiki/SliceTricks">Slice Tricks</a> page because I want to focus on actually solving the problem at hand, rather than plumbing like this.</p> <p>It’s also easy to get it (subtly) wrong or suboptimal, especially for less experienced programmers. For example compare the above to copying to a new array and copying to a new pre-allocated array (<code>make([]string, 0, len(list))</code>):</p> <div class="pre-wrap"> <pre class="ft-NONE"><code>InPlace 116 ns/op 0 B/op 0 allocs/op NewArrayPreAlloc 525 ns/op 896 B/op 1 allocs/op NewArray 1529 ns/op 2040 B/op 8 allocs/op </code></pre> </div> <p>While 1529ns is still plenty fast enough for many use cases and isn’t something to excessively worry about, there are plenty of cases where these things <em>do</em> matter and having the guarantee to always use the best possible algorithm with <code>list.delete(value)</code> has some value.</p> <hr /> <p>Goroutines are another good example. “Look how easy it is to start a goroutine! Just add <code>go</code> and you’re done!” Well, yes; you’re done until you have five million of those running at the same time and then you’re left wondering where all your memory went, and it’s not hard to “leak” goroutines by accident either.</p> <p>There are a number of patterns to limit the number of goroutines, and none of them are exactly easy. A simple example might be something like:</p> <div class="pre-wrap"> <pre class='hl ft-go'><code><span class="Statement">var</span> ( jobs = <span class="Constant">20</span> <span class="Comment">// Run 20 jobs in total.</span> running = <span class="Statement">make</span>(<span class="Type">chan</span> <span class="Type">bool</span>, <span class="Constant">3</span>) <span class="Comment">// Limit concurrent jobs to 3.</span> wg sync.WaitGroup <span class="Comment">// Keep track of which jobs are finished.</span> ) wg.Add(jobs) <span class="Statement">for</span> i := <span class="Constant">1</span>; i <= jobs; i++ { running <- <span class="Constant">true</span> <span class="Comment">// Fill running; this will block and wait if it's already full.</span> <span class="Comment">// Start a job.</span> <span class="Statement">go</span> <span class="Statement">func</span>(i <span class="Type">int</span>) { <span class="Statement">defer</span> <span class="Statement">func</span>() { <-running <span class="Comment">// Drain running so new jobs can be added.</span> wg.Done() <span class="Comment">// Signal that this job is done.</span> }() <span class="Comment">// "do work"</span> time.Sleep(<span class="Constant">1</span> * time.Second) fmt.Println(i) }(i) } wg.Wait() <span class="Comment">// Wait until all jobs are done.</span> fmt.Println(<span class="Constant">"done"</span>) </code></pre> </div> <p>There’s a reason I annotated this with some comments: for people not intimately familiar with Go this may take some effort to understand. This also won’t ensure that the numbers are printed in order (which may or may not be a requirement).</p> <p>Go’s concurrency primitives may be simple and easy to use, but combining them to solve common real-world scenarios is a lot less simple. The original version of the above example <a href="https://lobste.rs/s/ee6nsc/go_is_not_easy_language#c_gdnw5e">was actually incorrect</a> 😅</p> <hr /> <p>In <a href="https://www.infoq.com/presentations/Simple-Made-Easy/">Simple Made Easy</a> Rich Hickey argues that we shouldn’t confuse “simple” with “it’s easy to write”: just because you can do something useful in one or two lines doesn’t mean the underlying concepts – and therefore the entire program – are “simple” as in “simple to understand”.</p> <p>I feel there is some wisdom in this; in most cases we shouldn’t sacrifice “simple” for “easy”, but that doesn’t mean we can’t think at all about how to make things easier. Just because concepts are simple doesn’t mean they’re easy to use, can’t be misused, or can’t be used in ways that lead to (subtle) bugs. Pushing Hickey’s argument to the extreme we’d end up with something like <a href="https://en.wikipedia.org/wiki/Brainfuck">Brainfuck</a> and that would of course be silly.</p> <p>Ideally a language should reduce the cognitive load required to reason about its behaviour; there are many ways to increase this cognitive load: complex intertwined language features is one of them, and getting “distracted” by implementing fairly basic things from those simple concepts is another: it’s another block of code I need to reason about. While I’m not overly concerned about code formatting or syntax choices, I do think it can matter to reduce this cognitive load when reading code.</p> <p>The lack of generics probably plays some part here; implementing a <code>slices</code> package which does these kind of things in a generic way is hard right now. Generics make this possible and also makes things more complex (more language features are used), but they also make things easier and, arguably, less complex on other fronts.<sup id="fnref:g" role="doc-noteref"><a href="#fn:g" class="footnote" rel="footnote">[1]</a></sup></p> <hr /> <p>Are these insurmountable problems? No. I still use (and like) Go after all. But I also don’t think that Go is a language that you “could pick up in ~5-10 minutes”, which was the comment that prompted this post; a sentiment I’ve seen expressed many times, although usually with less extreme timeframes (“1-2 days”, “1 week”).</p> <p>As a corollary to all of the above; learning the language isn’t just about learning the syntax to write your <code>if</code>s and <code>for</code>s; it’s about learning a way of thinking. I’ve seen many people coming from Python or C♯ try to shoehorn concepts or patterns from those languages in Go. Common ones include using struct embedding as inheritance, panics as exceptions, “pseudo-dynamic programming” with interface{}, and so forth. It rarely ends well, if ever.</p> <p>I did this as well when I was writing my first Go program; it’s only natural. And when I started as a Ruby programmer I tried to write Python code in Ruby (although this works a bit better as the languages are more similar, but there are still plenty of odd things you can do such as using <code>for</code> loops).</p> <p>This is why I don’t like it when people get redirected to the Tour of Go to “learn the language”, as it just teaches basic syntax and little more. It’s nice as a little, well, <em>tour</em> to get a bit of a feel of the language and see how it roughly works and what it can roughly do, but it’s ill-suited to actually learn the language.</p> <div class="postscript" role="doc-endnotes"><strong>Footnotes</strong> <ol> <li id="fn:g"> <p>Contrary to popular belief the <a href="https://research.swtch.com/generic">Go team was never “against” generics</a>; I’ve seen many comments to the effect of “the Go team doesn’t think generics are useful”, but this was never the case. <a href="#fnref:g" class="reversefootnote" role="doc-backlink">↩</a></p> </li> </ol> </div> <footer class="postscript"> <strong>Feedback</strong> <p>Contact me at <a href="mailto:martin@arp242.net">martin@arp242.net</a> or <a href="https://github.com/arp242/arp242.net/issues/new">GitHub</a> for feedback, questions, etc.</p> </footer> </div> <div class="page related-posts"><strong>Other Go posts</strong><ul class='posts'><li><span>11 Apr 2020</span> <a href='/static-go.html'>Statically compiling Go programs</a></li> <li><span>10 Dec 2020</span> <a href='/bitmask.html'>Bitmasks for nicer APIs</a></li> <li><span> 1 May 2019</span> <a href='/flags-config-go.html'>Using flags for configuration in Go</a></li> <li><span>28 Oct 2024</span> <a href='/jia-tan-go.html'>Jia Tanning Go code</a></li> <li><span>10 Apr 2018</span> <a href='/go-testing-style.html'>Go testing style guide</a></li> <li><span> 5 Feb 2020</span> <a href='/wasm-cli.html'>Running Go CLI programs in the browser with WASM</a></li> <li><span>21 Nov 2019</span> <a href='/go-last-resort.html'>Go’s features of last resort</a></li></ul><strong>Other Programming posts</strong><ul class='posts'><li><span>25 Apr 2020</span> <a href='/dot-git.html'>Storing files in .git</a></li> <li><span>22 Mar 2019</span> <a href='/easy.html'>Easy means easy to debug</a></li> <li><span> 7 Feb 2016</span> <a href='/json-config.html'>The downsides of JSON for config files</a></li> <li><span>25 Nov 2020</span> <a href='/api-ux.html'>An API is a user interface</a></li> <li><span>16 Nov 2024</span> <a href='/best-practices.html'>Against best practices</a></li> <li><span> 5 Dec 2019</span> <a href='/comments.html'>Good comments read well and are to the point</a></li> <li><span> 4 Sep 2016</span> <a href='/yaml-config.html'>YAML: probably not so great after all</a></li> <li><span>10 Dec 2020</span> <a href='/bitmask.html'>Bitmasks for nicer APIs</a></li> <li><span> 7 Jan 2019</span> <a href='/testing.html'>Testing isn’t everything</a></li> <li><span>29 Nov 2020</span> <a href='/stupid-light.html'>Stupid light software</a></li> <li><span> 7 Oct 2019</span> <a href='/right-size.html'>On being the right size</a></li></ul></div> </article> </div> <script> // TODO: probably want to do this in Jekyll at some point; for now this is easier. (function() { var q = function(s) { return Array.prototype.slice.call(document.querySelectorAll(s)) } var go = function() { // Add title attribute to the footnotes. q('.footnote').forEach(function(fn) { fn.addEventListener('mouseenter', function(e) { if (e.target.title !== '') return e.target.title = document.getElementById(e.target.href.split('#')[1]). innerText.replace(/↩$/, '').trim() }, false) }) // Add "Copy" link to pre tags with vertical or horizontal scrollbars. q('pre').forEach(function(pre) { if (pre.clientHeight > pre.scrollHeight - 6 && pre.clientWidth > pre.scrollWidth - 6) return var btn = document.createElement('a') btn.href = '#' btn.className = 'pre-copy' btn.style.right = (pre.offsetWidth - pre.clientWidth - 2) + 'px' btn.innerHTML = '📋 Copy' btn.addEventListener('click', function(e) { e.preventDefault() var t = document.createElement('textarea') t.value = pre.innerText t.style.position = 'absolute' document.body.appendChild(t) t.select() t.setSelectionRange(0, pre.innerText.length) document.execCommand('copy') document.body.removeChild(t) btn.innerHTML = '📋 Done' setTimeout(function() { btn.innerHTML = '📋 Copy' }, 1000) }, false) pre.parentNode.appendChild(btn) }) } if (document.readyState === 'complete') go() else window.addEventListener('load', go, false) })() </script> <footer id="disclaimer" class="center-flex"><p>This document is licensed under a <a rel="license" href="http://creativecommons.org/licenses/by/4.0/">cc-by 4.0 license</a>. </p></footer> <script data-goatcounter="https://stats.arp242.net/count"> // GoatCounter: https://www.goatcounter.com // This file is released under the ISC license: https://opensource.org/licenses/ISC ;(function() { 'use strict'; if (window.goatcounter && window.goatcounter.vars) // Compatibility with very old version; do not use. window.goatcounter = window.goatcounter.vars else window.goatcounter = window.goatcounter || {} // Load settings from data-goatcounter-settings. var s = document.querySelector('script[data-goatcounter]') if (s && s.dataset.goatcounterSettings) { try { var set = JSON.parse(s.dataset.goatcounterSettings) } catch (err) { console.error('invalid JSON in data-goatcounter-settings: ' + err) } for (var k in set) if (['no_onload', 'no_events', 'allow_local', 'allow_frame', 'path', 'title', 'referrer', 'event'].indexOf(k) > -1) window.goatcounter[k] = set[k] } var enc = encodeURIComponent // Get all data we're going to send off to the counter endpoint. window.goatcounter.get_data = function(vars) { vars = vars || {} var data = { p: (vars.path === undefined ? goatcounter.path : vars.path), r: (vars.referrer === undefined ? goatcounter.referrer : vars.referrer), t: (vars.title === undefined ? goatcounter.title : vars.title), e: !!(vars.event || goatcounter.event), s: [window.screen.width, window.screen.height, (window.devicePixelRatio || 1)], b: is_bot(), q: location.search, } var rcb, pcb, tcb // Save callbacks to apply later. if (typeof(data.r) === 'function') rcb = data.r if (typeof(data.t) === 'function') tcb = data.t if (typeof(data.p) === 'function') pcb = data.p if (is_empty(data.r)) data.r = document.referrer if (is_empty(data.t)) data.t = document.title if (is_empty(data.p)) data.p = get_path() if (rcb) data.r = rcb(data.r) if (tcb) data.t = tcb(data.t) if (pcb) data.p = pcb(data.p) return data } // Check if a value is "empty" for the purpose of get_data(). var is_empty = function(v) { return v === null || v === undefined || typeof(v) === 'function' } // See if this looks like a bot; there is some additional filtering on the // backend, but these properties can't be fetched from there. var is_bot = function() { // Headless browsers are probably a bot. var w = window, d = document if (w.callPhantom || w._phantom || w.phantom) return 150 if (w.__nightmare) return 151 if (d.__selenium_unwrapped || d.__webdriver_evaluate || d.__driver_evaluate) return 152 if (navigator.webdriver) return 153 return 0 } // Object to urlencoded string, starting with a ?. var urlencode = function(obj) { var p = [] for (var k in obj) if (obj[k] !== '' && obj[k] !== null && obj[k] !== undefined && obj[k] !== false) p.push(enc(k) + '=' + enc(obj[k])) return '?' + p.join('&') } // Show a warning in the console. var warn = function(msg) { if (console && 'warn' in console) console.warn('goatcounter: ' + msg) } // Get the endpoint to send requests to. var get_endpoint = function() { var s = document.querySelector('script[data-goatcounter]') if (s && s.dataset.goatcounter) return s.dataset.goatcounter return (goatcounter.endpoint || window.counter) // counter is for compat; don't use. } // Get current path. var get_path = function() { var loc = location, c = document.querySelector('link[rel="canonical"][href]') if (c) { // May be relative or point to different domain. var a = document.createElement('a') a.href = c.href if (a.hostname.replace(/^www\./, '') === location.hostname.replace(/^www\./, '')) loc = a } return (loc.pathname + loc.search) || '/' } // Run function after DOM is loaded. var on_load = function(f) { if (document.body === null) document.addEventListener('DOMContentLoaded', function() { f() }, false) else f() } // Filter some requests that we (probably) don't want to count. window.goatcounter.filter = function() { if ('visibilityState' in document && document.visibilityState === 'prerender') return 'visibilityState' if (!goatcounter.allow_frame && location !== parent.location) return 'frame' if (!goatcounter.allow_local && location.hostname.match(/(localhost$|^127\.|^10\.|^172\.(1[6-9]|2[0-9]|3[0-1])\.|^192\.168\.|^0\.0\.0\.0$)/)) return 'localhost' if (!goatcounter.allow_local && location.protocol === 'file:') return 'localfile' if (localStorage && localStorage.getItem('skipgc') === 't') return 'disabled with #toggle-goatcounter' return false } // Get URL to send to GoatCounter. window.goatcounter.url = function(vars) { var data = window.goatcounter.get_data(vars || {}) if (data.p === null) // null from user callback. return data.rnd = Math.random().toString(36).substr(2, 5) // Browsers don't always listen to Cache-Control. var endpoint = get_endpoint() if (!endpoint) return warn('no endpoint found') return endpoint + urlencode(data) } // Count a hit. window.goatcounter.count = function(vars) { var f = goatcounter.filter() if (f) return warn('not counting because of: ' + f) var url = goatcounter.url(vars) if (!url) return warn('not counting because path callback returned null') if (!navigator.sendBeacon(url)) { // This mostly fails due to being blocked by CSP; try again with an // image-based fallback. var img = document.createElement('img') img.src = url img.style.position = 'absolute' // Affect layout less. img.style.bottom = '0px' img.style.width = '1px' img.style.height = '1px' img.loading = 'eager' img.setAttribute('alt', '') img.setAttribute('aria-hidden', 'true') var rm = function() { if (img && img.parentNode) img.parentNode.removeChild(img) } img.addEventListener('load', rm, false) document.body.appendChild(img) } } // Get a query parameter. window.goatcounter.get_query = function(name) { var s = location.search.substr(1).split('&') for (var i = 0; i < s.length; i++) if (s[i].toLowerCase().indexOf(name.toLowerCase() + '=') === 0) return s[i].substr(name.length + 1) } // Track click events. window.goatcounter.bind_events = function() { if (!document.querySelectorAll) // Just in case someone uses an ancient browser. return var send = function(elem) { return function() { goatcounter.count({ event: true, path: (elem.dataset.goatcounterClick || elem.name || elem.id || ''), title: (elem.dataset.goatcounterTitle || elem.title || (elem.innerHTML || '').substr(0, 200) || ''), referrer: (elem.dataset.goatcounterReferrer || elem.dataset.goatcounterReferral || ''), }) } } Array.prototype.slice.call(document.querySelectorAll("*[data-goatcounter-click]")).forEach(function(elem) { if (elem.dataset.goatcounterBound) return var f = send(elem) elem.addEventListener('click', f, false) elem.addEventListener('auxclick', f, false) // Middle click. elem.dataset.goatcounterBound = 'true' }) } // Add a "visitor counter" frame or image. window.goatcounter.visit_count = function(opt) { on_load(function() { opt = opt || {} opt.type = opt.type || 'html' opt.append = opt.append || 'body' opt.path = opt.path || get_path() opt.attr = opt.attr || {width: '200', height: (opt.no_branding ? '60' : '80')} opt.attr['src'] = get_endpoint() + 'er/' + enc(opt.path) + '.' + enc(opt.type) + '?' if (opt.no_branding) opt.attr['src'] += '&no_branding=1' if (opt.style) opt.attr['src'] += '&style=' + enc(opt.style) if (opt.start) opt.attr['src'] += '&start=' + enc(opt.start) if (opt.end) opt.attr['src'] += '&end=' + enc(opt.end) var tag = {png: 'img', svg: 'img', html: 'iframe'}[opt.type] if (!tag) return warn('visit_count: unknown type: ' + opt.type) if (opt.type === 'html') { opt.attr['frameborder'] = '0' opt.attr['scrolling'] = 'no' } var d = document.createElement(tag) for (var k in opt.attr) d.setAttribute(k, opt.attr[k]) var p = document.querySelector(opt.append) if (!p) return warn('visit_count: append not found: ' + opt.append) p.appendChild(d) }) } // Make it easy to skip your own views. if (location.hash === '#toggle-goatcounter') { if (localStorage.getItem('skipgc') === 't') { localStorage.removeItem('skipgc', 't') alert('GoatCounter tracking is now ENABLED in this browser.') } else { localStorage.setItem('skipgc', 't') alert('GoatCounter tracking is now DISABLED in this browser until ' + location + ' is loaded again.') } } if (!goatcounter.no_onload) on_load(function() { // 1. Page is visible, count request. // 2. Page is not yet visible; wait until it switches to 'visible' and count. // See #487 if (!('visibilityState' in document) || document.visibilityState === 'visible') goatcounter.count() else { var f = function(e) { if (document.visibilityState !== 'visible') return document.removeEventListener('visibilitychange', f) goatcounter.count() } document.addEventListener('visibilitychange', f) } if (!goatcounter.no_events) goatcounter.bind_events() }) })(); </script> </body> </html>