<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Posts on Very Good Software, Not Virus</title><link>https://blog.verygoodsoftwarenotvirus.ru/posts/</link><description>Recent content in Posts on Very Good Software, Not Virus</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Tue, 08 Apr 2025 11:04:56 -0500</lastBuildDate><atom:link href="https://blog.verygoodsoftwarenotvirus.ru/posts/index.xml" rel="self" type="application/rss+xml"/><item><title>AI as a means of evading accountability</title><link>https://blog.verygoodsoftwarenotvirus.ru/posts/ai-accountability/</link><pubDate>Tue, 08 Apr 2025 11:04:56 -0500</pubDate><guid>https://blog.verygoodsoftwarenotvirus.ru/posts/ai-accountability/</guid><description>There’s no doubt in my mind that the unifying characteristic of modern leadership is evasion of accountability and dereliction of responsibility. We’ve seen it in politics for probably as long as the sport has existed, but the recent Signal debacle was really a shining example of this trend. If I, as an employee of a private S-corp, discussed anything nearly as sensitive or detailed about my company’s inner dealings on a Signal group that had a lurking journalist, I’d be fired and at best have to take some lesser position for the rest of my career, but if you’re willing to fellate a dementia-addled billionaire, you will face zero consequences.</description><content>&lt;p>There’s no doubt in my mind that the unifying characteristic of modern leadership is evasion of accountability and dereliction of responsibility. We’ve seen it in politics for probably as long as the sport has existed, but &lt;a href="https://en.wikipedia.org/wiki/United_States_government_group_chat_leak">the recent Signal debacle&lt;/a> was really a shining example of this trend. If I, as an employee of a private S-corp, discussed anything nearly as sensitive or detailed about my company’s inner dealings on a Signal group that had a lurking journalist, I’d be fired and at best have to take some lesser position for the rest of my career, but if you’re willing to fellate a dementia-addled billionaire, you will face zero consequences. Lack of accountability is hot right now, and it’s only getting hotter.&lt;/p>
&lt;p>Speaking of work, recently the company I work for hired a new CTO, whose introductory statement to the company included a commitment to “jump start our usage of AI” at the company. They set a humble goal of merely having AI write most of our tests as well as implement new features by the end of the year. Setting goals we won’t meet is old hat for the company, but its fetishization of AI is comparatively new, though not uncommon. AI has become the hot topic that every company must acknowledge, lest they be regarded as ignorant. Every CEO that might emphasize how their company is meeting the needs of its customers in order to have those customers delightfully return to you in the future is met with a “who gives a fuck, how are you shoehorning LLMs into every layer of your operation?”&lt;/p>
&lt;p>I find the whole thing laughable. The reason features are slow to develop at the company I work at is, simply put, not because of implementation speed. An LLM is going to have just as hard a time gettings accurate requirements out of an offshored product manager as a human being will.&lt;/p>
&lt;p>The lack of accountability trend is older, but AI allows it to propagate, and in a way enforces it. Let’s say my company did somehow manage to write a feature with AI, who is going to be in the Zoom call when it doesn’t work as expected? Who is going to be held accountable when it’s not delivered on time because the LLM didn’t account for edge case X or stipulation Y? Who will be in the postmortems when the service it builds goes down? Will the AI be the incident commander?&lt;/p>
&lt;p>I saw a post on BlueSky today about a new-to-me command of the docker CLI: &lt;code>docker ai&lt;/code>.&lt;/p>
&lt;blockquote class="bluesky-embed" data-bluesky-uri="at://did:plc:e5nncb3dr5thdkjir5cfaqfe/app.bsky.feed.post/3lmaccuzqps2u" data-bluesky-cid="bafyreihnlylxkh6xubqvydgknpuqrts6yxkomxozr4fij2maz4seiwkm3e" data-bluesky-embed-color-mode="dark">&lt;p lang="en">god save us from ourselves&lt;br>&lt;br>&lt;a href="https://bsky.app/profile/did:plc:e5nncb3dr5thdkjir5cfaqfe/post/3lmaccuzqps2u?ref_src=embed">[image or embed]&lt;/a>&lt;/p>&amp;mdash; Xe (&lt;a href="https://bsky.app/profile/did:plc:e5nncb3dr5thdkjir5cfaqfe?ref_src=embed">@xeiaso.net&lt;/a>) &lt;a href="https://bsky.app/profile/did:plc:e5nncb3dr5thdkjir5cfaqfe/post/3lmaccuzqps2u?ref_src=embed">April 7, 2025 at 10:41 AM&lt;/a>&lt;/blockquote>&lt;script async src="https://embed.bsky.app/static/embed.js" charset="utf-8">&lt;/script>
&lt;p>You can do things like &lt;code>docker ai run redis&lt;/code>, and it will run commands for you, without telling you the output:&lt;/p>
&lt;blockquote class="bluesky-embed" data-bluesky-uri="at://did:plc:e5nncb3dr5thdkjir5cfaqfe/app.bsky.feed.post/3lmcqpqs2zs2h" data-bluesky-cid="bafyreifde2viffm7szt65eq64fgrpf7aehwqggcbzzub3gtothpunp7knu" data-bluesky-embed-color-mode="dark">&lt;p lang="en">Tried it on my work machine and it was able to create a Redis deployment. The prompt I gave was &amp;quot;set the password to hunter2&amp;quot; and it did not. The more horrifying part is that when I asked it to clean up the deployment it asked if it could remove a deployment WITHOUT SHOWING THE ARGUMENTS TO THE TOOL&lt;br>&lt;br>&lt;a href="https://bsky.app/profile/did:plc:e5nncb3dr5thdkjir5cfaqfe/post/3lmcqpqs2zs2h?ref_src=embed">[image or embed]&lt;/a>&lt;/p>&amp;mdash; Xe (&lt;a href="https://bsky.app/profile/did:plc:e5nncb3dr5thdkjir5cfaqfe?ref_src=embed">@xeiaso.net&lt;/a>) &lt;a href="https://bsky.app/profile/did:plc:e5nncb3dr5thdkjir5cfaqfe/post/3lmcqpqs2zs2h?ref_src=embed">April 8, 2025 at 10:04 AM&lt;/a>&lt;/blockquote>&lt;script async src="https://embed.bsky.app/static/embed.js" charset="utf-8">&lt;/script>
&lt;p>Who is going to be fired when the company discovers straggler Kubernetes clusters running myriad abandoned services that don’t even serve traffic, but do consume compute? AI allows the C-suite to take credit for pace of innovation, and mandates that human beings pay the prices for their failings.&lt;/p>
&lt;p>For whatever it’s worth, I actually love writing code with the assistance of LLMs, and the “thinking” models fascinate me more than I care to admit. They’re really useful assistants (keyword), and they may even one day be capable of doing the things the aforementioned CTO insisted were possible today, in simpler cases, but every experience I’ve ever had professionally tells me that the vast majority of productivity failures at companies are simply not because developers don’t write code fast enough.&lt;/p>
&lt;p>And what of the Juniors? I got my start in this industry by learning how to code during every free bit of time I had, and then taking a relatively low-paying position at a startup doing precisely the kinds of tasks we’re delegating to soulless robots today. Are programmers destined to be the factory workers of tomorrow, in that we’ll be replaced by machines, and only a few very experienced workers will be necessary to fill in the gaps those machines leave behind?&lt;/p>
&lt;p>Johnny Boursiquot summarizes my thoughts on this succinctly:&lt;/p>
&lt;blockquote class="bluesky-embed" data-bluesky-uri="at://did:plc:6llarrzjnwvveibfai2n3lvp/app.bsky.feed.post/3ljqhayxffs23" data-bluesky-cid="bafyreidug7d3q5zazlklgdw743aed5widvok7233jcggn5sxmq3szyxqxq" data-bluesky-embed-color-mode="dark">&lt;p lang="en">If you&amp;#x27;re a Senior+ Software Engineer today, you&amp;#x27;re among the last of your kind.
&lt;p>Think about that for a minute.&lt;/p>— Johnny Boursiquot (&lt;a href="https://bsky.app/profile/did:plc:6llarrzjnwvveibfai2n3lvp?ref_src=embed">@jboursiquot.com&lt;/a>) &lt;a href="https://bsky.app/profile/did:plc:6llarrzjnwvveibfai2n3lvp/post/3ljqhayxffs23?ref_src=embed">March 6, 2025 at 3:36 PM&lt;/a>&lt;/blockquote>&lt;script async src="https://embed.bsky.app/static/embed.js" charset="utf-8">&lt;/script>&lt;/p></content></item><item><title>How I write tests in Go</title><link>https://blog.verygoodsoftwarenotvirus.ru/posts/testing-in-go/</link><pubDate>Wed, 03 Apr 2024 17:38:19 -0600</pubDate><guid>https://blog.verygoodsoftwarenotvirus.ru/posts/testing-in-go/</guid><description>How I write unit tests in Go One of my favorite features of Go is that unlike many popular languages, it comes with it’s own testing framework, the testing package. Let’s say we have this trivial function in a file called numbers.go:
package numbers func addNumbers(numbers ...int) int { sum := 0 for _, number := range numbers { sum += number } return sum } The idiomatic way to test this function is to create a second file called numbers_test.</description><content>&lt;h1 id="how-i-write-unit-tests-in-go">How I write unit tests in Go&lt;/h1>
&lt;p>One of my favorite features of Go is that unlike many popular languages, it comes with it’s own testing framework, the &lt;a href="https://pkg.go.dev/testing">testing&lt;/a> package. Let’s say we have this trivial function in a file called &lt;code>numbers.go&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-go" data-lang="go">&lt;span style="color:#f92672">package&lt;/span> &lt;span style="color:#a6e22e">numbers&lt;/span>
&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">addNumbers&lt;/span>(&lt;span style="color:#a6e22e">numbers&lt;/span> &lt;span style="color:#f92672">...&lt;/span>&lt;span style="color:#66d9ef">int&lt;/span>) &lt;span style="color:#66d9ef">int&lt;/span> {
&lt;span style="color:#a6e22e">sum&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;span style="color:#66d9ef">for&lt;/span> &lt;span style="color:#a6e22e">_&lt;/span>, &lt;span style="color:#a6e22e">number&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#66d9ef">range&lt;/span> &lt;span style="color:#a6e22e">numbers&lt;/span> {
&lt;span style="color:#a6e22e">sum&lt;/span> &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#a6e22e">number&lt;/span>
}
&lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#a6e22e">sum&lt;/span>
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>The idiomatic way to test this function is to create a second file called &lt;code>numbers_test.go&lt;/code> and test it using the aforementioned &lt;code>testing&lt;/code> package:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-go" data-lang="go">&lt;span style="color:#f92672">package&lt;/span> &lt;span style="color:#a6e22e">numbers&lt;/span>
&lt;span style="color:#f92672">import&lt;/span> (
&lt;span style="color:#e6db74">&amp;#34;testing&amp;#34;&lt;/span>
)
&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">Test_addNumbers&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">testing&lt;/span>.&lt;span style="color:#a6e22e">T&lt;/span>) {
&lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">addNumbers&lt;/span>(&lt;span style="color:#ae81ff">3&lt;/span>, &lt;span style="color:#ae81ff">7&lt;/span>) &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#ae81ff">10&lt;/span> {
&lt;span style="color:#a6e22e">t&lt;/span>.&lt;span style="color:#a6e22e">FailNow&lt;/span>()
}
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>Then I can run &lt;code>go test&lt;/code> and see the output of the test:&lt;/p>
&lt;pre tabindex="0">&lt;code>ok github.com/example/package/numbers
&lt;/code>&lt;/pre>&lt;p>Alternatively, if the test fails (i.e. if I expect the result to be 11 instead), I get an appropriate error report:&lt;/p>
&lt;pre tabindex="0">&lt;code>--- FAIL: Test_addNumbers (0.00s)
FAIL
FAIL github.com/example/package/numbers 0.255s
FAIL
&lt;/code>&lt;/pre>&lt;p>This works pretty well, overall. When I need to find the tests for a given file, I can usually assume they’re in its corresponding &lt;code>_test.go&lt;/code> file. The &lt;code>testing&lt;/code> framework is built into the standard library, and does exactly what you’d expect with this code.&lt;/p>
&lt;h3 id="we-have-the-technology">We have the technology&lt;/h3>
&lt;p>This isn’t how I actually like to write Go unit tests, however. There are techniques and libraries I use to make unit testing in Go more effective to write and understand when things go (heh) awry.&lt;/p>
&lt;h3 id="the-test-command">The test command&lt;/h3>
&lt;p>You can run &lt;code>go test&lt;/code> in a given directory and see the output of your test, but that leaves a lot of functionality on the table that can and will be useful. The full command I typically use is:&lt;/p>
&lt;p>&lt;code>go test -cover -shuffle=on -race -vet=all -failfast &amp;lt;$RELEVANT_PACKAGE(S)&amp;gt;&lt;/code>&lt;/p>
&lt;p>Here’s what each of these flags do:&lt;/p>
&lt;ul>
&lt;li>&lt;code>-cover&lt;/code> instructs the test runner to determine coverage for a given package.&lt;/li>
&lt;li>&lt;code>-shuffle=on&lt;/code> runs the tests in a random order, instead of in declared order.&lt;/li>
&lt;li>&lt;code>-race&lt;/code> activates the data race detector.&lt;/li>
&lt;li>&lt;code>-vet=all&lt;/code> &amp;ldquo;&lt;a href="https://pkg.go.dev/cmd/go/internal/test@go1.22.1#pkg-variables">runs go vet on the package and its test source files to identify significant problems. If go vet finds any problems, go test reports those and does not run the test binary.&lt;/a>”&lt;/li>
&lt;li>&lt;code>-failfast&lt;/code> stops test execution when a given unit test fails. It allows tests executed in parallel (more on that in a bit) to finish.&lt;/li>
&lt;/ul>
&lt;h3 id="table-driven-tests">Table-driven tests&lt;/h3>
&lt;p>I’m going to say something sort of controversial in the Go zeitgeist: I find table tests very rarely useful. For something like our &lt;code>addNumbers&lt;/code> function, I might actually do it, because the inputs and outputs are simple. Here&amp;rsquo;s what that might look like:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-go" data-lang="go">&lt;span style="color:#f92672">package&lt;/span> &lt;span style="color:#a6e22e">numbers&lt;/span>
&lt;span style="color:#f92672">import&lt;/span> (
&lt;span style="color:#e6db74">&amp;#34;testing&amp;#34;&lt;/span>
)
&lt;span style="color:#66d9ef">type&lt;/span> &lt;span style="color:#a6e22e">addNumbersTestCase&lt;/span> &lt;span style="color:#66d9ef">struct&lt;/span> {
&lt;span style="color:#a6e22e">inputs&lt;/span> []&lt;span style="color:#66d9ef">int&lt;/span>
&lt;span style="color:#a6e22e">expectedOutput&lt;/span> &lt;span style="color:#66d9ef">int&lt;/span>
}
&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">Test_addNumbers_table&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">testing&lt;/span>.&lt;span style="color:#a6e22e">T&lt;/span>) {
&lt;span style="color:#a6e22e">testCases&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> []&lt;span style="color:#a6e22e">addNumbersTestCase&lt;/span>{
{
&lt;span style="color:#a6e22e">inputs&lt;/span>: []&lt;span style="color:#66d9ef">int&lt;/span>{&lt;span style="color:#ae81ff">4&lt;/span>, &lt;span style="color:#ae81ff">4&lt;/span>},
&lt;span style="color:#a6e22e">expectedOutput&lt;/span>: &lt;span style="color:#ae81ff">8&lt;/span>,
},
{
&lt;span style="color:#a6e22e">inputs&lt;/span>: []&lt;span style="color:#66d9ef">int&lt;/span>{&lt;span style="color:#ae81ff">10&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>},
&lt;span style="color:#a6e22e">expectedOutput&lt;/span>: &lt;span style="color:#ae81ff">10&lt;/span>,
},
{
&lt;span style="color:#a6e22e">inputs&lt;/span>: []&lt;span style="color:#66d9ef">int&lt;/span>{&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#ae81ff">1&lt;/span>},
&lt;span style="color:#a6e22e">expectedOutput&lt;/span>: &lt;span style="color:#ae81ff">0&lt;/span>,
},
}
&lt;span style="color:#66d9ef">for&lt;/span> &lt;span style="color:#a6e22e">_&lt;/span>, &lt;span style="color:#a6e22e">testCase&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#66d9ef">range&lt;/span> &lt;span style="color:#a6e22e">testCases&lt;/span> {
&lt;span style="color:#a6e22e">actual&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">addNumbers&lt;/span>(&lt;span style="color:#a6e22e">testCase&lt;/span>.&lt;span style="color:#a6e22e">inputs&lt;/span>&lt;span style="color:#f92672">...&lt;/span>)
&lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">testCase&lt;/span>.&lt;span style="color:#a6e22e">expectedOutput&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#a6e22e">actual&lt;/span> {
&lt;span style="color:#a6e22e">t&lt;/span>.&lt;span style="color:#a6e22e">FailNow&lt;/span>()
}
}
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>That said, it’s pretty easy to end up in a situation where your table tests need to be quite complicated and gnarly to get full coverage.&lt;/p>
&lt;p>Goland lets you generate tests for functions using a default template that attempts to adhere to the tradition of table-driven tests. Here’s what the generated test function looks like for a method in a side project. This method fetches a webhook object from the database, and accepts 3 total parameters — a context, a webhook ID, and an accountID:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-go" data-lang="go">&lt;span style="color:#f92672">package&lt;/span> &lt;span style="color:#a6e22e">whatever&lt;/span>
&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">TestQuerier_GetWebhook&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">testing&lt;/span>.&lt;span style="color:#a6e22e">T&lt;/span>) {
&lt;span style="color:#66d9ef">type&lt;/span> &lt;span style="color:#a6e22e">fields&lt;/span> &lt;span style="color:#66d9ef">struct&lt;/span> {
&lt;span style="color:#a6e22e">tracer&lt;/span> &lt;span style="color:#a6e22e">tracing&lt;/span>.&lt;span style="color:#a6e22e">Tracer&lt;/span>
&lt;span style="color:#a6e22e">logger&lt;/span> &lt;span style="color:#a6e22e">logging&lt;/span>.&lt;span style="color:#a6e22e">Logger&lt;/span>
&lt;span style="color:#a6e22e">secretGenerator&lt;/span> &lt;span style="color:#a6e22e">random&lt;/span>.&lt;span style="color:#a6e22e">Generator&lt;/span>
&lt;span style="color:#a6e22e">oauth2ClientTokenEncDec&lt;/span> &lt;span style="color:#a6e22e">encryption&lt;/span>.&lt;span style="color:#a6e22e">EncryptorDecryptor&lt;/span>
&lt;span style="color:#a6e22e">generatedQuerier&lt;/span> &lt;span style="color:#a6e22e">generated&lt;/span>.&lt;span style="color:#a6e22e">Querier&lt;/span>
&lt;span style="color:#a6e22e">timeFunc&lt;/span> &lt;span style="color:#66d9ef">func&lt;/span>() &lt;span style="color:#a6e22e">time&lt;/span>.&lt;span style="color:#a6e22e">Time&lt;/span>
&lt;span style="color:#a6e22e">config&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">dbconfig&lt;/span>.&lt;span style="color:#a6e22e">Config&lt;/span>
&lt;span style="color:#a6e22e">db&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">sql&lt;/span>.&lt;span style="color:#a6e22e">DB&lt;/span>
&lt;span style="color:#a6e22e">migrateOnce&lt;/span> &lt;span style="color:#a6e22e">sync&lt;/span>.&lt;span style="color:#a6e22e">Once&lt;/span>
}
&lt;span style="color:#66d9ef">type&lt;/span> &lt;span style="color:#a6e22e">args&lt;/span> &lt;span style="color:#66d9ef">struct&lt;/span> {
&lt;span style="color:#a6e22e">ctx&lt;/span> &lt;span style="color:#a6e22e">context&lt;/span>.&lt;span style="color:#a6e22e">Context&lt;/span>
&lt;span style="color:#a6e22e">webhookID&lt;/span> &lt;span style="color:#66d9ef">string&lt;/span>
&lt;span style="color:#a6e22e">accountID&lt;/span> &lt;span style="color:#66d9ef">string&lt;/span>
}
&lt;span style="color:#a6e22e">tests&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> []&lt;span style="color:#66d9ef">struct&lt;/span> {
&lt;span style="color:#a6e22e">name&lt;/span> &lt;span style="color:#66d9ef">string&lt;/span>
&lt;span style="color:#a6e22e">fields&lt;/span> &lt;span style="color:#a6e22e">fields&lt;/span>
&lt;span style="color:#a6e22e">args&lt;/span> &lt;span style="color:#a6e22e">args&lt;/span>
&lt;span style="color:#a6e22e">want&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">types&lt;/span>.&lt;span style="color:#a6e22e">Webhook&lt;/span>
&lt;span style="color:#a6e22e">wantErr&lt;/span> &lt;span style="color:#a6e22e">assert&lt;/span>.&lt;span style="color:#a6e22e">ErrorAssertionFunc&lt;/span>
}{
&lt;span style="color:#75715e">// TODO: Add test cases.
&lt;/span>&lt;span style="color:#75715e">&lt;/span> }
&lt;span style="color:#66d9ef">for&lt;/span> &lt;span style="color:#a6e22e">_&lt;/span>, &lt;span style="color:#a6e22e">tt&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#66d9ef">range&lt;/span> &lt;span style="color:#a6e22e">tests&lt;/span> {
&lt;span style="color:#a6e22e">t&lt;/span>.&lt;span style="color:#a6e22e">Run&lt;/span>(&lt;span style="color:#a6e22e">tt&lt;/span>.&lt;span style="color:#a6e22e">name&lt;/span>, &lt;span style="color:#66d9ef">func&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">testing&lt;/span>.&lt;span style="color:#a6e22e">T&lt;/span>) {
&lt;span style="color:#a6e22e">q&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#f92672">&amp;amp;&lt;/span>&lt;span style="color:#a6e22e">Querier&lt;/span>{
&lt;span style="color:#a6e22e">tracer&lt;/span>: &lt;span style="color:#a6e22e">tt&lt;/span>.&lt;span style="color:#a6e22e">fields&lt;/span>.&lt;span style="color:#a6e22e">tracer&lt;/span>,
&lt;span style="color:#a6e22e">logger&lt;/span>: &lt;span style="color:#a6e22e">tt&lt;/span>.&lt;span style="color:#a6e22e">fields&lt;/span>.&lt;span style="color:#a6e22e">logger&lt;/span>,
&lt;span style="color:#a6e22e">secretGenerator&lt;/span>: &lt;span style="color:#a6e22e">tt&lt;/span>.&lt;span style="color:#a6e22e">fields&lt;/span>.&lt;span style="color:#a6e22e">secretGenerator&lt;/span>,
&lt;span style="color:#a6e22e">oauth2ClientTokenEncDec&lt;/span>: &lt;span style="color:#a6e22e">tt&lt;/span>.&lt;span style="color:#a6e22e">fields&lt;/span>.&lt;span style="color:#a6e22e">oauth2ClientTokenEncDec&lt;/span>,
&lt;span style="color:#a6e22e">generatedQuerier&lt;/span>: &lt;span style="color:#a6e22e">tt&lt;/span>.&lt;span style="color:#a6e22e">fields&lt;/span>.&lt;span style="color:#a6e22e">generatedQuerier&lt;/span>,
&lt;span style="color:#a6e22e">timeFunc&lt;/span>: &lt;span style="color:#a6e22e">tt&lt;/span>.&lt;span style="color:#a6e22e">fields&lt;/span>.&lt;span style="color:#a6e22e">timeFunc&lt;/span>,
&lt;span style="color:#a6e22e">config&lt;/span>: &lt;span style="color:#a6e22e">tt&lt;/span>.&lt;span style="color:#a6e22e">fields&lt;/span>.&lt;span style="color:#a6e22e">config&lt;/span>,
&lt;span style="color:#a6e22e">db&lt;/span>: &lt;span style="color:#a6e22e">tt&lt;/span>.&lt;span style="color:#a6e22e">fields&lt;/span>.&lt;span style="color:#a6e22e">db&lt;/span>,
&lt;span style="color:#a6e22e">migrateOnce&lt;/span>: &lt;span style="color:#a6e22e">tt&lt;/span>.&lt;span style="color:#a6e22e">fields&lt;/span>.&lt;span style="color:#a6e22e">migrateOnce&lt;/span>,
}
&lt;span style="color:#a6e22e">got&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">q&lt;/span>.&lt;span style="color:#a6e22e">GetWebhook&lt;/span>(&lt;span style="color:#a6e22e">tt&lt;/span>.&lt;span style="color:#a6e22e">args&lt;/span>.&lt;span style="color:#a6e22e">ctx&lt;/span>, &lt;span style="color:#a6e22e">tt&lt;/span>.&lt;span style="color:#a6e22e">args&lt;/span>.&lt;span style="color:#a6e22e">webhookID&lt;/span>, &lt;span style="color:#a6e22e">tt&lt;/span>.&lt;span style="color:#a6e22e">args&lt;/span>.&lt;span style="color:#a6e22e">accountID&lt;/span>)
&lt;span style="color:#66d9ef">if&lt;/span> !&lt;span style="color:#a6e22e">tt&lt;/span>.&lt;span style="color:#a6e22e">wantErr&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span>, &lt;span style="color:#a6e22e">fmt&lt;/span>.&lt;span style="color:#a6e22e">Sprintf&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;GetWebhook(%v, %v, %v)&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">tt&lt;/span>.&lt;span style="color:#a6e22e">args&lt;/span>.&lt;span style="color:#a6e22e">ctx&lt;/span>, &lt;span style="color:#a6e22e">tt&lt;/span>.&lt;span style="color:#a6e22e">args&lt;/span>.&lt;span style="color:#a6e22e">webhookID&lt;/span>, &lt;span style="color:#a6e22e">tt&lt;/span>.&lt;span style="color:#a6e22e">args&lt;/span>.&lt;span style="color:#a6e22e">accountID&lt;/span>)) {
&lt;span style="color:#66d9ef">return&lt;/span>
}
&lt;span style="color:#a6e22e">assert&lt;/span>.&lt;span style="color:#a6e22e">Equalf&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span>, &lt;span style="color:#a6e22e">tt&lt;/span>.&lt;span style="color:#a6e22e">want&lt;/span>, &lt;span style="color:#a6e22e">got&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;GetWebhook(%v, %v, %v)&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">tt&lt;/span>.&lt;span style="color:#a6e22e">args&lt;/span>.&lt;span style="color:#a6e22e">ctx&lt;/span>, &lt;span style="color:#a6e22e">tt&lt;/span>.&lt;span style="color:#a6e22e">args&lt;/span>.&lt;span style="color:#a6e22e">webhookID&lt;/span>, &lt;span style="color:#a6e22e">tt&lt;/span>.&lt;span style="color:#a6e22e">args&lt;/span>.&lt;span style="color:#a6e22e">accountID&lt;/span>)
})
}
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>I think this is kind of a mess, and it doesn’t even include test cases yet. I’d rather have the querier built by an only-available-to-tests constructor that puts sensible defaults for all these values.&lt;/p>
&lt;p>The point of this is not to dunk on Goland (I am a happily paying customer and have been for many years), it’s just to illustrate how even a non-trivial function can really blow up the readability of a table-driven test.&lt;/p>
&lt;h3 id="parallel">Parallel&lt;/h3>
&lt;p>Go tests can be run in parallel, by declaring the test as parallel in the top level. This behavior is on by default, and can only be disabled by setting the &lt;code>-parallel=1&lt;/code> flag when invoking &lt;code>go test&lt;/code> (which can be useful when debugging flaky tests).&lt;/p>
&lt;p>I’m of the opinion that tests should be parallel unless they absolutely cannot be, and even then, I’d rather fix the flaw behind why they can’t be run in parallel than force them to be run sequentially.&lt;/p>
&lt;p>Running your tests in parallel generally allows you to suss out concurrency bugs earlier, even if you’re not using the race detector (which you should), and will eventually lead to you developing concurrency-safe habits when writing code in the first place. So generally, when I write a test, the very first line is the parallel declaration:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-go" data-lang="go">&lt;span style="color:#f92672">package&lt;/span> &lt;span style="color:#a6e22e">numbers&lt;/span>
&lt;span style="color:#f92672">import&lt;/span> (
&lt;span style="color:#e6db74">&amp;#34;testing&amp;#34;&lt;/span>
)
&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">Test_addNumbers&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">testing&lt;/span>.&lt;span style="color:#a6e22e">T&lt;/span>) {
&lt;span style="color:#a6e22e">t&lt;/span>.&lt;span style="color:#a6e22e">Parallel&lt;/span>()
&lt;span style="color:#a6e22e">expected&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#ae81ff">10&lt;/span>
&lt;span style="color:#a6e22e">actual&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">addNumbers&lt;/span>(&lt;span style="color:#ae81ff">3&lt;/span>, &lt;span style="color:#ae81ff">7&lt;/span>)
&lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">expected&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#a6e22e">actual&lt;/span> {
&lt;span style="color:#a6e22e">t&lt;/span>.&lt;span style="color:#a6e22e">FailNow&lt;/span>()
}
}
&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="subtests">Subtests&lt;/h3>
&lt;p>Subtests are a feature of Go’s testing library that allow you to have smaller tests for specific circumstances. These allow you to get some of the benefits of a behavior-driven testing style, without having to invoke an external dependency. I usually write subtests even when there’s only one test case I care about, because it incurs no real overhead to do so, and allows you to easily test a new case in the event the tested function gets more complex.&lt;/p>
&lt;p>When I write subtests, I usually name the top-level &lt;code>*testing.T&lt;/code> object a capitalized &lt;code>T&lt;/code>, and use the conventional lower-cased &lt;code>t&lt;/code> for subtests only. This way, the subtest logic always looks like an idiomatic Go test, and there’s no confusion of what variable is which or what shadows what. Calls to &lt;code>.Parallel()&lt;/code> will have to be made for both the big and little &lt;code>*T&lt;/code> variables.&lt;/p>
&lt;p>Here’s a demonstration that adapts our earlier example for the &lt;code>addNumbers&lt;/code> function:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-go" data-lang="go">&lt;span style="color:#f92672">package&lt;/span> &lt;span style="color:#a6e22e">numbers&lt;/span>
&lt;span style="color:#f92672">import&lt;/span> (
&lt;span style="color:#e6db74">&amp;#34;testing&amp;#34;&lt;/span>
)
&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">Test_addNumbers&lt;/span>(&lt;span style="color:#a6e22e">T&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">testing&lt;/span>.&lt;span style="color:#a6e22e">T&lt;/span>) {
&lt;span style="color:#a6e22e">T&lt;/span>.&lt;span style="color:#a6e22e">Parallel&lt;/span>()
&lt;span style="color:#a6e22e">T&lt;/span>.&lt;span style="color:#a6e22e">Run&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;standard&amp;#34;&lt;/span>, &lt;span style="color:#66d9ef">func&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">testing&lt;/span>.&lt;span style="color:#a6e22e">T&lt;/span>) {
&lt;span style="color:#a6e22e">t&lt;/span>.&lt;span style="color:#a6e22e">Parallel&lt;/span>()
&lt;span style="color:#a6e22e">expected&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#ae81ff">10&lt;/span>
&lt;span style="color:#a6e22e">actual&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">addNumbers&lt;/span>(&lt;span style="color:#ae81ff">3&lt;/span>, &lt;span style="color:#ae81ff">7&lt;/span>)
&lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">expected&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#a6e22e">actual&lt;/span> {
&lt;span style="color:#a6e22e">t&lt;/span>.&lt;span style="color:#a6e22e">FailNow&lt;/span>()
}
})
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>Now if I decide to add another test case, it’s simply a matter of copying the subtest block, changing the name, and then making the test match that description:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-go" data-lang="go">&lt;span style="color:#f92672">package&lt;/span> &lt;span style="color:#a6e22e">numbers&lt;/span>
&lt;span style="color:#f92672">import&lt;/span> (
&lt;span style="color:#e6db74">&amp;#34;testing&amp;#34;&lt;/span>
)
&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">Test_addNumbers&lt;/span>(&lt;span style="color:#a6e22e">T&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">testing&lt;/span>.&lt;span style="color:#a6e22e">T&lt;/span>) {
&lt;span style="color:#a6e22e">T&lt;/span>.&lt;span style="color:#a6e22e">Parallel&lt;/span>()
&lt;span style="color:#a6e22e">T&lt;/span>.&lt;span style="color:#a6e22e">Run&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;standard&amp;#34;&lt;/span>, &lt;span style="color:#66d9ef">func&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">testing&lt;/span>.&lt;span style="color:#a6e22e">T&lt;/span>) {
&lt;span style="color:#a6e22e">t&lt;/span>.&lt;span style="color:#a6e22e">Parallel&lt;/span>()
&lt;span style="color:#a6e22e">expected&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#ae81ff">10&lt;/span>
&lt;span style="color:#a6e22e">actual&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">addNumbers&lt;/span>(&lt;span style="color:#ae81ff">3&lt;/span>, &lt;span style="color:#ae81ff">7&lt;/span>)
&lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">expected&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#a6e22e">actual&lt;/span> {
&lt;span style="color:#a6e22e">t&lt;/span>.&lt;span style="color:#a6e22e">FailNow&lt;/span>()
}
})
&lt;span style="color:#a6e22e">T&lt;/span>.&lt;span style="color:#a6e22e">Run&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;with many numbers&amp;#34;&lt;/span>, &lt;span style="color:#66d9ef">func&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">testing&lt;/span>.&lt;span style="color:#a6e22e">T&lt;/span>) {
&lt;span style="color:#a6e22e">t&lt;/span>.&lt;span style="color:#a6e22e">Parallel&lt;/span>()
&lt;span style="color:#a6e22e">expected&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#ae81ff">15&lt;/span>
&lt;span style="color:#a6e22e">actual&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">addNumbers&lt;/span>(&lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#ae81ff">2&lt;/span>, &lt;span style="color:#ae81ff">3&lt;/span>, &lt;span style="color:#ae81ff">4&lt;/span>, &lt;span style="color:#ae81ff">5&lt;/span>)
&lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">expected&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#a6e22e">actual&lt;/span> {
&lt;span style="color:#a6e22e">t&lt;/span>.&lt;span style="color:#a6e22e">FailNow&lt;/span>()
}
})
&lt;span style="color:#a6e22e">T&lt;/span>.&lt;span style="color:#a6e22e">Run&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;with negative numbers&amp;#34;&lt;/span>, &lt;span style="color:#66d9ef">func&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">testing&lt;/span>.&lt;span style="color:#a6e22e">T&lt;/span>) {
&lt;span style="color:#a6e22e">t&lt;/span>.&lt;span style="color:#a6e22e">Parallel&lt;/span>()
&lt;span style="color:#a6e22e">expected&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#ae81ff">3&lt;/span>
&lt;span style="color:#a6e22e">actual&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">addNumbers&lt;/span>(&lt;span style="color:#ae81ff">4&lt;/span>, &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>)
&lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">expected&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#a6e22e">actual&lt;/span> {
&lt;span style="color:#a6e22e">t&lt;/span>.&lt;span style="color:#a6e22e">FailNow&lt;/span>()
}
})
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>Additionally, if there are test fixtures or constants that are relevant to all subtests, they can live in the space between the big-T call to &lt;code>.Parallel()&lt;/code> and the first declared subtest.&lt;/p>
&lt;h3 id="libraries">Libraries&lt;/h3>
&lt;p>While the standard library testing package is pretty great, there have been some great contributions by the community to the ecosystem that make testing in Go even better.&lt;/p>
&lt;h3 id="fake-values">Fake Values&lt;/h3>
&lt;p>Typically, when I need to interact with an object in a test, I want to have unpredictable values to the extent possible for that object, so that I’m discouraged from relying on those conventions when writing tests.&lt;/p>
&lt;p>So for instance, if I need a &lt;code>*User&lt;/code> object for a test, I want to have no idea what it’s username might be, as opposed to a common static value that I might copy/paste from place to place, which would make many tests fail if it was inadvertently changed.&lt;/p>
&lt;p>I like to use &lt;a href="https://pkg.go.dev/github.com/brianvoe/gofakeit/v7">brianvoe/gofakeit&lt;/a> for this. It comes with a ton of sensible functions with which to build fake instances of different types, and I’ve never had a problem with it. Here’s what that fake user builder might look like:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-go" data-lang="go">&lt;span style="color:#f92672">package&lt;/span> &lt;span style="color:#a6e22e">whatever&lt;/span>
&lt;span style="color:#f92672">import&lt;/span> (
&lt;span style="color:#e6db74">&amp;#34;github.com/example/project/internal/authorization&amp;#34;&lt;/span>
&lt;span style="color:#e6db74">&amp;#34;github.com/example/project/internal/pkg/pointer&amp;#34;&lt;/span>
&lt;span style="color:#e6db74">&amp;#34;github.com/example/project/pkg/types&amp;#34;&lt;/span>
&lt;span style="color:#a6e22e">fake&lt;/span> &lt;span style="color:#e6db74">&amp;#34;github.com/brianvoe/gofakeit/v7&amp;#34;&lt;/span>
)
&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">buildFakeTime&lt;/span>() &lt;span style="color:#a6e22e">time&lt;/span>.&lt;span style="color:#a6e22e">Time&lt;/span> {
&lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#a6e22e">fake&lt;/span>.&lt;span style="color:#a6e22e">Date&lt;/span>().&lt;span style="color:#a6e22e">Add&lt;/span>(&lt;span style="color:#ae81ff">0&lt;/span>).&lt;span style="color:#a6e22e">Truncate&lt;/span>(&lt;span style="color:#a6e22e">time&lt;/span>.&lt;span style="color:#a6e22e">Second&lt;/span>).&lt;span style="color:#a6e22e">UTC&lt;/span>()
}
&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">BuildFakeUser&lt;/span>() &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">types&lt;/span>.&lt;span style="color:#a6e22e">User&lt;/span> {
&lt;span style="color:#a6e22e">fakeDate&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">buildFakeTime&lt;/span>()
&lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#f92672">&amp;amp;&lt;/span>&lt;span style="color:#a6e22e">types&lt;/span>.&lt;span style="color:#a6e22e">User&lt;/span>{
&lt;span style="color:#a6e22e">ID&lt;/span>: &lt;span style="color:#a6e22e">identifiers&lt;/span>.&lt;span style="color:#a6e22e">New&lt;/span>(),
&lt;span style="color:#a6e22e">FirstName&lt;/span>: &lt;span style="color:#a6e22e">fake&lt;/span>.&lt;span style="color:#a6e22e">FirstName&lt;/span>(),
&lt;span style="color:#a6e22e">LastName&lt;/span>: &lt;span style="color:#a6e22e">fake&lt;/span>.&lt;span style="color:#a6e22e">LastName&lt;/span>(),
&lt;span style="color:#a6e22e">EmailAddress&lt;/span>: &lt;span style="color:#a6e22e">fake&lt;/span>.&lt;span style="color:#a6e22e">Email&lt;/span>(),
&lt;span style="color:#a6e22e">Username&lt;/span>: &lt;span style="color:#a6e22e">fmt&lt;/span>.&lt;span style="color:#a6e22e">Sprintf&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;%s_%d_%s&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">fake&lt;/span>.&lt;span style="color:#a6e22e">Username&lt;/span>(), &lt;span style="color:#a6e22e">fake&lt;/span>.&lt;span style="color:#a6e22e">Uint8&lt;/span>(), &lt;span style="color:#a6e22e">fake&lt;/span>.&lt;span style="color:#a6e22e">Username&lt;/span>()),
&lt;span style="color:#a6e22e">Birthday&lt;/span>: &lt;span style="color:#a6e22e">pointer&lt;/span>.&lt;span style="color:#a6e22e">To&lt;/span>(&lt;span style="color:#a6e22e">buildFakeTime&lt;/span>()),
&lt;span style="color:#a6e22e">AccountStatus&lt;/span>: string(&lt;span style="color:#a6e22e">types&lt;/span>.&lt;span style="color:#a6e22e">UnverifiedHouseholdStatus&lt;/span>),
&lt;span style="color:#a6e22e">TwoFactorSecret&lt;/span>: &lt;span style="color:#a6e22e">base32&lt;/span>.&lt;span style="color:#a6e22e">StdEncoding&lt;/span>.&lt;span style="color:#a6e22e">EncodeToString&lt;/span>([]byte(&lt;span style="color:#a6e22e">fake&lt;/span>.&lt;span style="color:#a6e22e">Password&lt;/span>(&lt;span style="color:#66d9ef">false&lt;/span>, &lt;span style="color:#66d9ef">true&lt;/span>, &lt;span style="color:#66d9ef">true&lt;/span>, &lt;span style="color:#66d9ef">false&lt;/span>, &lt;span style="color:#66d9ef">false&lt;/span>, &lt;span style="color:#ae81ff">32&lt;/span>))),
&lt;span style="color:#a6e22e">TwoFactorSecretVerifiedAt&lt;/span>: &lt;span style="color:#f92672">&amp;amp;&lt;/span>&lt;span style="color:#a6e22e">fakeDate&lt;/span>,
&lt;span style="color:#a6e22e">ServiceRole&lt;/span>: &lt;span style="color:#a6e22e">authorization&lt;/span>.&lt;span style="color:#a6e22e">ServiceUserRole&lt;/span>.&lt;span style="color:#a6e22e">String&lt;/span>(),
&lt;span style="color:#a6e22e">CreatedAt&lt;/span>: &lt;span style="color:#a6e22e">buildFakeTime&lt;/span>(),
}
}
&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="testify">Testify&lt;/h3>
&lt;p>I’m a huge fan of the &lt;code>(assert|require|mock)&lt;/code> libraries in &lt;a href="https://github.com/stretchr/testify">testify&lt;/a>. In our example test, using testify would look like:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-go" data-lang="go">&lt;span style="color:#f92672">package&lt;/span> &lt;span style="color:#a6e22e">numbers&lt;/span>
&lt;span style="color:#f92672">import&lt;/span> (
&lt;span style="color:#e6db74">&amp;#34;testing&amp;#34;&lt;/span>
&lt;span style="color:#e6db74">&amp;#34;github.com/stretchr/testify/assert&amp;#34;&lt;/span>
)
&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">Test_addNumbers&lt;/span>(&lt;span style="color:#a6e22e">T&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">testing&lt;/span>.&lt;span style="color:#a6e22e">T&lt;/span>) {
&lt;span style="color:#a6e22e">T&lt;/span>.&lt;span style="color:#a6e22e">Parallel&lt;/span>()
&lt;span style="color:#a6e22e">T&lt;/span>.&lt;span style="color:#a6e22e">Run&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;standard&amp;#34;&lt;/span>, &lt;span style="color:#66d9ef">func&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">testing&lt;/span>.&lt;span style="color:#a6e22e">T&lt;/span>) {
&lt;span style="color:#a6e22e">t&lt;/span>.&lt;span style="color:#a6e22e">Parallel&lt;/span>()
&lt;span style="color:#a6e22e">expected&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#ae81ff">10&lt;/span>
&lt;span style="color:#a6e22e">actual&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">addNumbers&lt;/span>(&lt;span style="color:#ae81ff">3&lt;/span>, &lt;span style="color:#ae81ff">7&lt;/span>)
&lt;span style="color:#a6e22e">assert&lt;/span>.&lt;span style="color:#a6e22e">Equal&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span>, &lt;span style="color:#a6e22e">expected&lt;/span>, &lt;span style="color:#a6e22e">actual&lt;/span>)
})
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>I think &lt;code>assert.Equal&lt;/code> is fairly obvious in its function for users who are new to a code base. &lt;code>testify&lt;/code> is probably the only library I can think of that I would wholeheartedly support being absorbed into the standard library.&lt;/p>
&lt;p>You can use &lt;code>require&lt;/code> instead if you want the test to stop in the event of a failure. This is usually good for dependencies of a test, i.e. if I need to create a file and pass it to a function, I should probably &lt;code>require&lt;/code> that no errors happen when creating it:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-go" data-lang="go">&lt;span style="color:#f92672">package&lt;/span> &lt;span style="color:#a6e22e">whatever&lt;/span>
&lt;span style="color:#f92672">import&lt;/span> (
&lt;span style="color:#e6db74">&amp;#34;os&amp;#34;&lt;/span>
&lt;span style="color:#e6db74">&amp;#34;testing&amp;#34;&lt;/span>
&lt;span style="color:#e6db74">&amp;#34;github.com/stretchr/testify/require&amp;#34;&lt;/span>
)
&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">TestSomeFunction&lt;/span>(&lt;span style="color:#a6e22e">T&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">testing&lt;/span>.&lt;span style="color:#a6e22e">T&lt;/span>) {
&lt;span style="color:#a6e22e">T&lt;/span>.&lt;span style="color:#a6e22e">Parallel&lt;/span>()
&lt;span style="color:#a6e22e">T&lt;/span>.&lt;span style="color:#a6e22e">Run&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;standard&amp;#34;&lt;/span>, &lt;span style="color:#66d9ef">func&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">testing&lt;/span>.&lt;span style="color:#a6e22e">T&lt;/span>) {
&lt;span style="color:#a6e22e">t&lt;/span>.&lt;span style="color:#a6e22e">Parallel&lt;/span>()
&lt;span style="color:#a6e22e">f&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">os&lt;/span>.&lt;span style="color:#a6e22e">Create&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;some_path.txt&amp;#34;&lt;/span>)
&lt;span style="color:#a6e22e">require&lt;/span>.&lt;span style="color:#a6e22e">NoError&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span>)
&lt;span style="color:#a6e22e">require&lt;/span>.&lt;span style="color:#a6e22e">NotNil&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span>, &lt;span style="color:#a6e22e">f&lt;/span>)
})
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>mock&lt;/code> is also useful for defining mock implementations of structures. Say I have the following interface:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-go" data-lang="go">&lt;span style="color:#f92672">package&lt;/span> &lt;span style="color:#a6e22e">something&lt;/span>
&lt;span style="color:#66d9ef">type&lt;/span> &lt;span style="color:#a6e22e">UserRetriever&lt;/span> &lt;span style="color:#66d9ef">interface&lt;/span> {
&lt;span style="color:#a6e22e">GetUser&lt;/span>(&lt;span style="color:#a6e22e">ctx&lt;/span> &lt;span style="color:#a6e22e">context&lt;/span>.&lt;span style="color:#a6e22e">Context&lt;/span>, &lt;span style="color:#a6e22e">userID&lt;/span> &lt;span style="color:#66d9ef">string&lt;/span>) (&lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">User&lt;/span>, &lt;span style="color:#66d9ef">error&lt;/span>)
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>Implementing a mock for this interface becomes a matter of:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-go" data-lang="go">&lt;span style="color:#f92672">package&lt;/span> &lt;span style="color:#a6e22e">something&lt;/span>
&lt;span style="color:#f92672">import&lt;/span> (
&lt;span style="color:#e6db74">&amp;#34;github.com/stretchr/testify/mock&amp;#34;&lt;/span>
)
&lt;span style="color:#66d9ef">type&lt;/span> &lt;span style="color:#a6e22e">mockUserRetriever&lt;/span> &lt;span style="color:#66d9ef">struct&lt;/span> {
&lt;span style="color:#a6e22e">mock&lt;/span>.&lt;span style="color:#a6e22e">Mock&lt;/span>
}
&lt;span style="color:#66d9ef">func&lt;/span> (&lt;span style="color:#a6e22e">m&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">mockUserRetriever&lt;/span>) &lt;span style="color:#a6e22e">GetUser&lt;/span>(&lt;span style="color:#a6e22e">ctx&lt;/span> &lt;span style="color:#a6e22e">context&lt;/span>.&lt;span style="color:#a6e22e">Context&lt;/span>, &lt;span style="color:#a6e22e">userID&lt;/span> &lt;span style="color:#66d9ef">string&lt;/span>) (&lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">User&lt;/span>, &lt;span style="color:#66d9ef">error&lt;/span>) {
&lt;span style="color:#a6e22e">returnValues&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">m&lt;/span>.&lt;span style="color:#a6e22e">Called&lt;/span>(&lt;span style="color:#a6e22e">ctx&lt;/span>, &lt;span style="color:#a6e22e">userID&lt;/span>)
&lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#a6e22e">returnValues&lt;/span>.&lt;span style="color:#a6e22e">Get&lt;/span>(&lt;span style="color:#ae81ff">0&lt;/span>).(&lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">User&lt;/span>), &lt;span style="color:#a6e22e">returnValues&lt;/span>.&lt;span style="color:#a6e22e">Error&lt;/span>(&lt;span style="color:#ae81ff">1&lt;/span>)
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>Using the above mock in a test looks like this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-go" data-lang="go">&lt;span style="color:#f92672">package&lt;/span> &lt;span style="color:#a6e22e">something&lt;/span>
&lt;span style="color:#f92672">import&lt;/span> (
&lt;span style="color:#e6db74">&amp;#34;testing&amp;#34;&lt;/span>
&lt;span style="color:#e6db74">&amp;#34;github.com/stretchr/testify/mock&amp;#34;&lt;/span>
)
&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">TestUserRetrieval&lt;/span>(&lt;span style="color:#a6e22e">T&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">testing&lt;/span>.&lt;span style="color:#a6e22e">T&lt;/span>) {
&lt;span style="color:#a6e22e">T&lt;/span>.&lt;span style="color:#a6e22e">Parallel&lt;/span>()
&lt;span style="color:#a6e22e">T&lt;/span>.&lt;span style="color:#a6e22e">Run&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;standard&amp;#34;&lt;/span>, &lt;span style="color:#66d9ef">func&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">testing&lt;/span>.&lt;span style="color:#a6e22e">T&lt;/span>) {
&lt;span style="color:#a6e22e">t&lt;/span>.&lt;span style="color:#a6e22e">Parallel&lt;/span>()
&lt;span style="color:#a6e22e">expected&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#f92672">&amp;amp;&lt;/span>&lt;span style="color:#a6e22e">User&lt;/span>{&lt;span style="color:#a6e22e">ID&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;something&amp;#34;&lt;/span>}
&lt;span style="color:#a6e22e">mur&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#f92672">&amp;amp;&lt;/span>&lt;span style="color:#a6e22e">mockUserRetriever&lt;/span>{}
&lt;span style="color:#a6e22e">mur&lt;/span>.&lt;span style="color:#a6e22e">On&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;GetUser&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">mock&lt;/span>.&lt;span style="color:#a6e22e">Anything&lt;/span>, &lt;span style="color:#a6e22e">expected&lt;/span>.&lt;span style="color:#a6e22e">ID&lt;/span>).&lt;span style="color:#a6e22e">Return&lt;/span>(&lt;span style="color:#a6e22e">expected&lt;/span>, &lt;span style="color:#66d9ef">nil&lt;/span>)
&lt;span style="color:#75715e">// pass our mock to whatever uses it and test it&amp;#39;s functionality
&lt;/span>&lt;span style="color:#75715e">&lt;/span>
&lt;span style="color:#a6e22e">mock&lt;/span>.&lt;span style="color:#a6e22e">AssertExpectationsForObjects&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span>, &lt;span style="color:#a6e22e">mur&lt;/span>)
})
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>I won’t lie, I’m not deeply impressed with the way that &lt;code>mock.Mock&lt;/code> determines which variables to return in what order, and when things go wrong (i.e. you copy and paste something in the wrong order or have the wrong indices on your return values) it’s VERY hard to suss out, but by and large, &lt;code>testify/mock&lt;/code> does an incredible job of what it needs to do.&lt;/p>
&lt;p>You can avoid having to put &lt;code>mock.Anything&lt;/code> in an expectation call by writing a type matcher. For &lt;code>context.Context&lt;/code>, it would look like this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-go" data-lang="go">&lt;span style="color:#a6e22e">mock&lt;/span>.&lt;span style="color:#a6e22e">MatchedBy&lt;/span>(&lt;span style="color:#66d9ef">func&lt;/span>(&lt;span style="color:#a6e22e">context&lt;/span>.&lt;span style="color:#a6e22e">Context&lt;/span>) &lt;span style="color:#66d9ef">bool&lt;/span> { &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">true&lt;/span> })
&lt;/code>&lt;/pre>&lt;/div>&lt;p>There’s also &lt;code>testify/suite&lt;/code>, which I &lt;em>have&lt;/em> used, but wouldn’t recommend. It’s useful when you have a ton of common prerequisites for tests that need to be spun up ahead of time, but I still think it makes more sense to just make those prerequisites the product of a common function, instead of writing non-standard test code.&lt;/p>
&lt;h3 id="testcontainers">Testcontainers&lt;/h3>
&lt;p>This is a neat tool I’ve only recently started using, but to great effect. It’s very common to have to write code that interfaces with a database, or a k/v store, or an in-memory cache, but before &lt;a href="https://testcontainers.com/">testcontainers&lt;/a>, you either had to write a suite of integration tests that used that code to verify it did what you wanted, or otherwise fly blind in production. With testcontainers, I can spin up ephemeral containers that run a given piece of software and have my code interact with that to verify functionality.&lt;/p>
&lt;p>In practice, it’s not perfect. These tests generally run great on my machine, and in Github Actions, but you must remember that you’re asking your computer to make a fake computer within itself, spin up a piece of software on that fake computer, and handle all the networking shenanigans that go along with interfacing with it, so there’s plenty of opportunity for mishaps and errors to occur.&lt;/p>
&lt;p>Sometimes a testcontainer invocation will just fail, and the reason behind it can be summarized up as: computers are hard. Since they can be a big more error prone than standard tests, I don’t typically write subtests for these. Instead, I tend to write one big function that uses a single instance of the container to test a bunch of related functions.&lt;/p>
&lt;p>Here’s an example of the test for that aforementioned webhook retrieval function:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-go" data-lang="go">&lt;span style="color:#f92672">package&lt;/span> &lt;span style="color:#a6e22e">whatever&lt;/span>
&lt;span style="color:#f92672">import&lt;/span> (
&lt;span style="color:#e6db74">&amp;#34;context&amp;#34;&lt;/span>
&lt;span style="color:#e6db74">&amp;#34;database/sql&amp;#34;&lt;/span>
&lt;span style="color:#e6db74">&amp;#34;fmt&amp;#34;&lt;/span>
&lt;span style="color:#e6db74">&amp;#34;testing&amp;#34;&lt;/span>
&lt;span style="color:#e6db74">&amp;#34;github.com/example/project/pkg/types&amp;#34;&lt;/span>
&lt;span style="color:#e6db74">&amp;#34;github.com/example/project/pkg/types/converters&amp;#34;&lt;/span>
&lt;span style="color:#e6db74">&amp;#34;github.com/example/project/pkg/types/fakes&amp;#34;&lt;/span>
&lt;span style="color:#e6db74">&amp;#34;github.com/stretchr/testify/assert&amp;#34;&lt;/span>
&lt;span style="color:#e6db74">&amp;#34;github.com/stretchr/testify/require&amp;#34;&lt;/span>
&lt;span style="color:#e6db74">&amp;#34;gopkg.in/matryer/try.v1&amp;#34;&lt;/span>
)
&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">buildDatabaseClientForTest&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">testing&lt;/span>.&lt;span style="color:#a6e22e">T&lt;/span>, &lt;span style="color:#a6e22e">ctx&lt;/span> &lt;span style="color:#a6e22e">context&lt;/span>.&lt;span style="color:#a6e22e">Context&lt;/span>) (&lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">Querier&lt;/span>, &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">postgres&lt;/span>.&lt;span style="color:#a6e22e">PostgresContainer&lt;/span>) {
&lt;span style="color:#a6e22e">t&lt;/span>.&lt;span style="color:#a6e22e">Helper&lt;/span>()
&lt;span style="color:#a6e22e">dbUsername&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">fmt&lt;/span>.&lt;span style="color:#a6e22e">Sprintf&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;%d&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">hashStringToNumber&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span>.&lt;span style="color:#a6e22e">Name&lt;/span>()))
&lt;span style="color:#a6e22e">testcontainers&lt;/span>.&lt;span style="color:#a6e22e">Logger&lt;/span> = &lt;span style="color:#a6e22e">log&lt;/span>.&lt;span style="color:#a6e22e">New&lt;/span>(&lt;span style="color:#a6e22e">io&lt;/span>.&lt;span style="color:#a6e22e">Discard&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">log&lt;/span>.&lt;span style="color:#a6e22e">LstdFlags&lt;/span>)
&lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">container&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">postgres&lt;/span>.&lt;span style="color:#a6e22e">PostgresContainer&lt;/span>
&lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">try&lt;/span>.&lt;span style="color:#a6e22e">Do&lt;/span>(&lt;span style="color:#66d9ef">func&lt;/span>(&lt;span style="color:#a6e22e">attempt&lt;/span> &lt;span style="color:#66d9ef">int&lt;/span>) (&lt;span style="color:#66d9ef">bool&lt;/span>, &lt;span style="color:#66d9ef">error&lt;/span>) {
&lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">containerErr&lt;/span> &lt;span style="color:#66d9ef">error&lt;/span>
&lt;span style="color:#a6e22e">container&lt;/span>, &lt;span style="color:#a6e22e">containerErr&lt;/span> = &lt;span style="color:#a6e22e">postgres&lt;/span>.&lt;span style="color:#a6e22e">RunContainer&lt;/span>(
&lt;span style="color:#a6e22e">ctx&lt;/span>,
&lt;span style="color:#a6e22e">testcontainers&lt;/span>.&lt;span style="color:#a6e22e">WithImage&lt;/span>(&lt;span style="color:#a6e22e">defaultImage&lt;/span>),
&lt;span style="color:#a6e22e">postgres&lt;/span>.&lt;span style="color:#a6e22e">WithDatabase&lt;/span>(&lt;span style="color:#a6e22e">splitReverseConcat&lt;/span>(&lt;span style="color:#a6e22e">dbUsername&lt;/span>)),
&lt;span style="color:#a6e22e">postgres&lt;/span>.&lt;span style="color:#a6e22e">WithUsername&lt;/span>(&lt;span style="color:#a6e22e">dbUsername&lt;/span>),
&lt;span style="color:#a6e22e">postgres&lt;/span>.&lt;span style="color:#a6e22e">WithPassword&lt;/span>(&lt;span style="color:#a6e22e">reverseString&lt;/span>(&lt;span style="color:#a6e22e">dbUsername&lt;/span>)),
&lt;span style="color:#a6e22e">testcontainers&lt;/span>.&lt;span style="color:#a6e22e">WithWaitStrategyAndDeadline&lt;/span>(&lt;span style="color:#ae81ff">2&lt;/span>&lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">time&lt;/span>.&lt;span style="color:#a6e22e">Minute&lt;/span>, &lt;span style="color:#a6e22e">wait&lt;/span>.&lt;span style="color:#a6e22e">ForLog&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;database system is ready to accept connections&amp;#34;&lt;/span>).&lt;span style="color:#a6e22e">WithOccurrence&lt;/span>(&lt;span style="color:#ae81ff">2&lt;/span>)),
)
&lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#a6e22e">attempt&lt;/span> &amp;lt; &lt;span style="color:#ae81ff">5&lt;/span>, &lt;span style="color:#a6e22e">containerErr&lt;/span>
})
&lt;span style="color:#a6e22e">require&lt;/span>.&lt;span style="color:#a6e22e">NoError&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span>)
&lt;span style="color:#a6e22e">require&lt;/span>.&lt;span style="color:#a6e22e">NotNil&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span>, &lt;span style="color:#a6e22e">container&lt;/span>)
&lt;span style="color:#a6e22e">connStr&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">container&lt;/span>.&lt;span style="color:#a6e22e">ConnectionString&lt;/span>(&lt;span style="color:#a6e22e">ctx&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;sslmode=disable&amp;#34;&lt;/span>)
&lt;span style="color:#a6e22e">require&lt;/span>.&lt;span style="color:#a6e22e">NoError&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span>)
&lt;span style="color:#a6e22e">dbc&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">ProvideDatabaseClient&lt;/span>(&lt;span style="color:#a6e22e">ctx&lt;/span>, &lt;span style="color:#a6e22e">logging&lt;/span>.&lt;span style="color:#a6e22e">NewNoopLogger&lt;/span>(), &lt;span style="color:#a6e22e">tracing&lt;/span>.&lt;span style="color:#a6e22e">NewNoopTracerProvider&lt;/span>(), &lt;span style="color:#f92672">&amp;amp;&lt;/span>&lt;span style="color:#a6e22e">config&lt;/span>.&lt;span style="color:#a6e22e">Config&lt;/span>{&lt;span style="color:#a6e22e">ConnectionDetails&lt;/span>: &lt;span style="color:#a6e22e">connStr&lt;/span>, &lt;span style="color:#a6e22e">RunMigrations&lt;/span>: &lt;span style="color:#66d9ef">true&lt;/span>, &lt;span style="color:#a6e22e">OAuth2TokenEncryptionKey&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;blahblahblahblahblahblahblahblah&amp;#34;&lt;/span>})
&lt;span style="color:#a6e22e">require&lt;/span>.&lt;span style="color:#a6e22e">NoError&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span>)
&lt;span style="color:#a6e22e">require&lt;/span>.&lt;span style="color:#a6e22e">NotNil&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span>, &lt;span style="color:#a6e22e">dbc&lt;/span>)
&lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#a6e22e">dbc&lt;/span>, &lt;span style="color:#a6e22e">container&lt;/span>
}
&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">createWebhookForTest&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">testing&lt;/span>.&lt;span style="color:#a6e22e">T&lt;/span>, &lt;span style="color:#a6e22e">ctx&lt;/span> &lt;span style="color:#a6e22e">context&lt;/span>.&lt;span style="color:#a6e22e">Context&lt;/span>, &lt;span style="color:#a6e22e">exampleWebhook&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">types&lt;/span>.&lt;span style="color:#a6e22e">Webhook&lt;/span>, &lt;span style="color:#a6e22e">dbc&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">Querier&lt;/span>) &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">types&lt;/span>.&lt;span style="color:#a6e22e">Webhook&lt;/span> {
&lt;span style="color:#a6e22e">t&lt;/span>.&lt;span style="color:#a6e22e">Helper&lt;/span>()
&lt;span style="color:#75715e">// create
&lt;/span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">exampleWebhook&lt;/span> &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span> {
&lt;span style="color:#a6e22e">exampleWebhook&lt;/span> = &lt;span style="color:#a6e22e">fakes&lt;/span>.&lt;span style="color:#a6e22e">BuildFakeWebhook&lt;/span>()
}
&lt;span style="color:#a6e22e">dbInput&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">converters&lt;/span>.&lt;span style="color:#a6e22e">ConvertWebhookToWebhookDatabaseCreationInput&lt;/span>(&lt;span style="color:#a6e22e">exampleWebhook&lt;/span>)
&lt;span style="color:#a6e22e">created&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">dbc&lt;/span>.&lt;span style="color:#a6e22e">CreateWebhook&lt;/span>(&lt;span style="color:#a6e22e">ctx&lt;/span>, &lt;span style="color:#a6e22e">dbInput&lt;/span>)
&lt;span style="color:#a6e22e">assert&lt;/span>.&lt;span style="color:#a6e22e">NoError&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span>)
&lt;span style="color:#a6e22e">require&lt;/span>.&lt;span style="color:#a6e22e">NotNil&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span>, &lt;span style="color:#a6e22e">created&lt;/span>)
&lt;span style="color:#a6e22e">assert&lt;/span>.&lt;span style="color:#a6e22e">Equal&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span>, &lt;span style="color:#a6e22e">exampleWebhook&lt;/span>, &lt;span style="color:#a6e22e">created&lt;/span>)
&lt;span style="color:#a6e22e">webhook&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">dbc&lt;/span>.&lt;span style="color:#a6e22e">GetWebhook&lt;/span>(&lt;span style="color:#a6e22e">ctx&lt;/span>, &lt;span style="color:#a6e22e">created&lt;/span>.&lt;span style="color:#a6e22e">ID&lt;/span>, &lt;span style="color:#a6e22e">created&lt;/span>.&lt;span style="color:#a6e22e">BelongsToAccount&lt;/span>)
&lt;span style="color:#a6e22e">assert&lt;/span>.&lt;span style="color:#a6e22e">NoError&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span>)
&lt;span style="color:#a6e22e">assert&lt;/span>.&lt;span style="color:#a6e22e">Equal&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span>, &lt;span style="color:#a6e22e">webhook&lt;/span>, &lt;span style="color:#a6e22e">exampleWebhook&lt;/span>)
&lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#a6e22e">created&lt;/span>
}
&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">TestQuerier_Integration_Webhooks&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">testing&lt;/span>.&lt;span style="color:#a6e22e">T&lt;/span>) {
&lt;span style="color:#a6e22e">ctx&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">context&lt;/span>.&lt;span style="color:#a6e22e">Background&lt;/span>()
&lt;span style="color:#a6e22e">dbc&lt;/span>, &lt;span style="color:#a6e22e">container&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">buildDatabaseClientForTest&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span>, &lt;span style="color:#a6e22e">ctx&lt;/span>)
&lt;span style="color:#a6e22e">databaseURI&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">container&lt;/span>.&lt;span style="color:#a6e22e">ConnectionString&lt;/span>(&lt;span style="color:#a6e22e">ctx&lt;/span>)
&lt;span style="color:#a6e22e">require&lt;/span>.&lt;span style="color:#a6e22e">NoError&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span>)
&lt;span style="color:#a6e22e">require&lt;/span>.&lt;span style="color:#a6e22e">NotEmpty&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span>, &lt;span style="color:#a6e22e">databaseURI&lt;/span>)
&lt;span style="color:#66d9ef">defer&lt;/span> &lt;span style="color:#66d9ef">func&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">testing&lt;/span>.&lt;span style="color:#a6e22e">T&lt;/span>) {
&lt;span style="color:#a6e22e">t&lt;/span>.&lt;span style="color:#a6e22e">Helper&lt;/span>()
&lt;span style="color:#a6e22e">assert&lt;/span>.&lt;span style="color:#a6e22e">NoError&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span>, &lt;span style="color:#a6e22e">container&lt;/span>.&lt;span style="color:#a6e22e">Terminate&lt;/span>(&lt;span style="color:#a6e22e">ctx&lt;/span>))
}(&lt;span style="color:#a6e22e">t&lt;/span>)
&lt;span style="color:#a6e22e">user&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">createUserForTest&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span>, &lt;span style="color:#a6e22e">ctx&lt;/span>, &lt;span style="color:#66d9ef">nil&lt;/span>, &lt;span style="color:#a6e22e">dbc&lt;/span>)
&lt;span style="color:#a6e22e">accountID&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">dbc&lt;/span>.&lt;span style="color:#a6e22e">GetDefaultAccountIDForUser&lt;/span>(&lt;span style="color:#a6e22e">ctx&lt;/span>, &lt;span style="color:#a6e22e">user&lt;/span>.&lt;span style="color:#a6e22e">ID&lt;/span>)
&lt;span style="color:#a6e22e">require&lt;/span>.&lt;span style="color:#a6e22e">NoError&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span>)
&lt;span style="color:#a6e22e">require&lt;/span>.&lt;span style="color:#a6e22e">NotEmpty&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span>, &lt;span style="color:#a6e22e">accountID&lt;/span>)
&lt;span style="color:#a6e22e">exampleWebhook&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">fakes&lt;/span>.&lt;span style="color:#a6e22e">BuildFakeWebhook&lt;/span>()
&lt;span style="color:#a6e22e">exampleWebhook&lt;/span>.&lt;span style="color:#a6e22e">BelongsToAccount&lt;/span> = &lt;span style="color:#a6e22e">accountID&lt;/span>
&lt;span style="color:#a6e22e">createdWebhooks&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> []&lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">types&lt;/span>.&lt;span style="color:#a6e22e">Webhook&lt;/span>{}
&lt;span style="color:#75715e">// create
&lt;/span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#a6e22e">createdWebhooks&lt;/span> = append(&lt;span style="color:#a6e22e">createdWebhooks&lt;/span>, &lt;span style="color:#a6e22e">createWebhookForTest&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span>, &lt;span style="color:#a6e22e">ctx&lt;/span>, &lt;span style="color:#a6e22e">exampleWebhook&lt;/span>, &lt;span style="color:#a6e22e">dbc&lt;/span>))
&lt;span style="color:#75715e">// other tests would go here
&lt;/span>&lt;span style="color:#75715e">&lt;/span>
&lt;span style="color:#75715e">// delete
&lt;/span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#66d9ef">for&lt;/span> &lt;span style="color:#a6e22e">_&lt;/span>, &lt;span style="color:#a6e22e">webhook&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#66d9ef">range&lt;/span> &lt;span style="color:#a6e22e">createdWebhooks&lt;/span> {
&lt;span style="color:#a6e22e">assert&lt;/span>.&lt;span style="color:#a6e22e">NoError&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span>, &lt;span style="color:#a6e22e">dbc&lt;/span>.&lt;span style="color:#a6e22e">ArchiveWebhook&lt;/span>(&lt;span style="color:#a6e22e">ctx&lt;/span>, &lt;span style="color:#a6e22e">webhook&lt;/span>.&lt;span style="color:#a6e22e">ID&lt;/span>, &lt;span style="color:#a6e22e">accountID&lt;/span>))
&lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">exists&lt;/span> &lt;span style="color:#66d9ef">bool&lt;/span>
&lt;span style="color:#a6e22e">exists&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> = &lt;span style="color:#a6e22e">dbc&lt;/span>.&lt;span style="color:#a6e22e">WebhookExists&lt;/span>(&lt;span style="color:#a6e22e">ctx&lt;/span>, &lt;span style="color:#a6e22e">webhook&lt;/span>.&lt;span style="color:#a6e22e">ID&lt;/span>, &lt;span style="color:#a6e22e">accountID&lt;/span>)
&lt;span style="color:#a6e22e">assert&lt;/span>.&lt;span style="color:#a6e22e">NoError&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span>)
&lt;span style="color:#a6e22e">assert&lt;/span>.&lt;span style="color:#a6e22e">False&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span>, &lt;span style="color:#a6e22e">exists&lt;/span>)
&lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">y&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">types&lt;/span>.&lt;span style="color:#a6e22e">Webhook&lt;/span>
&lt;span style="color:#a6e22e">y&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> = &lt;span style="color:#a6e22e">dbc&lt;/span>.&lt;span style="color:#a6e22e">GetWebhook&lt;/span>(&lt;span style="color:#a6e22e">ctx&lt;/span>, &lt;span style="color:#a6e22e">webhook&lt;/span>.&lt;span style="color:#a6e22e">ID&lt;/span>, &lt;span style="color:#a6e22e">accountID&lt;/span>)
&lt;span style="color:#a6e22e">assert&lt;/span>.&lt;span style="color:#a6e22e">Nil&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span>, &lt;span style="color:#a6e22e">y&lt;/span>)
&lt;span style="color:#a6e22e">assert&lt;/span>.&lt;span style="color:#a6e22e">Error&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span>)
&lt;span style="color:#a6e22e">assert&lt;/span>.&lt;span style="color:#a6e22e">ErrorIs&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span>, &lt;span style="color:#a6e22e">sql&lt;/span>.&lt;span style="color:#a6e22e">ErrNoRows&lt;/span>)
}
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>(I acknowledge that this code is missing some function definitions, but it&amp;rsquo;s meant for you to get an idea, not copy/paste)&lt;/p>
&lt;h3 id="failure-cases">Failure Cases&lt;/h3>
&lt;p>Some functions in the Go standard library return errors that you might surface to higher-level call sites, but in order to test that those errors are caught and returned, Here are some ways I manage to achieve failures for very specific circumstances in Go.&lt;/p>
&lt;h3 id="json-encoding">JSON Encoding&lt;/h3>
&lt;p>It&amp;rsquo;s a pretty common operation in Go code to take an instance of a given struct and render it as JSON. This function is capable of returning an error in the event the source struct is unrepresentable, but it&amp;rsquo;s actually pretty hard to make a struct unrepresentable in Go. You basically have to do it on purpose.&lt;/p>
&lt;p>The way I achieve this is by using &lt;a href="https://pkg.go.dev/encoding/json#Number">&lt;code>json.Number&lt;/code>&lt;/a>, a string alias meant to represent numbers, and giving it a non-numerical value. Here&amp;rsquo;s what that looks like:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-go" data-lang="go">&lt;span style="color:#66d9ef">type&lt;/span> &lt;span style="color:#a6e22e">BreakableStruct&lt;/span> &lt;span style="color:#66d9ef">struct&lt;/span> {
&lt;span style="color:#a6e22e">Thing&lt;/span> &lt;span style="color:#a6e22e">json&lt;/span>.&lt;span style="color:#a6e22e">Number&lt;/span>
}
&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">TestRenderToJSON&lt;/span>(&lt;span style="color:#a6e22e">T&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">testing&lt;/span>.&lt;span style="color:#a6e22e">T&lt;/span>) {
&lt;span style="color:#a6e22e">T&lt;/span>.&lt;span style="color:#a6e22e">Parallel&lt;/span>()
&lt;span style="color:#a6e22e">T&lt;/span>.&lt;span style="color:#a6e22e">Run&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;with invalid structure&amp;#34;&lt;/span>, &lt;span style="color:#66d9ef">func&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">testing&lt;/span>.&lt;span style="color:#a6e22e">T&lt;/span>) {
&lt;span style="color:#a6e22e">t&lt;/span>.&lt;span style="color:#a6e22e">Parallel&lt;/span>()
&lt;span style="color:#a6e22e">x&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#f92672">&amp;amp;&lt;/span>&lt;span style="color:#a6e22e">BreakableStruct&lt;/span>{&lt;span style="color:#a6e22e">Thing&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;stuff&amp;#34;&lt;/span>}
&lt;span style="color:#a6e22e">actual&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">json&lt;/span>.&lt;span style="color:#a6e22e">Marshal&lt;/span>(&lt;span style="color:#a6e22e">x&lt;/span>)
&lt;span style="color:#a6e22e">assert&lt;/span>.&lt;span style="color:#a6e22e">Nil&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span>, &lt;span style="color:#a6e22e">actual&lt;/span>)
&lt;span style="color:#a6e22e">assert&lt;/span>.&lt;span style="color:#a6e22e">Error&lt;/span>(&lt;span style="color:#a6e22e">t&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span>)
})
}
&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="url-parsing">URL Parsing&lt;/h3>
&lt;p>Parsing a URL in Go &lt;a href="https://pkg.go.dev/net/url#Parse">returns an error&lt;/a>, but trying to force this error to occur was very confusing to me. I had to dive into the actual test cases in the standard library for &lt;a href="https://pkg.go.dev/net/url">&lt;code>net/url&lt;/code>&lt;/a>. Here are some inputs you might think return an error from &lt;code>url.Parse&lt;/code>, but actually don&amp;rsquo;t:&lt;/p>
&lt;ul>
&lt;li>an empty string&lt;/li>
&lt;li>a URL with double dots in the path (i.e. &lt;code>https://google.com/../../var/data&lt;/code>)&lt;/li>
&lt;li>just a scheme (i.e. &lt;code>https://&lt;/code>)&lt;/li>
&lt;li>a URL with emojis in it (i.e. &lt;code>https://😘.🔥&lt;/code>)&lt;/li>
&lt;li>a URL with an invalid scheme (i.e. &lt;code>farts://&lt;/code>)&lt;/li>
&lt;/ul>
&lt;p>All of these actually don&amp;rsquo;t return an error. The only way I was able to force it to occur by using:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-go" data-lang="go">&lt;span style="color:#a6e22e">fmt&lt;/span>.&lt;span style="color:#a6e22e">Sprintf&lt;/span>(&lt;span style="color:#e6db74">`%s://`&lt;/span>, string(byte(&lt;span style="color:#ae81ff">127&lt;/span>)))
&lt;/code>&lt;/pre>&lt;/div>&lt;p>For whatever reason, this causes &lt;code>url.Parse&lt;/code> to fail.&lt;/p>
&lt;h3 id="conclusion">Conclusion&lt;/h3>
&lt;p>In many other languages, you have to not only evaluate testing libraries, but also write your tests in a style that complies with that library&amp;rsquo;s expectations. Gophers are blessed to have a thoroughly adequate solution out-of-the-box, and even further blessed to have an active ecosystem where folks are making in-depth testing a walk in the park.&lt;/p></content></item><item><title>Go’s Error Handling Is Perfect, Actually</title><link>https://blog.verygoodsoftwarenotvirus.ru/posts/errors-in-go/</link><pubDate>Mon, 12 Feb 2024 21:34:37 -0600</pubDate><guid>https://blog.verygoodsoftwarenotvirus.ru/posts/errors-in-go/</guid><description>Spend any amount of time in programming circles, and just as the sun rises and falls, you are certain to hear someone complain about error handling in Go. These complaints are, anecdotally, rarely well thought out suggestions on what error handling could or should be like in a language like Go, but often merely boil down to “I don’t like having to look at it”.
You’ll mainly see people complain about having to litter their apps with if err !</description><content>&lt;p>Spend any amount of time in programming circles, and just as the sun rises and falls, you are certain to hear someone complain about error handling in Go. These complaints are, anecdotally, rarely well thought out suggestions on what error handling could or should be like in a language like Go, but often merely boil down to “I don’t like having to look at it”.&lt;/p>
&lt;p>You’ll mainly see people complain about having to litter their apps with &lt;code>if err != nil&lt;/code>, how they feel it makes the code verbose, and how they miss just writing ten very consequential lines one after the other in more dynamic languages like Python or Javascript.&lt;/p>
&lt;p>Less often, you’ll see folks bemoan the lack of a stdlib-defined &lt;a href="https://doc.rust-lang.org/std/result/">Result type&lt;/a>, which would have either a value or an error, like in Rust. Every now and then you’ll see someone bemoan how error handling works in highly concurrent Go programs, or how &lt;a href="https://stackoverflow.com/questions/53892508/golang-returning-nil-does-not-return-nil">some errors can be nil and not-nil simultaneously&lt;/a>.&lt;/p>
&lt;h2 id="in-defense-of-if-err--nil">In defense of &lt;code>if err != nil&lt;/code>&lt;/h2>
&lt;p>I have a spicy hot take on the matter: Go’s error handling is not just fine, not just great, but actually perfect for 99% of programs. (I will readily admit the last thing about error nilness is annoying, but in the nearly decade of time I’ve spent writing Go, I can think of perhaps a handful of times where it actually reared its head. I also ascribe that issue to the subject of nil in Go, which is a whole other topic, this article is about errors.)&lt;/p>
&lt;p>I regard the matter like this: do you want to know when something bad can happen in your program, or don’t you? I asked ChatGPT to &lt;code>Please write some python code for me that goes through every row of a CSV file, performs a get request for the value in the first column, and parses the responses as a JSON map of strings to strings&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-python" data-lang="python">&lt;span style="color:#f92672">import&lt;/span> csv
&lt;span style="color:#f92672">import&lt;/span> requests
&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">process_csv&lt;/span>(csv_file):
&lt;span style="color:#66d9ef">with&lt;/span> open(csv_file, &lt;span style="color:#e6db74">&amp;#39;r&amp;#39;&lt;/span>) &lt;span style="color:#66d9ef">as&lt;/span> file:
csv_reader &lt;span style="color:#f92672">=&lt;/span> csv&lt;span style="color:#f92672">.&lt;/span>reader(file)
next(csv_reader)
&lt;span style="color:#66d9ef">for&lt;/span> row &lt;span style="color:#f92672">in&lt;/span> csv_reader:
url &lt;span style="color:#f92672">=&lt;/span> row[&lt;span style="color:#ae81ff">0&lt;/span>]
data &lt;span style="color:#f92672">=&lt;/span> requests&lt;span style="color:#f92672">.&lt;/span>get(url)&lt;span style="color:#f92672">.&lt;/span>json()
print(data)
&lt;span style="color:#66d9ef">if&lt;/span> __name__ &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#e6db74">&amp;#34;__main__&amp;#34;&lt;/span>:
csv_file &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;your_csv_file.csv&amp;#34;&lt;/span>
process_csv(csv_file)
&lt;/code>&lt;/pre>&lt;/div>&lt;p>There’s basically no error handling here, but there’s a ton that can go wrong. What happens if the file doesn’t exist, or is corrupted? What happens if you don’t have permissions to read it? What happens if the &lt;code>GET&lt;/code> request fails? What happens if the response body isn’t valid JSON, or doesn’t match the expected shape? The answer, in the case of Python, is an exception gets thrown, and since there’s no code to catch it, it’s handled by the broader runtime, printing a stack trace.&lt;/p>
&lt;pre tabindex="0">&lt;code>Traceback (most recent call last):
File &amp;quot;main.py&amp;quot;, line 15, in &amp;lt;module&amp;gt;
process_csv(csv_file)
File &amp;quot;main.py&amp;quot;, line 5, in process_csv
with open(csv_file, 'r') as file:
^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: 'your_csv_file.csv'
&lt;/code>&lt;/pre>&lt;p>Note that for any sufficiently complex program that invokes many dependencies, this stack trace will be so far down the chain that you may not even see where you’re making the call that causes it.&lt;/p>
&lt;p>I asked ChatGPT to write the same code, but in Go:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-go" data-lang="go">&lt;span style="color:#f92672">package&lt;/span> &lt;span style="color:#a6e22e">main&lt;/span>
&lt;span style="color:#f92672">import&lt;/span> (
&lt;span style="color:#e6db74">&amp;#34;encoding/csv&amp;#34;&lt;/span>
&lt;span style="color:#e6db74">&amp;#34;encoding/json&amp;#34;&lt;/span>
&lt;span style="color:#e6db74">&amp;#34;fmt&amp;#34;&lt;/span>
&lt;span style="color:#e6db74">&amp;#34;io&amp;#34;&lt;/span>
&lt;span style="color:#e6db74">&amp;#34;net/http&amp;#34;&lt;/span>
&lt;span style="color:#e6db74">&amp;#34;os&amp;#34;&lt;/span>
)
&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">fetchData&lt;/span>(&lt;span style="color:#a6e22e">url&lt;/span> &lt;span style="color:#66d9ef">string&lt;/span>) (&lt;span style="color:#66d9ef">map&lt;/span>[&lt;span style="color:#66d9ef">string&lt;/span>]&lt;span style="color:#66d9ef">string&lt;/span>, &lt;span style="color:#66d9ef">error&lt;/span>) {
&lt;span style="color:#a6e22e">response&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">http&lt;/span>.&lt;span style="color:#a6e22e">Get&lt;/span>(&lt;span style="color:#a6e22e">url&lt;/span>)
&lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span> {
&lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span>
}
&lt;span style="color:#66d9ef">defer&lt;/span> &lt;span style="color:#a6e22e">response&lt;/span>.&lt;span style="color:#a6e22e">Body&lt;/span>.&lt;span style="color:#a6e22e">Close&lt;/span>()
&lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">data&lt;/span> &lt;span style="color:#66d9ef">map&lt;/span>[&lt;span style="color:#66d9ef">string&lt;/span>]&lt;span style="color:#66d9ef">string&lt;/span>
&lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">json&lt;/span>.&lt;span style="color:#a6e22e">NewDecoder&lt;/span>(&lt;span style="color:#a6e22e">response&lt;/span>.&lt;span style="color:#a6e22e">Body&lt;/span>).&lt;span style="color:#a6e22e">Decode&lt;/span>(&lt;span style="color:#f92672">&amp;amp;&lt;/span>&lt;span style="color:#a6e22e">data&lt;/span>); &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span> {
&lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span>
}
&lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#a6e22e">data&lt;/span>, &lt;span style="color:#66d9ef">nil&lt;/span>
}
&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">processCSV&lt;/span>(&lt;span style="color:#a6e22e">csvFile&lt;/span> &lt;span style="color:#66d9ef">string&lt;/span>) &lt;span style="color:#66d9ef">error&lt;/span> {
&lt;span style="color:#a6e22e">file&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">os&lt;/span>.&lt;span style="color:#a6e22e">Open&lt;/span>(&lt;span style="color:#a6e22e">csvFile&lt;/span>)
&lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span> {
&lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#a6e22e">err&lt;/span>
}
&lt;span style="color:#66d9ef">defer&lt;/span> &lt;span style="color:#a6e22e">file&lt;/span>.&lt;span style="color:#a6e22e">Close&lt;/span>()
&lt;span style="color:#a6e22e">reader&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">csv&lt;/span>.&lt;span style="color:#a6e22e">NewReader&lt;/span>(&lt;span style="color:#a6e22e">file&lt;/span>)
&lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">_&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> = &lt;span style="color:#a6e22e">reader&lt;/span>.&lt;span style="color:#a6e22e">Read&lt;/span>(); &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span> &lt;span style="color:#f92672">&amp;amp;&amp;amp;&lt;/span> &lt;span style="color:#a6e22e">err&lt;/span>.&lt;span style="color:#a6e22e">Error&lt;/span>() &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;EOF&amp;#34;&lt;/span> {
&lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#a6e22e">err&lt;/span>
}
&lt;span style="color:#66d9ef">for&lt;/span> {
&lt;span style="color:#a6e22e">row&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">reader&lt;/span>.&lt;span style="color:#a6e22e">Read&lt;/span>()
&lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span> {
&lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">err&lt;/span>.&lt;span style="color:#a6e22e">Error&lt;/span>() &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#a6e22e">io&lt;/span>.&lt;span style="color:#a6e22e">EOF&lt;/span> {
&lt;span style="color:#66d9ef">break&lt;/span>
}
&lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#a6e22e">err&lt;/span>
}
&lt;span style="color:#a6e22e">url&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">row&lt;/span>[&lt;span style="color:#ae81ff">0&lt;/span>]
&lt;span style="color:#a6e22e">data&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">fetchData&lt;/span>(&lt;span style="color:#a6e22e">url&lt;/span>)
&lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span> {
&lt;span style="color:#a6e22e">fmt&lt;/span>.&lt;span style="color:#a6e22e">Printf&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;Failed to fetch data from %s: %v\n&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">url&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span>)
&lt;span style="color:#66d9ef">continue&lt;/span>
}
&lt;span style="color:#a6e22e">fmt&lt;/span>.&lt;span style="color:#a6e22e">Printf&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;Data from %s:\n&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">url&lt;/span>)
&lt;span style="color:#66d9ef">for&lt;/span> &lt;span style="color:#a6e22e">key&lt;/span>, &lt;span style="color:#a6e22e">value&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#66d9ef">range&lt;/span> &lt;span style="color:#a6e22e">data&lt;/span> {
&lt;span style="color:#a6e22e">fmt&lt;/span>.&lt;span style="color:#a6e22e">Printf&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;%s: %s\n&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">key&lt;/span>, &lt;span style="color:#a6e22e">value&lt;/span>)
}
&lt;span style="color:#a6e22e">fmt&lt;/span>.&lt;span style="color:#a6e22e">Println&lt;/span>()
}
&lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span>
}
&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">main&lt;/span>() {
&lt;span style="color:#a6e22e">csvFile&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;your_csv_file.csv&amp;#34;&lt;/span>
&lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">processCSV&lt;/span>(&lt;span style="color:#a6e22e">csvFile&lt;/span>); &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span> {
&lt;span style="color:#a6e22e">fmt&lt;/span>.&lt;span style="color:#a6e22e">Printf&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;Error processing CSV: %v\n&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span>)
}
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>Wouldn’t you know it, we have all the aforementioned errors handled! If there’s something wrong with the file, that will get surfaced. If there’s something wrong with the &lt;code>GET&lt;/code> request, that will be surfaced. If the response doesn’t contain valid JSON, that will get surfaced. Is there more error handling code in the Go version? Yes, because that’s how Go is idiomatically written.&lt;/p>
&lt;p>The Python I posted above, while it certainly could be written a better way, doesn’t look meaningfully different from 90%+ of the Python I’ve ever had to work with professionally. My only major gripe with the Go variant is that it doesn’t check the length of row before accessing that index, but that’s ChatGPT for you.&lt;/p>
&lt;p>There are folks who will say that the Go version is less readable than the Python version. For me, this depends on how you measure readability. If you measure it from the time you first see the code to when you understand what it’s trying to accomplish, I could entertain the suggestion that Python wins. If you measure it from the perspective of how long it takes to suss out what the different execution paths or outcomes could possibly be from a given chunk of code, I think Go wins.&lt;/p>
&lt;p>Even if you didn’t know that file reads could fail, or network requests could fail, you would understand that both are possible after reading the Go code, but not from the Python code.&lt;/p>
&lt;p>I’d rather see a billion &lt;code>if err != nil&lt;/code> statements in my code than have an error occur that I cannot quickly and effectively diagnose because it comes with a bunch of unrelated noise.&lt;/p>
&lt;h2 id="result-types-in-go">Result types in Go&lt;/h2>
&lt;p>I&amp;rsquo;ve seen the &lt;a href="https://transitiontech.ca/random/RIIR">RIIR&lt;/a> crew suggest that one of the things that would make Go tolerable to them would be some equivalent of the &lt;code>Result&lt;/code> type in Rust. The &lt;code>Result&lt;/code> type allows for you to return one value that can contain either the anticipated value, or an error, but not both. So you sort of get the chance to collapse the standard &lt;code>res, err := someOperation()&lt;/code> pattern you see in a lot of Go into just &lt;code>res := someOperation()&lt;/code>.&lt;/p>
&lt;p>I asked ChatGPT to &lt;code>Please write some rust code that returns a Result container with a string, have it check the current time and return an error if the unix timestamp is even or a positive result if the unix timestamp is odd&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-rust" data-lang="rust">&lt;span style="color:#66d9ef">use&lt;/span> std::time::{SystemTime, UNIX_EPOCH};
&lt;span style="color:#66d9ef">fn&lt;/span> &lt;span style="color:#a6e22e">check_current_time&lt;/span>() -&amp;gt; Result&lt;span style="color:#f92672">&amp;lt;&lt;/span>String, String&lt;span style="color:#f92672">&amp;gt;&lt;/span> {
&lt;span style="color:#66d9ef">let&lt;/span> current_time &lt;span style="color:#f92672">=&lt;/span> SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
&lt;span style="color:#66d9ef">if&lt;/span> current_time &lt;span style="color:#f92672">%&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span> &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> {
Err(&lt;span style="color:#e6db74">&amp;#34;Even timestamp&amp;#34;&lt;/span>.to_string())
} &lt;span style="color:#66d9ef">else&lt;/span> {
Ok(&lt;span style="color:#e6db74">&amp;#34;Odd timestamp&amp;#34;&lt;/span>.to_string())
}
}
&lt;span style="color:#66d9ef">fn&lt;/span> &lt;span style="color:#a6e22e">main&lt;/span>() {
&lt;span style="color:#66d9ef">match&lt;/span> check_current_time() {
Ok(msg) &lt;span style="color:#f92672">=&amp;gt;&lt;/span> println!(&lt;span style="color:#e6db74">&amp;#34;Result: {}&amp;#34;&lt;/span>, msg),
Err(err) &lt;span style="color:#f92672">=&amp;gt;&lt;/span> println!(&lt;span style="color:#e6db74">&amp;#34;Error: {}&amp;#34;&lt;/span>, err),
}
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>This is, frankly, neat, and not a bad idea. There’s an alternative universe where Go had generics from the jump and the &lt;code>Result&lt;/code> type was implemented/utilized in the standard library, and it’s not the worst outcome I could think of. There &lt;a href="https://github.com/MisterKaiou/go-functional">are libraries&lt;/a> you can use now, post-generics, that do just this, and &lt;a href="https://github.com/golang/go/issues/19991">an old and long-closed proposal&lt;/a> to add it to Go specifically cites the Rust variant.&lt;/p>
&lt;p>The only opposition I have to the Result type in Go is that we wouldn’t be able to make use of it in the standard library without either breaking backwards compatibility, writing &lt;code>Result&lt;/code> variants of existing API calls (so &lt;code>NewRequest&lt;/code>, &lt;code>NewRequestWithContext&lt;/code>, and &lt;code>NewRequestWithContextAndResult&lt;/code>), or issuing new &lt;code>/v2&lt;/code> variants of existing packages (like the &lt;a href="https://tip.golang.org/doc/go1.22#math_rand_v2">recently-released &lt;code>math/rand/v2&lt;/code> package&lt;/a>), which then means we’ll have some libraries and programs that use the old style with one return value, some with the new style, and many instances of confused programmers using the wrong one. It would be as close to a Go equivalent of the Python 2/3 transition debacle as I think we could manage.&lt;/p>
&lt;p>I also don’t really think it meaningfully improves readability. Compare the above Rust code to the Go equivalent:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-go" data-lang="go">&lt;span style="color:#f92672">package&lt;/span> &lt;span style="color:#a6e22e">main&lt;/span>
&lt;span style="color:#f92672">import&lt;/span> (
&lt;span style="color:#e6db74">&amp;#34;errors&amp;#34;&lt;/span>
&lt;span style="color:#e6db74">&amp;#34;fmt&amp;#34;&lt;/span>
&lt;span style="color:#e6db74">&amp;#34;time&amp;#34;&lt;/span>
)
&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">checkCurrentTime&lt;/span>() (&lt;span style="color:#66d9ef">string&lt;/span>, &lt;span style="color:#66d9ef">error&lt;/span>) {
&lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">time&lt;/span>.&lt;span style="color:#a6e22e">Now&lt;/span>().&lt;span style="color:#a6e22e">Unix&lt;/span>()&lt;span style="color:#f92672">%&lt;/span>&lt;span style="color:#ae81ff">2&lt;/span> &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> {
&lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">errors&lt;/span>.&lt;span style="color:#a6e22e">New&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;even timestamp&amp;#34;&lt;/span>)
}
&lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#a6e22e">time&lt;/span>.&lt;span style="color:#a6e22e">Now&lt;/span>().&lt;span style="color:#a6e22e">Format&lt;/span>(&lt;span style="color:#a6e22e">time&lt;/span>.&lt;span style="color:#a6e22e">Kitchen&lt;/span>), &lt;span style="color:#66d9ef">nil&lt;/span>
}
&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">main&lt;/span>() {
&lt;span style="color:#a6e22e">result&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">checkCurrentTime&lt;/span>()
&lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span> {
&lt;span style="color:#a6e22e">fmt&lt;/span>.&lt;span style="color:#a6e22e">Println&lt;/span>(&lt;span style="color:#a6e22e">err&lt;/span>)
} &lt;span style="color:#66d9ef">else&lt;/span> {
&lt;span style="color:#a6e22e">fmt&lt;/span>.&lt;span style="color:#a6e22e">Println&lt;/span>(&lt;span style="color:#a6e22e">result&lt;/span>)
}
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>Our main function is 6 lines, compared to Rust’s 4. I suppose that adds up over time and with a larger project, but I still just don’t think it’s the massive win for readability that some folks proclaim it to be.&lt;/p>
&lt;h2 id="conclusion">Conclusion&lt;/h2>
&lt;p>None of this post was meant to denigrate Python, Rust, Javascript, or any other language, or its fans, or indeed anything at all. I just think a lot of the criticism around this particular element of the Go programming language is missing the forest for the trees.&lt;/p></content></item><item><title>Preventing generated files from causing problems</title><link>https://blog.verygoodsoftwarenotvirus.ru/posts/generated-files/</link><pubDate>Tue, 29 Aug 2023 23:40:19 -0600</pubDate><guid>https://blog.verygoodsoftwarenotvirus.ru/posts/generated-files/</guid><description>The premise Generating files is an inevitable consequence of writing most software, but in particular Go. For a very long time, the collective wisdom when asked about support for generics could be summarized as “just generate files for the types you need, that’s what generics support in the compiler would amount to,” and the ethos stuck with the community.
In my primary side project, for instance, I generate service configs from Config literals, so that I know they will properly unmarshal when the service tries to load them.</description><content>&lt;h1 id="the-premise">The premise&lt;/h1>
&lt;p>Generating files is an inevitable consequence of writing most software, but in particular Go. For a very long time, the collective wisdom when asked about support for generics could be summarized as &lt;a href="https://www.calhoun.io/using-code-generation-to-survive-without-generics-in-go/">“just generate files for the types you need&lt;/a>, that’s what generics support in the compiler would amount to,” and the ethos stuck with the community.&lt;/p>
&lt;p>In my primary side project, for instance, I generate service configs from &lt;code>Config&lt;/code> literals, so that I know they will properly unmarshal when the service tries to load them. I also use &lt;a href="https://github.com/google/wire">wire&lt;/a> for dependency injection, which as you may have guessed, generates a big function with a bunch of constructors invoked in the correct order. A not-unpopular choice of library for interacting with SQL databases is the beloved &lt;a href="https://sqlc.dev/">sqlc&lt;/a>, which generates Go code from properly annotated SQL query files.&lt;/p>
&lt;h1 id="the-problem">The problem&lt;/h1>
&lt;p>The problem with generated files is that they can become out of data. Someone on the team changes a query in a PR, but doesn’t re-run the &lt;code>sqlc&lt;/code> compiler to produce fresh output. Someone changes a config, which causes subsequent deployments to fail because the name of a JSON field no longer matches. Someone changes the order of parameters in a constructor, and all of its tests, but doesn’t re-run wire to ensure the build step fails. (The last one is actually fairly interceptable, assuming you’re doing the bare minimum of trying to build your binary in CI, but I’m not here to judge anybody’s shortcomings).&lt;/p>
&lt;h1 id="the-solution">THe solution&lt;/h1>
&lt;p>How do I prevent these inevitabilities from ruining any given evening? I require that when myself or a team member introduces a new class of generated files, we also introduce a CI step that runs the required generation command, and fails if git detects changes afterwards. Here’s my sqlc setup, for instance:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="color:#f92672">on&lt;/span>:
&lt;span style="color:#f92672">pull_request&lt;/span>:
&lt;span style="color:#f92672">paths&lt;/span>:
- &lt;span style="color:#ae81ff">cmd/**&lt;/span>
- &lt;span style="color:#ae81ff">pkg/**&lt;/span>
- &lt;span style="color:#ae81ff">internal/**&lt;/span>
&lt;span style="color:#f92672">concurrency&lt;/span>:
&lt;span style="color:#f92672">group&lt;/span>: &lt;span style="color:#ae81ff">${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}&lt;/span>
&lt;span style="color:#f92672">cancel-in-progress&lt;/span>: &lt;span style="color:#66d9ef">true&lt;/span>
&lt;span style="color:#f92672">name&lt;/span>: &lt;span style="color:#ae81ff">generated_files&lt;/span>
&lt;span style="color:#f92672">jobs&lt;/span>:
&lt;span style="color:#f92672">queries&lt;/span>:
&lt;span style="color:#f92672">timeout-minutes&lt;/span>: &lt;span style="color:#ae81ff">10&lt;/span>
&lt;span style="color:#f92672">strategy&lt;/span>:
&lt;span style="color:#f92672">matrix&lt;/span>:
&lt;span style="color:#f92672">go-version&lt;/span>: [ &lt;span style="color:#e6db74">&amp;#39;1.21.x&amp;#39;&lt;/span> ]
&lt;span style="color:#f92672">runs-on&lt;/span>: &lt;span style="color:#ae81ff">ubuntu-latest&lt;/span>
&lt;span style="color:#f92672">steps&lt;/span>:
- &lt;span style="color:#f92672">name&lt;/span>: &lt;span style="color:#ae81ff">Install Go&lt;/span>
&lt;span style="color:#f92672">uses&lt;/span>: &lt;span style="color:#ae81ff">actions/setup-go@v4&lt;/span>
&lt;span style="color:#f92672">with&lt;/span>:
&lt;span style="color:#f92672">go-version&lt;/span>: &lt;span style="color:#ae81ff">${{ matrix.go-version }}&lt;/span>
- &lt;span style="color:#f92672">name&lt;/span>: &lt;span style="color:#ae81ff">Checkout code&lt;/span>
&lt;span style="color:#f92672">uses&lt;/span>: &lt;span style="color:#ae81ff">actions/checkout@v3&lt;/span>
- &lt;span style="color:#f92672">name&lt;/span>: &lt;span style="color:#ae81ff">Ensure configs can be generated&lt;/span>
&lt;span style="color:#f92672">run&lt;/span>: &lt;span style="color:#ae81ff">make queries&lt;/span>
- &lt;span style="color:#f92672">name&lt;/span>: &lt;span style="color:#ae81ff">Check for changes&lt;/span>
&lt;span style="color:#f92672">run&lt;/span>: &lt;span style="color:#ae81ff">git diff --exit-code&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>This installs Go, checks out the code, runs the query generation command, and asks Git for any diffs and to fail if they’re present. Note that, in my case, I’m using &lt;a href="https://www.gnu.org/software/make/manual/make.html">make&lt;/a> and &lt;a href="https://www.docker.com/">Docker&lt;/a> to compile the sqlc queries with &lt;code>docker run --rm --volume $(shell pwd):/src --workdir /src --user $(shell id -u):$(shell id -g) sqlc/sqlc:1.22.0 compile --no-database --no-remote&lt;/code>, but in the event I couldn’t or weren’t, you’d also then be able to document exactly what tools are necessary for generated which files, which helps newcomers to the repository.&lt;/p></content></item><item><title>Bring your own interface</title><link>https://blog.verygoodsoftwarenotvirus.ru/posts/interfaces/</link><pubDate>Tue, 29 Aug 2023 23:04:56 -0600</pubDate><guid>https://blog.verygoodsoftwarenotvirus.ru/posts/interfaces/</guid><description>I have a side project which, like most good software, uses a structured logger. Initially I used logrus, then I used zap, and then I found zerolog, which I’ve used now for a number of years.
How did I switch loggers as many times without causing myself a headache? Easy, I maintained a simple Logger interface:
type Logger interface { Info(string) Debug(string) Error(err error, whatWasHappeningWhenTheErrorOccurred string) WithValue(string, any) Logger WithValues(map[string]any) Logger WithRequest(*http.</description><content>&lt;p>I have a side project which, like most good software, uses a &lt;a href="https://stackify.com/what-is-structured-logging-and-why-developers-need-it/">structured logger&lt;/a>. Initially I used &lt;a href="https://github.com/sirupsen/logrus">logrus&lt;/a>, then I used &lt;a href="https://github.com/uber-go/zap">zap&lt;/a>, and then I found &lt;a href="https://github.com/rs/zerolog">zerolog&lt;/a>, which I’ve used now for a number of years.&lt;/p>
&lt;p>How did I switch loggers as many times without causing myself a headache? Easy, I maintained a simple Logger interface:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-go" data-lang="go">&lt;span style="color:#66d9ef">type&lt;/span> &lt;span style="color:#a6e22e">Logger&lt;/span> &lt;span style="color:#66d9ef">interface&lt;/span> {
&lt;span style="color:#a6e22e">Info&lt;/span>(&lt;span style="color:#66d9ef">string&lt;/span>)
&lt;span style="color:#a6e22e">Debug&lt;/span>(&lt;span style="color:#66d9ef">string&lt;/span>)
&lt;span style="color:#a6e22e">Error&lt;/span>(&lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#66d9ef">error&lt;/span>, &lt;span style="color:#a6e22e">whatWasHappeningWhenTheErrorOccurred&lt;/span> &lt;span style="color:#66d9ef">string&lt;/span>)
&lt;span style="color:#a6e22e">WithValue&lt;/span>(&lt;span style="color:#66d9ef">string&lt;/span>, &lt;span style="color:#a6e22e">any&lt;/span>) &lt;span style="color:#a6e22e">Logger&lt;/span>
&lt;span style="color:#a6e22e">WithValues&lt;/span>(&lt;span style="color:#66d9ef">map&lt;/span>[&lt;span style="color:#66d9ef">string&lt;/span>]&lt;span style="color:#a6e22e">any&lt;/span>) &lt;span style="color:#a6e22e">Logger&lt;/span>
&lt;span style="color:#a6e22e">WithRequest&lt;/span>(&lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">http&lt;/span>.&lt;span style="color:#a6e22e">Request&lt;/span>) &lt;span style="color:#a6e22e">Logger&lt;/span>
&lt;span style="color:#a6e22e">WithResponse&lt;/span>(&lt;span style="color:#a6e22e">response&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">http&lt;/span>.&lt;span style="color:#a6e22e">Response&lt;/span>) &lt;span style="color:#a6e22e">Logger&lt;/span>
&lt;span style="color:#a6e22e">WithError&lt;/span>(&lt;span style="color:#66d9ef">error&lt;/span>) &lt;span style="color:#a6e22e">Logger&lt;/span>
&lt;span style="color:#a6e22e">WithSpan&lt;/span>(&lt;span style="color:#a6e22e">span&lt;/span> &lt;span style="color:#a6e22e">trace&lt;/span>.&lt;span style="color:#a6e22e">Span&lt;/span>) &lt;span style="color:#a6e22e">Logger&lt;/span>
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>At every place in the code that has even the slight inclination of needing to log, I use this &lt;code>Logger&lt;/code> type. This stands in contrast with most of the professional code I’ve written which makes use of a dependency-specific &lt;code>*zap.Logger&lt;/code> everywhere. When I wanted to use zap instead of logrus, I very simply wrote a zap implementation of the above interface, and made use of it instead of the logrus one. They’re very shallow wrappers; here’s what &lt;code>WithValue&lt;/code> looks like for the zap implementation, for example:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-go" data-lang="go">&lt;span style="color:#66d9ef">func&lt;/span> (&lt;span style="color:#a6e22e">l&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">zapLogger&lt;/span>) &lt;span style="color:#a6e22e">WithValue&lt;/span>(&lt;span style="color:#a6e22e">key&lt;/span> &lt;span style="color:#66d9ef">string&lt;/span>, &lt;span style="color:#a6e22e">value&lt;/span> &lt;span style="color:#a6e22e">any&lt;/span>) &lt;span style="color:#a6e22e">logging&lt;/span>.&lt;span style="color:#a6e22e">Logger&lt;/span> {
&lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#f92672">&amp;amp;&lt;/span>&lt;span style="color:#a6e22e">zapLogger&lt;/span>{&lt;span style="color:#a6e22e">logger&lt;/span>: &lt;span style="color:#a6e22e">l&lt;/span>.&lt;span style="color:#a6e22e">logger&lt;/span>.&lt;span style="color:#a6e22e">With&lt;/span>(&lt;span style="color:#a6e22e">zap&lt;/span>.&lt;span style="color:#a6e22e">Any&lt;/span>(&lt;span style="color:#a6e22e">key&lt;/span>, &lt;span style="color:#a6e22e">value&lt;/span>))}
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>The way the app conjures a &lt;code>Logger&lt;/code> instance is from a logging-specific &lt;code>Config&lt;/code> object, which has a method for providing a logger:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-go" data-lang="go">&lt;span style="color:#66d9ef">func&lt;/span> (&lt;span style="color:#a6e22e">cfg&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">Config&lt;/span>) &lt;span style="color:#a6e22e">ProvideLogger&lt;/span>() &lt;span style="color:#a6e22e">logging&lt;/span>.&lt;span style="color:#a6e22e">Logger&lt;/span> {
&lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">cfg&lt;/span> &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span> {
&lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#a6e22e">logging&lt;/span>.&lt;span style="color:#a6e22e">NewNoopLogger&lt;/span>()
}
&lt;span style="color:#66d9ef">switch&lt;/span> &lt;span style="color:#a6e22e">cfg&lt;/span>.&lt;span style="color:#a6e22e">Provider&lt;/span> {
&lt;span style="color:#66d9ef">case&lt;/span> &lt;span style="color:#a6e22e">ProviderZerolog&lt;/span>:
&lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#a6e22e">zerolog&lt;/span>.&lt;span style="color:#a6e22e">NewZerologLogger&lt;/span>(&lt;span style="color:#a6e22e">cfg&lt;/span>.&lt;span style="color:#a6e22e">Level&lt;/span>)
&lt;span style="color:#66d9ef">case&lt;/span> &lt;span style="color:#a6e22e">ProviderZap&lt;/span>:
&lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#a6e22e">zap&lt;/span>.&lt;span style="color:#a6e22e">NewZapLogger&lt;/span>(&lt;span style="color:#a6e22e">cfg&lt;/span>.&lt;span style="color:#a6e22e">Level&lt;/span>)
&lt;span style="color:#66d9ef">default&lt;/span>:
&lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#a6e22e">logging&lt;/span>.&lt;span style="color:#a6e22e">NewNoopLogger&lt;/span>()
}
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>Having things configured this way makes it trivial to switch between logging instances. I can change one line of config and have the entire application’s logging behavior change in response.&lt;/p>
&lt;h1 id="new-toys">New Toys&lt;/h1>
&lt;p>Recently Go 1.21 introduced &lt;a href="https://pkg.go.dev/log/slog">the slog package&lt;/a>, which is the standard library implementation of a structured logger like those I mentioned above. I wanted to use it, and I was able to start doing so in my app by:&lt;/p>
&lt;ol>
&lt;li>writing a &lt;code>slog&lt;/code>-compatible implementation of the &lt;code>logging.Logger&lt;/code> interface and&lt;/li>
&lt;li>changing the config to specify that the &lt;code>slog&lt;/code> logging provider should be used.&lt;/li>
&lt;li>adding a case to the above switch statement to account for the new &lt;code>ProviderSlog&lt;/code> option.&lt;/li>
&lt;/ol>
&lt;p>One quick PR and the whole app uses &lt;code>slog&lt;/code> now.&lt;/p>
&lt;p>It doesn’t stop at logging. I have similar interfaces for:&lt;/p>
&lt;ul>
&lt;li>message queues (Redis, SQS, Pub/Sub)&lt;/li>
&lt;li>search indices (Algolia, Elasticsearch)&lt;/li>
&lt;li>talking to analytics providers (Segment, PostHog)&lt;/li>
&lt;li>feature flag checking (LaunchDarkly, PostHog, Split)&lt;/li>
&lt;li>sending emails through a service (Segment, Mailjet, Mailgun)&lt;/li>
&lt;li>object storage (S3, GCS, local disk)&lt;/li>
&lt;/ul>
&lt;p>At any moment, the primary implementation provider for these core functions can be changed with a simple config update.&lt;/p>
&lt;p>What makes a good use case for this pattern?&lt;/p>
&lt;ol>
&lt;li>The functionality should have a core purpose that probably won’t meaningfully change. For instance, the message queue code will only ever deal with reading from or writing to message queues. At no point will it suddenly also be responsible for sending emails. Logging packages will only ever be responsible for logging values, etc.&lt;/li>
&lt;li>There should also be a number of reasonable implementations. There are many logging libraries, email service providers, and there will be more of them in the future, too.&lt;/li>
&lt;li>Little meaningful impact on switching between said providers. For example, I have an &lt;code>authentication&lt;/code> package, which is responsible for verifying TOTP codes and checking submitted passwords against their hashes. I could hypothetically have a &lt;code>bcrypt&lt;/code> implementation, and an &lt;code>scrypt&lt;/code> implementation, but instead I only have an &lt;code>argon2id&lt;/code> implementation, because if I were to suddenly switch from that to scrypt, nothing stored in the database would ever work properly barring a huge migration I wouldn’t want to account for in my implementation.&lt;/li>
&lt;/ol>
&lt;p>Adopting this pattern gives you flexibility at the cost of having to write a bit of glue code (like the Config stuff I showed above). Another benefit of this is that you get to define what you really need that interface for. Notice, for instance, that I don’t have a &lt;code>Warn&lt;/code> method in my Logger interface. That’s because I basically never log at that level in practice. This is just a personal quirk of mine, I don’t know when you’d &lt;code>Warn&lt;/code> in the API server code I write, so I just never found a need to implement it.&lt;/p></content></item><item><title>Sylvia's Law</title><link>https://blog.verygoodsoftwarenotvirus.ru/posts/sylvias-law/</link><pubDate>Sun, 09 Jul 2023 00:45:12 -0500</pubDate><guid>https://blog.verygoodsoftwarenotvirus.ru/posts/sylvias-law/</guid><description>The Life and Death of Sylvia Joyce My aunt Sylvia was my grandmother&amp;rsquo;s first child. My grandmother had married my biological grandfather against her guardians' wishes, he was in the military, so she was entirely alone when Sylvia came. My aunt Sylvia would live a full life of ups, downs, and the myriad things in between. She got married, divorced, had children, found love again, vacationed, sang, sewed, and more than anything, loved her family.</description><content>&lt;h2 id="the-life-and-death-of-sylvia-joyce">The Life and Death of Sylvia Joyce&lt;/h2>
&lt;p>My aunt Sylvia was my grandmother&amp;rsquo;s first child. My grandmother had married my biological grandfather against her guardians' wishes, he was in the military, so she was entirely alone when Sylvia came. My aunt Sylvia would live a full life of ups, downs, and the myriad things in between. She got married, divorced, had children, found love again, vacationed, sang, sewed, and more than anything, loved her family. In 2018, she was diagnosed with metastatic liver cancer.&lt;/p>
&lt;p>She spent her last days in a hospice care facility in San Antonio. We visited her and she remarked how the hospice care facility didn&amp;rsquo;t have good food, and all she wanted was some decent italian. It&amp;rsquo;s San Antonio, so they don&amp;rsquo;t have any, but I looked up the best-rated local italian place and had them deliver a whole lasagna and slice of chocolate cake to her room. It was the first and last kindness I ever did for her.&lt;/p>
&lt;p>At Aunt Sylvia&amp;rsquo;s funeral, we were making casual conversation when her daughter mentioned the nightmare she had just had to deal with when closing out her mom&amp;rsquo;s affairs. Aunt Sylvia had been renting an apartment on her meager Social Security checks when she got her diagnosis. When my cousin went to fetch family heirlooms from her mom&amp;rsquo;s last dwelling, the landlord stood between her and the doorway and demanded a lease termination fee be paid before letting my cousin in to fetch her family&amp;rsquo;s heirlooms. As we were driving home, I told my wife &amp;ldquo;that shit should be fucking illegal.&amp;rdquo;&lt;/p>
&lt;h3 id="legislating">Legislating&lt;/h3>
&lt;p>My wife, at the time, was Chief of Staff for the state representative who presided over the district containing the hospice center and apartment complex. I don&amp;rsquo;t think I could have declared that it should have been illegal to a person more capable of making it so.&lt;/p>
&lt;p>My wife had &lt;a href="https://capitol.texas.gov/BillLookup/History.aspx?LegSess=86R&amp;amp;Bill=HB69">legislation banning lease termination fees for deceased tenants&lt;/a> drafted very quickly. When you draft law at the Texas House, you invoke the &lt;a href="https://tlc.texas.gov/">Lege Council&lt;/a>, who are a team of lawyers who know where laws pertaining to given matters are kept, and how to change it in such a way that would both achieve some goal and (ideally) survive judicial scrutiny down the line. Drafting law involves telling them in plain English the effects you&amp;rsquo;d like the law to have, and on the other end of it, they give you a piece of paper that you can bring to the House floor to propose as law. My wife walked in and said &amp;ldquo;make it against the law to charge a survivor of a deceased tenant a lease termination fee.&amp;rdquo; It seems so simple, maybe even a bit dystopian in restrospect.&lt;/p>
&lt;h3 id="lobbying-pushback">Lobbying Pushback&lt;/h3>
&lt;p>I can only recall this story second hand, but I recall that the bill actually didn&amp;rsquo;t receive much pushback from landlords or apartment company lobbies. When the subject of that law came up with one such lobbyist, he remarked something to my wife along the lines of &amp;ldquo;yeah, that&amp;rsquo;s the sort of bill we&amp;rsquo;d normally fight, but if we do that, we look like monsters, so congrats on that one.&amp;rdquo;&lt;/p>
&lt;p>When the bill finally passed, it was done so with explicit acknowledgement of my aunt. May she rest in peace.&lt;/p></content></item><item><title>What I Learned at Founders Academy</title><link>https://blog.verygoodsoftwarenotvirus.ru/posts/what-i-learned-at-founders-academy/</link><pubDate>Fri, 29 Jul 2022 12:45:12 -0500</pubDate><guid>https://blog.verygoodsoftwarenotvirus.ru/posts/what-i-learned-at-founders-academy/</guid><description>Context Recently I attended a 3-day seminar offered by a local venture capital firm called “Founder’s Academy,” hosted by Gordon Dougherty. I enjoyed my time in the lectures, and learned quite a bit, which I wanted to document here.
I’m grateful to Mr. Dougherty for offering this free series of lectures, and was glad to have had the chance to attend. I’m also grateful to my managers at Allma, who gave me ample time off from work in the middle of the day to attend, and for being very encouraging of me when I was honest with them about what I was attending and why.</description><content>&lt;h3 id="context">Context&lt;/h3>
&lt;p>Recently I attended &lt;a href="%5Bhttps://www.eventbrite.com/e/founders-academy-essentials-tickets-330209063197%5D(https://www.eventbrite.com/e/founders-academy-essentials-tickets-330209063197)">a 3-day seminar offered by a local venture capital firm called “Founder’s Academy,”&lt;/a> hosted by &lt;a href="https://twitter.com/gordondaugherty">Gordon Dougherty&lt;/a>. I enjoyed my time in the lectures, and learned quite a bit, which I wanted to document here.&lt;/p>
&lt;p>I’m grateful to Mr. Dougherty for offering this free series of lectures, and was glad to have had the chance to attend. I’m also grateful to my managers at &lt;a href="https://allma.io">Allma&lt;/a>, who gave me ample time off from work in the middle of the day to attend, and for being very encouraging of me when I was honest with them about what I was attending and why.&lt;/p>
&lt;h3 id="spoiler-alert">Spoiler Alert&lt;/h3>
&lt;p>I’m the sort of learner who obsessively watches YouTube videos for weeks on end about a given topic until I absorb just enough details via osmosis to appear outwardly competent. I have taught myself this way:&lt;/p>
&lt;ul>
&lt;li>how to drive a stick shift&lt;/li>
&lt;li>how to regularly win Dark Souls encounters&lt;/li>
&lt;li>how to write code&lt;/li>
&lt;li>etc.&lt;/li>
&lt;/ul>
&lt;p>So naturally, I’ve been watching a ton of Startup School / This Week in Startups / miscellaneous other channels content on various things about pitching, product development, timing of fundraising, etc. I want to emphasize that this is not me bragging, merely taking inventory of the little nest of artifacts I’ve assembled in the corner of my brain labelled “business.”&lt;/p>
&lt;h3 id="observation-much-of-early-startup-advice-is-geared-towards-b2b-saas-companies">Observation: much of early startup advice is geared towards B2B SaaS companies&lt;/h3>
&lt;p>Every company idea I’ve ever had has been intended to serve normal, working-class folks. If what I build ends up serving folks who simply rentier capitalism their way to an identity, then that’s fine too, but only if it happens to serve them well because it primarily serves normal people well.&lt;/p>
&lt;p>There were large portions of the Founder’s Academy curriculum (and anecdotally, most startup economy curriculum) that simply do not apply to B2C businesses. There was a large portion of the lecture that talked about strategic partnerships that involved two software vendors with symbiotic functions collaborating to make large money sales to big enterprise businesses. It was hard for me to extrapolate how that might apply to a B2C, but I wouldn’t rule out that I’m merely not creative a thinker enough.&lt;/p>
&lt;p>I’d go as far as to say much of the prevailing wisdom about how to start a tech company tacitly expects you to be building a B2B SaaS of some kind. I have a couple theories as to why, but I would still very much describe myself as “not knowing anything” about business, so I’ll keep them to myself. :)&lt;/p>
&lt;h3 id="validating-product-assumptions">Validating Product Assumptions&lt;/h3>
&lt;p>Let’s compare a tech product to a cheeseburger. If I were to endeavor to develop a burger preparation process to produce a burger worth paying for, my personal approach would be to arrive at and end result where I make everything on the burger. That means I’m deciding what the recipe for our buns are, what the meat to fat ratio is in the patty, what temperature it’s cooked at, what time of day I need to make sure the mustard is prepared by, how much salt is going into my pickling brine, etc.&lt;/p>
&lt;p>The conventional startup entrepreneurial wisdom seems instead to be: pick one small part of the overall problem, and get your attempt at solving it out the door as fast as possible. Figure out how to make buns OR a patty OR some condiments, and buy the rest from other, established providers, and get feedback on the end result.&lt;/p>
&lt;p>That’s all well and good, but what if my thesis is that the whole burger is awful and needs change? If I make just the patty, and then customer feedback amounts to “well, I mean…it’s a burger,” is it because my thesis (that replacing the whole burger is what the customer wants) is wrong, or because I haven’t done it yet?&lt;/p>
&lt;p>This seems to be the essential risk of entrepreneurship, and the very first risk that most investors seek to avoid. You just sort of silently acknowledge before getting real customers that you could be wasting a lot of your own time (and possibly your friends and family’s money, more on that later) until you have some objective data that convinces you otherwise.&lt;/p>
&lt;h2 id="privilege">Privilege&lt;/h2>
&lt;p>Before this event, all my research around starting a company had this weird undertone of privilege. It’s not uncommon to hear founders talking about raising six-figure friends &amp;amp; family rounds.&lt;/p>
&lt;p>I’m happy for those folks, that their loved ones can facilitate their endeavors like that, but I simply do not have friends or family capable of doing something like that. In the event they were, it would probably be only barely so, and would consequently have so much guilt associated with any potential loss that I wouldn’t be able to, in good conscience, ask them for that much money in the first place.&lt;/p>
&lt;p>I’ve deleted two fairly personal paragraphs attempting to detail my situation, but suffice it to say: if I aim to build a company, there’s no nest egg somewhere for my family to rely upon while I build it. I’m simply going to have to work a day job, and build it in my (increasingly rare) spare time. This is a reality I’ve been aware of the entire time I’ve had entrepreneurial aspirations, but I assumed I’d have to keep it a dirty little secret, like, I have a company, and I play as a full-time CEO while still going to my day job’s standup at 9:30AM or something.&lt;/p>
&lt;p>I was surprised to see my kind of situation was not only explicitly acknowledged, but apparently common. One major question I had going into this was “is it okay to pay myself enough to make ends meet, even if that amount is actually quite large because I’ve been bad with money for most of my life?” Apparently the answer is “yes, but, y’know, talk to your investors about it ahead of time.” I came away feeling much more comfortable about ever having to approach a VC to ask for funding.&lt;/p>
&lt;h3 id="you-can-screw-over-your-companys-future-and-not-realize-it">You can screw over your company’s future and not realize it&lt;/h3>
&lt;p>The lecturer detailed a situation where a company had agreed to a pair of early contracts with very large customers that had wholly untenable contract positions. That is to say, whoever attempted to acquire the company would have a very sincere interest in not servicing those parts of the contracts, and there’s basically no way to guarantee that the contracted party would agree to amending it.&lt;/p>
&lt;p>So even though these contracts had been in place for multiple years, and were set to continue for years to come, and the company had been operating normally prior to seeking acquisition, it was nevertheless operating comfortably from the dressed-up confines of a death trap. I would have assumed something like that would at least have some level of obviousness.&lt;/p>
&lt;h3 id="investor-management-is-a-crucial-aspect-of-running-a-growing-company">Investor management is a crucial aspect of running a growing company&lt;/h3>
&lt;p>My prior understanding of the relationship between investor and entrepreneur was akin to client and contractor. I know investors meet with company leadership at some regular interval to see how things are going, and obviously more of that happens during fundraising periods.&lt;/p>
&lt;p>What I learned at Founder’s Academy is that actually investors lose focus and interest in your company, and will simply forget you exist if you don’t keep them excited with updates. Say one of your investors is an expert in a particular niche, and you’ve just had a decent win in that arm of the company, that’s a situation I would have imagined simply writing down to bring up in those routine meetings, but the prevailing wisdom is to go out of your way to email that investor. “Look, we just had &lt;some success> in &amp;lt;some arena we’ve discussed previously&amp;gt;! &lt;link to article documenting success>”&lt;/p>
&lt;p>I guess I imagined it went more like “hey, it’s your money, if you want to check in on what we’re doing with it, the onus is on you to reach out”, but it somehow makes much more sense that the person who provides the capital not only doesn’t feel obligated to check up on it, but in fact feels entitled to updates from those who received it.&lt;/p>
&lt;h3 id="founder-compositions-vary-and-matter">Founder Compositions vary and matter&lt;/h3>
&lt;p>When I think of all the companies I’ve worked for that were started by people I could talk to, there are one or maybe two total early founders who actually put work into a product that ends up playing a role in the company’s formation. I once worked for a company that had 4 founders, but it definitely felt like the odd one out.&lt;/p>
&lt;p>At Founder’s Academy, they taught us that even when you have just two founders, it’s a very very good idea to just give 1% of your company away to a trusted advisor for the sake of splitting tie votes. Apparently two founders with 50/50 equity is a warning sign to some investors.&lt;/p>
&lt;p>Additionally, “cofounder” is really more of a vibe than an artifact of equity distribution. The lecturer said what investors really want out of a founder relationship is a “partner-in-crime” aspect. That is to say, you are both, without needing to acknowledge it, working towards the same goal as a natural consequence of your day-to-day impulses.&lt;/p>
&lt;p>You can have any number of “founders”, but some may be founders only in the sense that they’ve been involved in the company during its inception, not because they own any significant portion of equity.&lt;/p></content></item><item><title>Greener Cloud Pastures</title><link>https://blog.verygoodsoftwarenotvirus.ru/posts/greener-cloud-pastures/</link><pubDate>Tue, 29 Mar 2022 23:04:56 -0600</pubDate><guid>https://blog.verygoodsoftwarenotvirus.ru/posts/greener-cloud-pastures/</guid><description>Preface I want to take the time before writing to note that I have great respect and appreciation for the folks who work at any of the companies mentioned and/or on any of the products mentioned. Computers are hard; making stuff that is primarily meant to be consumed by them is even harder. My goals here are similar to those of that post. I hope that there can be something constructive that comes out of this post, but at the very worst, I hope it simply doesn&amp;rsquo;t offend or disparage anybody.</description><content>&lt;h2 id="preface">Preface&lt;/h2>
&lt;p>I want to take the time before writing to note that I have great respect and appreciation for the folks who work at any of the companies mentioned and/or on any of the products mentioned. Computers are hard; making stuff that is primarily meant to be consumed by them is even harder. My goals here are similar to those of that post. I hope that there can be something constructive that comes out of this post, but at the very worst, I hope it simply doesn&amp;rsquo;t offend or disparage anybody.&lt;/p>
&lt;h2 id="background">Background&lt;/h2>
&lt;p>In &lt;a href="https://blog.verygoodsoftwarenotvirus.ru/posts/babys-first-aws/">my last article&lt;/a>, I wrote about my endeavor to get a side project deployed to a professional cloud, specifically AWS. To briefly summarize, it didn&amp;rsquo;t end up going to plan, and it proved to be quite expensive to host a very simple application there. I still want to host this application somewhere, so I started to look at alternatives to AWS for deployment.&lt;/p>
&lt;p>After all was said and done, I was left with a lot of indecision and fatigue. I liked the level of service quality I got from AWS; I just really didn&amp;rsquo;t like paying for it. When I thought about all the moving bits and pieces of the application, it just didn&amp;rsquo;t feel like this thing should cost more than, say, $40 at most to host per month. I was hoping I could land somewhere that would serve my app basically just as well for about 1/3 of the price if at all possible.&lt;/p>
&lt;p>What I wanted from a provider on the technical side was simply a way to ship an app container and have the provider put it on the public internet. Let me connect that container to a database, maybe some object storage, and a queue, and I&amp;rsquo;ll be good to go.&lt;/p>
&lt;h2 id="first-candidate-doap">First Candidate: DOAP&lt;/h2>
&lt;p>I started to look at &lt;a href="https://www.digitalocean.com">DigitalOcean&amp;rsquo;s&lt;/a> &lt;a href="https://www.digitalocean.com/products/app-platform">App Platform&lt;/a>, which seemed to touch a lot of these bases. I could define a containerized app, explicitly associate a database with it, and leave the rest to them.&lt;/p>
&lt;p>After playing with this for about two days, I ended up deciding against it. It seemed to work well enough, but I also felt like I was gracing the outer limits of when it was appropriate to use the platform. It felt as though if my app decided to do even one more thing, I wouldn&amp;rsquo;t be able to host it that way anymore. That I&amp;rsquo;d be back to the same drawing board I was sitting at in no time.&lt;/p>
&lt;p>I was left with the impression that the target audience for App Platform is agencies ñ folks approached by non-technical parties who need a specific kind of application to be made and hosted. I could see a workflow where you basically just have one App Platform definition you use to deploy any number of hyper-similar WordPress sites or whatever. If the client came back and asked for something that exceeded the capabilities of DOAP, the agency could just quote a wild price, but I&amp;rsquo;m trying to avoid wild prices.&lt;/p>
&lt;h2 id="digitalocean-shortcomings">DigitalOcean Shortcomings&lt;/h2>
&lt;p>One thing I made very heavy use of on AWS was managed services, specifically RDS and SQS. Being able to have a queue without having to worry about that queue&amp;rsquo;s infrastructure was very nice, but DigitalOcean has no analog for these. You could probably write up an App Platform template for deploying a containerized version of &lt;a href="https://nats.io">NATS&lt;/a> or whatever, and &lt;a href="https://www.digitalocean.com/products/managed-databases-redis">they do have a managed Redis offering&lt;/a>, but it&amp;rsquo;s much more expensive ($15/month at the time of writing) than SQS (consumption-based, with a generous free tier ñ one of the few things that didn&amp;rsquo;t cost me money on AWS). I also just sort of liked not having to think about it as anything other than a generic queue.&lt;/p>
&lt;h2 id="kubernetes">Kubernetes&lt;/h2>
&lt;p>One thing DigitalOcean does have that is actually priced fairly decently is &lt;a href="https://docs.digitalocean.com/products/kubernetes/">their managed Kubernetes offering&lt;/a>. Previously, I have stated that using Kubernetes is a mistake until proven otherwise. I violated these tenets and explored using Kubernetes for this app in DigitalOcean for about three days before I gave up. I should have trusted my gut.&lt;/p>
&lt;p>I could probably write an article just on why Kubernetes didn&amp;rsquo;t work out, but I&amp;rsquo;ll spare us all the misery. I got as far as having a local Kubernetes setup for the app that worked, but I could never manage to get the one in DigitalOcean connected to the public internet.&lt;/p>
&lt;h2 id="where-to-now">Where to Now?&lt;/h2>
&lt;p>So, I&amp;rsquo;ve fully ruled out DigitalOcean, App Platform, and the Kubernetes offering, which leaves only &lt;a href="https://azure.microsoft.com/">Azure&lt;/a> and &lt;a href="https://cloud.google.com/">Google Cloud Platform&lt;/a> (GCP).&lt;/p>
&lt;p>Azure has &lt;a href="https://azure.microsoft.com/en-us/product-categories/containers/">a number of container hosting solutions&lt;/a>, but the one that seems most akin to ECS is called &lt;a href="https://azure.microsoft.com/en-us/services/container-apps/#features">Azure Container Apps&lt;/a>. It&amp;rsquo;s in preview and &lt;a href="https://azure.microsoft.com/en-us/updates/public-preview-azure-container-apps/">was launched very recently&lt;/a>, so while I&amp;rsquo;m sure it&amp;rsquo;s probably fine, I don&amp;rsquo;t really have a lot of confidence using it just yet. They also have &lt;a href="https://azure.microsoft.com/en-us/services/kubernetes-service/#overview">AKS&lt;/a> ñ a managed Kubernetes offering ñ but I&amp;rsquo;m not doing that either, for the aforementioned reasons.&lt;/p>
&lt;p>I&amp;rsquo;ve used GCP before in my career and for side projects. This very blog is hosted there, as a static site, and I pay something like $20/year all said and done. When I worked at WP Engine, we made very heavy use of GCP for all of our internal software and a not insignificant chunk of our actual business offering as well, so I had a vague impression of its production reliability. Somewhat like my AWS experience, I&amp;rsquo;d only really bothered writing code that deployed there, not actually deploying it. (I did have a &lt;a href="https://blog.verygoodsoftwarenotvirus.ru/posts/the-story-of-porktrack/">fairly successful endeavor a few years ago&lt;/a> to rebuild and redeploy an old application to Cloud Run that went very well.)&lt;/p>
&lt;p>I decided to give GCP a shot.&lt;/p>
&lt;h2 id="app-recap">App Recap&lt;/h2>
&lt;p>To briefly recall the app we&amp;rsquo;re talking about and its needs, it&amp;rsquo;s a meal management app. You put in recipes, create meal plans for the week from those recipes, and allow others in your household to participate. From a technical perspective, it needs:&lt;/p>
&lt;ul>
&lt;li>a database&lt;/li>
&lt;li>a server container&lt;/li>
&lt;li>an event queue&lt;/li>
&lt;li>something to trigger code execution when events arrive on the queue&lt;/li>
&lt;/ul>
&lt;p>I decided to use &lt;a href="https://cloud.google.com/run">Cloud Run&lt;/a> to host the server container, &lt;a href="https://cloud.google.com/sql">Cloud SQL&lt;/a> for the database, &lt;a href="https://cloud.google.com/pubsub">Cloud Pub/Sub&lt;/a> for the event queue, and &lt;a href="https://cloud.google.com/functions">Cloud Functions&lt;/a> for the event responders. GCP &lt;a href="https://cloud.google.com/scheduler/docs/tut-pub-sub">supports assigning Cloud Functions as responders to Pub/Sub events&lt;/a>, like how you can &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html">configure Lambda functions to respond to SQS events&lt;/a>.&lt;/p>
&lt;h2 id="gcp-terraform-obstacles">GCP Terraform Obstacles&lt;/h2>
&lt;p>One goal I maintained from the prior effort was to have it so that the full environment could be spun up from nothing. That I could take a bare project with no resources and, after one CI task run, have the full app running and available. This was beneficial last time in reducing costs, since I could use Terraform to destroy everything in one run if I met this condition.&lt;/p>
&lt;p>AWS allowed me to accomplish this particular goal. I could go from having nothing to having the full environment up in 30 minutes, with no preconfiguration in my AWS account other than the Terraform Cloud user stuff (which I&amp;rsquo;m not counting).&lt;/p>
&lt;p>GCP, however, had a bunch of hurdles in the way for me. For one, every API needs explicit authorization.&lt;/p>
&lt;ul>
&lt;li>Want to create a Container Registry? You must first enable the &lt;a href="https://console.cloud.google.com/apis/library/containerregistry.googleapis.com">Container Registry API&lt;/a>.&lt;/li>
&lt;li>Want to administer Cloud SQL databases? You must first enable the &lt;a href="https://console.cloud.google.com/apis/library/sqladmin.googleapis.com">Cloud SQL Admin API&lt;/a>.&lt;/li>
&lt;li>Want to administer Cloud Run instances? You must first enable the &lt;a href="https://console.cloud.google.com/apis/library/run.googleapis.com">Cloud Run Admin API&lt;/a>.&lt;/li>
&lt;/ul>
&lt;p>After a while of playing API authorization whack-a-mole, I discovered what I thought would be a shortcut to this step:&lt;/p>
&lt;ol>
&lt;li>Enable the &lt;a href="https://console.cloud.google.com/apis/library/cloudresourcemanager.googleapis.com?pli=1">Cloud Resource Manager API&lt;/a>&lt;/li>
&lt;li>Use Terraform to enable all the stuff I&amp;rsquo;d need with &lt;a href="https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/google_project_service">google_project_service resources&lt;/a>.&lt;/li>
&lt;/ol>
&lt;p>This didn&amp;rsquo;t work. In fact, it did the opposite of working. Because I (foolishly) decided to add a bunch of the aforementioned resources that I had already enabled by hand, and hadn&amp;rsquo;t granted the lesser Terraform Cloud user rights to disable these things, it really messed with my Terraform state. Thankfully, cleaning that up is easy, but I learned so that you don&amp;rsquo;t have to, hopefully.&lt;/p>
&lt;p>Additionally, for things like &lt;a href="https://support.google.com/a/answer/60216?hl=en">Domain verification (ugh)&lt;/a>, there didn&amp;rsquo;t immediately appear to be a good way of doing that automatically, which I&amp;rsquo;m not exactly against.&lt;/p>
&lt;p>I will say, the GCP Terraform provider does a better job of catching errors in the &amp;ldquo;terraform validate&amp;rdquo; process. Many times with the AWS work, I would have something pass &amp;ldquo;validate&amp;rdquo; but fail to &amp;ldquo;apply&amp;rdquo; for something that could have totally been caught in &amp;ldquo;validate&amp;rdquo;. I very much appreciate whoever is responsible for this improvement.&lt;/p>
&lt;h2 id="iam-woes">IAM Woes&lt;/h2>
&lt;p>IAM is also pretty confusing. In the console, users are called &amp;ldquo;Principals&amp;rdquo;, but in some old docs and guides, they seem to just be called &amp;ldquo;Users&amp;rdquo;? Users can have roles and service accounts, and those service accounts can have a subset of roles. I think I&amp;rsquo;ve recalled all this correctly, but I&amp;rsquo;m not confident I have!&lt;/p>
&lt;p>Another thing that threw me for a loop was how to create a user for the Cloud Run API service. The Terraform definition has space for a Service Account ID, so I created a service account in Terraform and assigned it the &amp;ldquo;Secret Manager Secret Accessor&amp;rdquo; role. Only, Terraform gave me an error:&lt;/p>
&lt;pre tabindex="0">&lt;code>| Error: Error setting IAM policy for service account 'projects/xxxxxxxxxxx/serviceAccounts/api-server@xxxxxxxxxxx.iam.gserviceaccount.com': googleapi: Error 400: Role roles/secretmanager.secretAccessor is not supported for this resource., badRequest
&lt;/code>&lt;/pre>&lt;p>&lt;sub>(Yes, the &lt;code>., badRequest&lt;/code> part is really in there.)&lt;/sub>
So, I needed a Principal, and it took me a while to figure out the right incantation of Terraform to produce it, because they are not named kindly:&lt;/p>
&lt;pre tabindex="0">&lt;code>resource &amp;quot;google_service_account&amp;quot; &amp;quot;api_server_account&amp;quot; {
account_id = &amp;quot;api-server&amp;quot;
display_name = &amp;quot;API Server&amp;quot;
}
resource &amp;quot;google_project_iam_member&amp;quot; &amp;quot;api_server_user&amp;quot; {
project = local.project_id
role = &amp;quot;roles/viewer&amp;quot;
member = format(&amp;quot;serviceAccount:%s&amp;quot;, google_service_account.api_server_account.email)
}
&lt;/code>&lt;/pre>&lt;p>From there, you can grant that Principal specific roled permissions:&lt;/p>
&lt;pre tabindex="0">&lt;code>resource &amp;quot;google_project_iam_binding&amp;quot; &amp;quot;api_user_secret_accessor&amp;quot; {
project = local.project_id
role = &amp;quot;roles/secretmanager.secretAccessor&amp;quot;
members = [
google_project_iam_member.api_server_user.member,
]
}
&lt;/code>&lt;/pre>&lt;p>and then assign it to your Cloud Run app:&lt;/p>
&lt;pre tabindex="0">&lt;code>resource &amp;quot;google_cloud_run_service&amp;quot; &amp;quot;api_server&amp;quot; {
name = &amp;quot;api-server&amp;quot;
location = &amp;quot;us-central1&amp;quot;
traffic {
percent = 100
latest_revision = true
}
autogenerate_revision_name = true
template {
spec {
service_account_name = google_service_account.api_server_account.email
}
}
# yadda yadda yadda
}
&lt;/code>&lt;/pre>&lt;p>This, combined with some new code to handle environment-mounted secrets, led to my first &amp;ldquo;successful&amp;rdquo; deployment to Cloud Run. Only, I couldn&amp;rsquo;t access it because I hadn&amp;rsquo;t granted &amp;ldquo;allUsers&amp;rdquo; the permission to invoke this particular Cloud Run application. Nice default, I think, but requires more terraform IAM finagling:&lt;/p>
&lt;pre tabindex="0">&lt;code>data &amp;quot;google_iam_policy&amp;quot; &amp;quot;public_access&amp;quot; {
binding {
role = &amp;quot;roles/run.invoker&amp;quot;
members = [
&amp;quot;allUsers&amp;quot;,
]
}
}
resource &amp;quot;google_cloud_run_service_iam_policy&amp;quot; &amp;quot;public_access&amp;quot; {
location = google_cloud_run_service.api_server.location
project = google_cloud_run_service.api_server.project
service = google_cloud_run_service.api_server.name
policy_data = data.google_iam_policy.public_access.policy_data
}
&lt;/code>&lt;/pre>&lt;p>This is called out in &lt;a href="https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/cloud_run_service#example-usage---cloud-run-service-noauth">the Terraform docs for GCP&amp;rsquo;s Cloud Run resource&lt;/a>, but it&amp;rsquo;s called &amp;ldquo;noauth&amp;rdquo;, which I somehow failed to parse as &amp;ldquo;make public&amp;rdquo;.&lt;/p>
&lt;h2 id="cloud-run-dns-woes">Cloud Run DNS Woes&lt;/h2>
&lt;p>Another thing I couldn&amp;rsquo;t manage to automate my way out of was the explicit domain name association that GCP requires for Cloud Run. When you create a Cloud Run service, you get a URL like &amp;ldquo;&lt;a href="https://app-name-blah-blah-blah.run.app">https://app-name-blah-blah-blah.run.app&lt;/a>&amp;rdquo;, which you can&amp;rsquo;t just CNAME. So, you instead have to create a &amp;ldquo;Domain Mapping&amp;rdquo; that seems to tell Google&amp;rsquo;s routers, &amp;ldquo;It&amp;rsquo;s okay to respect this other domain.&amp;rdquo; The IAM around this is really funky, though. &lt;a href="https://cloud.google.com/run/docs/reference/iam/roles">The Cloud Run IAM Roles documentation&lt;/a> page explicitly calls out that:&lt;/p>
&lt;pre tabindex="0">&lt;code>Roles only apply to Cloud Run services, they do not apply to Cloud Run domain mappings. The Project &amp;gt; Editor role is needed to create or update domain mappings.
&lt;/code>&lt;/pre>&lt;p>&amp;ldquo;So much for my preciously curated list of permissions; stupid Terraform has to be an editor!&amp;rdquo; I cursed. Only it turned out that even when I granted it that permission, Terraform still couldn&amp;rsquo;t manage the domain mapping. I kept getting the same error over and over again:&lt;/p>
&lt;pre tabindex="0">&lt;code>| Error: Error waiting to create DomainMapping: resource is in failed state &amp;quot;Ready:False&amp;quot;, message: Caller is not authorized to administer the domain 'web.site'. If you own 'web.site', you can obtain authorization by verifying ownership of the domain, or any of its parent domains, via the Webmaster Central portal: https://www.google.com/webmasters/verification/verification?domain=web.site. We recommend verifying ownership of the largest scope you wish to use with subdomains (e.g. verify 'example.com' if you wish to map 'subdomain.example.com').
&lt;/code>&lt;/pre>&lt;p>That link would take me to a page that gleefully told me I had already verified it. I think it was just the case that the Terraform user isn&amp;rsquo;t the explicit owner of the Domain like I am? Honestly not sure what happened here. I resolved to manage the damn mapping manually myself, which worked.&lt;/p>
&lt;h2 id="cloud-functions-vs-lambda">Cloud Functions vs. Lambda&lt;/h2>
&lt;p>When I used Lambda, I quite liked that I could provide it a compiled binary in a zip folder. It felt a little old fashioned, but it definitely worked. Cloud Functions works with a little more magic ñ the bad kind. Rather than provide a binary, you upload code to a &lt;a href="https://cloud.google.com/storage">Google Cloud Storage&lt;/a> bucket, and that sets up a &lt;a href="https://cloud.google.com/build">Cloud Build&lt;/a> trigger, which runs a preconfigured script against the code in question depending on your chosen runtime. Getting even a simple Cloud Function to upload via Terraform proved very difficult for me.&lt;/p>
&lt;p>While nearly all the provided example Cloud Function code samples work, they do so because they don&amp;rsquo;t use anything but the standard library. If you want to use any of your own private code as libraries in your Cloud Function, you basically have to have a &amp;ldquo;go.mod&amp;rdquo; file &lt;em>per Cloud Function.&lt;/em> This is pretty different from how I write Go code day to day, where I generally have exactly one go.mod file per repository. No big whoop.&lt;/p>
&lt;p>Building Cloud Function artifacts &lt;a href="https://buildpacks.io/docs/tools/pack/">can be done with the &amp;ldquo;pack&amp;rdquo; CLI&lt;/a> for local testing, but you can&amp;rsquo;t ship those artifacts to Cloud Functions, you can only ship raw code to be built by Cloud Build. Maybe it&amp;rsquo;s documented somewhere, but this whole build process felt very opaque and yields confusing errors that didn&amp;rsquo;t aid me in diagnosis. Some examples:&lt;/p>
&lt;pre tabindex="0">&lt;code>Info 2022-02-08 19:48:39.274 CST Step #1 - &amp;quot;build&amp;quot;: ERROR: No buildpack groups passed detection.
Info 2022-02-08 19:48:39.274 CST Step #1 - &amp;quot;build&amp;quot;: ERROR: Please check that you are running against the correct path.
Info 2022-02-08 19:48:39.274 CST Step #1 - &amp;quot;build&amp;quot;: ERROR: failed to detect: no buildpacks participating.
&lt;/code>&lt;/pre>&lt;p>(I was uploading a .zip that had an empty folder in it.)&lt;/p>
&lt;pre tabindex="0">&lt;code>Info 2022-02-08 22:26:55.090 CST Step #1 - &amp;quot;build&amp;quot;: Running &amp;quot;go run /cnb/buildpacks/google.go.functions-framework/0.9.4/converter/get_package/main.go -dir /workspace/serverless_function_source_code (GOCACHE=/layers/google.go.functions-framework/gcpbuildpack-tmp/app)&amp;quot;
Info 2022-02-08 22:26:55.270 CST Step #1 - &amp;quot;build&amp;quot;: 2022/02/09 04:26:55 Unable to extract package name and imports: unable to find Go package in /workspace/serverless_function_source_code.
Info 2022-02-08 22:26:55.272 CST Step #1 - &amp;quot;build&amp;quot;: exit status 1
Info 2022-02-08 22:26:55.278 CST Step #1 - &amp;quot;build&amp;quot;: Done &amp;quot;go run /cnb/buildpacks/google.go.functions-framework/0.9.4/c...&amp;quot; (187.47553ms)
Info 2022-02-08 22:26:55.278 CST Step #1 - &amp;quot;build&amp;quot;: Failure: (ID: 7a420ccf) 2022/02/09 04:26:55 Unable to extract package name and imports: unable to find Go package in /workspace/serverless_function_source_code.
Info 2022-02-08 22:26:55.278 CST Step #1 - &amp;quot;build&amp;quot;: exit status 1
&lt;/code>&lt;/pre>&lt;p>(No root-level Go file.)&lt;/p>
&lt;pre tabindex="0">&lt;code>Info 2022-02-09 23:25:32.516 CST Step #1 - &amp;quot;build&amp;quot;: Done &amp;quot;go list -m&amp;quot; (4.498213ms)
Info 2022-02-09 23:25:32.516 CST Step #1 - &amp;quot;build&amp;quot;: Failure: (ID: 03a1e2f7) ...ry.io/otel/exporters/otlp/internal/retry@v1.3.0: is explicitly required in go.mod, but not marked as explicit in vendor/modules.txt
Info 2022-02-09 23:25:32.516 CST Step #1 - &amp;quot;build&amp;quot;: go.opentelemetry.io/otel/exporters/otlp/otlpmetric@v0.26.0: is explicitly required in go.mod, but not marked as explicit in vendor/modules.txt
Info 2022-02-09 23:25:32.516 CST Step #1 - &amp;quot;build&amp;quot;: go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc@v0.26.0: is explicitly required in go.mod, but not marked as explicit in vendor/modules.txt
Info 2022-02-09 23:25:32.516 CST Step #1 - &amp;quot;build&amp;quot;: go.opentelemetry.io/otel/exporters/otlp/otlptrace@v1.3.0: is explicitly required in go.mod, but not marked as explicit in vendor/modules.txt
Info 2022-02-09 23:25:32.516 CST Step #1 - &amp;quot;build&amp;quot;: go.opentelemetry.io/otel/internal/metric@v0.26.0: is explicitly required in go.mod, but not marked as explicit in vendor/modules.txt
...A few dozen more of those.
Info 2022-02-09 23:25:32.516 CST Step #1 - &amp;quot;build&amp;quot;: To ignore the vendor directory, use -mod=readonly or -mod=mod.
Info 2022-02-09 23:25:32.516 CST Step #1 - &amp;quot;build&amp;quot;: To sync the vendor directory, run:
Info 2022-02-09 23:25:32.516 CST Step #1 - &amp;quot;build&amp;quot;: go mod vendor
&lt;/code>&lt;/pre>&lt;p>(This was from trying to not have more than one go.mod file. I&amp;rsquo;d never seen this error in any Go I&amp;rsquo;ve ever written before.)&lt;/p>
&lt;pre tabindex="0">&lt;code> - &amp;quot;build&amp;quot;: Failure: (ID: 7a966edd) vendored dependencies must include &amp;quot;github.com/GoogleCloudPlatform/functions-framework-go&amp;quot;; if your function does not depend on the module, please add a blank import: `_ &amp;quot;github.com/GoogleCloudPlatform/functions-framework-go/funcframework&amp;quot;`
&lt;/code>&lt;/pre>&lt;p>(The why behind this is never really explained, but I&amp;rsquo;m not worried about a random empty import.)&lt;/p>
&lt;p>All said and done, it took me two evenings staying up to 1:30AM or so to get all the lights to turn green.&lt;/p>
&lt;h2 id="cloud-function-woes">Cloud Function Woes&lt;/h2>
&lt;p>I kept trying to diagnose why the function that finalizes meal plans was failing, and it didn&amp;rsquo;t seem to matter what I changed in code, the subsequent logs would never show up. I was eventually able to suss out that Terraform wasn&amp;rsquo;t updating my Cloud Function at all. It was still on version one, despite a dozen or so &amp;ldquo;successful&amp;rdquo; deploys. It turned out &lt;a href="https://github.com/hashicorp/terraform-provider-google/issues/1938">I wasn&amp;rsquo;t the only person who had experienced this&lt;/a>, and after applying that solution, was finally able to start debugging in proper form.&lt;/p>
&lt;p>After some normal user errors, I encountered an issue getting my now-running Cloud Function to connect to the database.&lt;/p>
&lt;pre tabindex="0">&lt;code>Cloud SQL connection failed. Please see https://cloud.google.com/sql/docs/mysql/connect-overview for additional details: ensure that the account has access to &amp;quot;xxxxxxxx-dev:us-central1:dev&amp;quot; (and make sure there's no typo in that name). Error during generateEphemeral for xxxxxxxx-dev:us-central1:dev: googleapi: Error 403: The client is not authorized to make this request., notAuthorized
&lt;/code>&lt;/pre>&lt;p>(Again, the &amp;ldquo;., notAuthorized&amp;rdquo; part was actually present in the output. Funnily enough, every time I get errors related to this, they link to the &lt;a href="https://cloud.google.com/sql/docs/mysql/connect-overview">MySQL docs&lt;/a> even though I&amp;rsquo;m using the Postgres version of CloudSQL. I let it pass because you can easily navigate to the Postgres version from those docs.)&lt;/p>
&lt;h2 id="cloud-run-woes">Cloud Run Woes&lt;/h2>
&lt;p>Cloud Run was not without its own, similar, issues. I&amp;rsquo;d be updating code and a new revision wouldn&amp;rsquo;t get deployed. I realized I was probably asking too much of Terraform here, so I tried to go about it via &lt;a href="https://github.com/google-github-actions/deploy-cloudrun">the official Github Action for deploying Cloud Run services&lt;/a>. Immediately, I encountered an error:&lt;/p>
&lt;pre tabindex="0">&lt;code>ERROR: (gcloud.run.deploy) PERMISSION_DENIED: Permission `iam.serviceaccounts.actAs` denied on service account api-server@service-dev.iam.gserviceaccount.com (or it may not exist).
&lt;/code>&lt;/pre>&lt;p>After some searching, I happened upon &lt;a href="https://stackoverflow.com/questions/55788714/deploying-to-cloud-run-with-a-custom-service-account-failed-with-iam-serviceacco">this StackOverflow answer&lt;/a> for precisely this problem and realized I needed to add the &amp;ldquo;Service Account User&amp;rdquo; permission to my GitHub Deployer IAM Principal. This caused my next deploy to work, but the one after that failed with the familiar error message. I discovered that somehow the Service Account User role was being removed from the Google Actions user after each deploy. So I put the relevant permission (&amp;ldquo;iam.serviceaccounts.actAs&amp;rdquo;) in a custom role and gave that role to the Actions user. That worked, and I could continue deploying without interruption.&lt;/p>
&lt;h2 id="database-connectivity">Database Connectivity&lt;/h2>
&lt;p>On AWS, whenever I needed to talk to the database, I employed this awful trick where I would spin up a Cloud 9 instance and use the terminal for that instance to connect to the RDS instance. Since the RDS instance wasn&amp;rsquo;t configured for public connectivity, this was the only way I could manage to run raw queries against it.&lt;/p>
&lt;p>For GCP, this is even harder because, by default, the Postgres instances are configured to reject connections that don&amp;rsquo;t have TLS enabled, and the certs you can download from the GCP interface don&amp;rsquo;t seem to work on localhost or with an app like &lt;a href="https://github.com/beekeeper-studio/beekeeper-studio">Beekeeper&lt;/a>. You can connect via Cloud Shell, but you can&amp;rsquo;t use the certs there either.&lt;/p>
&lt;p>The only way I&amp;rsquo;ve found to consistently run raw queries against the database is to temporarily disable the TLS requirement and then connect from the Cloud Shell while remembering to reactivate it. Kind of painful, but not the world&amp;rsquo;s biggest inconvenience.&lt;/p>
&lt;h2 id="google-cloud-console">Google Cloud Console&lt;/h2>
&lt;p>Just a quick note, but I quite like it, actually. It&amp;rsquo;s just as easy to find things on GCP as it is on AWS. One complaint is that the interface, at times, completely fails to work if you disable common NoScript domains like &amp;ldquo;googletagmanager.com&amp;rdquo;.&lt;/p>
&lt;h2 id="static-site-woes">Static Site Woes&lt;/h2>
&lt;p>This project also makes use of Google Cloud Storage for the static page that interacts with the API. When I tried to create a bucket with the appropriate name for my domain in GCP, I encountered this error:&lt;/p>
&lt;pre tabindex="0">&lt;code>The bucket you tried to create is a domain name owned by another user.
&lt;/code>&lt;/pre>&lt;p>This ended up being that my Terraform service account user wasn&amp;rsquo;t listed as an owner of the domain in the Webmaster controls. Adding it was easy enough, but the form didn&amp;rsquo;t trim whitespace so complained that the service account I provided it with (which has a &amp;ldquo;.iam.gserviceaccount.com&amp;rdquo; domain name) was not a valid Google account. Deleting the very hard to notice leading space was the ticket, though.&lt;/p>
&lt;h2 id="monitoring-improvements">Monitoring Improvements&lt;/h2>
&lt;p>One of the worst things about AWS was feeling like I basically could not have metrics or decent traces, feeling like I had to use X-Ray because of the state of the observability product market. Happy to report that &lt;a href="https://cloud.google.com/trace">Cloud Trace&lt;/a> is leagues better than X-Ray, in my opinion, as is the log manager for GCP. This service collects 100% of traces the service produces.&lt;/p>
&lt;p>Additionally, GCP has these things called &lt;a href="https://cloud.google.com/monitoring/uptime-checks">Uptime Checks&lt;/a>, which, uh, make HTTP requests to your hosted services and report on the latency encountered. I was able to set one up for both the user-facing static webapp and the API server being hosted in Cloud Run.&lt;/p>
&lt;p>&lt;img src="https://blog.verygoodsoftwarenotvirus.ru/05-greener-cloud-pastures/images/api_server_uptime_check_example.png" alt="">&lt;/p>
&lt;p>&lt;img src="https://blog.verygoodsoftwarenotvirus.ru/05-greener-cloud-pastures/images/webapp_uptime_check_example.png" alt="">&lt;/p>
&lt;p>As you can see, aside from the occasional outlier, it&amp;rsquo;s been pretty reliable.
One thing I wasn&amp;rsquo;t able to accomplish in either AWS or GCP was app-level metrics, specifically Go runtime metrics. Locally, I can visualize how much time is spent, for instance, collecting garbage, but I cannot do this on GCP. Rather, I probably could, but my understanding of how Cloud Run operates is that the application itself is spun up and down depending on inbound traffic, so I&amp;rsquo;m not sure that collecting them would even be useful under that scheme.&lt;/p>
&lt;h2 id="frontend-cookie-frustrations">Frontend Cookie Frustrations&lt;/h2>
&lt;p>The webapp for this service is a static site, which talks to the API server over HTTP, authenticated by a cookie. The API server issues a cookie for the &amp;ldquo;www.&amp;rdquo; part of the app, but the browser will not include this cookie in requests to the API server because it has a different subdomain. Something needs to watch for requests to a given set of path prefixes on the webapp and forward it to the API service.&lt;/p>
&lt;p>On AWS, I was able to use a &lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/GettingStarted.SimpleDistribution.html">CloudFront Distribution&lt;/a> to do this:&lt;/p>
&lt;pre tabindex="0">&lt;code>ordered_cache_behavior {
path_pattern = &amp;quot;/api/v1/*&amp;quot;
allowed_methods = [&amp;quot;GET&amp;quot;, &amp;quot;HEAD&amp;quot;, &amp;quot;OPTIONS&amp;quot;, &amp;quot;PUT&amp;quot;, &amp;quot;POST&amp;quot;, &amp;quot;PATCH&amp;quot;, &amp;quot;DELETE&amp;quot;]
cached_methods = [&amp;quot;GET&amp;quot;, &amp;quot;HEAD&amp;quot;, &amp;quot;OPTIONS&amp;quot;]
target_origin_id = local.api_origin_id
forwarded_values {
query_string = true
headers = [&amp;quot;Origin&amp;quot;]
cookies {
forward = &amp;quot;whitelist&amp;quot;
whitelisted_names = [&amp;quot;servicecookie&amp;quot;]
}
}
min_ttl = 0
default_ttl = 0
max_ttl = 0
compress = true
viewer_protocol_policy = &amp;quot;redirect-to-https&amp;quot;
}
&lt;/code>&lt;/pre>&lt;p>It&amp;rsquo;s been long enough that I can&amp;rsquo;t explain to you &lt;em>how&lt;/em> that worked, but I can testify that it &lt;em>did&lt;/em> work. GCP&amp;rsquo;s Cloud Storage doesn&amp;rsquo;t, as far as I can discern, have a comparable feature. Locally, I was solving this problem by having a Caddy instance that proxied requests, so I thought maybe deploying a Caddy instance would be the most effortless solution, but I&amp;rsquo;d just use Cloud Run to host it, which would mean I&amp;rsquo;m effectively paying twice for any request made to the API service.&lt;/p>
&lt;p>&lt;a href="https://developers.cloudflare.com/workers/">Cloudflare Workers&lt;/a> ended up being the solution that worked best for me. They have an example that I was able to very easily adapt to my usecase:&lt;/p>
&lt;pre tabindex="0">&lt;code>addEventListener('fetch', event =&amp;gt; {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
var url = new URL(request.url);
if (url.pathname.startsWith('/api/') || url.pathname.startsWith('/users/')) {
url.hostname = 'api.service.dev';
}
return await fetch(url, request);
}
&lt;/code>&lt;/pre>&lt;p>And the corresponding Terraform:&lt;/p>
&lt;pre tabindex="0">&lt;code>resource &amp;quot;cloudflare_worker_script&amp;quot; &amp;quot;dev_reverse_proxy&amp;quot; {
name = &amp;quot;dev_reverse_proxy&amp;quot;
content = file(&amp;quot;reverse_proxy.js&amp;quot;)
}
resource &amp;quot;cloudflare_worker_route&amp;quot; &amp;quot;users_route&amp;quot; {
zone_id = var.CLOUDFLARE_ZONE_ID
pattern = &amp;quot;https://www.service.dev/users/*&amp;quot;
script_name = cloudflare_worker_script.dev_reverse_proxy.name
}
resource &amp;quot;cloudflare_worker_route&amp;quot; &amp;quot;api_route&amp;quot; {
zone_id = var.CLOUDFLARE_ZONE_ID
pattern = &amp;quot;https://www.service.dev/api/*&amp;quot;
script_name = cloudflare_worker_script.dev_reverse_proxy.name
}
&lt;/code>&lt;/pre>&lt;p>Cloudflare Workers come with a &lt;a href="https://developers.cloudflare.com/workers/platform/limits/#worker-limits">quite generous free tier&lt;/a>, which I&amp;rsquo;m not worried about really ever exhausting.&lt;/p>
&lt;h2 id="pricing-outcomes">Pricing Outcomes&lt;/h2>
&lt;p>Since the impetus to all this was the price for AWS, it seems fair to evaluate the price of running on GCP. Just so we&amp;rsquo;re all up to date, here&amp;rsquo;s what the bill is for:&lt;/p>
&lt;ul>
&lt;li>A Cloud SQL Postgres 13 instance running 24/7&lt;/li>
&lt;li>A Cloud Run API server running when traffic is received&lt;/li>
&lt;li>A Pub/Sub topic that triggers Cloud Functions&lt;/li>
&lt;li>Cloud Functions that run on a cron to manipulate data&lt;/li>
&lt;li>Prober Cloud Functions that run every five minutes and actually make use of the hosted service.&lt;/li>
&lt;/ul>
&lt;p>The prober function basically signs up a four-person household, creates some recipes, creates a meal plan, and votes on it for all members. It then verifies that the finalizer is running by waiting until the meal plan is finalized after all votes are in. So, I&amp;rsquo;m actually getting regular traffic to this service.&lt;/p>
&lt;p>&lt;img src="https://blog.verygoodsoftwarenotvirus.ru/05-greener-cloud-pastures/images/march_bill.png" alt="">&lt;/p>
&lt;p>Here&amp;rsquo;s the bill I received for running the service in March. I&amp;rsquo;ve left out details specific to my account like the ID of the project, about $5 in credits towards my account, as well as things like SKU IDs which are probably not unique but are definitely not helpful in understanding the cost.&lt;/p>
&lt;!-- take a deep breath -->
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Service description&lt;/th>
&lt;th>SKU description&lt;/th>
&lt;th>Cost type&lt;/th>
&lt;th>Usage start date&lt;/th>
&lt;th>Usage end date&lt;/th>
&lt;th>Usage amount&lt;/th>
&lt;th>Usage unit&lt;/th>
&lt;th>Unrounded Cost ($)&lt;/th>
&lt;th>Cost ($)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Cloud SQL&lt;/td>
&lt;td>Cloud SQL for PostgreSQL: Zonal - Micro instance in Americas&lt;/td>
&lt;td>Usage&lt;/td>
&lt;td>2022-03-01&lt;/td>
&lt;td>2022-03-31&lt;/td>
&lt;td>742.765&lt;/td>
&lt;td>hour&lt;/td>
&lt;td>7.799017&lt;/td>
&lt;td>7.8&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Stackdriver Trace&lt;/td>
&lt;td>Spans ingested&lt;/td>
&lt;td>Usage&lt;/td>
&lt;td>2022-03-01&lt;/td>
&lt;td>2022-03-31&lt;/td>
&lt;td>&amp;ldquo;36,903,907&amp;rdquo;&lt;/td>
&lt;td>count&lt;/td>
&lt;td>6.877799&lt;/td>
&lt;td>6.88&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cloud SQL&lt;/td>
&lt;td>Cloud SQL for PostgreSQL: Zonal - Standard storage in Americas&lt;/td>
&lt;td>Usage&lt;/td>
&lt;td>2022-03-01&lt;/td>
&lt;td>2022-03-31&lt;/td>
&lt;td>19.994&lt;/td>
&lt;td>gibibyte month&lt;/td>
&lt;td>3.398864&lt;/td>
&lt;td>3.4&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Secret Manager&lt;/td>
&lt;td>Secret access operations&lt;/td>
&lt;td>Usage&lt;/td>
&lt;td>2022-03-01&lt;/td>
&lt;td>2022-03-31&lt;/td>
&lt;td>&amp;ldquo;1,004,162&amp;rdquo;&lt;/td>
&lt;td>count&lt;/td>
&lt;td>2.982484&lt;/td>
&lt;td>2.98&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cloud Run&lt;/td>
&lt;td>CPU Allocation Time&lt;/td>
&lt;td>Usage&lt;/td>
&lt;td>2022-03-01&lt;/td>
&lt;td>2022-03-31&lt;/td>
&lt;td>&amp;ldquo;124,279.60&amp;rdquo;&lt;/td>
&lt;td>vCPU-second&lt;/td>
&lt;td>2.981886&lt;/td>
&lt;td>2.98&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cloud Functions&lt;/td>
&lt;td>CPU Time&lt;/td>
&lt;td>Usage&lt;/td>
&lt;td>2022-03-01&lt;/td>
&lt;td>2022-03-31&lt;/td>
&lt;td>&amp;ldquo;197,184.96&amp;rdquo;&lt;/td>
&lt;td>GHz-second&lt;/td>
&lt;td>1.968551&lt;/td>
&lt;td>1.97&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cloud Functions&lt;/td>
&lt;td>Memory Time&lt;/td>
&lt;td>Usage&lt;/td>
&lt;td>2022-03-01&lt;/td>
&lt;td>2022-03-31&lt;/td>
&lt;td>&amp;ldquo;123,073.24&amp;rdquo;&lt;/td>
&lt;td>gibibyte second&lt;/td>
&lt;td>0.304218&lt;/td>
&lt;td>0.3&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Secret Manager&lt;/td>
&lt;td>Secret version replica storage&lt;/td>
&lt;td>Usage&lt;/td>
&lt;td>2022-03-01&lt;/td>
&lt;td>2022-03-31&lt;/td>
&lt;td>8.997&lt;/td>
&lt;td>month&lt;/td>
&lt;td>0.179528&lt;/td>
&lt;td>0.18&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cloud Run&lt;/td>
&lt;td>Memory Allocation Time&lt;/td>
&lt;td>Usage&lt;/td>
&lt;td>2022-03-01&lt;/td>
&lt;td>2022-03-31&lt;/td>
&lt;td>&amp;ldquo;62,138.05&amp;rdquo;&lt;/td>
&lt;td>GiB-second&lt;/td>
&lt;td>0.154453&lt;/td>
&lt;td>0.15&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cloud Storage&lt;/td>
&lt;td>Standard Storage US Multi-region&lt;/td>
&lt;td>Usage&lt;/td>
&lt;td>2022-03-01&lt;/td>
&lt;td>2022-03-31&lt;/td>
&lt;td>5.635&lt;/td>
&lt;td>gibibyte month&lt;/td>
&lt;td>0.146461&lt;/td>
&lt;td>0.15&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cloud Storage&lt;/td>
&lt;td>Multi-Region Standard Class B Operations&lt;/td>
&lt;td>Usage&lt;/td>
&lt;td>2022-03-01&lt;/td>
&lt;td>2022-03-31&lt;/td>
&lt;td>&amp;ldquo;84,984&amp;rdquo;&lt;/td>
&lt;td>count&lt;/td>
&lt;td>0.033944&lt;/td>
&lt;td>0.03&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cloud Run&lt;/td>
&lt;td>Cloud Run Network Egress via Carrier Peering Network - North America Based&lt;/td>
&lt;td>Usage&lt;/td>
&lt;td>2022-03-01&lt;/td>
&lt;td>2022-03-31&lt;/td>
&lt;td>1.049&lt;/td>
&lt;td>gibibyte&lt;/td>
&lt;td>0.01781&lt;/td>
&lt;td>0.02&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cloud Storage&lt;/td>
&lt;td>NA-based Storage egress via peered/interconnect network&lt;/td>
&lt;td>Usage&lt;/td>
&lt;td>2022-03-01&lt;/td>
&lt;td>2022-03-31&lt;/td>
&lt;td>0.231&lt;/td>
&lt;td>gibibyte&lt;/td>
&lt;td>0.009187&lt;/td>
&lt;td>0.01&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cloud Storage&lt;/td>
&lt;td>Multi-Region Standard Class A Operations&lt;/td>
&lt;td>Usage&lt;/td>
&lt;td>2022-03-01&lt;/td>
&lt;td>2022-03-29&lt;/td>
&lt;td>194&lt;/td>
&lt;td>count&lt;/td>
&lt;td>0.00097&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cloud SQL&lt;/td>
&lt;td>Network Internet Egress from Americas to Americas&lt;/td>
&lt;td>Usage&lt;/td>
&lt;td>2022-03-01&lt;/td>
&lt;td>2022-03-31&lt;/td>
&lt;td>0.005&lt;/td>
&lt;td>gibibyte&lt;/td>
&lt;td>0.000932&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cloud SQL&lt;/td>
&lt;td>Network Internet Egress from Americas to EMEA&lt;/td>
&lt;td>Usage&lt;/td>
&lt;td>2022-03-01&lt;/td>
&lt;td>2022-03-31&lt;/td>
&lt;td>0.001&lt;/td>
&lt;td>gibibyte&lt;/td>
&lt;td>0.000133&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cloud SQL&lt;/td>
&lt;td>Network Internet Egress from Americas to China&lt;/td>
&lt;td>Usage&lt;/td>
&lt;td>2022-03-01&lt;/td>
&lt;td>2022-03-30&lt;/td>
&lt;td>0&lt;/td>
&lt;td>gibibyte&lt;/td>
&lt;td>0.000003&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cloud SQL&lt;/td>
&lt;td>Network Internet Egress from Americas to APAC&lt;/td>
&lt;td>Usage&lt;/td>
&lt;td>2022-03-06&lt;/td>
&lt;td>2022-03-26&lt;/td>
&lt;td>0&lt;/td>
&lt;td>gibibyte&lt;/td>
&lt;td>0.000002&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cloud Run&lt;/td>
&lt;td>Memory Allocation Time&lt;/td>
&lt;td>Usage&lt;/td>
&lt;td>2022-03-06&lt;/td>
&lt;td>2022-03-31&lt;/td>
&lt;td>1.75&lt;/td>
&lt;td>GiB-second&lt;/td>
&lt;td>0&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cloud Run&lt;/td>
&lt;td>Requests&lt;/td>
&lt;td>Usage&lt;/td>
&lt;td>2022-03-01&lt;/td>
&lt;td>2022-03-31&lt;/td>
&lt;td>&amp;ldquo;1,049,960&amp;rdquo;&lt;/td>
&lt;td>Requests&lt;/td>
&lt;td>0&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cloud Run&lt;/td>
&lt;td>Cloud Run Network Intra Region Egress&lt;/td>
&lt;td>Usage&lt;/td>
&lt;td>2022-03-01&lt;/td>
&lt;td>2022-03-31&lt;/td>
&lt;td>2.496&lt;/td>
&lt;td>gibibyte&lt;/td>
&lt;td>0&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cloud Scheduler&lt;/td>
&lt;td>Jobs&lt;/td>
&lt;td>Usage&lt;/td>
&lt;td>2022-03-01&lt;/td>
&lt;td>2022-03-31&lt;/td>
&lt;td>62&lt;/td>
&lt;td>Job-days&lt;/td>
&lt;td>0&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cloud Functions&lt;/td>
&lt;td>Invocations&lt;/td>
&lt;td>Usage&lt;/td>
&lt;td>2022-03-01&lt;/td>
&lt;td>2022-03-31&lt;/td>
&lt;td>&amp;ldquo;1,011,934&amp;rdquo;&lt;/td>
&lt;td>invocations&lt;/td>
&lt;td>0&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cloud Functions&lt;/td>
&lt;td>Network Egress from us-central1&lt;/td>
&lt;td>Usage&lt;/td>
&lt;td>2022-03-01&lt;/td>
&lt;td>2022-03-31&lt;/td>
&lt;td>1.549&lt;/td>
&lt;td>gibibyte&lt;/td>
&lt;td>0&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cloud Functions&lt;/td>
&lt;td>CPU Time&lt;/td>
&lt;td>Usage&lt;/td>
&lt;td>2022-03-01&lt;/td>
&lt;td>2022-03-31&lt;/td>
&lt;td>13.06&lt;/td>
&lt;td>GHz-second&lt;/td>
&lt;td>0&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cloud Functions&lt;/td>
&lt;td>Memory Time&lt;/td>
&lt;td>Usage&lt;/td>
&lt;td>2022-03-01&lt;/td>
&lt;td>2022-03-31&lt;/td>
&lt;td>175.525&lt;/td>
&lt;td>gibibyte second&lt;/td>
&lt;td>0&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cloud Logging&lt;/td>
&lt;td>Log Volume&lt;/td>
&lt;td>Usage&lt;/td>
&lt;td>2022-03-01&lt;/td>
&lt;td>2022-03-31&lt;/td>
&lt;td>4.507&lt;/td>
&lt;td>gibibyte&lt;/td>
&lt;td>0&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cloud Build&lt;/td>
&lt;td>Build time&lt;/td>
&lt;td>Usage&lt;/td>
&lt;td>2022-03-15&lt;/td>
&lt;td>2022-03-29&lt;/td>
&lt;td>32.583&lt;/td>
&lt;td>minutes of build time&lt;/td>
&lt;td>0&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cloud Storage&lt;/td>
&lt;td>Download Worldwide Destinations (excluding Asia &amp;amp; Australia)&lt;/td>
&lt;td>Usage&lt;/td>
&lt;td>2022-03-01&lt;/td>
&lt;td>2022-03-31&lt;/td>
&lt;td>0.042&lt;/td>
&lt;td>gibibyte&lt;/td>
&lt;td>0&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cloud Storage&lt;/td>
&lt;td>Regional Standard Class B Operations&lt;/td>
&lt;td>Usage&lt;/td>
&lt;td>2022-03-15&lt;/td>
&lt;td>2022-03-29&lt;/td>
&lt;td>107&lt;/td>
&lt;td>count&lt;/td>
&lt;td>0&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cloud Storage&lt;/td>
&lt;td>Standard Storage US Regional&lt;/td>
&lt;td>Usage&lt;/td>
&lt;td>2022-03-01&lt;/td>
&lt;td>2022-03-31&lt;/td>
&lt;td>0.099&lt;/td>
&lt;td>gibibyte month&lt;/td>
&lt;td>0&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cloud SQL&lt;/td>
&lt;td>Network Google Egress from Americas to Americas&lt;/td>
&lt;td>Usage&lt;/td>
&lt;td>2022-02-28&lt;/td>
&lt;td>2022-03-31&lt;/td>
&lt;td>5.633&lt;/td>
&lt;td>gibibyte&lt;/td>
&lt;td>0&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cloud Pub/Sub&lt;/td>
&lt;td>Message Delivery Basic&lt;/td>
&lt;td>Usage&lt;/td>
&lt;td>2022-03-01&lt;/td>
&lt;td>2022-03-31&lt;/td>
&lt;td>0.003&lt;/td>
&lt;td>tebibyte&lt;/td>
&lt;td>0&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cloud Pub/Sub&lt;/td>
&lt;td>Intra-region data delivery&lt;/td>
&lt;td>Usage&lt;/td>
&lt;td>2022-03-01&lt;/td>
&lt;td>2022-03-31&lt;/td>
&lt;td>1.102&lt;/td>
&lt;td>gibibyte&lt;/td>
&lt;td>0&lt;/td>
&lt;td>0&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="conclusion">Conclusion&lt;/h2>
&lt;p>I knew going into this I could probably hit a hosting target price of about $40, but hitting $27 was just very sweet. This puts the GCP cost at about $100 less than the equivalent AWS hosting. Even with &lt;a href="https://cloud.google.com/blog/products/infrastructure/updates-to-google-clouds-infrastructure-pricing">the recent changes to pricing that everybody seemed to freak out about&lt;/a>, to the best of my ability, I think this would raise my cost by maybe $5/month in the worst circumstances?&lt;/p>
&lt;p>I&amp;rsquo;m quite happy with the level of service quality I get, I feel confident in the application I&amp;rsquo;ve deployed, and I can easily pay for it without even having to let my spouse know ahead of time. I think GCP will be the platform I opt for going forward.&lt;/p></content></item><item><title>Baby's First AWS Deployment</title><link>https://blog.verygoodsoftwarenotvirus.ru/posts/babys-first-aws/</link><pubDate>Sat, 29 Jan 2022 23:34:37 -0600</pubDate><guid>https://blog.verygoodsoftwarenotvirus.ru/posts/babys-first-aws/</guid><description>Preface I intend for this post to serve as an experience report from a newbie AWS user. Hopefully you enjoy it and don&amp;rsquo;t feel like you&amp;rsquo;ve wasted your time at the end.
I want to take the time to note that I have great respect for all the folks who make these companies work and who build these products. None of my opinions are directed at a human being who has a name and/or respirates.</description><content>&lt;h3 id="preface">Preface&lt;/h3>
&lt;p>I intend for this post to serve as &lt;a href="https://github.com/golang/go/wiki/ExperienceReports">an experience report&lt;/a> from a newbie AWS user. Hopefully you enjoy it and don&amp;rsquo;t feel like you&amp;rsquo;ve wasted your time at the end.&lt;/p>
&lt;p>I want to take the time to note that I have great respect for all the folks who make these companies work and who build these products. None of my opinions are directed at a human being who has a name and/or respirates. If I couldn&amp;rsquo;t figure something out, I assume (and expect you to assume as well) that at the very least, I am more responsible for that shortcoming than the service providers or their documentation. In the extremely unlikely event anybody but me and my proofreader (hi &lt;a href="https://www.cloughproofreading.co.uk/">Caroline&lt;/a>!) ever even reads this, I hope that I will have provided enough information on whatever confusions I experienced that maybe something actionable can come of it.&lt;/p>
&lt;h3 id="the-app-in-question">The App in Question&lt;/h3>
&lt;p>The app itself is pretty simple. It’s a meal plan manager, where you can organize and propose a meal plan for your household for the week. So, you can say, for example, &amp;ldquo;For dinner on Wednesday, we can do spaghetti and meatballs, or egg fried rice or baba ganoush,&amp;rdquo; and then every member of your household can vote on which one they’d prefer to eat. An async worker tallies the rankings using the &lt;a href="https://en.wikipedia.org/wiki/Schulze_method">Schulze method&lt;/a> and finalizes the meal plan. Thus far, it has lived as a local-only project I work on in my spare time. I’ve sort of over-engineered it on purpose in the spirit of learning.&lt;/p>
&lt;p>A worker process checks the database every minute for meal plan proposals that are still awaiting votes but have expired voting deadlines so it can tally up the winner and finalize the meal plan. This is done in an external worker so that I don’t bog down any of the HTTP traffic the server handles with unrelated compute burden.&lt;/p>
&lt;p>Why a data changes queue? I don’t have any concrete product plans for this yet, as it were, but I think you could do something like email the meal plan creator if a vote is received and the meal plan is consequently finalized, for instance. You could produce audit logs for a more serious application. It&amp;rsquo;s also where any customer data analytics collection should occur, so you don&amp;rsquo;t inherit that latency during user requests.&lt;/p>
&lt;p>This might seem like a lot, but it’s actually quite pared down from a previous iteration of the system where there were separate queues for writes, updates, deletes, the meal plan finalization worker (which I had set up as merely the sole component of a broader &amp;ldquo;chores&amp;rdquo; worker), and the data changes queue.&lt;/p>
&lt;h3 id="goals">Goals&lt;/h3>
&lt;p>I had a few things I really wanted to make sure I accomplished:&lt;/p>
&lt;ul>
&lt;li>I want to run it in AWS. To call it cargo culting is likely fair, but every single company I’ve ever worked for has used AWS. I’ve never been on the team that actually, y’know, &lt;em>uses&lt;/em> it, though. Plus, it’s bound to be a valuable skill for a software engineer to have.&lt;/li>
&lt;li>Since it’s going to be coming out of my own wallet, I want to – as best I can and where appropriate – avoid expensive pitfalls.
&lt;ul>
&lt;li>I follow Corey Quinn on Twitter (and, because of my recent foray into AWS, the Last Week in AWS newsletter he also organizes), so I know about the pitfalls of &lt;a href="https://www.lastweekinaws.com/blog/the-aws-managed-nat-gateway-is-unpleasant-and-not-recommended/">Managed NAT Gateways&lt;/a> and such. So I’d like to avoid that, if possible.&lt;/li>
&lt;li>In the AWS Pricing Calculator, I ran what I thought I would need as a bare minimum, what I thought I’d really need, and what I would recommend if I was spending someone else’s money. The estimates came out to be something like $80, $120, and $210 each (they also ended up being meaningfully inaccurate). I resolved to be willing to tolerate up to $200/month on this service in the event unforeseen costs occurred for this first go-around.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>No Kubernetes, if possible.
&lt;ul>
&lt;li>Invariably someone is going to read this and say, &amp;ldquo;What about Kubernetes?&amp;rdquo;. Kubernetes is a great solution for the right problems, but I don&amp;rsquo;t think a simple CRUD site connecting to a database is a worthy candidate. Also, since I&amp;rsquo;m on AWS, their offering, EKS, costs $75 without any compute to even start using, which I think will put me over my acceptable budget threshold.&lt;/li>
&lt;li>If I switch to Kubernetes, then I have to run it locally to build the configuration and to validate that it works the way it should. Last time I had to do this for work, it involved Vagrant and Minikube, and well&amp;hellip;please don’t make me do this.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>I want in on the Serverless hype.
&lt;ul>
&lt;li>That is to say, ideally, I end up with absolutely no machines that I, or anybody else, could SSH into.&lt;/li>
&lt;li>Ideally, I could define my infrastructure in terms of how little and how many resources it should receive, and have AWS do the plumbing to make it all function properly.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>I want it to scale.
&lt;ul>
&lt;li>If you’re groaning, well, same. &amp;ldquo;Scale&amp;rdquo; has many meanings, and I certainly can’t profess that I’ve built something that can handle most of those meanings.&lt;/li>
&lt;li>Ultimately, what I would like is to be at least positioned such that my little service could use as few resources as possible when (in the most likely case) nobody is using it, but also be able to handle a sudden spike in popularity without completely dying.&lt;/li>
&lt;li>As someone who has &lt;a href="https://blog.verygoodsoftwarenotvirus.ru/posts/the-story-of-porktrack/">had services eat shit before&lt;/a>, that’s &amp;ldquo;scaling&amp;rdquo; to me.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>I want to maintain as close to 100% of this infrastructure in code form as possible.
&lt;ul>
&lt;li>To satisfy this, I chose Terraform. I’ve never gotten a chance to write Terraform professionally, but I have seen it used frequently at my workplaces over the last few years, so I feel confident in using it. I was afraid of screwing up state management, and was glad to discover that Hashicorp (who make Terraform) have an official offering in the form of Terraform Cloud that can manage the state for you.&lt;/li>
&lt;li>I considered using Pulumi, which seems like a really cool project (shout out to all the folks building Pulumi!). That said, &lt;a href="https://www.pulumi.com/pricing/">their pricing model&lt;/a> for their cloud offering is based on hours in which they’re responsible for managing your infrastructure, measured in their own little credit currency, and I just didn’t like that. I don’t like the idea of wondering if I have enough funny money to get through the month, and I think it’s incorrect to suggest that Pulumi is &amp;ldquo;actively managing&amp;rdquo; anything when it isn’t running.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>As close to 100% of the infrastructure has to be able to be spun up from start to finish in as few steps as possible, and ideally, just one.
&lt;ul>
&lt;li>I want to be able to go from having some AWS credentials attached to an account with no resources to a fully functioning website, ideally with a single CI run.&lt;/li>
&lt;li>I don’t actually know how much all of this is going to cost me. If I can spin it up and down on demand, then I can iterate on it when I have the time and save myself the money when I am just too swamped to touch it.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>I want to be able to view logs, traces, and metrics. The three pillars, as it were.&lt;/li>
&lt;li>Ideally, no vendor-specific code written.
&lt;ul>
&lt;li>Let me clarify: I don’t want to have to end up writing some glue code that is only relevant in AWS and is significant in scope. I make an exception for Lambdas themselves, since the actual wrapper stuff you’d have to implement is not significant. I could very easily take a Go Lambda and turn it into a &lt;a href="https://cloud.google.com/functions">Cloud Function&lt;/a>. I’m talking about code that you simply couldn’t port over in any fashion to another environment. I don&amp;rsquo;t want to write, for instance, &lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html">API Gateway Authorization Lambdas&lt;/a>.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;h3 id="translating-to-the-cloud">Translating to the Cloud&lt;/h3>
&lt;p>For AWS, I went with &lt;a href="https://aws.amazon.com/fargate/">ECS Fargate&lt;/a> for the main API server hosting. There are a few ways of hosting web servers in AWS, but Fargate seems to be the newest and most container-oriented. It seemed to be the closest thing to a &amp;ldquo;give me a container and some operational specifications, and I’ll take it from here&amp;rdquo; service, and the pricing didn’t seem too bad. When I did the calculation, the bare minimum you could spend (which was plenty for my little server) was just shy of $8/month.&lt;/p>
&lt;p>For the data changes queue, I used SQS, and for the changes worker that listens on that queue, I went with Lambda. I’ve used Lambda before, though never triggered by an SQS queue. I decided to use Lambda for the meal plan finalization worker as well.&lt;/p>
&lt;h3 id="database">Database&lt;/h3>
&lt;p>I opted for Aurora Serverless in my first go around, because I was really sold on the &amp;ldquo;this is THE serverless database option&amp;rdquo; hype. I imagined all these infinite scaling possibilities, and I assumed a low-tier RDS instance would get something paltry, like 10 or 15 connections per instance. &amp;ldquo;I’m gonna have so much traffic!&amp;rdquo; I told myself. &amp;ldquo;I’m gonna end up firing dozens if not hundreds of Lambdas at once and easily overwhelm whatever RDS instance I can afford!&amp;rdquo;&lt;/p>
&lt;p>This was very wrong. According to the relevant AWS documentation page, I discovered that a db.t2.micro should have 1000000000 ÷ 9531392 = 104.9 connections, if I’m doing that math right (I may very well not be). I also learned that having lots of traffic doesn’t mean you will spin up lots of Lambdas, as they execute in batches, and you have to be very explicit to set it up to react in a one-to-one manner.&lt;/p>
&lt;p>Aurora Serverless proved to be much too expensive, and came with some, in my opinion, major caveats. For one, they &lt;a href="https://web.archive.org/web/20220129203152/https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/aurora-serverless.relnotes.html">only let you use version 10.14&lt;/a> of Postgres, which &lt;a href="https://www.postgresql.org/docs/release/12.4/">came out&lt;/a> on &lt;a href="https://www.postgresql.org/docs/release/12.4/">the same day&lt;/a> as version 12.4.&lt;/p>
&lt;p>Additionally, &lt;a href="https://calculator.aws/#/">the AWS calculator&lt;/a> lets you configure a &lt;em>*sigh&lt;/em>* &amp;ldquo;Aurora Serverless PostgreSQL-Compatible Edition&amp;rdquo; (they must get paid by the syllable at AWS) with only one ACU (Aurora Capacity Unit – nine syllable bonus points for you), and it will tell you that one ACU is $43.80, at the time of this writing. You actually cannot configure a Serverless Postgres Aurora instance with less than two ACUs, which means the &lt;em>least&lt;/em> you can spend is nearly $90, but there is no indication of this &amp;ldquo;gotcha&amp;rdquo; in the billing calculator. I ended up using a small RDS instance for the database.&lt;/p>
&lt;h3 id="diving-in">Diving In&lt;/h3>
&lt;p>I got started by creating a Terraform Cloud account. I played around with Terraform first by setting up a version controlled workspace and wrote some minor code to ensure all the organization’s repositories have access to the same base set of issue labels:&lt;/p>
&lt;pre tabindex="0">&lt;code>variable &amp;quot;issue_label_configs&amp;quot; {
type = list(any)
default = [
{
Name = &amp;quot;blocked&amp;quot;,
Description = &amp;quot;blocked by other issues&amp;quot;,
Color = &amp;quot;B60205&amp;quot;,
},
{
Name = &amp;quot;nice-to-have&amp;quot;
Description = &amp;quot;things we should maybe do. maybe not.&amp;quot;
Color = &amp;quot;E5E4D3&amp;quot;
},
{
Name = &amp;quot;tech debt&amp;quot;
Description = &amp;quot;paying back technical debt.&amp;quot;
Color = &amp;quot;9AFE3B&amp;quot;
},
# etc...
]
}
variable &amp;quot;repositories&amp;quot; {
type = list(string)
default = [
&amp;quot;api_server&amp;quot;,
&amp;quot;webapp&amp;quot;,
# etc...
]
}
locals {
all_labels = setproduct(var.repositories, var.issue_label_configs)
}
resource &amp;quot;github_issue_label&amp;quot; &amp;quot;labels&amp;quot; {
count = length(local.all_labels)
repository = local.all_labels[count.index][0]
name = local.all_labels[count.index][1].Name
description = local.all_labels[count.index][1].Description
color = local.all_labels[count.index][1].Color
}
&lt;/code>&lt;/pre>&lt;p>This allowed me to get a little comfortable with HCL and Terraform Cloud before writing anything that would spend money. Highly recommended.&lt;/p>
&lt;p>I already had an AWS account, and I briefly tried to set up something that made sense for this little organization, but I wasn’t left, after about a day of searching, with a clear picture of how I should manage that. I could use &lt;a href="https://aws.amazon.com/organizations/">AWS Organizations&lt;/a> to link multiple companies and have one account per environment, but that seems like overkill and a lot to set up for a little app, so I just used the account I already had and created a key for Terraform and GitHub Actions. Terraform needs the key to create the infrastructure; GitHub Actions needs the key to push container images on merge.&lt;/p>
&lt;p>While I used the VCS configuration for the global org Terraform workspace, I opted to use the remote configuration for the actual project repositories. This is because I want to be able to control when the actual Terraform apply happens, whereas if it’s VCS-backed, it fires off as soon as the commit happens.&lt;/p>
&lt;h3 id="configuration">Configuration&lt;/h3>
&lt;p>I chose to use &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html">AWS Systems Manager Parameter Store&lt;/a> (more syllable bonus points for you!) to store things like the address of the SQS queues and database credentials. That way, I can have Terraform create those values and update them when appropriate, and my deployed software can have a stable place from which to fetch configuration values. This helps me keep deployment to one step, as opposed to a method where I&amp;rsquo;d have to first create the resources and &lt;em>then&lt;/em> put their respective values into a config in a second step.&lt;/p>
&lt;p>There are a LOT of ways to host configuration values in AWS. You could use Parameter Store (and, from within that, you can use a standard or &lt;em>advanced&lt;/em> parameter, the only difference of which seems to be a larger payload limit), you could use &lt;a href="https://docs.aws.amazon.com/appconfig/latest/userguide/what-is-appconfig.html">AWS AppConfig&lt;/a> if you need really fancy features like schema validation, you could plop it into an S3 bucket, or you could probably use &lt;a href="https://aws.amazon.com/dynamodb/">Dynamo&lt;/a> if you really wanted. There are no consistent guidelines for which of these products is advisable for any given situation, and I ended up using Parameter Store because it&amp;rsquo;s what we do at work.&lt;/p>
&lt;h3 id="observability">Observability&lt;/h3>
&lt;p>Figuring out an observability stack that worked ended up feeling impossible. Locally, I use &lt;a href="https://opentelemetry.io/">OpenTelemetry&lt;/a> reporting straight to a &lt;a href="https://www.jaegertracing.io/">Jaeger&lt;/a> instance for traces, a &lt;a href="https://prometheus.io/">Prometheus&lt;/a> instance for metrics, and a &lt;a href="https://grafana.com/">Grafana&lt;/a> instance to visualize all of it. I feel it&amp;rsquo;s important to note &lt;a href="https://opentelemetry.io/docs/collector/">the Collector&lt;/a>&amp;rsquo;s absence here, as it is the main way OpenTelemetry advises getting data out to various providers, via a collector instance or sidecar. I have experience using the Collector, but the OpenTelemetry code I wrote for this repository comes from an era where the best way to get this done was to use &lt;a href="https://github.com/open-telemetry/opentelemetry-go-contrib">the Contrib libraries&lt;/a>.&lt;/p>
&lt;p>In AWS, I just used X-Ray for traces and CloudWatch for logs, but CloudWatch metrics are &lt;a href="https://aws.amazon.com/cloudwatch/pricing/">pretty expensive&lt;/a>. At $0.30/metric (as of the time of this writing), the &lt;a href="https://pkg.go.dev/runtime/metrics#hdr-Supported_metrics">standard suite of Go runtime metrics&lt;/a> would cost $8.70/month. I just assume that blows up if I have multiple services.&lt;/p>
&lt;p>AWS offers &lt;a href="https://aws.amazon.com/grafana/">a managed Grafana service&lt;/a> &lt;em>and&lt;/em> &lt;a href="https://aws.amazon.com/prometheus/">a managed Prometheus service&lt;/a>. However, only the Prometheus service is set up in the Terraform provider, which means I couldn’t provision them together. I had half a mind to add the provider myself, but even if we ignore that I know nothing about how either product works or how to stitch the two of them together, am I going to spend my precious free weekend time adding features to not one but TWO publicly traded companies’ products?&lt;/p>
&lt;p>&lt;img src="https://blog.verygoodsoftwarenotvirus.ru/04-babys-first-aws/images/no-i-dont-think-i-will.png" alt="">&lt;/p>
&lt;p>I tried to implement &lt;a href="https://grafana.com/products/cloud/">Grafana Cloud&lt;/a>, since they offer a pretty competent free tier, and I very much enjoy using Grafana in my local stack. They require you run their own &lt;a href="https://grafana.com/docs/grafana-cloud/agent/">Grafana Agent&lt;/a> sidecar, but I was never quite able to get it all working correctly. The team itself was very communicative and &lt;a href="https://github.com/grafana/agent/discussions/593">invited me to troubleshoot with them on GitHub&lt;/a>. This was how I discovered a lot of my failings were user error. I still like Grafana Cloud quite a bit, and would like to get to use it in the future. The team inspired great confidence in their product for me.&lt;/p>
&lt;p>I took a gander at &lt;a href="https://www.datadoghq.com/">DataDog&lt;/a>, which I&amp;rsquo;ve also used at nearly every job I&amp;rsquo;ve ever had, but it is very difficult to determine how much it would cost me. I know it’s $15/host, but does the OpenTelemetry sidecar I’d have to run in ECS count as a host? Each container? Each service? Each Lambda invocation? The best I could gather was that it would cost me anything from $50 to $400 to use, and either side of that huge range still puts me outside my budget.&lt;/p>
&lt;p>&lt;a href="https://www.honeycomb.io/">Honeycomb&lt;/a> is an attractive offering and was easy to get working. It wasn&amp;rsquo;t until I learned that they only support logs and traces on the free plan, and metrics aren’t achievable without calling a sales rep, that I felt dismayed. Every indication I could find for how much this would cost me (to be fair, there were few) looked like it started at $1k.&lt;/p>
&lt;p>There are a ton of &lt;a href="https://www.logicmonitor.com/cloud-monitoring">other&lt;/a> &lt;a href="https://www.dynatrace.com/platform/infrastructure-monitoring/">providers&lt;/a> in this space, but they all feel either dated compared to DataDog and Honeycomb or geared exclusively towards large Enterprise contracts. I don’t begrudge these companies for pricing the way they do, because they’re not thinking of folks like me when they make these decisions, and it probably makes sense for them not to. Doesn’t help me out all that much, though.&lt;/p>
&lt;p>I ended up using X-Ray for tracing and CloudWatch for logging. I never managed to implement the Go runtime metrics I wanted because I couldn&amp;rsquo;t manage to get the OpenTelemetry code working in AWS, despite a few days of my best efforts.&lt;/p>
&lt;blockquote class="twitter-tweet">&lt;p lang="und" dir="ltr">😎 &lt;a href="https://t.co/8cD6wT0S0x">pic.twitter.com/8cD6wT0S0x&lt;/a>&lt;/p>&amp;mdash; footloose and fancy-free (@vgsnv) &lt;a href="https://twitter.com/vgsnv/status/1470958221850558467">December 15, 2021&lt;/a>&lt;/blockquote> &lt;script async src="https://platform.twitter.com/widgets.js" charset="utf-8">&lt;/script>
&lt;h3 id="elasticsearch-woes">Elasticsearch Woes&lt;/h3>
&lt;p>Locally, the service used Elasticsearch to allow users to search through ingredient names and such. At first, I tried to use &lt;a href="https://www.elastic.co/cloud/">Elastic.co&amp;rsquo;s Cloud offering&lt;/a>, which is about $17/month. I found that the base Cloud offering seems to be geared towards folks using the ELK stack, but I didn&amp;rsquo;t need the LK part of it. Also, VPC restrictions meant I couldn&amp;rsquo;t get the Lambdas to write to it.&lt;/p>
&lt;p>I tried to use &lt;a href="https://aws.amazon.com/opensearch-service/the-elk-stack/what-is-opensearch/">AWS' managed Elasticsearch offering&lt;/a>, but it came with some serious restrictions. &lt;a href="https://docs.aws.amazon.com/opensearch-service/latest/developerguide/supported-instance-types.html">&amp;ldquo;The t2.micro.search instance type supports only Elasticsearch 1.5 and 2.3.&amp;quot;&lt;/a> (a version that came out at the end of March 2016, almost six years ago). I also wasn&amp;rsquo;t ever able to get the code I&amp;rsquo;d written (that worked with Elastic Cloud) to communicate with it, though in hindsight, it must have been some combination of IAM and VPC failures on my part. I spent the better part of a week trying to get this to work, and it was literally so frustrating that I rewrote the search code to just do ILIKE queries to the database and removed Elasticsearch from the project altogether.&lt;/p>
&lt;blockquote class="twitter-tweet">&lt;p lang="en" dir="ltr">if you have the nerve, gall, the unmitigated audacity to try and run an Elasticsearch machine in AWS on something approachable like a &lt;a href="https://t.co/8eNdwUy2NB">https://t.co/8eNdwUy2NB&lt;/a>, they punish you by making you use a version of Elasticsearch from nearly six years ago. This is customer obsession, folks.&lt;/p>&amp;mdash; footloose and fancy-free (@vgsnv) &lt;a href="https://twitter.com/vgsnv/status/1464610856046583815">November 27, 2021&lt;/a>&lt;/blockquote> &lt;script async src="https://platform.twitter.com/widgets.js" charset="utf-8">&lt;/script>
&lt;h3 id="aspiration-shopping">Aspiration Shopping&lt;/h3>
&lt;p>A phenomenon I noticed when I was looking through the (sincerely, epic) list of services that AWS offers is that it’s easy to start adding things to your mental cart that might be premature for your needs. Around the time I started planning all this stuff, AWS &lt;a href="https://aws.amazon.com/blogs/aws/cloudwatch-evidently/">announced Cloudwatch Evidently&lt;/a>, and I got really into the idea that I would end up writing some Evidently tests after all was said and done. AppConfig was another thing like that, a moment where I said, &amp;ldquo;OoooOOOOOoooo, that &lt;em>would&lt;/em> be fancy!&amp;rdquo;. I don’t have any good advice for avoiding this phenomenon but feel nevertheless obligated to call attention to it.&lt;/p>
&lt;h3 id="ordering-is-important">Ordering is Important&lt;/h3>
&lt;p>One of the first obstacles I encountered was maintaining my desired operational order. I wanted to not only be able to spin everything up from scratch, but also have all these Lambdas set up before pushing code. The deploy job has two phases that operate in tandem: &amp;ldquo;build&amp;rdquo;, which compiles binaries for the Lambdas, and &amp;ldquo;scaffold&amp;rdquo;, which effectively runs &amp;ldquo;Terraform apply&amp;rdquo;. After those, the &amp;ldquo;deploy&amp;rdquo; job runs, building and shipping containers and the artifacts from &amp;ldquo;build&amp;rdquo;. I haven’t managed to find time to figure out how to build the containers and download them in later jobs.&lt;/p>
&lt;p>The problem comes from Terraform requiring some artifact to set up a Lambda function. I could make the &amp;ldquo;scaffold&amp;rdquo; step have &amp;ldquo;build&amp;rdquo; as a prerequisite, and have &amp;ldquo;scaffold&amp;rdquo; download those artifacts, but I don’t like that. Instead, a neat trick I learned was that you can give the Lambda an empty zip folder by using the &amp;ldquo;hashicorp/archive&amp;rdquo; provider to create an empty zip folder:&lt;/p>
&lt;pre tabindex="0">&lt;code>data &amp;quot;archive_file&amp;quot; &amp;quot;dummy_zip&amp;quot; {
type = &amp;quot;zip&amp;quot;
output_path = &amp;quot;${path.module}/data_changes_lambda.zip&amp;quot;
source {
content = &amp;quot;hello&amp;quot;
filename = &amp;quot;dummy.txt&amp;quot;
}
}
&lt;/code>&lt;/pre>&lt;p>Then, you can reference it in your Lambda definitions:&lt;/p>
&lt;pre tabindex="0">&lt;code>resource &amp;quot;aws_lambda_function&amp;quot; &amp;quot;example&amp;quot; {
# required things here
filename = data.archive_file.dummy_zip.output_path
}
&lt;/code>&lt;/pre>&lt;p>This allowed my desired order to work correctly, with the caveat that if your Lambdas manage to get invoked before the &amp;ldquo;deploy&amp;rdquo; job can finish deploying the relevant Lambda artifacts, they will fail with a spectacular error.&lt;/p>
&lt;p>You could probably also use containers, since the Lambda runtime supports those as well, but I haven’t tried it, so I don’t know what happens when you set it to use a Lambda that has no images pushed to it yet.&lt;/p>
&lt;h3 id="vpc-headaches">VPC Headaches&lt;/h3>
&lt;p>Whenever I’d worked in AWS before, I invariably ran into IAM issues, which led me to believe that IAM would be the thing I fought with most in this effort, but far and away, it was VPC. There are basically no good tutorials for how to provision this correctly; you kind of just have to copy the same IP ranges everybody else has copy/pasted and hope you get there. I had at least two moments where something I was pretty sure I’d already tried a few times that didn’t work (but just wanted to check one last time) worked all of a sudden, and I was too frustrated and tired to even be happy about it.&lt;/p>
&lt;p>Troubleshooting &lt;a href="https://docs.aws.amazon.com/vpc/latest/userguide/VPC_SecurityGroups.html">security groups&lt;/a> proved to be quite difficult. There’s no &amp;ldquo;hey, can an ECS cluster with security group X talk to database Y?&amp;rdquo; button, but oh boy, did I need one. There is the &lt;a href="https://docs.aws.amazon.com/vpc/latest/reachability/what-is-reachability-analyzer.html">AWS VPC Reachability Analyzer&lt;/a> (fat syllable bonuses all around), but it is not useful for analyzing security groups, as far as I can tell.&lt;/p>
&lt;p>Speaking of security groups, I was particularly confused by what counts as ingress. If I have a service that makes an outbound request on port 443, do I also have to allow inbound traffic on 443 for the response? Maybe revealing I had this question makes me look stupid or something, but when all you have to go on is that your service can’t make HTTP requests (because the security group is misconfigured), it’s very hard to rule that out without trying it.&lt;/p>
&lt;h3 id="avoiding-managed-nat-gateways">Avoiding Managed NAT Gateways&lt;/h3>
&lt;p>This proved to be more difficult than I thought it would be. For instance, I found &lt;a href="https://section411.com/2019/07/hello-world/">this very, very good tutorial&lt;/a> on how to not only use ECS and Terraform, but also specifically for a Go project like mine, which I followed as close to the letter as I could. Everything worked after applying it, too, only I noticed too late that &lt;code>resource &amp;quot;aws_nat_gateway&amp;quot; &amp;quot;ngw&amp;quot;&lt;/code> was the notorious, wretched beast I’d feared all along. I was able to find a way to remove it, but it became clear that this is a pattern. Other tutorials I looked at for how to troubleshoot problems asked to use a Managed NAT Gateway as well, because it&amp;rsquo;s an easy answer to these problems, and most AWS users don&amp;rsquo;t seem to have to pay the bill.&lt;/p>
&lt;p>When I tried to get my workers, uhh&amp;hellip;working, they were failing to access SSM Parameter Store because I had put them in a private subnet so they could access the database, which was also in the private subnet. It turns out doing this means they don’t have access to the public internet, and consequently, to access any of a number of AWS services, you have to spin up &lt;a href="https://docs.aws.amazon.com/vpc/latest/privatelink/vpc-endpoints.html">VPC Endpoints&lt;/a>. They are priced in a way that is very much similar to Managed NAT Gateways, and they serve the same functional purpose, only they&amp;rsquo;re restricted to a singular service, which I took to mean they&amp;rsquo;re the same service with a different name and maybe simpler configuration.&lt;/p>
&lt;p>Later, when I tried to move the meal plan finalizer worker into ECS to try and save on these costs (thinking it would be easier for them to access SSM from ECS than from Lambda), I found I had the exact same problem: the worker couldn’t connect to any AWS services unless it was running in the same task that ran the server (because that server container has an Internet Gateway to receive traffic).&lt;/p>
&lt;h3 id="ecsalb-headaches">ECS/ALB Headaches&lt;/h3>
&lt;p>It took me a few days to get my IAM set up so that the ECS service could actually, you know, talk to SSM or RDS or anything. Once I had gotten that up and I could see that my logs were getting to the &amp;ldquo;server&amp;rsquo;s up, serving traffic&amp;rdquo; phase, they were still being systematically killed every 30 seconds or so. They were in this state for a couple of days before I eventually realized that when you set up an ALB to talk to a web service hosted in ECS, it comes with a health check, and this defaults to &amp;ldquo;/&amp;rdquo;. This server doesn&amp;rsquo;t serve anything at the root, so it was failing the health check even though it was healthy. That was pretty confusing, but also pretty easy to fix. Soon enough, the server was up and enjoying life without premature destruction.&lt;/p>
&lt;h3 id="sqs-headaches">SQS Headaches&lt;/h3>
&lt;p>For about a day, I had a Lambda deployed to AWS that, because of a minor bug, couldn&amp;rsquo;t successfully execute. By default, a successful Lambda execution is required to acknowledge a message and get it off of the queue. So, if you don&amp;rsquo;t configure &lt;a href="https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-dead-letter-queues.html">a dead letter queue&lt;/a> (I hadn&amp;rsquo;t), and your code will never succeed (aforementioned), then the message will just get re-placed on the queue, and you&amp;rsquo;ve got yourself an infinite loop that eats through your budgets:&lt;/p>
&lt;blockquote class="twitter-tweet">&lt;p lang="en" dir="ltr">i want to know how &lt;a href="https://t.co/K7yxo9WN8F">pic.twitter.com/K7yxo9WN8F&lt;/a>&lt;/p>&amp;mdash; footloose and fancy-free (@vgsnv) &lt;a href="https://twitter.com/vgsnv/status/1473533020900663306">December 22, 2021&lt;/a>&lt;/blockquote> &lt;script async src="https://platform.twitter.com/widgets.js" charset="utf-8">&lt;/script>
&lt;sub>(I figured out how)&lt;/sub>
&lt;h3 id="outcome">Outcome&lt;/h3>
&lt;p>I was able to get the service spun up in AWS as I had designed it and able to confirm it worked as I suspected. I also set up a service prober Lambda that would run every five minutes, create a household with four members, propose a meal plan, have some users vote for some options, wait for the meal plan to get finalized, and then validate the outcome. This prober ran successfully for a month or so, and the service handled everything like a champ.&lt;/p>
&lt;p>That said, it was VERY expensive. When I was building this in December, I would start my evening at about 7:30PM by deploying and having Terraform spin everything up, and then I would have Terraform Cloud destroy whatever inventory was present by 1:30AM. Nonetheless, my bill for December was $130-ish. I realized the Aurora problem, switched to RDS, and got rid of a lot of the Lambdas to simplify the application and consequent bill. I let the new iteration run for almost a month and was headed towards another $120 bill, which I think means the old system would have cost something like $190/month if I hadn’t swapped databases.&lt;/p>
&lt;blockquote class="twitter-tweet">&lt;p lang="en" dir="ltr">so this seems bad &lt;a href="https://t.co/3eu3kM2Nna">pic.twitter.com/3eu3kM2Nna&lt;/a>&lt;/p>&amp;mdash; footloose and fancy-free (@vgsnv) &lt;a href="https://twitter.com/vgsnv/status/1479299794992570375">January 7, 2022&lt;/a>&lt;/blockquote> &lt;script async src="https://platform.twitter.com/widgets.js" charset="utf-8">&lt;/script>
&lt;p>January&amp;rsquo;s bill was about $130. I started to move the worker into ECS then, to save costs, but ultimately felt burned by the whole thing. The networking charges aren&amp;rsquo;t even really that much money in the grand scheme of it all, but the principle of having to pay AWS to connect infrastructure it runs to infrastructure it runs just didn&amp;rsquo;t sit right with me. I spun down all my infrastructure, and will be taking my code and service elsewhere.&lt;/p>
&lt;h3 id="whats-next">What’s Next?&lt;/h3>
&lt;p>I don’t want to keep building on AWS, because it feels like going to a sleepover at the house of a girl who has told everyone she hates you. I don’t know where I’ll go next with all of this. Azure doesn’t feel ready yet. DigitalOcean has an app platform now that I might try out. GCP was okay, but has positioned itself of late as the enterprise-y provider of choice. Oh no, am I going to have to use Kubernetes?! I&amp;rsquo;ll figure it out and make another post.&lt;/p>
&lt;h3 id="conclusions">Conclusions&lt;/h3>
&lt;p>AWS is a really solid platform that will reliably serve your applications. Once you get through the IAM/VPC hurdles, everything works well and feels sturdy. There are lots of solutions to the various problems one can encounter when building services or tackling new problems. The quality of service you get is truly great, but it is not offered with any benevolence or charity. You will get very high quality service, and you absolutely will pay a pretty penny for it. It&amp;rsquo;s &lt;a href="https://twitter.com/vgsnv/status/1465029923006029840">not without its quirks&lt;/a>, but obviously serviceable.&lt;/p>
&lt;p>The biggest lesson I’ve taken away from this endeavor is that AWS assumes you have access to some VC-backed bank account, and thus litter their documentation with traps where you have no choice but to give them more money. The broader community aids them in this goal by never mentioning the specter of cost in tutorial blogs. It’s not enough that you’d write your congressional representative about; nobody writes their rep about a $35 business expense. It is more than enough, however, to make me feel like I’m not working with a partner, I’m inflicting myself with a parasite.&lt;/p>
&lt;p>I&amp;rsquo;m really happy with both GitHub Actions and Terraform. There were a few times where Terraform screwed up and left me in a state where I had to manually intervene, but considering the nature of what it does and how well it normally does it, I&amp;rsquo;m more than willing to tolerate that. Cloudflare was awesome, too, and I&amp;rsquo;ve long been a fan of the service they operate.&lt;/p></content></item><item><title>Porktrack: how I turned a goofy idea into a real career</title><link>https://blog.verygoodsoftwarenotvirus.ru/posts/the-story-of-porktrack/</link><pubDate>Thu, 01 Aug 2019 11:11:13 -0600</pubDate><guid>https://blog.verygoodsoftwarenotvirus.ru/posts/the-story-of-porktrack/</guid><description>A time gone by I&amp;rsquo;d like to tell the story of how I got into the software engineering field, but like any post I make on this website, I feel the need to justify the endeavor first. I&amp;rsquo;m sharing because whenever I&amp;rsquo;ve shared it with folks in the past, they seem to have enjoyed it and almost always ask clarifying questions in disbelief, which I think is a safe indicator that the story is maybe good.</description><content>&lt;h2 id="a-time-gone-by">A time gone by&lt;/h2>
&lt;p>I&amp;rsquo;d like to tell the story of how I got into the software engineering field, but like any post I make on this website, I feel the need to justify the endeavor first. I&amp;rsquo;m sharing because whenever I&amp;rsquo;ve shared it with folks in the past, they seem to have enjoyed it and almost always ask clarifying questions in disbelief, which I think is a safe indicator that the story is maybe good. I&amp;rsquo;m also interested in simply documenting this story while the details are still immediately recallable.&lt;/p>
&lt;p>To set up some context, in 2012, I took CS101 and was primarily left with a fondness for programming. I loved figuring out how to make the computer do things, and I loved finding new things to make the computer do. I kept writing trivial programs and exploring programming in my spare time even after I left school. I wrote some &lt;a href="https://github.com/verygoodsoftwarenotvirus/CSharpMathQuizzer">math&lt;/a> &lt;a href="https://github.com/verygoodsoftwarenotvirus/Assistmetic">quizzers&lt;/a>, &lt;a href="https://github.com/verygoodsoftwarenotvirus/AndroidColorGenerator">a random color generator&lt;/a>, and some other miscellaneous things that were useful to nobody, myself included. I remember contemplating building something like a Twitter client next, but going from generating a random color to authenticating over OAuth when you don&amp;rsquo;t understand how websites work is a daunting task.&lt;/p>
&lt;p>I was working as a security guard for a local condominium complex. I made $11 an hour, and I had just gotten married. I remember sitting next to my new bride, in awe of the responsibility that she had entrusted me by marrying me, and feeling wholly unworthy of the privilege. I was, in colloquial terms, &lt;a href="https://www.youtube.com/watch?v=FrLequ6dUdM">a scrub&lt;/a>, but I was at least a self-aware scrub. I wanted a way to turn my fondness of programming into a career, so as to rectify my scrub stature.&lt;/p>
&lt;!-- markdownlint-disable MD033 -->
&lt;iframe width="560" height="315" src="https://www.youtube.com/embed/FrLequ6dUdM?start=8" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen style="display: block; margin: 0 auto;">&lt;/iframe>
&lt;!-- markdownlint-disable MD033 -->
&lt;img src="https://blog.verygoodsoftwarenotvirus.ru/03-porktrack/images/colorapp.gif" alt="a thorough demonstration of my C# color generator Windows application" style="display: block; margin: 0 auto;">
&lt;hr>
&lt;img src="https://blog.verygoodsoftwarenotvirus.ru/03-porktrack/images/problemapp.png" alt="a screenshot of my Android math quizzer app in the Android Emulator" style="display: block; margin: 0 auto;">
&lt;h2 id="what-to-build">What to build&lt;/h2>
&lt;p>The thing that is hardest about learning how to write code is knowing &lt;em>what&lt;/em> to work on. You can be the best and brightest student our planet has ever known, and you could spend hours absorbing the knowledge of how conditionals work, where and when to attach a method to a class, etc., but if you don&amp;rsquo;t know to what end it&amp;rsquo;s for, you will never apply it meaningfully, and your understanding will dwindle. I recognized this problem early on and sort of just dealt with it by coming up with (and getting myself hyped about) contrived applications to build. I was never really motivated by games programming either, which is not to cast aspersions at anybody, just a note that I didn&amp;rsquo;t have an interest in that vehicle for developing programming skills.&lt;/p>
&lt;h2 id="seeking-guidance">Seeking guidance&lt;/h2>
&lt;p>Thankfully, the building I worked at had lots of software engineers as tenants, and I was their middleman to receiving their packages. One day, I asked a resident for guidance around what I should build next. He responded by suggesting I create a website, which I initially rejected categorically. &amp;ldquo;I don&amp;rsquo;t want to be a web developer!&amp;rdquo; I whined. Web developers, I&amp;rsquo;d surmised, spent their days changing div colors and filling websites with ads. I wanted to do Real Programming™. &amp;ldquo;The web is dumb! I want to be a Real Programmer™!&amp;rdquo; I whined further. What he said next was a pivotal moment for me:&lt;/p>
&lt;pre>&lt;code>&amp;quot;The web's not dumb, you're dumb.&amp;quot;
&lt;/code>&lt;/pre>
&lt;p>Okay, so he didn&amp;rsquo;t actually say that, but he did walk me through the logical inconsistencies of wanting to do &amp;ldquo;real programming&amp;rdquo; that somehow didn&amp;rsquo;t involve the web in a meaningful way. He made it clear that I couldn&amp;rsquo;t do the larger things I wanted to do without some foundational knowledge best acquired by building a website.&lt;/p>
&lt;p>I&amp;rsquo;d had a really dumb idea for a website I was actually interested in building. The idea was to present a user with an interface for putting in their birthday, then calculating the day they were conceived and looking up what the #1 song on the Billboard Hot 100 was that week, the implication being that if any song was most likely to be on the radio when your parents did the deed to bear your existence, it would likely be the most popular one. I also thought to include an interface for noting if you were a late or early delivery, as that obviously impacts the results.&lt;/p>
&lt;p>I explained it to the man (who really just wanted to pick up his new headphones or whatever) and worried aloud that I didn&amp;rsquo;t think I could put such a silly thing on a résumé, and asked if he had any better ideas. He said something to the tune of &amp;ldquo;in this field, and in this city, you can put whatever the fuck you want on a résumé and they&amp;rsquo;ll probably bring you in.&amp;rdquo;&lt;/p>
&lt;h2 id="actually-building-porktrack">Actually building Porktrack&lt;/h2>
&lt;p>I knew I had to start by collecting the relevant data first. I chose (and learned) Python in order to fetch the data because I&amp;rsquo;d read in &lt;a href="https://www.amazon.com/Plex-Google-Thinks-Works-Shapes/dp/1416596585">Steven Levy&amp;rsquo;s In The Plex&lt;/a> that Google had chosen Python for their web crawling tasks, and I didn&amp;rsquo;t know what &lt;a href="https://web.archive.org/web/20140209204420/http://en.wikipedia.org/wiki/Cargo_cult_programming">cargo culting&lt;/a> was. My first stab was to take the data from &lt;a href="https://web.archive.org/web/20141028071259/http://en.wikipedia.org/wiki/List_of_Hot_100_number-one_singles_of_1959_(U.S.)">the Wikipedia pages for the Hot 100 chart by year&lt;/a> (1959 as an example). Initially, this meant reading the HTML of the page as a string and using Python&amp;rsquo;s string find() and replace() methods on various HTML properties to extract the actual data. I say HTML properties and not, like, XPath because I definitely did not know what XPath was (and performed an actual facepalm when I finally discovered them).&lt;/p>
&lt;p>This obviously didn&amp;rsquo;t scale well, as some of these pages are formatted wildly differently from each other. I briefly contemplated &amp;ldquo;standardizing&amp;rdquo; the formatting by being a particularly pedantic member of the Wikipedia community, but it didn&amp;rsquo;t feel appropriate to let this project have any lasting impacts on Wikipedia. In &lt;a href="https://github.com/verygoodsoftwarenotvirus/Porktrack/blob/5a9334ba984ba109b0a7773df1a72a9ad9a05117/tools/wikithief.py">the oldest version of the source code that I can find&lt;/a>, I&amp;rsquo;d already realized how poorly this scaled and ended up switching to a modified &lt;a href="https://web.archive.org/web/20140428031148/https://docs.python.org/3/library/html.parser.html">HTMLParser object&lt;/a>. I probably went that way because a Stack Overflow post suggested that was how you scraped the web with Python.&lt;/p>
&lt;p>The parser would traverse the elements on the page, and incremented state based on the current state. So, the data would come in the form of three table cells in a row, the first with the date, the second with the artist, and the third with the song title. Once the script got to where it could semi-reliably ascertain who had what hits when, it spat them out as SQL queries to stdout and saved the output as what I would learn is called a migration script. I would then execute the file&amp;rsquo;s query in PHPMyAdmin, which meant I had a database. Eventually, I modified the script so that it would also fetch the top YouTube video ID from a search for &amp;ldquo;{artist} {song_name}&amp;rdquo;.&lt;/p>
&lt;p>My understanding of the LAMP stack was that it was literally[^1] the only way to make a website where the response was dynamic. I just dove straight into the Codecademy course for PHP. If you had told me you could write web servers in Python, I would have rejoiced, because building the scraper made me really fall in love with the language.&lt;/p>
&lt;h2 id="development-environment">Development environment&lt;/h2>
&lt;p>I didn&amp;rsquo;t know how to set up a local development environment and, indeed, was using Windows mostly, where these things tend to be harder than they should. My workflow ended up being that I would write PHP files on my local machine, transfer them to the appropriate place on the machine via FileZilla, and then hit the web page in order to see my changes. It is still, to this day, one of the best development environments I&amp;rsquo;ve ever had. I&amp;rsquo;m not even being kinda sarcastic. I love instant feedback, and had I gone through the process of setting up &lt;a href="https://web.archive.org/web/20141231060440/http://ampps.com/wamp">WAMP&lt;/a> or whatever, I likely would have doubted that whatever was working on my machine would actually work on production. When your dev environment &lt;em>is&lt;/em> production, though, no such doubt can exist. (Note: I am not recommending this workflow for anything meaningful.)&lt;/p>
&lt;p>One of the main (only?) benefits to being a security guard is the downtime. Most of the time, your job is just to be present, but only physically. Most security guards don&amp;rsquo;t have access to a computer, and many times if they do, they are subject to rigorous IT net nanny policies because their computers are just a workstation in a warehouse full of workstations, and they aren&amp;rsquo;t going to provision them differently because the security guard is bored. We&amp;rsquo;re all bored. I was lucky, though, because I worked for a condo complex, so there were no IT firms, and the computer was a Dell shitbox I bought from the Office Depot myself on the HOA credit card, if I recall correctly. I was able to install Python on it and able to use IDLE during lulls in the workday in order to write the scripts that fetched data for the database. I was also able to install FileZilla and change the actual production website.&lt;/p>
&lt;h2 id="infrastructure">Infrastructure&lt;/h2>
&lt;p>The only infrastructure provider I felt comfortable using was DigitalOcean, who I still think really deserve a lot of credit for building an interface that someone who knows nothing about programming can still use and feel like they haven&amp;rsquo;t screwed themselves out of a paycheck. I used the one-click-setup LAMP stack droplet. I don&amp;rsquo;t remember if this configuration came with PHPMyAdmin, but I didn&amp;rsquo;t know it existed before this experience, so it wouldn&amp;rsquo;t surprise me at all. I probably could have swung the $10/month droplet, but I stuck with $5 because of how little I made. I used DigitalOcean&amp;rsquo;s DNS tool and nameservers as well.&lt;/p>
&lt;p>I eventually cooked up enough crappy PHP that the site did what I thought it should do. I was also learning HTML/CSS/JavaScript/SQL in order to accomplish this task, and I really enjoyed myself. I put the files onto the droplet via FTP, and the site seemed to work as I expected. I did some basic testing around awful input, but I didn&amp;rsquo;t understand what unit tests were or how to write them, so I didn&amp;rsquo;t do any of that.&lt;/p>
&lt;h2 id="15-minutes-of-fame">15 minutes of fame&lt;/h2>
&lt;p>What happened next is still one of the wildest things to happen to me in my entire life. Once I felt like the site was in decent shape, I posted it onto
&lt;a href="https://web.archive.org/web/20140529114059/https://www.reddit.com/r/Music/comments/26jdry/i_made_a_website_that_estimates_what_song_you/">/r/music&lt;/a>, and it took off like a rocket. My post received thousands of upvotes and made it to the front page, which led Porktrack to receive just over 2.1M hits in the first 10 days it was live. I know this because I had the foresight to include Google Analytics on the page. The site didn&amp;rsquo;t have ads on it for almost all of it, if I recall correctly (eventually did place ads on the site via AdSense).&lt;/p>
&lt;!-- markdownlint-disable MD033 -->
&lt;img src="https://blog.verygoodsoftwarenotvirus.ru/03-porktrack/images/analytics.png" alt="google analytics screenshot showing traffic results from May 26th, 2014 to June 4th 2014" style="width: 100%;">
&lt;p>Most of the traffic fizzled out and died by mid June, but I kept checking the AdSense results because I had vowed to shut down Porktrack the month it stopped making enough to pay for its own droplet. Almost half a year later, I was running an errand and mindlessly checked AdSense to find that I had made $2,500 that day. It was the day before Thanksgiving, and someone had posted it to a different subreddit (that I can&amp;rsquo;t manage to find at the moment), where it was gaining a similar influx of traffic from bored Americans waiting for planes. I ended up making $3,300 after all was said and done, which was cool.&lt;/p>
&lt;!-- markdownlint-disable MD033 -->
&lt;img src="https://blog.verygoodsoftwarenotvirus.ru/03-porktrack/images/november-bump.png" alt="google analytics screenshot showing traffic results from November 25th, 2014 to November 30th 2014" style="width: 100%;">
&lt;p>One of my favorite parts about this experience was other folks' reactions. People genuinely seemed to think the idea was amusing, and nobody seemed to be particularly offended. One of the other folks who lived at those condos was a guy who was part of a &lt;a href="https://web.archive.org/web/20190405221737/https://mix947.radio.com/shows/booker-alex-sara">morning radio trio&lt;/a>. He came up to the guardhouse the day after it went viral telling us about this crazy website his broadcasting company had approved as a topic of morning conversation. He explained to me that radio stations all over the country get the exact same list, and it&amp;rsquo;s up to the individual hosts to decide what they&amp;rsquo;ll talk about. He said they hadn&amp;rsquo;t opted to talk about it that morning, but he said it had been a close contender. The look on his face when he found out it was &lt;em>my&lt;/em> website is something I don&amp;rsquo;t think I&amp;rsquo;ll ever forget, if I&amp;rsquo;m lucky.&lt;/p>
&lt;p>Indeed, I was reached out to by a few different radio stations who let me know they spoke about my website on air. &lt;a href="https://web.archive.org/web/20140531005207/https://www.complex.com/music/2014/05/porktrack-site-guesses-what-song-you-were-conceived-to">Some&lt;/a> &lt;a href="https://web.archive.org/web/20140527212857/https://time.com/120613/what-song-was-playing-when-you-were-conceived/">larger&lt;/a> &lt;a href="https://web.archive.org/web/20190802152949/https://acclaimmag.com/culture/porktrack-will-tell-song-probably-conceived/">websites&lt;/a> also wrote about the project, which I interpret not as a sign of incoming fame or fortune, but more as a sign that content folks are desperate to talk about anything at all.&lt;/p>
&lt;!-- markdownlint-disable MD033 -->
&lt;audio controls style="width: 100%;">
&lt;source src="https://blog.verygoodsoftwarenotvirus.ru/03-porktrack/sounds/FM96_london_ontario_canada.mp3" type="audio/mpeg">
Your browser does not support the audio element.
&lt;/audio>
&lt;p style="text-align: center;">&lt;sup>&lt;sup>edited audio clip of FM96 in London, ON, CA talking about Porktrack&lt;/sup>&lt;/sup>&lt;/p>
&lt;audio controls style="width: 100%;">
&lt;source src="https://blog.verygoodsoftwarenotvirus.ru/03-porktrack/sounds/FM96_london_ontario_canada.mp3" type="audio/mpeg">
Your browser does not support the audio element.
&lt;/audio>
&lt;p style="text-align: center;">&lt;sup>&lt;sup>edited audio clip of FM94.5 The Buzz in Houston, TX, US talking about Porktrack&lt;/sup>&lt;/sup>&lt;/p>
&lt;h2 id="encouragement-and-detraction">Encouragement and detraction&lt;/h2>
&lt;p>I mentioned in the comments on the Reddit post that it was my first website and I&amp;rsquo;d spent almost a month writing Python &amp;ldquo;scraping&amp;rdquo; code to get the database, and then built a website around it, yadda yadda. I thought I&amp;rsquo;d include this screenshot, as it was really motivating, and I think I even made it my desktop wallpaper for a hot second.&lt;/p>
&lt;!-- markdownlint-disable MD033 -->
&lt;img src="https://blog.verygoodsoftwarenotvirus.ru/03-porktrack/images/progress.png" alt="kind words from a stranger via tweet about the work I'd done" style="display: block; margin: 0 auto; width: 100%;">
&lt;p>Some folks thought the idea was good and tried their own hands at building the app. I got contacted by somebody via Reddit message who said that someone had &amp;ldquo;copied&amp;rdquo; my website and made a Dutch (I think?) version of it. This is the only evidence I have that the website existed:&lt;/p>
&lt;!-- markdownlint-disable MD033 -->
&lt;img src="https://blog.verygoodsoftwarenotvirus.ru/03-porktrack/images/dutch-clone.png" alt="screenshot of a website with a similar purpose to Porktrack, with a picture of a baby wearing headphones and text in the Dutch language" style="display: block; margin: 0 auto; width: 50%;">
&lt;p>Another person had seen from the comments/tweets/looking at my code on GitHub that I was a beginner and took it upon themselves to implement it their own way and send it to me. It certainly looked nicer than any of the Python I&amp;rsquo;d written up to that point, and, for some reason, I kept this screenshot, likely smitten that anybody else would write code after something I&amp;rsquo;d written:&lt;/p>
&lt;!-- markdownlint-disable MD033 -->
&lt;img src="https://blog.verygoodsoftwarenotvirus.ru/03-porktrack/images/forktrack.png" alt="screenshot of a paste bin containing a stranger's code that implemented the porktrack algorithm" style="width: 100%;">
&lt;p>It is still the Internet, though, so there was, of course, the usual pettifoggery.&lt;/p>
&lt;!-- markdownlint-disable MD033 -->
&lt;img src="https://blog.verygoodsoftwarenotvirus.ru/03-porktrack/images/whyumakeshit.png" alt="screenshot of a private message I received which asks why my website is not good" style="display: block; margin: 0 auto; width: 50%;">
&lt;h2 id="database-debacles">Database debacles&lt;/h2>
&lt;p>Crucial to remember in all this is that for me, the request-response lifecycle, the notion of a CRUD app, was becoming obviously clear to me, and its power was palpable. &amp;ldquo;This is how the Internet works!&amp;rdquo; I rejoiced, with the realization that if I wanted to build a website for keeping track of cats I saw on the street, I was just a little schema and SQL away from having exactly that!&lt;/p>
&lt;p>I was on top of the world and wanted to put my new knowledge to great use, so I did what any hapless fool in that circumstance would do: I reinvented analytics. I created a table in PHPMyAdmin that had columns for some basic stuff around timestamp, source IP, referring URL, etc. I queried this new database table the way you might expect of someone who doesn&amp;rsquo;t know what they&amp;rsquo;re doing: by opening a connection to it. Every time someone hit the website, the page would make two MySQL connections to the exact same database, one for the tracks, one for the analytics data.&lt;/p>
&lt;p>Shortly after launching the site, folks objected to the process by which their Porktracks were determined, noting that their parents were immigrants/die-hard country fans/otherwise would never have listened to the Hot 100. So, I very quickly adapted my scripts to account for the country and Latin charts, created new tables for them, and queried them the way I had done thus far. Yup, another two connections.&lt;/p>
&lt;p>This very quickly unraveled and resulted in my $5 DigitalOcean droplet soliciting union membership interest from the other droplets. I think it might actually be impossible to be stupid enough to look at a degraded web page initiating four connections to the same database and not figure out the solution, because I&amp;rsquo;m certain I would have just speechlessly accepted my plight if it were. After switching to just the one connection per request and eliminating data gathering in its entirety, I never had issues with the site going down ever again.&lt;/p>
&lt;p>Even though I knew nothing, I knew that &amp;ldquo;real&amp;rdquo; websites did load tests and other validation work before shipping software. I allowed myself to be convinced that I shouldn&amp;rsquo;t put in similar efforts for this website because of how goofy it was. I didn&amp;rsquo;t want to be the person who spent a great deal of time making a gimmick website reliable, but as a consequence of that attitude, I suffered the failing defeat of having my site go down. I learned the very valuable lesson that if something is worth doing, it&amp;rsquo;s probably worth treating it like it&amp;rsquo;s going to be the sort of runaway success that would give you uptime problems. If you treat everything like it&amp;rsquo;s going to go gangbusters, then you&amp;rsquo;ll make very different decisions around how your application is designed/hosted/maintained.&lt;/p>
&lt;h2 id="the-real-benefit">The real benefit&lt;/h2>
&lt;p>Ultimately, the consequence of building the site that I&amp;rsquo;m most grateful about was that it gave me something to do. I&amp;rsquo;d heard that single-page applications were a thing, and I didn&amp;rsquo;t know how they worked, but I had a working site receiving steady traffic. I would wait until late at night and cobble together some awkward vanilla JS, a results-specific route that spat out HTML, and accomplished a &amp;ldquo;single-page app&amp;rdquo; by replacing the &lt;code>.innerHTML&lt;/code> field. Then, I heard that nobody writes JavaScript, everybody writes jQuery, so I rewrote my crappy vanilla JS into crappy jQuery JS. Then, I wanted to create a mobile app for it, so I started down that process too (but never actually finished it, probably to everybody&amp;rsquo;s benefit).&lt;/p>
&lt;p>I got to where I was so comfortable talking shop with other software developers at meetups and such that they started to inquire where I was working and would react with disbelief upon learning that I was a security guard. I got a lot of encouragement from the Austin tech community in general, before or after feeling acclimated to the task. I eventually mustered up the courage to put together a résumé and started applying to junior developer jobs. I was told twice after being rejected that the novelty of Porktrack and the implied chutzpah required to put something like that on my résumé were too alluring for some folks to pass on at least an interview.&lt;/p>
&lt;p>It took me a solid eight months of applying every day, going to interviews where the expectations were completely out of line with what I&amp;rsquo;d advertised on my résumé, and failing to get those jobs but getting better at and more comfortable with the process with each step. Eventually, I was hired at a startup as a junior web developer, and the rest of my career is what I&amp;rsquo;m in the middle of now.&lt;/p>
&lt;h2 id="luck">Luck&lt;/h2>
&lt;p>The thing I always try to emphasize when I recount this whole thing is that much of my &amp;ldquo;success&amp;rdquo; came in the form of good fortune. I was/am fortunate in so many ways:&lt;/p>
&lt;ul>
&lt;li>
&lt;p>I was fortunate that I didn&amp;rsquo;t have any children or sick relatives to tend to, and a supportive spouse, so I had all the time and space I needed to try and accomplish my goal. I routinely had 4+ hours of free time on weekdays and virtually the entire weekend to read tutorials and run little experiments. From what I gather, that would never fly in a house with a greater than zero amount of children.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>I was also lucky to work at a place that was more than fine with me doing extracurricular activities while &amp;ldquo;on the job.&amp;rdquo; My bosses at the time both knew that I was working on Porktrack and that I wanted to find a way to be a software developer. They always allowed me to spend time at work reading about code or writing dumb programs, provided I took care of my actual work obligations (which I always did). They helped test Porktrack for me a few times, and when the time came to interview, they were very generous and understanding about the necessary away time I needed to attend them. If I had worked somewhere where resources were more scarce or had otherwise made myself intractably necessary somewhere, I don&amp;rsquo;t think I could have done it.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>I mentioned this earlier, but it&amp;rsquo;s worth repeating that I am lucky that the friendly folks who lived at the condos indulged my endless pestering for suggestions on what to learn or work on next. I had actually managed to finagle an internship from one of them as a QA intern but got hired at the startup shortly after starting, so I left for that.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>I was lucky that I lived in a city with an active tech economy/community. Most cities in the United States cannot facilitate the sort of access I took advantage of to learn from professionals. Many cities with good tech economies have pretty pitiful meetup scenes, too.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;p>I worked really hard, to be certain, but all the hard work in the world would have been for naught if my situation had been like that of the average. I had a blast, got to bring folks a very small amount of joy, and it never once felt onerous. I feel sometimes as though I found a cheat code by accident. It&amp;rsquo;s not supposed to be that you can have a lot of fun and end up making good money. It&amp;rsquo;s supposed to be that you spend at least three decades doing menial tasks for people who don&amp;rsquo;t care about you and remark upon the expense of your wage with disgust.&lt;/p>
&lt;h2 id="porktrack-redux">Porktrack redux&lt;/h2>
&lt;p>Recently, I saw this neat video where a man recreated his very first flipbook from 30 years ago:&lt;/p>
&lt;!-- markdownlint-disable MD033 -->
&lt;iframe width="560" height="315" src="https://www.youtube.com/embed/4Uz58BFl8zE" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" style="display: block; margin: 0 auto;" allowfullscreen>&lt;/iframe>
&lt;p>I felt inspired, and obviously thought about my very first website and realized that I&amp;rsquo;ve learned so much in the last five years. Almost nothing about how I build personal projects is the same as it was. In fact, quite a few options for hosting such a website have come about in the intervening years. I resolved to rebuild the application and take the time to address some of the complaints I had as a maintainer back in the day. The main things I want to tackle:&lt;/p>
&lt;ol>
&lt;li>Scaling: Porktrack didn&amp;rsquo;t scale well at all, either up or down. I want to rebuild it in a way where it can be hosted basically forever, without costing me a tremendous amount of money to do so. Simply put, it&amp;rsquo;s not important enough to me to warrant $60/year to host. To give you an idea, if I can&amp;rsquo;t pay for this site&amp;rsquo;s hosting costs with a dime per month and have change in return, I will consider myself unsuccessful.&lt;/li>
&lt;li>Language: Shortly after starting my first job, I discovered Go and have not really written much Python outside of work since then. I want to rewrite the core components of Porktrack in Go.&lt;/li>
&lt;li>Data completeness: When I last ran Porktrack, I had a reminder every month to run the script again and add things to the database so that I wouldn&amp;rsquo;t lose track of that. I&amp;rsquo;d like the new version of the site to update itself as much as possible, without human intervention. Also, the YouTube video IDs I&amp;rsquo;d save would very frequently get axed for having copyrighted content, so I want to minimize that consequence to whatever extent I can.&lt;/li>
&lt;/ol>
&lt;h2 id="sketch-out-a-plan">Sketch out a plan&lt;/h2>
&lt;p>The first order of business was to build the thing that fetched the database. In the old application, I had Python scripts that spat out SQL migrations for all the Porktracks. This was handy because it took a week or more before I finally felt like the script did the thing it was supposed to do every time. This way, in the event that the script got 95% of the entries correct but maybe futzed up a single quote here or there, I could just manually fix it in a text editor without having to add a conditional to my code or otherwise write a bunch of safeguards.&lt;/p>
&lt;p>This time around, I&amp;rsquo;m doing something similar, only I&amp;rsquo;m writing to a local SQLite database that doesn&amp;rsquo;t get added to version control. This accomplishes a few things:&lt;/p>
&lt;ul>
&lt;li>It gives me a similar sort of in-between inspection scheme where I can validate that the data looks the way it&amp;rsquo;s supposed to.&lt;/li>
&lt;li>It makes it so that I don&amp;rsquo;t have to constantly fetch from Billboard and risk detection/IP blocking.&lt;/li>
&lt;li>Lots of tools exist for querying a local SQLite database.&lt;/li>
&lt;/ul>
&lt;h2 id="architectural-choices">Architectural choices&lt;/h2>
&lt;p>I decided to host the application on &lt;a href="https://cloud.google.com/run/">Cloud Run&lt;/a>. Cloud Run is a managed serverless application platform, announced this year at Google Next, where you publish a container to a private registry and then tell GCP to associate that container with an HTTPS-enabled URL, which will execute your server when it receives a request. Like most serverless schemes, you are only charged for the time your application is alive. Thankfully, you just write a server. Standard routing frameworks work just fine.&lt;/p>
&lt;p>There is one main consideration for serverless applications, however, which is the database. &lt;a href="https://cloud.google.com/sql/">Cloud SQL&lt;/a> is completely viable for Cloud Run use, (and as a matter of fact, the Cloud Run interface provides a convenient mechanism for pre-defining Cloud SQL connections) but a Cloud SQL instance costs ~$8/month minimum, which, as we all know, is greater than a dime. &lt;a href="https://firebase.google.com/docs/firestore">Cloud Firestore&lt;/a>, however, supports tons of simultaneous connections for queries and gives you (as of the time of this writing) 50,000 free queries per day.&lt;/p>
&lt;p>I ended up using the Spotify API to provide sound samples of the songs instead of YouTube. In the years since I last built Porktrack, YouTube has started requiring JavaScript to load search results, and &lt;a href="https://stackoverflow.com/questions/15568405/youtube-api-limitations">their API limitations&lt;/a> are, in my humble opinion, disrespectful. There basically wasn&amp;rsquo;t a way I felt comfortable retrieving that data and validating that it was still relevant, whereas from what I can tell, the Spotify IDs are more permanent.&lt;/p>
&lt;p>So, I&amp;rsquo;ve rewritten Porktrack as an entirely serverless application and made a serverless data store its primary source of truth. I have code that initializes the database locally, and after the local SQLite database is loaded and I feel confident that data is collected well, I run a simple function that will iterate over every entry in this database and save it to a Cloud Firestore collection.&lt;/p>
&lt;p>I also have a &lt;a href="https://cloud.google.com/functions/">Cloud Function&lt;/a> being activated by a &lt;a href="https://cloud.google.com/scheduler/">Cloud Scheduler job&lt;/a> once a week on Sundays to check the Billboard site and run the same function that gets run when we build the SQLite database, and then save that data into Cloud Firestore.&lt;/p>
&lt;p>I got to handle the scaling problems by waiting for infrastructure providers to get WAY better at what they did. I got to port it from a good language to a better one, and I don&amp;rsquo;t have to set a reminder on my phone to run a script and execute some SQL, because managed infrastructure will handle that for me. &lt;del>You can check out the current version of the site by going to &lt;code>https://www.porktrack.com&lt;/code>.&lt;/del>&lt;/p>
&lt;h2 id="update-2120">Update 2/1/20&lt;/h2>
&lt;p>The new app has been running for a few months now, I wanted to demonstrate the actual monthly costs of the app.&lt;/p>
&lt;p>Here&amp;rsquo;s October 2019&amp;rsquo;s bill:
&lt;img src="https://blog.verygoodsoftwarenotvirus.ru/03-porktrack/images/october-19-bill.png" alt="the GCP bill for Porktrack for October 2019" style="display: block; margin: 0 auto; width: 100%;">&lt;/p>
&lt;p>Here&amp;rsquo;s November 2019&amp;rsquo;s bill:
&lt;img src="https://blog.verygoodsoftwarenotvirus.ru/03-porktrack/images/november-19-bill.png" alt="the GCP bill for Porktrack for November 2019" style="display: block; margin: 0 auto; width: 100%;">&lt;/p>
&lt;p>Here&amp;rsquo;s December 2019&amp;rsquo;s bill:
&lt;img src="https://blog.verygoodsoftwarenotvirus.ru/03-porktrack/images/december-19-bill.png" alt="the GCP bill for Porktrack for December 2019" style="display: block; margin: 0 auto; width: 100%;">&lt;/p>
&lt;p>I&amp;rsquo;ve shut down the site now, since I&amp;rsquo;ve proven my point, and really, this site shouldn&amp;rsquo;t exist. :)&lt;/p>
&lt;p>[^1] I mean &lt;em>literally&lt;/em> literally, not figuratively literally.&lt;/p></content></item><item><title>Weapons and Tools</title><link>https://blog.verygoodsoftwarenotvirus.ru/posts/weapons-and-tools/</link><pubDate>Tue, 26 Feb 2019 19:04:35 -0600</pubDate><guid>https://blog.verygoodsoftwarenotvirus.ru/posts/weapons-and-tools/</guid><description>A hammer wielded by a worker trained to hit things is a tool. A hammer wielded by someone intent on doing harm is a weapon. The hammer requires no metamorphosis yet starts as a tool and ends as a weapon. So, there must be a threshold where the tool becomes a weapon, what is it?
When I write software, I don&amp;rsquo;t build things that do things, I build tools for doing things with.</description><content>&lt;p>A hammer wielded by a worker trained to hit things is a tool. A hammer wielded by someone intent on doing harm is a weapon. The hammer requires no metamorphosis yet starts as a tool and ends as a weapon. So, there must be a threshold where the tool becomes a weapon, what is it?&lt;/p>
&lt;p>When I write software, I don&amp;rsquo;t build things that do things, I build tools for doing things with. If I&amp;rsquo;m tasked with communicating with an external API, I don&amp;rsquo;t just write the code for executing raw requests. I write a client for interacting with that external API, and give it an either straightforward or thought-out abstraction. This way, when the time arises for another person to interact with that same API, it&amp;rsquo;s however much easier to do than it would have been.&lt;/p>
&lt;p>Lately, though, I&amp;rsquo;ve been asking myself how I could weaponize whatever tool I&amp;rsquo;m building. That external API client, in the hands of someone who knows how to use concurrency well, could be a very precise DDoS tool, for instance. Most of the time my answers are some awkward mental stretch like that, because I&amp;rsquo;m fortunate to work for a WordPress hosting company. I suppose if your evil scheme involved spinning up thousands of WordPress instances, we&amp;rsquo;d be your huckleberry.&lt;/p>
&lt;!-- markdownlint-disable MD033 -->
&lt;iframe width="560" height="315" src="https://www.youtube.com/embed/plD1MbOGLfQ" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen style="display: block; margin: 0 auto;">&lt;/iframe>
&lt;p>My first stab at answering this question was that misuse transforms tools into weapons. After all, the hammer was not meant to harm anyone, yet there are an infinite number of ways to misuse a hammer without hurting anybody at all. In our premise, however, the hammer wasn&amp;rsquo;t wielded by a fool who didn&amp;rsquo;t know how to use a hammer properly; it was wielded by someone intent on doing harm. Maybe what transforms a tool into a weapon is motivated misuse, which is to say that turning a tool into a weapon requires not only its deliberate misuse (weaponization), but also the desire to do so.&lt;/p>
&lt;p>That seems satisfactory, but it instantly begs the question in my mind: &lt;em>Can you design a tool that is impossible to weaponize&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>?&lt;/em>&lt;/p>
&lt;p>Since there are two hard dependencies for the weaponization of tools (motive and misuse), can we design tools so that they aren&amp;rsquo;t capable of being misused, thus eliminating the second factor? I&amp;rsquo;d posit that the statement &amp;ldquo;some tools are more weaponizable than others&amp;rdquo; is non-controversially true. A hammer is weaponizable, I think, in part because its purpose is relatively generic. Its purpose is to hit things&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>, ostensibly nails, but not necessarily. There&amp;rsquo;s nothing nail-exclusive about the shape of your run-of-the-mill hammer&amp;rsquo;s head.&lt;/p>
&lt;p>Can we make the hammer unweaponizable? I think we can get pretty close, but what we&amp;rsquo;d have wouldn&amp;rsquo;t be called a hammer because nobody would recognize it as such&lt;sup id="fnref:3">&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref">3&lt;/a>&lt;/sup>. Nevertheless, it doesn&amp;rsquo;t take a particularly vivid imagination to conjure up a horror movie where someone befalls a fatality at the hands of a particularly motivated user of the marketed-as-non-weaponizable hammer.&lt;/p>
&lt;p>I beat this dead horse only to illustrate that the line between weapon and tool is quickly and unremarkably crossed. It should be noted that a weaponized hammer has the obvious implication of a consequential murder, but you could make the case that tools are weaponized to nobody&amp;rsquo;s harm, or even for pure benefit, all the time. Chemotherapy is weaponized against cancer cells, scissors are weaponized against excess paper, caffeine is weaponized against poor decision making the night prior.&lt;/p>
&lt;p>Our world is simultaneously riddled with tools generic enough to be weaponized and people with the motivation to misuse them. Even more remarkable, to me, is that I&amp;rsquo;m fortunate enough to be able to spend the majority of my day with the most fascinating and generic tool humanity has ever devised: the computer!&lt;/p>
&lt;p>The computer is a bit like the hammer, only several orders of magnitude deeper. It&amp;rsquo;s a tool so generic it can make other tools. It can make tools that help it make tools. It can make tools that help the tools that allow it to make the tools that the tools it makes are trying to make for it. That&amp;rsquo;s cool as heck, but as you&amp;rsquo;ll recall, we&amp;rsquo;ve assumed the more generic the tool, the more weaponizable it is (computers are, like, &lt;em>&lt;strong>super&lt;/strong>&lt;/em> weaponizable, ya&amp;rsquo;ll). Furthermore, there is an entire industry chock-full of folks like me who use these very generic tools to make tools, which we&amp;rsquo;ve assumed are also potential weapons.&lt;/p>
&lt;p>I&amp;rsquo;m certain that there is no easy, memorable catchphrase that will bestow upon anyone who hears it the knowledge to make unweaponizable tools. Acknowledging that doesn&amp;rsquo;t absolve our profession of the obligation to ensure that we build safeguards into tools we think might be particularly weaponizable. Engineers at &lt;a href="https://www.nytimes.com/2018/08/16/technology/google-employees-protest-search-censored-china.html">Google&lt;/a> and &lt;a href="https://www.theguardian.com/technology/2019/feb/22/microsoft-protest-us-army-augmented-reality-headsets">Microsoft&lt;/a> have had to take the extraordinary step of publicly registering their fervent opposition to the weaponization of the tools they&amp;rsquo;ve developed. That&amp;rsquo;s a great first step, and the braver comrades of those groups have &lt;a href="https://theintercept.com/2018/09/13/google-china-search-engine-employee-resigns/">put their resignation letters where their mouth is&lt;/a>. More of that, please.&lt;/p>
&lt;p>These engineers' protests may be some of the first we&amp;rsquo;ve seen, but they won&amp;rsquo;t be the last. I won&amp;rsquo;t work at the hosting company forever, and students won&amp;rsquo;t be in a position of writing relatively harmless applications under the critical gaze of an academic forever either. At some point, more of us than we&amp;rsquo;re comfortable acknowledging will be asked to build something that has truly horrifying and obvious weaponization opportunities, or even be asked to build literal weapons. If you&amp;rsquo;re a professional software engineer at any level, that is a scenario you need to think about and reconcile with your morality now, so that you may be firm in your decision later, to whatever consequence.&lt;/p>
&lt;section class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1" role="doc-endnote">
&lt;p>It&amp;rsquo;s worthwhile to take the time to point out that regulation in itself is both a tool and weapon. Regulations aiming to prevent the production of meaningful weapons could be weaponized against the developers of tools that either occupy the grey area between tool and weapon or are otherwise capable of marketing to folks as such in order to justify their regulation.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2" role="doc-endnote">
&lt;p>The hammer is such a generic tool that someone smart, long (enough) ago, decided, &amp;ldquo;This thing doesn&amp;rsquo;t do enough,&amp;rdquo; and added a nail puller to the other side. So even though the hammer came with two sides of its primary function, someone decided that was too much hammer by half and invented another thing to replace it.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:3" role="doc-endnote">
&lt;p>In my amateur science fiction imagination, I can picture something the size of a shot glass that would have some form of smaller-diameter beater inside it that would try to ensure its own surface was as level with the surface of the outer lid as it could manage, applying linearly greater force to the nail head until it determined that progress would not be made.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/section></content></item><item><title>Using Docker to compile Golang plugins on OS X</title><link>https://blog.verygoodsoftwarenotvirus.ru/posts/golang-docker-plugin/</link><pubDate>Thu, 11 Jan 2018 20:23:35 -0600</pubDate><guid>https://blog.verygoodsoftwarenotvirus.ru/posts/golang-docker-plugin/</guid><description>Okay so this might be old news to some of ya&amp;rsquo;ll, but I just got it figured out, and any time you figure something out that didn&amp;rsquo;t have an immediate answer online, you should blog about it, right?
The Problem I&amp;rsquo;ve been building a side project for a while that has an API server component. It&amp;rsquo;s built with Postgres in mind, but being that it&amp;rsquo;s something meant to be self-hosted in the future, I wanted to provide any potential users with the option of using whatever database they chose.</description><content>&lt;p>Okay so this might be old news to some of ya&amp;rsquo;ll, but I just got it figured out, and any time you figure something out that didn&amp;rsquo;t have an immediate answer online, you should blog about it, right?&lt;/p>
&lt;h2 id="the-problem">The Problem&lt;/h2>
&lt;p>I&amp;rsquo;ve been building a side project for a while that has an API server component. It&amp;rsquo;s built with Postgres in mind, but being that it&amp;rsquo;s something meant to be self-hosted in the future, I wanted to provide any potential users with the option of using whatever database they chose.&lt;/p>
&lt;p>The server in question is written in Go, and the main problem I was worried about facing was related to imports. Sure, I could write a &lt;code>postgres&lt;/code> package, and a &lt;code>mysql&lt;/code> package, and I could even import them both in my &lt;code>main.go&lt;/code> file and instantiate the appropriate struct based on a user config, but where does that madness stop? Could my precious &lt;code>main.go&lt;/code> one day look like this?&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-go" data-lang="go">&lt;span style="color:#f92672">package&lt;/span> &lt;span style="color:#a6e22e">main&lt;/span>
&lt;span style="color:#f92672">import&lt;/span>(
&lt;span style="color:#e6db74">&amp;#34;database/sql&amp;#34;&lt;/span>
&lt;span style="color:#e6db74">&amp;#34;github.com/org/project/api/storage/db/postgres&amp;#34;&lt;/span>
&lt;span style="color:#e6db74">&amp;#34;github.com/org/project/api/storage/db/mysql&amp;#34;&lt;/span>
&lt;span style="color:#e6db74">&amp;#34;github.com/org/project/api/storage/db/mongodb&amp;#34;&lt;/span>
&lt;span style="color:#e6db74">&amp;#34;github.com/org/project/api/storage/db/sqlite&amp;#34;&lt;/span>
&lt;span style="color:#e6db74">&amp;#34;github.com/org/project/api/storage/db/cockroachdb&amp;#34;&lt;/span>
&lt;span style="color:#e6db74">&amp;#34;github.com/org/project/api/storage/db/esoteric_database_you_have_never_heard_of&amp;#34;&lt;/span>
&lt;span style="color:#e6db74">&amp;#34;github.com/org/project/api/storage/db/plain_old_txt_files_lol&amp;#34;&lt;/span>
&lt;span style="color:#a6e22e">_&lt;/span> &lt;span style="color:#e6db74">&amp;#34;github.com/lib/pq&amp;#34;&lt;/span>
&lt;span style="color:#a6e22e">_&lt;/span> &lt;span style="color:#e6db74">&amp;#34;github.com/lib/repeat&amp;#34;&lt;/span>
&lt;span style="color:#a6e22e">_&lt;/span> &lt;span style="color:#e6db74">&amp;#34;github.com/lib/ad&amp;#34;&lt;/span>
&lt;span style="color:#a6e22e">_&lt;/span> &lt;span style="color:#e6db74">&amp;#34;github.com/lib/infinitum&amp;#34;&lt;/span>
)
&lt;/code>&lt;/pre>&lt;/div>&lt;p>That obviously won&amp;rsquo;t do. I remember reading about &lt;a href="https://golang.org/doc/go1.8#plugin">Go&amp;rsquo;s plugins when they came out&lt;/a> a while back and decided that might be an interesting way to tackle this problem. I could break my &lt;code>postgres&lt;/code> package out into its own repo, import it as the default option, and accept plugins that were built by developers for their favorite databases, so long as they satisfied the interface I had defined for what constituted a database storer.&lt;/p>
&lt;p>So I built a tiny demo app that executed a very simple query, and used some code from the existing repo to create a tiny package that I could then compile into a plugin to test my theory. I ran &lt;code>go build -buildmode=plugin&lt;/code>, and received this output:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-bash" data-lang="bash">-buildmode&lt;span style="color:#f92672">=&lt;/span>plugin not supported on darwin/amd64
&lt;/code>&lt;/pre>&lt;/div>&lt;p>Note that Go returns this error even if there&amp;rsquo;s a syntax error in the code. It won&amp;rsquo;t even bother trying to see if your code is okay if you&amp;rsquo;re trying to use &lt;code>-buildmode=plugin&lt;/code> on an unsupported platform.&lt;/p>
&lt;p>Rats! In all my excitement and fervor, I never thought to check if plugins were even supported on my platform. It even says on the release notes that I linked above! &lt;code>Plugin support is currently only available on Linux.&lt;/code> It was a nice reminder that because I sort of naturally gravitated towards being a Go developer on OS X, I&amp;rsquo;m much more frequently in a position to beta test new things than be shut out of them.&lt;/p>
&lt;p>But what to do about my problem?&lt;/p>
&lt;h2 id="docker-to-the-rescue">Docker to the rescue&lt;/h2>
&lt;p>I wasn&amp;rsquo;t worried about the API being able to use the generated files, because they run entirely in Docker containers anyway. I run debug builds locally and integration tests everywhere using Docker compose. So I wondered if I could set up a Dockerfile that would add my package, build the .so file I needed, and spit it back out to my host machine so I could hand it over to my other package and get all the things I wanted out of plugins without having to futz with partitioning drives or picking a distro out and otherwise completely reconfiguring how I do my work.&lt;/p>
&lt;p>Here&amp;rsquo;s what I came up with:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-docker" data-lang="docker">&lt;span style="color:#66d9ef">FROM&lt;/span>&lt;span style="color:#e6db74"> golang:latest&lt;/span>&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;span style="color:#960050;background-color:#1e0010">&lt;/span>&lt;span style="color:#66d9ef">WORKDIR&lt;/span>&lt;span style="color:#e6db74"> /go/src/github.com/verygoodsoftwarenotvirus/plugintest&lt;/span>&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;span style="color:#960050;background-color:#1e0010">&lt;/span>&lt;span style="color:#66d9ef">ADD&lt;/span> . .&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;span style="color:#960050;background-color:#1e0010">&lt;/span>&lt;span style="color:#66d9ef">CMD&lt;/span> go build -buildmode&lt;span style="color:#f92672">=&lt;/span>plugin -o /output/result.so&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>and the corresponding bash script:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-bash" data-lang="bash">mkdir -p output
docker build -t plugins .
docker run --volume&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">$(&lt;/span>pwd&lt;span style="color:#66d9ef">)&lt;/span>/output:/output --rm -t plugins
&lt;/code>&lt;/pre>&lt;/div>&lt;p>Run your bash script, and you&amp;rsquo;ll have &lt;code>output/$PKG_NAME.so&lt;/code> in your folder in relatively no time. Note that you still have to run the program that intends to use the plugin in a linux-based Docker container, but if you&amp;rsquo;ve made it this far, you probably know how to do that.&lt;/p>
&lt;p>Hopefully this helps somebody else who faced the same predicament I did. :)&lt;/p></content></item></channel></rss>