<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.2.2">Jekyll</generator><link href="https://www.kimsereylam.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://www.kimsereylam.com/" rel="alternate" type="text/html" /><updated>2026-09-14T01:04:22-05:00</updated><id>https://www.kimsereylam.com/feed.xml</id><title type="html">Kimserey Lam’s website, Software Development blog posts, videos and tutorials</title><subtitle>Kimserey Lam's website, Software Development Blog with tutorials and videos on backend, frontend and infrastructure.</subtitle><author><name>Kimserey Lam</name></author><entry><title type="html">Go Building Services — HTTP, Signals, Migrations and Reflection</title><link href="https://www.kimsereylam.com/go/2026/09/12/go-building-services-http-signals-migrations-and-reflection.html" rel="alternate" type="text/html" title="Go Building Services — HTTP, Signals, Migrations and Reflection" /><published>2026-09-12T00:00:00-05:00</published><updated>2026-09-12T00:00:00-05:00</updated><id>https://www.kimsereylam.com/go/2026/09/12/go-building-services-http-signals-migrations-and-reflection</id><content type="html" xml:base="https://www.kimsereylam.com/go/2026/09/12/go-building-services-http-signals-migrations-and-reflection.html"><![CDATA[<p>The previous tutorials covered Go’s type system, concurrency primitives, and error handling. This one shifts to production patterns you need when building real services: serving HTTP requests, managing process lifecycle with signal handling, running database migrations with embedded files, and using reflection for metaprogramming. Each topic stands on its own, but together they represent the kind of infrastructure code that appears in nearly every Go service.</p>

<!--more-->

<h2 id="nethttp">net/http</h2>

<p>Go’s standard library includes a full HTTP server and client in <code class="language-plaintext highlighter-rouge">net/http</code>. There are no frameworks to install for basic web services — the standard library handles routing, request parsing, response writing, and even test servers.</p>

<h3 id="http-handler-basics">HTTP Handler Basics</h3>

<p>An HTTP handler in Go is any function with the signature <code class="language-plaintext highlighter-rouge">func(http.ResponseWriter, *http.Request)</code>. The <code class="language-plaintext highlighter-rouge">ResponseWriter</code> is where you write the response, and the <code class="language-plaintext highlighter-rouge">*Request</code> carries everything about the incoming request — method, URL, headers, body. To use a plain function as a handler, wrap it with <code class="language-plaintext highlighter-rouge">http.HandlerFunc</code>:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">helloHandler</span><span class="p">(</span><span class="n">w</span> <span class="n">http</span><span class="o">.</span><span class="n">ResponseWriter</span><span class="p">,</span> <span class="n">r</span> <span class="o">*</span><span class="n">http</span><span class="o">.</span><span class="n">Request</span><span class="p">)</span> <span class="p">{</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Fprintf</span><span class="p">(</span><span class="n">w</span><span class="p">,</span> <span class="s">"Hello, %s %s!"</span><span class="p">,</span> <span class="n">r</span><span class="o">.</span><span class="n">Method</span><span class="p">,</span> <span class="n">r</span><span class="o">.</span><span class="n">URL</span><span class="o">.</span><span class="n">Path</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">ts</span> <span class="o">:=</span> <span class="n">httptest</span><span class="o">.</span><span class="n">NewServer</span><span class="p">(</span><span class="n">http</span><span class="o">.</span><span class="n">HandlerFunc</span><span class="p">(</span><span class="n">helloHandler</span><span class="p">))</span>
	<span class="k">defer</span> <span class="n">ts</span><span class="o">.</span><span class="n">Close</span><span class="p">()</span>

	<span class="n">resp</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">http</span><span class="o">.</span><span class="n">Get</span><span class="p">(</span><span class="n">ts</span><span class="o">.</span><span class="n">URL</span> <span class="o">+</span> <span class="s">"/world"</span><span class="p">)</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="n">log</span><span class="o">.</span><span class="n">Fatal</span><span class="p">(</span><span class="n">err</span><span class="p">)</span>
	<span class="p">}</span>
	<span class="k">defer</span> <span class="n">resp</span><span class="o">.</span><span class="n">Body</span><span class="o">.</span><span class="n">Close</span><span class="p">()</span>

	<span class="n">body</span><span class="p">,</span> <span class="n">_</span> <span class="o">:=</span> <span class="n">io</span><span class="o">.</span><span class="n">ReadAll</span><span class="p">(</span><span class="n">resp</span><span class="o">.</span><span class="n">Body</span><span class="p">)</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  status: %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">resp</span><span class="o">.</span><span class="n">Status</span><span class="p">)</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  body:   %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="kt">string</span><span class="p">(</span><span class="n">body</span><span class="p">))</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">httptest.NewServer</code> starts a real HTTP server on a random port and returns a test server whose <code class="language-plaintext highlighter-rouge">URL</code> field gives you the base address. This is the standard way to test HTTP handlers without binding to a fixed port. The handler receives a <code class="language-plaintext highlighter-rouge">GET</code> request for <code class="language-plaintext highlighter-rouge">/world</code> and writes back <code class="language-plaintext highlighter-rouge">Hello, GET /world!</code>.</p>

<h3 id="servemux-routing">ServeMux Routing</h3>

<p>For services with multiple endpoints, <code class="language-plaintext highlighter-rouge">http.NewServeMux</code> provides a router that maps URL patterns to handlers. Go 1.22 introduced method-based patterns, so you can write <code class="language-plaintext highlighter-rouge">"GET /health"</code> or <code class="language-plaintext highlighter-rouge">"POST /echo"</code> directly in the pattern string:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
</pre></td><td class="rouge-code"><pre><span class="n">mux</span> <span class="o">:=</span> <span class="n">http</span><span class="o">.</span><span class="n">NewServeMux</span><span class="p">()</span>

<span class="n">mux</span><span class="o">.</span><span class="n">HandleFunc</span><span class="p">(</span><span class="s">"GET /health"</span><span class="p">,</span> <span class="k">func</span><span class="p">(</span><span class="n">w</span> <span class="n">http</span><span class="o">.</span><span class="n">ResponseWriter</span><span class="p">,</span> <span class="n">r</span> <span class="o">*</span><span class="n">http</span><span class="o">.</span><span class="n">Request</span><span class="p">)</span> <span class="p">{</span>
	<span class="n">w</span><span class="o">.</span><span class="n">WriteHeader</span><span class="p">(</span><span class="n">http</span><span class="o">.</span><span class="n">StatusOK</span><span class="p">)</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Fprint</span><span class="p">(</span><span class="n">w</span><span class="p">,</span> <span class="s">"ok"</span><span class="p">)</span>
<span class="p">})</span>

<span class="n">mux</span><span class="o">.</span><span class="n">HandleFunc</span><span class="p">(</span><span class="s">"GET /greet"</span><span class="p">,</span> <span class="k">func</span><span class="p">(</span><span class="n">w</span> <span class="n">http</span><span class="o">.</span><span class="n">ResponseWriter</span><span class="p">,</span> <span class="n">r</span> <span class="o">*</span><span class="n">http</span><span class="o">.</span><span class="n">Request</span><span class="p">)</span> <span class="p">{</span>
	<span class="n">name</span> <span class="o">:=</span> <span class="n">r</span><span class="o">.</span><span class="n">URL</span><span class="o">.</span><span class="n">Query</span><span class="p">()</span><span class="o">.</span><span class="n">Get</span><span class="p">(</span><span class="s">"name"</span><span class="p">)</span>
	<span class="k">if</span> <span class="n">name</span> <span class="o">==</span> <span class="s">""</span> <span class="p">{</span>
		<span class="n">name</span> <span class="o">=</span> <span class="s">"stranger"</span>
	<span class="p">}</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Fprintf</span><span class="p">(</span><span class="n">w</span><span class="p">,</span> <span class="s">"Hello, %s!"</span><span class="p">,</span> <span class="n">name</span><span class="p">)</span>
<span class="p">})</span>

<span class="n">mux</span><span class="o">.</span><span class="n">HandleFunc</span><span class="p">(</span><span class="s">"POST /echo"</span><span class="p">,</span> <span class="k">func</span><span class="p">(</span><span class="n">w</span> <span class="n">http</span><span class="o">.</span><span class="n">ResponseWriter</span><span class="p">,</span> <span class="n">r</span> <span class="o">*</span><span class="n">http</span><span class="o">.</span><span class="n">Request</span><span class="p">)</span> <span class="p">{</span>
	<span class="n">body</span><span class="p">,</span> <span class="n">_</span> <span class="o">:=</span> <span class="n">io</span><span class="o">.</span><span class="n">ReadAll</span><span class="p">(</span><span class="n">r</span><span class="o">.</span><span class="n">Body</span><span class="p">)</span>
	<span class="n">w</span><span class="o">.</span><span class="n">Header</span><span class="p">()</span><span class="o">.</span><span class="n">Set</span><span class="p">(</span><span class="s">"Content-Type"</span><span class="p">,</span> <span class="s">"text/plain"</span><span class="p">)</span>
	<span class="n">w</span><span class="o">.</span><span class="n">Write</span><span class="p">(</span><span class="n">body</span><span class="p">)</span>
<span class="p">})</span>

<span class="n">ts</span> <span class="o">:=</span> <span class="n">httptest</span><span class="o">.</span><span class="n">NewServer</span><span class="p">(</span><span class="n">mux</span><span class="p">)</span>
<span class="k">defer</span> <span class="n">ts</span><span class="o">.</span><span class="n">Close</span><span class="p">()</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">/health</code> endpoint returns a simple status check. The <code class="language-plaintext highlighter-rouge">/greet</code> endpoint reads a query parameter with <code class="language-plaintext highlighter-rouge">r.URL.Query().Get("name")</code>. The <code class="language-plaintext highlighter-rouge">/echo</code> endpoint reads the request body with <code class="language-plaintext highlighter-rouge">io.ReadAll(r.Body)</code> and writes it back. Each route is restricted to a specific HTTP method — a <code class="language-plaintext highlighter-rouge">POST</code> to <code class="language-plaintext highlighter-rouge">/health</code> would return a 405 Method Not Allowed.</p>

<h3 id="json-api">JSON API</h3>

<p>Most services exchange JSON. The pattern is straightforward: define request and response structs with <code class="language-plaintext highlighter-rouge">json</code> tags, decode the request body with <code class="language-plaintext highlighter-rouge">json.NewDecoder</code>, and encode the response with <code class="language-plaintext highlighter-rouge">json.NewEncoder</code>:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
</pre></td><td class="rouge-code"><pre><span class="k">type</span> <span class="n">MathRequest</span> <span class="k">struct</span> <span class="p">{</span>
	<span class="n">A</span>  <span class="kt">float64</span> <span class="s">`json:"a"`</span>
	<span class="n">B</span>  <span class="kt">float64</span> <span class="s">`json:"b"`</span>
	<span class="n">Op</span> <span class="kt">string</span>  <span class="s">`json:"op"`</span>
<span class="p">}</span>

<span class="k">type</span> <span class="n">MathResponse</span> <span class="k">struct</span> <span class="p">{</span>
	<span class="n">Result</span> <span class="kt">float64</span> <span class="s">`json:"result"`</span>
	<span class="n">Error</span>  <span class="kt">string</span>  <span class="s">`json:"error,omitempty"`</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">mathHandler</span><span class="p">(</span><span class="n">w</span> <span class="n">http</span><span class="o">.</span><span class="n">ResponseWriter</span><span class="p">,</span> <span class="n">r</span> <span class="o">*</span><span class="n">http</span><span class="o">.</span><span class="n">Request</span><span class="p">)</span> <span class="p">{</span>
	<span class="n">w</span><span class="o">.</span><span class="n">Header</span><span class="p">()</span><span class="o">.</span><span class="n">Set</span><span class="p">(</span><span class="s">"Content-Type"</span><span class="p">,</span> <span class="s">"application/json"</span><span class="p">)</span>

	<span class="k">var</span> <span class="n">req</span> <span class="n">MathRequest</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">json</span><span class="o">.</span><span class="n">NewDecoder</span><span class="p">(</span><span class="n">r</span><span class="o">.</span><span class="n">Body</span><span class="p">)</span><span class="o">.</span><span class="n">Decode</span><span class="p">(</span><span class="o">&amp;</span><span class="n">req</span><span class="p">);</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="n">w</span><span class="o">.</span><span class="n">WriteHeader</span><span class="p">(</span><span class="n">http</span><span class="o">.</span><span class="n">StatusBadRequest</span><span class="p">)</span>
		<span class="n">json</span><span class="o">.</span><span class="n">NewEncoder</span><span class="p">(</span><span class="n">w</span><span class="p">)</span><span class="o">.</span><span class="n">Encode</span><span class="p">(</span><span class="n">MathResponse</span><span class="p">{</span><span class="n">Error</span><span class="o">:</span> <span class="s">"invalid JSON"</span><span class="p">})</span>
		<span class="k">return</span>
	<span class="p">}</span>

	<span class="k">var</span> <span class="n">result</span> <span class="kt">float64</span>
	<span class="k">switch</span> <span class="n">req</span><span class="o">.</span><span class="n">Op</span> <span class="p">{</span>
	<span class="k">case</span> <span class="s">"add"</span><span class="o">:</span>
		<span class="n">result</span> <span class="o">=</span> <span class="n">req</span><span class="o">.</span><span class="n">A</span> <span class="o">+</span> <span class="n">req</span><span class="o">.</span><span class="n">B</span>
	<span class="k">case</span> <span class="s">"sub"</span><span class="o">:</span>
		<span class="n">result</span> <span class="o">=</span> <span class="n">req</span><span class="o">.</span><span class="n">A</span> <span class="o">-</span> <span class="n">req</span><span class="o">.</span><span class="n">B</span>
	<span class="k">case</span> <span class="s">"mul"</span><span class="o">:</span>
		<span class="n">result</span> <span class="o">=</span> <span class="n">req</span><span class="o">.</span><span class="n">A</span> <span class="o">*</span> <span class="n">req</span><span class="o">.</span><span class="n">B</span>
	<span class="k">case</span> <span class="s">"div"</span><span class="o">:</span>
		<span class="k">if</span> <span class="n">req</span><span class="o">.</span><span class="n">B</span> <span class="o">==</span> <span class="m">0</span> <span class="p">{</span>
			<span class="n">w</span><span class="o">.</span><span class="n">WriteHeader</span><span class="p">(</span><span class="n">http</span><span class="o">.</span><span class="n">StatusBadRequest</span><span class="p">)</span>
			<span class="n">json</span><span class="o">.</span><span class="n">NewEncoder</span><span class="p">(</span><span class="n">w</span><span class="p">)</span><span class="o">.</span><span class="n">Encode</span><span class="p">(</span><span class="n">MathResponse</span><span class="p">{</span><span class="n">Error</span><span class="o">:</span> <span class="s">"division by zero"</span><span class="p">})</span>
			<span class="k">return</span>
		<span class="p">}</span>
		<span class="n">result</span> <span class="o">=</span> <span class="n">req</span><span class="o">.</span><span class="n">A</span> <span class="o">/</span> <span class="n">req</span><span class="o">.</span><span class="n">B</span>
	<span class="k">default</span><span class="o">:</span>
		<span class="n">w</span><span class="o">.</span><span class="n">WriteHeader</span><span class="p">(</span><span class="n">http</span><span class="o">.</span><span class="n">StatusBadRequest</span><span class="p">)</span>
		<span class="n">json</span><span class="o">.</span><span class="n">NewEncoder</span><span class="p">(</span><span class="n">w</span><span class="p">)</span><span class="o">.</span><span class="n">Encode</span><span class="p">(</span><span class="n">MathResponse</span><span class="p">{</span><span class="n">Error</span><span class="o">:</span> <span class="s">"unknown op: "</span> <span class="o">+</span> <span class="n">req</span><span class="o">.</span><span class="n">Op</span><span class="p">})</span>
		<span class="k">return</span>
	<span class="p">}</span>

	<span class="n">json</span><span class="o">.</span><span class="n">NewEncoder</span><span class="p">(</span><span class="n">w</span><span class="p">)</span><span class="o">.</span><span class="n">Encode</span><span class="p">(</span><span class="n">MathResponse</span><span class="p">{</span><span class="n">Result</span><span class="o">:</span> <span class="n">result</span><span class="p">})</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The handler sets <code class="language-plaintext highlighter-rouge">Content-Type: application/json</code> first, then decodes the request body into a <code class="language-plaintext highlighter-rouge">MathRequest</code>. If decoding fails or the operation is invalid, it writes an error response with an appropriate status code and returns early. On success, it encodes the result. The <code class="language-plaintext highlighter-rouge">omitempty</code> tag on the <code class="language-plaintext highlighter-rouge">Error</code> field means it is omitted from the JSON output when empty, so successful responses only contain the <code class="language-plaintext highlighter-rouge">result</code> field.</p>

<h3 id="middleware">Middleware</h3>

<p>Middleware is a function that wraps an <code class="language-plaintext highlighter-rouge">http.Handler</code> and adds behavior — logging, authentication, rate limiting — without changing the handler itself. The pattern is: take a handler in, return a handler out. To capture the response status code, you need a wrapper around <code class="language-plaintext highlighter-rouge">ResponseWriter</code> that intercepts the <code class="language-plaintext highlighter-rouge">WriteHeader</code> call:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
</pre></td><td class="rouge-code"><pre><span class="k">type</span> <span class="n">statusRecorder</span> <span class="k">struct</span> <span class="p">{</span>
	<span class="n">http</span><span class="o">.</span><span class="n">ResponseWriter</span>
	<span class="n">status</span> <span class="kt">int</span>
<span class="p">}</span>

<span class="k">func</span> <span class="p">(</span><span class="n">sr</span> <span class="o">*</span><span class="n">statusRecorder</span><span class="p">)</span> <span class="n">WriteHeader</span><span class="p">(</span><span class="n">code</span> <span class="kt">int</span><span class="p">)</span> <span class="p">{</span>
	<span class="n">sr</span><span class="o">.</span><span class="n">status</span> <span class="o">=</span> <span class="n">code</span>
	<span class="n">sr</span><span class="o">.</span><span class="n">ResponseWriter</span><span class="o">.</span><span class="n">WriteHeader</span><span class="p">(</span><span class="n">code</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">type</span> <span class="n">logEntry</span> <span class="k">struct</span> <span class="p">{</span>
	<span class="n">method</span> <span class="kt">string</span>
	<span class="n">path</span>   <span class="kt">string</span>
	<span class="n">status</span> <span class="kt">int</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">loggingMiddleware</span><span class="p">(</span><span class="n">logs</span> <span class="o">*</span><span class="p">[]</span><span class="n">logEntry</span><span class="p">,</span> <span class="n">next</span> <span class="n">http</span><span class="o">.</span><span class="n">Handler</span><span class="p">)</span> <span class="n">http</span><span class="o">.</span><span class="n">Handler</span> <span class="p">{</span>
	<span class="k">return</span> <span class="n">http</span><span class="o">.</span><span class="n">HandlerFunc</span><span class="p">(</span><span class="k">func</span><span class="p">(</span><span class="n">w</span> <span class="n">http</span><span class="o">.</span><span class="n">ResponseWriter</span><span class="p">,</span> <span class="n">r</span> <span class="o">*</span><span class="n">http</span><span class="o">.</span><span class="n">Request</span><span class="p">)</span> <span class="p">{</span>
		<span class="n">rec</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="n">statusRecorder</span><span class="p">{</span><span class="n">ResponseWriter</span><span class="o">:</span> <span class="n">w</span><span class="p">,</span> <span class="n">status</span><span class="o">:</span> <span class="n">http</span><span class="o">.</span><span class="n">StatusOK</span><span class="p">}</span>
		<span class="n">next</span><span class="o">.</span><span class="n">ServeHTTP</span><span class="p">(</span><span class="n">rec</span><span class="p">,</span> <span class="n">r</span><span class="p">)</span>
		<span class="o">*</span><span class="n">logs</span> <span class="o">=</span> <span class="nb">append</span><span class="p">(</span><span class="o">*</span><span class="n">logs</span><span class="p">,</span> <span class="n">logEntry</span><span class="p">{</span>
			<span class="n">method</span><span class="o">:</span> <span class="n">r</span><span class="o">.</span><span class="n">Method</span><span class="p">,</span>
			<span class="n">path</span><span class="o">:</span>   <span class="n">r</span><span class="o">.</span><span class="n">URL</span><span class="o">.</span><span class="n">Path</span><span class="p">,</span>
			<span class="n">status</span><span class="o">:</span> <span class="n">rec</span><span class="o">.</span><span class="n">status</span><span class="p">,</span>
		<span class="p">})</span>
	<span class="p">})</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">statusRecorder</code> embeds <code class="language-plaintext highlighter-rouge">http.ResponseWriter</code> so it satisfies the interface automatically. It overrides only <code class="language-plaintext highlighter-rouge">WriteHeader</code> to capture the status code before forwarding the call. The <code class="language-plaintext highlighter-rouge">loggingMiddleware</code> function wraps any handler: it creates a recorder, passes it to the inner handler via <code class="language-plaintext highlighter-rouge">ServeHTTP</code>, and after the handler returns, records the method, path, and status. Because the signature is <code class="language-plaintext highlighter-rouge">func(http.Handler) http.Handler</code>, middleware composes naturally — you can stack multiple layers by wrapping one around another.</p>

<h3 id="http-client">HTTP Client</h3>

<p>Go’s <code class="language-plaintext highlighter-rouge">net/http</code> package also provides a client. For simple requests, <code class="language-plaintext highlighter-rouge">http.Get</code> and <code class="language-plaintext highlighter-rouge">http.Post</code> work directly. When you need custom headers or methods, build an <code class="language-plaintext highlighter-rouge">*http.Request</code> with <code class="language-plaintext highlighter-rouge">http.NewRequest</code> and execute it with a <code class="language-plaintext highlighter-rouge">client.Do</code> call:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
</pre></td><td class="rouge-code"><pre><span class="c">// Simple GET</span>
<span class="n">resp</span><span class="p">,</span> <span class="n">_</span> <span class="o">:=</span> <span class="n">http</span><span class="o">.</span><span class="n">Get</span><span class="p">(</span><span class="n">ts</span><span class="o">.</span><span class="n">URL</span> <span class="o">+</span> <span class="s">"/simple"</span><span class="p">)</span>
<span class="n">body</span><span class="p">,</span> <span class="n">_</span> <span class="o">:=</span> <span class="n">io</span><span class="o">.</span><span class="n">ReadAll</span><span class="p">(</span><span class="n">resp</span><span class="o">.</span><span class="n">Body</span><span class="p">)</span>
<span class="n">resp</span><span class="o">.</span><span class="n">Body</span><span class="o">.</span><span class="n">Close</span><span class="p">()</span>

<span class="c">// Simple POST</span>
<span class="n">resp</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">http</span><span class="o">.</span><span class="n">Post</span><span class="p">(</span><span class="n">ts</span><span class="o">.</span><span class="n">URL</span><span class="o">+</span><span class="s">"/data"</span><span class="p">,</span> <span class="s">"text/plain"</span><span class="p">,</span> <span class="n">bytes</span><span class="o">.</span><span class="n">NewBufferString</span><span class="p">(</span><span class="s">"payload"</span><span class="p">))</span>
<span class="n">body</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">io</span><span class="o">.</span><span class="n">ReadAll</span><span class="p">(</span><span class="n">resp</span><span class="o">.</span><span class="n">Body</span><span class="p">)</span>
<span class="n">resp</span><span class="o">.</span><span class="n">Body</span><span class="o">.</span><span class="n">Close</span><span class="p">()</span>

<span class="c">// Custom headers with NewRequest + client.Do</span>
<span class="n">req</span><span class="p">,</span> <span class="n">_</span> <span class="o">:=</span> <span class="n">http</span><span class="o">.</span><span class="n">NewRequest</span><span class="p">(</span><span class="s">"GET"</span><span class="p">,</span> <span class="n">ts</span><span class="o">.</span><span class="n">URL</span><span class="o">+</span><span class="s">"/custom"</span><span class="p">,</span> <span class="no">nil</span><span class="p">)</span>
<span class="n">req</span><span class="o">.</span><span class="n">Header</span><span class="o">.</span><span class="n">Set</span><span class="p">(</span><span class="s">"User-Agent"</span><span class="p">,</span> <span class="s">"GoLearn/1.0"</span><span class="p">)</span>
<span class="n">req</span><span class="o">.</span><span class="n">Header</span><span class="o">.</span><span class="n">Set</span><span class="p">(</span><span class="s">"X-Custom"</span><span class="p">,</span> <span class="s">"my-value"</span><span class="p">)</span>

<span class="n">client</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="n">http</span><span class="o">.</span><span class="n">Client</span><span class="p">{}</span>
<span class="n">resp</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">Do</span><span class="p">(</span><span class="n">req</span><span class="p">)</span>
<span class="n">body</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">io</span><span class="o">.</span><span class="n">ReadAll</span><span class="p">(</span><span class="n">resp</span><span class="o">.</span><span class="n">Body</span><span class="p">)</span>
<span class="n">resp</span><span class="o">.</span><span class="n">Body</span><span class="o">.</span><span class="n">Close</span><span class="p">()</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">http.Get</code> and <code class="language-plaintext highlighter-rouge">http.Post</code> are convenience wrappers that use the default client. For anything beyond trivial requests — setting headers, configuring timeouts, controlling redirects — create an <code class="language-plaintext highlighter-rouge">*http.Request</code> explicitly and pass it to <code class="language-plaintext highlighter-rouge">client.Do</code>. Always close <code class="language-plaintext highlighter-rouge">resp.Body</code> when you are done reading, typically with <code class="language-plaintext highlighter-rouge">defer resp.Body.Close()</code>, to avoid leaking connections.</p>

<h2 id="signal-handling-and-graceful-shutdown">Signal Handling and Graceful Shutdown</h2>

<p>When a service receives a termination signal (Ctrl+C sends <code class="language-plaintext highlighter-rouge">SIGINT</code>, container orchestrators send <code class="language-plaintext highlighter-rouge">SIGTERM</code>), it should not just crash. It should stop accepting new work, finish in-flight operations, flush buffers, close connections, and then exit. Go makes this possible with <code class="language-plaintext highlighter-rouge">os/signal</code>, channels, and context cancellation.</p>

<h3 id="the-shutdown-pattern">The Shutdown Pattern</h3>

<p>The standard approach is: create a cancellable context, listen for signals on a buffered channel, cancel the context when a signal arrives, and use a <code class="language-plaintext highlighter-rouge">WaitGroup</code> to wait for goroutines to finish their cleanup.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">ctx</span><span class="p">,</span> <span class="n">cancel</span> <span class="o">:=</span> <span class="n">context</span><span class="o">.</span><span class="n">WithCancel</span><span class="p">(</span><span class="n">context</span><span class="o">.</span><span class="n">Background</span><span class="p">())</span>
	<span class="k">defer</span> <span class="n">cancel</span><span class="p">()</span>

	<span class="n">sigCh</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="k">chan</span> <span class="n">os</span><span class="o">.</span><span class="n">Signal</span><span class="p">,</span> <span class="m">1</span><span class="p">)</span>
	<span class="n">signal</span><span class="o">.</span><span class="n">Notify</span><span class="p">(</span><span class="n">sigCh</span><span class="p">,</span> <span class="n">os</span><span class="o">.</span><span class="n">Interrupt</span><span class="p">,</span> <span class="n">syscall</span><span class="o">.</span><span class="n">SIGTERM</span><span class="p">)</span>

	<span class="k">go</span> <span class="k">func</span><span class="p">()</span> <span class="p">{</span>
		<span class="n">sig</span> <span class="o">:=</span> <span class="o">&lt;-</span><span class="n">sigCh</span>
		<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"</span><span class="se">\n</span><span class="s">  [signal] received %v, initiating shutdown...</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">sig</span><span class="p">)</span>
		<span class="n">cancel</span><span class="p">()</span>
	<span class="p">}()</span>

	<span class="n">proc</span> <span class="o">:=</span> <span class="n">NewProcessor</span><span class="p">()</span>
	<span class="k">var</span> <span class="n">wg</span> <span class="n">sync</span><span class="o">.</span><span class="n">WaitGroup</span>

	<span class="n">wg</span><span class="o">.</span><span class="n">Add</span><span class="p">(</span><span class="m">1</span><span class="p">)</span>
	<span class="k">go</span> <span class="k">func</span><span class="p">()</span> <span class="p">{</span>
		<span class="k">defer</span> <span class="n">wg</span><span class="o">.</span><span class="n">Done</span><span class="p">()</span>
		<span class="k">defer</span> <span class="k">func</span><span class="p">()</span> <span class="p">{</span>
			<span class="n">proc</span><span class="o">.</span><span class="n">Flush</span><span class="p">()</span>
			<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"  [worker] cleanup complete"</span><span class="p">)</span>
		<span class="p">}()</span>

		<span class="n">ticker</span> <span class="o">:=</span> <span class="n">time</span><span class="o">.</span><span class="n">NewTicker</span><span class="p">(</span><span class="m">500</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Millisecond</span><span class="p">)</span>
		<span class="k">defer</span> <span class="n">ticker</span><span class="o">.</span><span class="n">Stop</span><span class="p">()</span>

		<span class="n">flushTicker</span> <span class="o">:=</span> <span class="n">time</span><span class="o">.</span><span class="n">NewTicker</span><span class="p">(</span><span class="m">2</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Second</span><span class="p">)</span>
		<span class="k">defer</span> <span class="n">flushTicker</span><span class="o">.</span><span class="n">Stop</span><span class="p">()</span>

		<span class="n">items</span> <span class="o">:=</span> <span class="p">[]</span><span class="kt">string</span><span class="p">{</span>
			<span class="s">"INSERT users alice"</span><span class="p">,</span>
			<span class="s">"UPDATE users bob"</span><span class="p">,</span>
			<span class="s">"INSERT orders ord-1"</span><span class="p">,</span>
			<span class="s">"DELETE sessions expired"</span><span class="p">,</span>
			<span class="s">"INSERT events login"</span><span class="p">,</span>
		<span class="p">}</span>
		<span class="n">idx</span> <span class="o">:=</span> <span class="m">0</span>

		<span class="k">for</span> <span class="p">{</span>
			<span class="k">select</span> <span class="p">{</span>
			<span class="k">case</span> <span class="o">&lt;-</span><span class="n">ctx</span><span class="o">.</span><span class="n">Done</span><span class="p">()</span><span class="o">:</span>
				<span class="k">return</span>
			<span class="k">case</span> <span class="o">&lt;-</span><span class="n">ticker</span><span class="o">.</span><span class="n">C</span><span class="o">:</span>
				<span class="k">if</span> <span class="n">idx</span> <span class="o">&lt;</span> <span class="nb">len</span><span class="p">(</span><span class="n">items</span><span class="p">)</span> <span class="p">{</span>
					<span class="n">proc</span><span class="o">.</span><span class="n">Process</span><span class="p">(</span><span class="n">items</span><span class="p">[</span><span class="n">idx</span><span class="p">])</span>
					<span class="n">idx</span><span class="o">++</span>
				<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
					<span class="n">idx</span> <span class="o">=</span> <span class="m">0</span>
				<span class="p">}</span>
			<span class="k">case</span> <span class="o">&lt;-</span><span class="n">flushTicker</span><span class="o">.</span><span class="n">C</span><span class="o">:</span>
				<span class="n">proc</span><span class="o">.</span><span class="n">Flush</span><span class="p">()</span>
			<span class="p">}</span>
		<span class="p">}</span>
	<span class="p">}()</span>

	<span class="n">wg</span><span class="o">.</span><span class="n">Wait</span><span class="p">()</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"  [main] all goroutines stopped, exiting cleanly"</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>There are several details worth noting here. The signal channel has a buffer of 1 — <code class="language-plaintext highlighter-rouge">make(chan os.Signal, 1)</code>. This is important because the signal delivery is non-blocking: if the channel is full, the signal is dropped. A buffer of 1 ensures the signal is captured even if the goroutine is momentarily busy. <code class="language-plaintext highlighter-rouge">signal.Notify</code> registers the channel to receive <code class="language-plaintext highlighter-rouge">os.Interrupt</code> (Ctrl+C) and <code class="language-plaintext highlighter-rouge">syscall.SIGTERM</code> (the standard termination signal from process managers).</p>

<p>The signal-handling goroutine blocks on <code class="language-plaintext highlighter-rouge">&lt;-sigCh</code> until a signal arrives, then calls <code class="language-plaintext highlighter-rouge">cancel()</code>. This cancels the context, which causes <code class="language-plaintext highlighter-rouge">ctx.Done()</code> to close in every goroutine that is selecting on it. The worker goroutine sees the cancellation, returns from the loop, and its deferred functions run in LIFO order — first <code class="language-plaintext highlighter-rouge">flushTicker.Stop()</code>, then <code class="language-plaintext highlighter-rouge">ticker.Stop()</code>, then <code class="language-plaintext highlighter-rouge">proc.Flush()</code>, and finally <code class="language-plaintext highlighter-rouge">wg.Done()</code>.</p>

<p>The <code class="language-plaintext highlighter-rouge">Processor</code> type buffers work items and flushes them either periodically or during shutdown:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
</pre></td><td class="rouge-code"><pre><span class="k">type</span> <span class="n">Processor</span> <span class="k">struct</span> <span class="p">{</span>
	<span class="n">buffer</span> <span class="p">[]</span><span class="kt">string</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">NewProcessor</span><span class="p">()</span> <span class="o">*</span><span class="n">Processor</span> <span class="p">{</span>
	<span class="k">return</span> <span class="o">&amp;</span><span class="n">Processor</span><span class="p">{</span><span class="n">buffer</span><span class="o">:</span> <span class="nb">make</span><span class="p">([]</span><span class="kt">string</span><span class="p">,</span> <span class="m">0</span><span class="p">,</span> <span class="m">100</span><span class="p">)}</span>
<span class="p">}</span>

<span class="k">func</span> <span class="p">(</span><span class="n">p</span> <span class="o">*</span><span class="n">Processor</span><span class="p">)</span> <span class="n">Process</span><span class="p">(</span><span class="n">item</span> <span class="kt">string</span><span class="p">)</span> <span class="p">{</span>
	<span class="n">p</span><span class="o">.</span><span class="n">buffer</span> <span class="o">=</span> <span class="nb">append</span><span class="p">(</span><span class="n">p</span><span class="o">.</span><span class="n">buffer</span><span class="p">,</span> <span class="n">item</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">func</span> <span class="p">(</span><span class="n">p</span> <span class="o">*</span><span class="n">Processor</span><span class="p">)</span> <span class="n">Flush</span><span class="p">()</span> <span class="p">{</span>
	<span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">p</span><span class="o">.</span><span class="n">buffer</span><span class="p">)</span> <span class="o">==</span> <span class="m">0</span> <span class="p">{</span>
		<span class="k">return</span>
	<span class="p">}</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  [processor] flushing %d items</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">p</span><span class="o">.</span><span class="n">buffer</span><span class="p">))</span>
	<span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">item</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">p</span><span class="o">.</span><span class="n">buffer</span> <span class="p">{</span>
		<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"    -&gt; %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">item</span><span class="p">)</span>
	<span class="p">}</span>
	<span class="n">p</span><span class="o">.</span><span class="n">buffer</span> <span class="o">=</span> <span class="n">p</span><span class="o">.</span><span class="n">buffer</span><span class="p">[</span><span class="o">:</span><span class="m">0</span><span class="p">]</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">Flush</code> method resets the buffer with <code class="language-plaintext highlighter-rouge">p.buffer[:0]</code>, which keeps the underlying array allocated but sets the length to zero. This pattern avoids allocating a new slice on every flush. The key guarantee of this architecture is that no data is silently lost — the deferred <code class="language-plaintext highlighter-rouge">Flush</code> call in the worker ensures that any items buffered between the last periodic flush and the shutdown signal are still written out before the process exits.</p>

<h2 id="database-migrations">Database Migrations</h2>

<p>Production services need to evolve their database schema over time — adding tables, altering columns, creating indexes. Migration tools track which changes have been applied and run only the new ones. In Go, the <code class="language-plaintext highlighter-rouge">go:embed</code> directive lets you compile migration files directly into the binary, and <code class="language-plaintext highlighter-rouge">goose</code> provides the migration engine.</p>

<h3 id="goembed-and-embedfs">go:embed and embed.FS</h3>

<p>The <code class="language-plaintext highlighter-rouge">go:embed</code> directive is a compiler instruction that bakes file contents into the binary at build time. The variable it annotates becomes an <code class="language-plaintext highlighter-rouge">embed.FS</code> — a read-only filesystem that you can read from at runtime without any external files:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="k">import</span> <span class="s">"embed"</span>

<span class="c">//go:embed migrations/*.sql</span>
<span class="k">var</span> <span class="n">migrationFS</span> <span class="n">embed</span><span class="o">.</span><span class="n">FS</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The comment <code class="language-plaintext highlighter-rouge">//go:embed migrations/*.sql</code> must appear directly above the variable declaration with no blank line in between. It tells the compiler to find every <code class="language-plaintext highlighter-rouge">.sql</code> file in the <code class="language-plaintext highlighter-rouge">migrations/</code> directory and embed their contents. At runtime, <code class="language-plaintext highlighter-rouge">migrationFS</code> is a fully functional read-only filesystem — you can list directories, read files, and pass it to any library that accepts an <code class="language-plaintext highlighter-rouge">fs.FS</code>:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">part1_whatIsEmbed</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">entries</span><span class="p">,</span> <span class="n">_</span> <span class="o">:=</span> <span class="n">migrationFS</span><span class="o">.</span><span class="n">ReadDir</span><span class="p">(</span><span class="s">"migrations"</span><span class="p">)</span>
	<span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">e</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">entries</span> <span class="p">{</span>
		<span class="n">data</span><span class="p">,</span> <span class="n">_</span> <span class="o">:=</span> <span class="n">migrationFS</span><span class="o">.</span><span class="n">ReadFile</span><span class="p">(</span><span class="s">"migrations/"</span> <span class="o">+</span> <span class="n">e</span><span class="o">.</span><span class="n">Name</span><span class="p">())</span>
		<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  %-30s (%d bytes)</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">e</span><span class="o">.</span><span class="n">Name</span><span class="p">(),</span> <span class="nb">len</span><span class="p">(</span><span class="n">data</span><span class="p">))</span>
	<span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>This prints the name and size of each embedded SQL file. The files exist inside the compiled binary — if you delete the <code class="language-plaintext highlighter-rouge">migrations/</code> directory after building, the binary still works. This is particularly useful for deployment: your migration files ship with the binary rather than needing to be copied alongside it.</p>

<h3 id="migration-files">Migration Files</h3>

<p>Each migration file contains a <code class="language-plaintext highlighter-rouge">-- +goose Up</code> section with the forward change and a <code class="language-plaintext highlighter-rouge">-- +goose Down</code> section with the rollback. Goose uses these annotations to know which SQL to run in each direction:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="c1">-- +goose Up</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">IF</span> <span class="k">NOT</span> <span class="k">EXISTS</span> <span class="n">users</span> <span class="p">(</span>
    <span class="n">id</span>         <span class="nb">SERIAL</span> <span class="k">PRIMARY</span> <span class="k">KEY</span><span class="p">,</span>
    <span class="n">name</span>       <span class="nb">TEXT</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>
    <span class="n">email</span>      <span class="nb">TEXT</span> <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">UNIQUE</span><span class="p">,</span>
    <span class="n">created_at</span> <span class="n">TIMESTAMPTZ</span> <span class="k">DEFAULT</span> <span class="n">now</span><span class="p">()</span>
<span class="p">);</span>

<span class="c1">-- +goose Down</span>
<span class="k">DROP</span> <span class="k">TABLE</span> <span class="n">IF</span> <span class="k">EXISTS</span> <span class="n">users</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>A second migration adds a column to the existing table:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="c1">-- +goose Up</span>
<span class="k">ALTER</span> <span class="k">TABLE</span> <span class="n">users</span> <span class="k">ADD</span> <span class="k">COLUMN</span> <span class="n">IF</span> <span class="k">NOT</span> <span class="k">EXISTS</span> <span class="k">role</span> <span class="nb">TEXT</span> <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">DEFAULT</span> <span class="s1">'member'</span><span class="p">;</span>

<span class="c1">-- +goose Down</span>
<span class="k">ALTER</span> <span class="k">TABLE</span> <span class="n">users</span> <span class="k">DROP</span> <span class="k">COLUMN</span> <span class="n">IF</span> <span class="k">EXISTS</span> <span class="k">role</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Migrations are numbered sequentially. Goose runs them in order and records the current version in a <code class="language-plaintext highlighter-rouge">goose_db_version</code> table so it knows which migrations have already been applied.</p>

<h3 id="running-migrations-with-goose">Running Migrations with Goose</h3>

<p>The workflow has four steps: open a database connection, point goose at the embedded filesystem, set the SQL dialect, and call <code class="language-plaintext highlighter-rouge">Up</code> to apply pending migrations:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
</pre></td><td class="rouge-code"><pre><span class="k">import</span> <span class="p">(</span>
	<span class="s">"database/sql"</span>
	<span class="s">"embed"</span>
	<span class="n">_</span> <span class="s">"github.com/jackc/pgx/v5/stdlib"</span>
	<span class="s">"github.com/pressly/goose/v3"</span>
<span class="p">)</span>

<span class="c">//go:embed migrations/*.sql</span>
<span class="k">var</span> <span class="n">migrationFS</span> <span class="n">embed</span><span class="o">.</span><span class="n">FS</span>

<span class="k">func</span> <span class="n">part2_runMigrations</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">db</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">sql</span><span class="o">.</span><span class="n">Open</span><span class="p">(</span><span class="s">"pgx"</span><span class="p">,</span> <span class="n">dbDSN</span><span class="p">)</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="n">log</span><span class="o">.</span><span class="n">Fatalf</span><span class="p">(</span><span class="s">"open db: %v"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
	<span class="p">}</span>
	<span class="k">defer</span> <span class="n">db</span><span class="o">.</span><span class="n">Close</span><span class="p">()</span>

	<span class="n">goose</span><span class="o">.</span><span class="n">SetBaseFS</span><span class="p">(</span><span class="n">migrationFS</span><span class="p">)</span>

	<span class="k">if</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">goose</span><span class="o">.</span><span class="n">SetDialect</span><span class="p">(</span><span class="s">"postgres"</span><span class="p">);</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="n">log</span><span class="o">.</span><span class="n">Fatalf</span><span class="p">(</span><span class="s">"set dialect: %v"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
	<span class="p">}</span>

	<span class="k">if</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">goose</span><span class="o">.</span><span class="n">Up</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="s">"migrations"</span><span class="p">);</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="n">log</span><span class="o">.</span><span class="n">Fatalf</span><span class="p">(</span><span class="s">"migration failed: %v"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
	<span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">goose.SetBaseFS(migrationFS)</code> tells goose to read migration files from the embedded filesystem instead of disk. <code class="language-plaintext highlighter-rouge">goose.SetDialect("postgres")</code> configures the SQL generation for PostgreSQL. <code class="language-plaintext highlighter-rouge">goose.Up(db, "migrations")</code> scans the <code class="language-plaintext highlighter-rouge">migrations</code> directory, compares the file versions against the <code class="language-plaintext highlighter-rouge">goose_db_version</code> table, and runs any that have not been applied yet. If all migrations are already applied, it does nothing.</p>

<p>The blank import <code class="language-plaintext highlighter-rouge">_ "github.com/jackc/pgx/v5/stdlib"</code> registers the <code class="language-plaintext highlighter-rouge">pgx</code> driver with <code class="language-plaintext highlighter-rouge">database/sql</code> so that <code class="language-plaintext highlighter-rouge">sql.Open("pgx", dsn)</code> works. This is a common Go pattern: importing a package solely for its <code class="language-plaintext highlighter-rouge">init</code> function’s side effects.</p>

<p>To check the current version after migrations have run:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">part3_checkVersion</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">db</span><span class="p">,</span> <span class="n">_</span> <span class="o">:=</span> <span class="n">sql</span><span class="o">.</span><span class="n">Open</span><span class="p">(</span><span class="s">"pgx"</span><span class="p">,</span> <span class="n">dbDSN</span><span class="p">)</span>
	<span class="k">defer</span> <span class="n">db</span><span class="o">.</span><span class="n">Close</span><span class="p">()</span>

	<span class="n">version</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">goose</span><span class="o">.</span><span class="n">GetDBVersion</span><span class="p">(</span><span class="n">db</span><span class="p">)</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="n">log</span><span class="o">.</span><span class="n">Fatal</span><span class="p">(</span><span class="n">err</span><span class="p">)</span>
	<span class="p">}</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"Current version: %d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">version</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">goose.GetDBVersion</code> returns the highest migration version that has been applied. After running both migration files, this returns <code class="language-plaintext highlighter-rouge">2</code>. To add a new migration, you create <code class="language-plaintext highlighter-rouge">003_something.sql</code> in the <code class="language-plaintext highlighter-rouge">migrations/</code> directory and rebuild. The next call to <code class="language-plaintext highlighter-rouge">goose.Up</code> applies only the new file.</p>

<h2 id="reflection">Reflection</h2>

<p>Go is a statically typed language, but sometimes you need to inspect or manipulate types at runtime. The <code class="language-plaintext highlighter-rouge">reflect</code> package provides this capability. Libraries like <code class="language-plaintext highlighter-rouge">encoding/json</code>, <code class="language-plaintext highlighter-rouge">database/sql</code>, and <code class="language-plaintext highlighter-rouge">fmt</code> all use reflection internally to work with arbitrary types. Understanding reflection helps you read those libraries and write generic utilities of your own.</p>

<h3 id="reflecttypeof-and-reflectvalueof">reflect.TypeOf and reflect.ValueOf</h3>

<p>The two entry points to reflection are <code class="language-plaintext highlighter-rouge">reflect.TypeOf</code>, which returns the type of a value, and <code class="language-plaintext highlighter-rouge">reflect.ValueOf</code>, which returns a <code class="language-plaintext highlighter-rouge">reflect.Value</code> wrapping the actual data. Every reflected value has a <code class="language-plaintext highlighter-rouge">Kind</code> (the broad category: <code class="language-plaintext highlighter-rouge">int</code>, <code class="language-plaintext highlighter-rouge">string</code>, <code class="language-plaintext highlighter-rouge">struct</code>, <code class="language-plaintext highlighter-rouge">slice</code>, <code class="language-plaintext highlighter-rouge">ptr</code>) and a <code class="language-plaintext highlighter-rouge">Type</code> (the specific type: <code class="language-plaintext highlighter-rouge">main.Employee</code>, <code class="language-plaintext highlighter-rouge">[]int</code>, <code class="language-plaintext highlighter-rouge">*main.Employee</code>):</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
</pre></td><td class="rouge-code"><pre><span class="n">i</span> <span class="o">:=</span> <span class="m">42</span>
<span class="n">s</span> <span class="o">:=</span> <span class="s">"hello"</span>
<span class="n">f</span> <span class="o">:=</span> <span class="m">3.14</span>
<span class="n">sl</span> <span class="o">:=</span> <span class="p">[]</span><span class="kt">int</span><span class="p">{</span><span class="m">1</span><span class="p">,</span> <span class="m">2</span><span class="p">,</span> <span class="m">3</span><span class="p">}</span>
<span class="n">m</span> <span class="o">:=</span> <span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">int</span><span class="p">{</span><span class="s">"a"</span><span class="o">:</span> <span class="m">1</span><span class="p">,</span> <span class="s">"b"</span><span class="o">:</span> <span class="m">2</span><span class="p">}</span>
<span class="n">e</span> <span class="o">:=</span> <span class="n">Employee</span><span class="p">{</span><span class="n">Name</span><span class="o">:</span> <span class="s">"Alice"</span><span class="p">,</span> <span class="n">Age</span><span class="o">:</span> <span class="m">30</span><span class="p">,</span> <span class="n">Department</span><span class="o">:</span> <span class="s">"Engineering"</span><span class="p">,</span> <span class="n">Salary</span><span class="o">:</span> <span class="m">120000</span><span class="p">}</span>
<span class="n">p</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="n">e</span>

<span class="n">values</span> <span class="o">:=</span> <span class="p">[]</span><span class="k">interface</span><span class="p">{}{</span><span class="n">i</span><span class="p">,</span> <span class="n">s</span><span class="p">,</span> <span class="n">f</span><span class="p">,</span> <span class="n">sl</span><span class="p">,</span> <span class="n">m</span><span class="p">,</span> <span class="n">e</span><span class="p">,</span> <span class="n">p</span><span class="p">}</span>
<span class="n">labels</span> <span class="o">:=</span> <span class="p">[]</span><span class="kt">string</span><span class="p">{</span><span class="s">"int"</span><span class="p">,</span> <span class="s">"string"</span><span class="p">,</span> <span class="s">"float64"</span><span class="p">,</span> <span class="s">"slice"</span><span class="p">,</span> <span class="s">"map"</span><span class="p">,</span> <span class="s">"struct"</span><span class="p">,</span> <span class="s">"pointer"</span><span class="p">}</span>

<span class="k">for</span> <span class="n">idx</span><span class="p">,</span> <span class="n">v</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">values</span> <span class="p">{</span>
	<span class="n">t</span> <span class="o">:=</span> <span class="n">reflect</span><span class="o">.</span><span class="n">TypeOf</span><span class="p">(</span><span class="n">v</span><span class="p">)</span>
	<span class="n">rv</span> <span class="o">:=</span> <span class="n">reflect</span><span class="o">.</span><span class="n">ValueOf</span><span class="p">(</span><span class="n">v</span><span class="p">)</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  %-8s  Kind=%-10s  Type=%-28s  Value=%v</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span>
		<span class="n">labels</span><span class="p">[</span><span class="n">idx</span><span class="p">],</span> <span class="n">t</span><span class="o">.</span><span class="n">Kind</span><span class="p">(),</span> <span class="n">t</span><span class="p">,</span> <span class="n">rv</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The distinction between <code class="language-plaintext highlighter-rouge">Kind</code> and <code class="language-plaintext highlighter-rouge">Type</code> matters. A <code class="language-plaintext highlighter-rouge">Kind</code> of <code class="language-plaintext highlighter-rouge">struct</code> tells you the value is some struct, while the <code class="language-plaintext highlighter-rouge">Type</code> tells you it is specifically <code class="language-plaintext highlighter-rouge">main.Employee</code>. A pointer to that struct has <code class="language-plaintext highlighter-rouge">Kind</code> of <code class="language-plaintext highlighter-rouge">ptr</code> and <code class="language-plaintext highlighter-rouge">Type</code> of <code class="language-plaintext highlighter-rouge">*main.Employee</code>. This distinction is how generic code decides what operations are valid — you check <code class="language-plaintext highlighter-rouge">Kind</code> to branch on the category, then use <code class="language-plaintext highlighter-rouge">Type</code> for specifics.</p>

<h3 id="inspecting-structs-and-tags">Inspecting Structs and Tags</h3>

<p>Reflection can iterate over a struct’s fields, read their types, check whether they are exported, and extract struct tags. This is exactly how <code class="language-plaintext highlighter-rouge">encoding/json</code> decides which fields to include and what JSON key names to use:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
</pre></td><td class="rouge-code"><pre><span class="k">type</span> <span class="n">Employee</span> <span class="k">struct</span> <span class="p">{</span>
	<span class="n">Name</span>       <span class="kt">string</span>  <span class="s">`json:"name"`</span>
	<span class="n">Age</span>        <span class="kt">int</span>     <span class="s">`json:"age,omitempty"`</span>
	<span class="n">Department</span> <span class="kt">string</span>  <span class="s">`json:"department"`</span>
	<span class="n">Salary</span>     <span class="kt">float64</span> <span class="s">`json:"salary,omitempty"`</span>
	<span class="n">active</span>     <span class="kt">bool</span>
<span class="p">}</span>

<span class="n">e</span> <span class="o">:=</span> <span class="n">Employee</span><span class="p">{</span>
	<span class="n">Name</span><span class="o">:</span> <span class="s">"Bob"</span><span class="p">,</span> <span class="n">Age</span><span class="o">:</span> <span class="m">25</span><span class="p">,</span> <span class="n">Department</span><span class="o">:</span> <span class="s">"Marketing"</span><span class="p">,</span> <span class="n">Salary</span><span class="o">:</span> <span class="m">85000</span><span class="p">,</span> <span class="n">active</span><span class="o">:</span> <span class="no">true</span><span class="p">,</span>
<span class="p">}</span>

<span class="n">t</span> <span class="o">:=</span> <span class="n">reflect</span><span class="o">.</span><span class="n">TypeOf</span><span class="p">(</span><span class="n">e</span><span class="p">)</span>
<span class="n">v</span> <span class="o">:=</span> <span class="n">reflect</span><span class="o">.</span><span class="n">ValueOf</span><span class="p">(</span><span class="n">e</span><span class="p">)</span>

<span class="k">for</span> <span class="n">i</span> <span class="o">:=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">t</span><span class="o">.</span><span class="n">NumField</span><span class="p">();</span> <span class="n">i</span><span class="o">++</span> <span class="p">{</span>
	<span class="n">field</span> <span class="o">:=</span> <span class="n">t</span><span class="o">.</span><span class="n">Field</span><span class="p">(</span><span class="n">i</span><span class="p">)</span>
	<span class="n">val</span> <span class="o">:=</span> <span class="n">v</span><span class="o">.</span><span class="n">Field</span><span class="p">(</span><span class="n">i</span><span class="p">)</span>

	<span class="n">exported</span> <span class="o">:=</span> <span class="n">field</span><span class="o">.</span><span class="n">IsExported</span><span class="p">()</span>
	<span class="n">jsonTag</span> <span class="o">:=</span> <span class="n">field</span><span class="o">.</span><span class="n">Tag</span><span class="o">.</span><span class="n">Get</span><span class="p">(</span><span class="s">"json"</span><span class="p">)</span>

	<span class="n">valStr</span> <span class="o">:=</span> <span class="s">""</span>
	<span class="k">if</span> <span class="n">exported</span> <span class="p">{</span>
		<span class="n">valStr</span> <span class="o">=</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"%v"</span><span class="p">,</span> <span class="n">val</span><span class="o">.</span><span class="n">Interface</span><span class="p">())</span>
	<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
		<span class="n">valStr</span> <span class="o">=</span> <span class="s">"(unexported)"</span>
	<span class="p">}</span>

	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  %-12s %-10s exported=%-5t tag=%q  value=%s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span>
		<span class="n">field</span><span class="o">.</span><span class="n">Name</span><span class="p">,</span> <span class="n">field</span><span class="o">.</span><span class="n">Type</span><span class="p">,</span> <span class="n">exported</span><span class="p">,</span> <span class="n">jsonTag</span><span class="p">,</span> <span class="n">valStr</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">t.NumField()</code> returns the number of fields. <code class="language-plaintext highlighter-rouge">t.Field(i)</code> returns a <code class="language-plaintext highlighter-rouge">reflect.StructField</code> with the field’s name, type, tag string, and export status. <code class="language-plaintext highlighter-rouge">field.Tag.Get("json")</code> extracts the value of the <code class="language-plaintext highlighter-rouge">json</code> key from the struct tag. The unexported <code class="language-plaintext highlighter-rouge">active</code> field is visible to reflection — you can see its name and type — but calling <code class="language-plaintext highlighter-rouge">Interface()</code> on its value will panic. The <code class="language-plaintext highlighter-rouge">CanInterface()</code> check (or <code class="language-plaintext highlighter-rouge">IsExported()</code> on the field descriptor) tells you whether it is safe to read the value.</p>

<h3 id="modifying-values">Modifying Values</h3>

<p>Reflection can also modify values, but only if you pass a pointer. <code class="language-plaintext highlighter-rouge">reflect.ValueOf(x)</code> gives you a read-only copy. To get a settable value, you need <code class="language-plaintext highlighter-rouge">reflect.ValueOf(&amp;x).Elem()</code> — the <code class="language-plaintext highlighter-rouge">Elem()</code> call dereferences the pointer, giving reflection access to the original variable:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="n">x</span> <span class="o">:=</span> <span class="m">10</span>
<span class="n">rv</span> <span class="o">:=</span> <span class="n">reflect</span><span class="o">.</span><span class="n">ValueOf</span><span class="p">(</span><span class="o">&amp;</span><span class="n">x</span><span class="p">)</span><span class="o">.</span><span class="n">Elem</span><span class="p">()</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  CanSet(): %t</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">rv</span><span class="o">.</span><span class="n">CanSet</span><span class="p">())</span> <span class="c">// true</span>
<span class="n">rv</span><span class="o">.</span><span class="n">SetInt</span><span class="p">(</span><span class="m">42</span><span class="p">)</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  x after SetInt(42): %d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">x</span><span class="p">)</span> <span class="c">// 42</span>

<span class="c">// Without pointer — CanSet() returns false</span>
<span class="n">rvNoPtr</span> <span class="o">:=</span> <span class="n">reflect</span><span class="o">.</span><span class="n">ValueOf</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  CanSet(): %t</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">rvNoPtr</span><span class="o">.</span><span class="n">CanSet</span><span class="p">())</span> <span class="c">// false</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The same principle applies to struct fields. Pass a pointer to the struct, call <code class="language-plaintext highlighter-rouge">Elem()</code>, then use <code class="language-plaintext highlighter-rouge">FieldByName</code> to find and modify individual fields:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
</pre></td><td class="rouge-code"><pre><span class="n">e</span> <span class="o">:=</span> <span class="n">Employee</span><span class="p">{</span><span class="n">Name</span><span class="o">:</span> <span class="s">"Charlie"</span><span class="p">,</span> <span class="n">Age</span><span class="o">:</span> <span class="m">28</span><span class="p">,</span> <span class="n">Department</span><span class="o">:</span> <span class="s">"Sales"</span><span class="p">,</span> <span class="n">Salary</span><span class="o">:</span> <span class="m">70000</span><span class="p">}</span>

<span class="n">rv</span> <span class="o">:=</span> <span class="n">reflect</span><span class="o">.</span><span class="n">ValueOf</span><span class="p">(</span><span class="o">&amp;</span><span class="n">e</span><span class="p">)</span><span class="o">.</span><span class="n">Elem</span><span class="p">()</span>

<span class="n">nameField</span> <span class="o">:=</span> <span class="n">rv</span><span class="o">.</span><span class="n">FieldByName</span><span class="p">(</span><span class="s">"Name"</span><span class="p">)</span>
<span class="k">if</span> <span class="n">nameField</span><span class="o">.</span><span class="n">IsValid</span><span class="p">()</span> <span class="o">&amp;&amp;</span> <span class="n">nameField</span><span class="o">.</span><span class="n">CanSet</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">nameField</span><span class="o">.</span><span class="n">SetString</span><span class="p">(</span><span class="s">"Diana"</span><span class="p">)</span>
<span class="p">}</span>

<span class="n">salaryField</span> <span class="o">:=</span> <span class="n">rv</span><span class="o">.</span><span class="n">FieldByName</span><span class="p">(</span><span class="s">"Salary"</span><span class="p">)</span>
<span class="k">if</span> <span class="n">salaryField</span><span class="o">.</span><span class="n">IsValid</span><span class="p">()</span> <span class="o">&amp;&amp;</span> <span class="n">salaryField</span><span class="o">.</span><span class="n">CanSet</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">salaryField</span><span class="o">.</span><span class="n">SetFloat</span><span class="p">(</span><span class="m">95000</span><span class="p">)</span>
<span class="p">}</span>

<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  after: %+v</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">e</span><span class="p">)</span> <span class="c">// {Name:Diana Age:28 Department:Sales Salary:95000 active:false}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Unexported fields cannot be set even with a pointer — <code class="language-plaintext highlighter-rouge">CanSet()</code> returns <code class="language-plaintext highlighter-rouge">false</code> for them. Go enforces its visibility rules through reflection, not just at compile time. Always check <code class="language-plaintext highlighter-rouge">IsValid()</code> (the field exists) and <code class="language-plaintext highlighter-rouge">CanSet()</code> (the field is settable) before calling a setter.</p>

<h3 id="dynamic-function-calls">Dynamic Function Calls</h3>

<p>Reflection can inspect function signatures and call functions dynamically. <code class="language-plaintext highlighter-rouge">reflect.ValueOf(fn)</code> wraps the function, and <code class="language-plaintext highlighter-rouge">Call</code> invokes it with a slice of <code class="language-plaintext highlighter-rouge">reflect.Value</code> arguments:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">add</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span> <span class="kt">int</span><span class="p">)</span> <span class="kt">int</span> <span class="p">{</span> <span class="k">return</span> <span class="n">a</span> <span class="o">+</span> <span class="n">b</span> <span class="p">}</span>
<span class="k">func</span> <span class="n">greet</span><span class="p">(</span><span class="n">name</span> <span class="kt">string</span><span class="p">,</span> <span class="n">excited</span> <span class="kt">bool</span><span class="p">)</span> <span class="kt">string</span> <span class="p">{</span>
	<span class="k">if</span> <span class="n">excited</span> <span class="p">{</span>
		<span class="k">return</span> <span class="s">"Hello, "</span> <span class="o">+</span> <span class="n">name</span> <span class="o">+</span> <span class="s">"!"</span>
	<span class="p">}</span>
	<span class="k">return</span> <span class="s">"Hello, "</span> <span class="o">+</span> <span class="n">name</span> <span class="o">+</span> <span class="s">"."</span>
<span class="p">}</span>

<span class="n">addVal</span> <span class="o">:=</span> <span class="n">reflect</span><span class="o">.</span><span class="n">ValueOf</span><span class="p">(</span><span class="n">add</span><span class="p">)</span>
<span class="n">addType</span> <span class="o">:=</span> <span class="n">addVal</span><span class="o">.</span><span class="n">Type</span><span class="p">()</span>

<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  NumIn: %d, NumOut: %d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">addType</span><span class="o">.</span><span class="n">NumIn</span><span class="p">(),</span> <span class="n">addType</span><span class="o">.</span><span class="n">NumOut</span><span class="p">())</span>
<span class="k">for</span> <span class="n">i</span> <span class="o">:=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">addType</span><span class="o">.</span><span class="n">NumIn</span><span class="p">();</span> <span class="n">i</span><span class="o">++</span> <span class="p">{</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  Arg[%d]: %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">i</span><span class="p">,</span> <span class="n">addType</span><span class="o">.</span><span class="n">In</span><span class="p">(</span><span class="n">i</span><span class="p">))</span>
<span class="p">}</span>

<span class="n">args</span> <span class="o">:=</span> <span class="p">[]</span><span class="n">reflect</span><span class="o">.</span><span class="n">Value</span><span class="p">{</span><span class="n">reflect</span><span class="o">.</span><span class="n">ValueOf</span><span class="p">(</span><span class="m">3</span><span class="p">),</span> <span class="n">reflect</span><span class="o">.</span><span class="n">ValueOf</span><span class="p">(</span><span class="m">4</span><span class="p">)}</span>
<span class="n">results</span> <span class="o">:=</span> <span class="n">addVal</span><span class="o">.</span><span class="n">Call</span><span class="p">(</span><span class="n">args</span><span class="p">)</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  add(3, 4) = %v</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">results</span><span class="p">[</span><span class="m">0</span><span class="p">]</span><span class="o">.</span><span class="n">Interface</span><span class="p">())</span> <span class="c">// 7</span>

<span class="n">greetArgs</span> <span class="o">:=</span> <span class="p">[]</span><span class="n">reflect</span><span class="o">.</span><span class="n">Value</span><span class="p">{</span><span class="n">reflect</span><span class="o">.</span><span class="n">ValueOf</span><span class="p">(</span><span class="s">"Go"</span><span class="p">),</span> <span class="n">reflect</span><span class="o">.</span><span class="n">ValueOf</span><span class="p">(</span><span class="no">true</span><span class="p">)}</span>
<span class="n">greetResults</span> <span class="o">:=</span> <span class="n">reflect</span><span class="o">.</span><span class="n">ValueOf</span><span class="p">(</span><span class="n">greet</span><span class="p">)</span><span class="o">.</span><span class="n">Call</span><span class="p">(</span><span class="n">greetArgs</span><span class="p">)</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  greet(</span><span class="se">\"</span><span class="s">Go</span><span class="se">\"</span><span class="s">, true) = %q</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">greetResults</span><span class="p">[</span><span class="m">0</span><span class="p">]</span><span class="o">.</span><span class="n">Interface</span><span class="p">())</span> <span class="c">// "Hello, Go!"</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">NumIn()</code> and <code class="language-plaintext highlighter-rouge">NumOut()</code> tell you how many parameters and return values the function has. <code class="language-plaintext highlighter-rouge">In(i)</code> gives the type of each parameter. <code class="language-plaintext highlighter-rouge">Call</code> takes a <code class="language-plaintext highlighter-rouge">[]reflect.Value</code> and returns a <code class="language-plaintext highlighter-rouge">[]reflect.Value</code>. Each argument must match the expected type exactly — passing a <code class="language-plaintext highlighter-rouge">float64</code> where an <code class="language-plaintext highlighter-rouge">int</code> is expected will panic. This is the mechanism that RPC frameworks and dependency injection containers use to call functions they only know about at runtime.</p>

<h3 id="practical-example-printtable">Practical Example: PrintTable</h3>

<p>To see reflection applied to a real problem, here is <code class="language-plaintext highlighter-rouge">PrintTable</code> — a function that takes any slice of structs and prints it as a formatted table. It does not know the struct type at compile time; it discovers the field names and values at runtime:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">PrintTable</span><span class="p">(</span><span class="n">slice</span> <span class="k">interface</span><span class="p">{})</span> <span class="p">{</span>
	<span class="n">v</span> <span class="o">:=</span> <span class="n">reflect</span><span class="o">.</span><span class="n">ValueOf</span><span class="p">(</span><span class="n">slice</span><span class="p">)</span>
	<span class="k">if</span> <span class="n">v</span><span class="o">.</span><span class="n">Kind</span><span class="p">()</span> <span class="o">!=</span> <span class="n">reflect</span><span class="o">.</span><span class="n">Slice</span> <span class="p">{</span>
		<span class="k">return</span>
	<span class="p">}</span>
	<span class="k">if</span> <span class="n">v</span><span class="o">.</span><span class="n">Len</span><span class="p">()</span> <span class="o">==</span> <span class="m">0</span> <span class="p">{</span>
		<span class="k">return</span>
	<span class="p">}</span>

	<span class="n">elemType</span> <span class="o">:=</span> <span class="n">v</span><span class="o">.</span><span class="n">Type</span><span class="p">()</span><span class="o">.</span><span class="n">Elem</span><span class="p">()</span>
	<span class="k">if</span> <span class="n">elemType</span><span class="o">.</span><span class="n">Kind</span><span class="p">()</span> <span class="o">!=</span> <span class="n">reflect</span><span class="o">.</span><span class="n">Struct</span> <span class="p">{</span>
		<span class="k">return</span>
	<span class="p">}</span>

	<span class="n">numFields</span> <span class="o">:=</span> <span class="n">elemType</span><span class="o">.</span><span class="n">NumField</span><span class="p">()</span>

	<span class="c">// Collect headers from field names</span>
	<span class="n">headers</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="kt">string</span><span class="p">,</span> <span class="n">numFields</span><span class="p">)</span>
	<span class="k">for</span> <span class="n">i</span> <span class="o">:=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">numFields</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span> <span class="p">{</span>
		<span class="n">headers</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">elemType</span><span class="o">.</span><span class="n">Field</span><span class="p">(</span><span class="n">i</span><span class="p">)</span><span class="o">.</span><span class="n">Name</span>
	<span class="p">}</span>

	<span class="c">// Collect row values as strings</span>
	<span class="n">rows</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([][]</span><span class="kt">string</span><span class="p">,</span> <span class="n">v</span><span class="o">.</span><span class="n">Len</span><span class="p">())</span>
	<span class="k">for</span> <span class="n">i</span> <span class="o">:=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">v</span><span class="o">.</span><span class="n">Len</span><span class="p">();</span> <span class="n">i</span><span class="o">++</span> <span class="p">{</span>
		<span class="n">row</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="kt">string</span><span class="p">,</span> <span class="n">numFields</span><span class="p">)</span>
		<span class="k">for</span> <span class="n">j</span> <span class="o">:=</span> <span class="m">0</span><span class="p">;</span> <span class="n">j</span> <span class="o">&lt;</span> <span class="n">numFields</span><span class="p">;</span> <span class="n">j</span><span class="o">++</span> <span class="p">{</span>
			<span class="n">field</span> <span class="o">:=</span> <span class="n">v</span><span class="o">.</span><span class="n">Index</span><span class="p">(</span><span class="n">i</span><span class="p">)</span><span class="o">.</span><span class="n">Field</span><span class="p">(</span><span class="n">j</span><span class="p">)</span>
			<span class="k">if</span> <span class="n">field</span><span class="o">.</span><span class="n">CanInterface</span><span class="p">()</span> <span class="p">{</span>
				<span class="n">row</span><span class="p">[</span><span class="n">j</span><span class="p">]</span> <span class="o">=</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"%v"</span><span class="p">,</span> <span class="n">field</span><span class="o">.</span><span class="n">Interface</span><span class="p">())</span>
			<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
				<span class="n">row</span><span class="p">[</span><span class="n">j</span><span class="p">]</span> <span class="o">=</span> <span class="s">"(unexported)"</span>
			<span class="p">}</span>
		<span class="p">}</span>
		<span class="n">rows</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span>
	<span class="p">}</span>

	<span class="c">// Compute column widths</span>
	<span class="n">widths</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="kt">int</span><span class="p">,</span> <span class="n">numFields</span><span class="p">)</span>
	<span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">h</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">headers</span> <span class="p">{</span>
		<span class="n">widths</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">h</span><span class="p">)</span>
	<span class="p">}</span>
	<span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">row</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">rows</span> <span class="p">{</span>
		<span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">cell</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">row</span> <span class="p">{</span>
			<span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">cell</span><span class="p">)</span> <span class="o">&gt;</span> <span class="n">widths</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="p">{</span>
				<span class="n">widths</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">cell</span><span class="p">)</span>
			<span class="p">}</span>
		<span class="p">}</span>
	<span class="p">}</span>

	<span class="c">// Print header</span>
	<span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">h</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">headers</span> <span class="p">{</span>
		<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  %-*s"</span><span class="p">,</span> <span class="n">widths</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">h</span><span class="p">)</span>
		<span class="k">if</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">numFields</span><span class="o">-</span><span class="m">1</span> <span class="p">{</span>
			<span class="n">fmt</span><span class="o">.</span><span class="n">Print</span><span class="p">(</span><span class="s">" | "</span><span class="p">)</span>
		<span class="p">}</span>
	<span class="p">}</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">()</span>

	<span class="c">// Print separator</span>
	<span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">w</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">widths</span> <span class="p">{</span>
		<span class="n">fmt</span><span class="o">.</span><span class="n">Print</span><span class="p">(</span><span class="s">"  "</span> <span class="o">+</span> <span class="n">strings</span><span class="o">.</span><span class="n">Repeat</span><span class="p">(</span><span class="s">"-"</span><span class="p">,</span> <span class="n">w</span><span class="p">))</span>
		<span class="k">if</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">numFields</span><span class="o">-</span><span class="m">1</span> <span class="p">{</span>
			<span class="n">fmt</span><span class="o">.</span><span class="n">Print</span><span class="p">(</span><span class="s">"-+-"</span><span class="p">)</span>
		<span class="p">}</span>
	<span class="p">}</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">()</span>

	<span class="c">// Print rows</span>
	<span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">row</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">rows</span> <span class="p">{</span>
		<span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">cell</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">row</span> <span class="p">{</span>
			<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  %-*s"</span><span class="p">,</span> <span class="n">widths</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">cell</span><span class="p">)</span>
			<span class="k">if</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">numFields</span><span class="o">-</span><span class="m">1</span> <span class="p">{</span>
				<span class="n">fmt</span><span class="o">.</span><span class="n">Print</span><span class="p">(</span><span class="s">" | "</span><span class="p">)</span>
			<span class="p">}</span>
		<span class="p">}</span>
		<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">()</span>
	<span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The function starts by validating that the input is a non-empty slice of structs. It reads the element type with <code class="language-plaintext highlighter-rouge">v.Type().Elem()</code> to discover the struct’s fields. Headers come from the field names, row values from formatting each field with <code class="language-plaintext highlighter-rouge">fmt.Sprintf("%v", field.Interface())</code>. The column width calculation passes over headers and all rows to find the widest string in each column, then uses <code class="language-plaintext highlighter-rouge">%-*s</code> format padding to align everything.</p>

<p>You can call it with any struct type:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre></td><td class="rouge-code"><pre><span class="k">type</span> <span class="n">Book</span> <span class="k">struct</span> <span class="p">{</span>
	<span class="n">Title</span>  <span class="kt">string</span>
	<span class="n">Author</span> <span class="kt">string</span>
	<span class="n">Pages</span>  <span class="kt">int</span>
<span class="p">}</span>

<span class="n">books</span> <span class="o">:=</span> <span class="p">[]</span><span class="n">Book</span><span class="p">{</span>
	<span class="p">{</span><span class="s">"The Go Programming Language"</span><span class="p">,</span> <span class="s">"Donovan &amp; Kernighan"</span><span class="p">,</span> <span class="m">380</span><span class="p">},</span>
	<span class="p">{</span><span class="s">"Concurrency in Go"</span><span class="p">,</span> <span class="s">"Katherine Cox-Buday"</span><span class="p">,</span> <span class="m">238</span><span class="p">},</span>
	<span class="p">{</span><span class="s">"Go in Action"</span><span class="p">,</span> <span class="s">"Kennedy, Ketelsen, St. Martin"</span><span class="p">,</span> <span class="m">264</span><span class="p">},</span>
<span class="p">}</span>

<span class="n">PrintTable</span><span class="p">(</span><span class="n">books</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>This prints a neatly aligned table with <code class="language-plaintext highlighter-rouge">Title</code>, <code class="language-plaintext highlighter-rouge">Author</code>, and <code class="language-plaintext highlighter-rouge">Pages</code> columns — without <code class="language-plaintext highlighter-rouge">PrintTable</code> knowing anything about the <code class="language-plaintext highlighter-rouge">Book</code> type at compile time. This is the same approach that <code class="language-plaintext highlighter-rouge">encoding/json</code> uses internally: it inspects struct fields via reflection to decide what to encode, reads struct tags to determine JSON key names, and handles exported and unexported fields differently. The difference is that <code class="language-plaintext highlighter-rouge">encoding/json</code> does this at scale with caching and optimization, but the underlying mechanism is the same <code class="language-plaintext highlighter-rouge">reflect</code> API shown here.</p>]]></content><author><name>kimserey</name></author><category term="go" /><summary type="html"><![CDATA[The previous tutorials covered Go’s type system, concurrency primitives, and error handling. This one shifts to production patterns you need when building real services: serving HTTP requests, managing process lifecycle with signal handling, running database migrations with embedded files, and using reflection for metaprogramming. Each topic stands on its own, but together they represent the kind of infrastructure code that appears in nearly every Go service.]]></summary></entry><entry><title type="html">Go Standard Library — Packages, IO, JSON and Testing</title><link href="https://www.kimsereylam.com/go/2026/09/09/go-standard-library-packages-io-json-and-testing.html" rel="alternate" type="text/html" title="Go Standard Library — Packages, IO, JSON and Testing" /><published>2026-09-09T00:00:00-05:00</published><updated>2026-09-09T00:00:00-05:00</updated><id>https://www.kimsereylam.com/go/2026/09/09/go-standard-library-packages-io-json-and-testing</id><content type="html" xml:base="https://www.kimsereylam.com/go/2026/09/09/go-standard-library-packages-io-json-and-testing.html"><![CDATA[<p>This post covers four day-to-day essentials for writing Go programs: organizing code into packages and modules, streaming data through the <code class="language-plaintext highlighter-rouge">io.Reader</code> and <code class="language-plaintext highlighter-rouge">io.Writer</code> interfaces, serializing and deserializing JSON, and writing tests. These are the building blocks you will reach for in nearly every Go project — packages keep code organized, the I/O interfaces let you compose data pipelines, JSON handles serialization, and the testing package gives you a lightweight framework for verifying everything works.</p>

<!--more-->

<h2 id="packages-and-modules">Packages and Modules</h2>

<p>Go code is organized into packages. Every <code class="language-plaintext highlighter-rouge">.go</code> file starts with a <code class="language-plaintext highlighter-rouge">package</code> declaration, and all files in the same directory must use the same package name. A module is a collection of packages versioned together, defined by a <code class="language-plaintext highlighter-rouge">go.mod</code> file at the root. When you import a package, Go resolves it through the module system.</p>

<h3 id="importing-packages">Importing Packages</h3>

<p>Go’s standard library provides a rich set of packages out of the box. You import them by path, and each import gives you access to the package’s exported identifiers. Here is a program that uses several standard library packages to demonstrate the breadth of what is available without any third-party dependencies:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
</pre></td><td class="rouge-code"><pre><span class="k">package</span> <span class="n">main</span>

<span class="k">import</span> <span class="p">(</span>
    <span class="s">"fmt"</span>
    <span class="s">"math"</span>
    <span class="s">"sort"</span>
    <span class="s">"strconv"</span>
    <span class="s">"strings"</span>
<span class="p">)</span>

<span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
    <span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"=== Standard Library Packages ==="</span><span class="p">)</span>

    <span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="n">strings</span><span class="o">.</span><span class="n">ToUpper</span><span class="p">(</span><span class="s">"hello"</span><span class="p">))</span>
    <span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="n">strings</span><span class="o">.</span><span class="n">Contains</span><span class="p">(</span><span class="s">"go is great"</span><span class="p">,</span> <span class="s">"great"</span><span class="p">))</span>
    <span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="n">strings</span><span class="o">.</span><span class="n">Join</span><span class="p">([]</span><span class="kt">string</span><span class="p">{</span><span class="s">"a"</span><span class="p">,</span> <span class="s">"b"</span><span class="p">,</span> <span class="s">"c"</span><span class="p">},</span> <span class="s">"-"</span><span class="p">))</span>

    <span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="n">math</span><span class="o">.</span><span class="n">Sqrt</span><span class="p">(</span><span class="m">144</span><span class="p">))</span>
    <span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="n">math</span><span class="o">.</span><span class="n">Pi</span><span class="p">)</span>

    <span class="n">nums</span> <span class="o">:=</span> <span class="p">[]</span><span class="kt">int</span><span class="p">{</span><span class="m">5</span><span class="p">,</span> <span class="m">2</span><span class="p">,</span> <span class="m">8</span><span class="p">,</span> <span class="m">1</span><span class="p">,</span> <span class="m">9</span><span class="p">}</span>
    <span class="n">sort</span><span class="o">.</span><span class="n">Ints</span><span class="p">(</span><span class="n">nums</span><span class="p">)</span>
    <span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="n">nums</span><span class="p">)</span>

    <span class="n">n</span><span class="p">,</span> <span class="n">_</span> <span class="o">:=</span> <span class="n">strconv</span><span class="o">.</span><span class="n">Atoi</span><span class="p">(</span><span class="s">"42"</span><span class="p">)</span>
    <span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"converted string to int: %d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">n</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Each import path corresponds to a directory in the standard library or in your module. The <code class="language-plaintext highlighter-rouge">strings</code> package provides string manipulation, <code class="language-plaintext highlighter-rouge">math</code> provides mathematical constants and functions, <code class="language-plaintext highlighter-rouge">sort</code> provides sorting algorithms for slices, and <code class="language-plaintext highlighter-rouge">strconv</code> handles conversions between strings and other types. You only pay for what you import — the Go compiler rejects unused imports.</p>

<h3 id="exported-vs-unexported">Exported vs Unexported</h3>

<p>Go uses a simple naming convention for visibility. Any identifier (variable, function, type, field) that starts with an uppercase letter is exported — accessible from other packages. Anything starting with a lowercase letter is unexported — visible only within its own package.</p>

<p>Consider a <code class="language-plaintext highlighter-rouge">calculator</code> package:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
</pre></td><td class="rouge-code"><pre><span class="k">package</span> <span class="n">calculator</span>

<span class="k">import</span> <span class="p">(</span>
    <span class="s">"errors"</span>
    <span class="s">"fmt"</span>
<span class="p">)</span>

<span class="k">var</span> <span class="n">Version</span> <span class="o">=</span> <span class="s">"1.0.0"</span>       <span class="c">// exported — other packages can read this</span>
<span class="k">var</span> <span class="n">maxValue</span> <span class="o">=</span> <span class="m">1</span><span class="n">_000_000</span>    <span class="c">// unexported — only visible inside calculator</span>

<span class="k">type</span> <span class="n">Result</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">Value</span>   <span class="kt">int</span>       <span class="c">// exported field</span>
    <span class="n">Label</span>   <span class="kt">string</span>    <span class="c">// exported field</span>
    <span class="n">logNote</span> <span class="kt">string</span>    <span class="c">// unexported field — hidden from other packages</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">Add</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span> <span class="kt">int</span><span class="p">)</span> <span class="kt">int</span>      <span class="p">{</span> <span class="k">return</span> <span class="n">a</span> <span class="o">+</span> <span class="n">b</span> <span class="p">}</span>       <span class="c">// exported</span>
<span class="k">func</span> <span class="n">Multiply</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span> <span class="kt">int</span><span class="p">)</span> <span class="kt">int</span> <span class="p">{</span> <span class="k">return</span> <span class="n">a</span> <span class="o">*</span> <span class="n">b</span> <span class="p">}</span>        <span class="c">// exported</span>

<span class="k">func</span> <span class="n">validate</span><span class="p">(</span><span class="n">n</span> <span class="kt">int</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>                         <span class="c">// unexported</span>
    <span class="k">if</span> <span class="n">n</span> <span class="o">&lt;</span> <span class="m">0</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">errors</span><span class="o">.</span><span class="n">New</span><span class="p">(</span><span class="s">"negative numbers not allowed"</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="k">if</span> <span class="n">n</span> <span class="o">&gt;</span> <span class="n">maxValue</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"value %d exceeds max %d"</span><span class="p">,</span> <span class="n">n</span><span class="p">,</span> <span class="n">maxValue</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="no">nil</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">SafeAdd</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span> <span class="kt">int</span><span class="p">)</span> <span class="p">(</span><span class="kt">int</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">validate</span><span class="p">(</span><span class="n">a</span><span class="p">);</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
        <span class="k">return</span> <span class="m">0</span><span class="p">,</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"invalid first argument: %w"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="k">if</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">validate</span><span class="p">(</span><span class="n">b</span><span class="p">);</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
        <span class="k">return</span> <span class="m">0</span><span class="p">,</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"invalid second argument: %w"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="n">a</span> <span class="o">+</span> <span class="n">b</span><span class="p">,</span> <span class="no">nil</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>From another package, you can call <code class="language-plaintext highlighter-rouge">calculator.Add</code>, <code class="language-plaintext highlighter-rouge">calculator.SafeAdd</code>, and read <code class="language-plaintext highlighter-rouge">calculator.Version</code>. You cannot call <code class="language-plaintext highlighter-rouge">calculator.validate</code> or access <code class="language-plaintext highlighter-rouge">calculator.maxValue</code> — the compiler will reject it. Likewise, if you create a <code class="language-plaintext highlighter-rouge">calculator.Result</code> from outside the package, you can set <code class="language-plaintext highlighter-rouge">Value</code> and <code class="language-plaintext highlighter-rouge">Label</code> but not <code class="language-plaintext highlighter-rouge">logNote</code>. This visibility rule is Go’s entire encapsulation mechanism — there are no <code class="language-plaintext highlighter-rouge">public</code>/<code class="language-plaintext highlighter-rouge">private</code> keywords.</p>

<p>A constructor function is the idiomatic way to initialize structs with unexported fields:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">NewResult</span><span class="p">(</span><span class="n">value</span> <span class="kt">int</span><span class="p">,</span> <span class="n">label</span> <span class="kt">string</span><span class="p">)</span> <span class="n">Result</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">Result</span><span class="p">{</span>
        <span class="n">Value</span><span class="o">:</span>   <span class="n">value</span><span class="p">,</span>
        <span class="n">Label</span><span class="o">:</span>   <span class="n">label</span><span class="p">,</span>
        <span class="n">logNote</span><span class="o">:</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"computed %s = %d"</span><span class="p">,</span> <span class="n">label</span><span class="p">,</span> <span class="n">value</span><span class="p">),</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="k">func</span> <span class="p">(</span><span class="n">r</span> <span class="n">Result</span><span class="p">)</span> <span class="n">String</span><span class="p">()</span> <span class="kt">string</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"Result{Value: %d, Label: %q, logNote: %q}"</span><span class="p">,</span>
        <span class="n">r</span><span class="o">.</span><span class="n">Value</span><span class="p">,</span> <span class="n">r</span><span class="o">.</span><span class="n">Label</span><span class="p">,</span> <span class="n">r</span><span class="o">.</span><span class="n">logNote</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Calling <code class="language-plaintext highlighter-rouge">calculator.NewResult(42, "sum")</code> returns a <code class="language-plaintext highlighter-rouge">Result</code> with all fields properly set, including the unexported <code class="language-plaintext highlighter-rouge">logNote</code>. The <code class="language-plaintext highlighter-rouge">String()</code> method can read <code class="language-plaintext highlighter-rouge">logNote</code> because it is defined within the same package.</p>

<h3 id="the-init-function">The init() Function</h3>

<p>Each package can define one or more <code class="language-plaintext highlighter-rouge">init()</code> functions. These run automatically when the package is loaded — after all package-level variables are initialized, but before <code class="language-plaintext highlighter-rouge">main()</code> starts. You do not call <code class="language-plaintext highlighter-rouge">init()</code> yourself; Go handles the timing.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="k">package</span> <span class="n">calculator</span>

<span class="k">func</span> <span class="n">init</span><span class="p">()</span> <span class="p">{</span>
    <span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"  [calculator.init] calculator package initialized!"</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>When any other package imports <code class="language-plaintext highlighter-rouge">calculator</code>, Go runs this <code class="language-plaintext highlighter-rouge">init()</code> function as a side effect. The order follows the import dependency graph: if package A imports package B, then B’s <code class="language-plaintext highlighter-rouge">init()</code> runs before A’s. Within a single package, <code class="language-plaintext highlighter-rouge">init()</code> functions run in the order they appear in source files (sorted by filename).</p>

<p>The <code class="language-plaintext highlighter-rouge">init()</code> function is useful for registering drivers, validating configuration, or setting up package-level state. But use it sparingly — implicit initialization can make code harder to reason about.</p>

<h3 id="package-organization">Package Organization</h3>

<p>As a project grows, you split code into multiple packages. A common pattern is a registry package that other packages register with during <code class="language-plaintext highlighter-rouge">init()</code>. This decouples the registry from knowing about specific implementations.</p>

<p>Here is a <code class="language-plaintext highlighter-rouge">registry</code> package that stores encoders by name:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
</pre></td><td class="rouge-code"><pre><span class="k">package</span> <span class="n">registry</span>

<span class="k">import</span> <span class="s">"fmt"</span>

<span class="k">var</span> <span class="n">drivers</span> <span class="o">=</span> <span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="n">Encoder</span><span class="p">{}</span>

<span class="k">type</span> <span class="n">Encoder</span> <span class="k">interface</span> <span class="p">{</span>
    <span class="n">Encode</span><span class="p">(</span><span class="n">v</span> <span class="n">any</span><span class="p">)</span> <span class="p">([]</span><span class="kt">byte</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">Register</span><span class="p">(</span><span class="n">name</span> <span class="kt">string</span><span class="p">,</span> <span class="n">enc</span> <span class="n">Encoder</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">drivers</span><span class="p">[</span><span class="n">name</span><span class="p">]</span> <span class="o">=</span> <span class="n">enc</span>
    <span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"    [registry] registered %q driver</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">name</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">List</span><span class="p">()</span> <span class="p">[]</span><span class="kt">string</span> <span class="p">{</span>
    <span class="n">names</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="kt">string</span><span class="p">,</span> <span class="m">0</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">drivers</span><span class="p">))</span>
    <span class="k">for</span> <span class="n">name</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">drivers</span> <span class="p">{</span>
        <span class="n">names</span> <span class="o">=</span> <span class="nb">append</span><span class="p">(</span><span class="n">names</span><span class="p">,</span> <span class="n">name</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="n">names</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">Get</span><span class="p">(</span><span class="n">name</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="n">Encoder</span><span class="p">,</span> <span class="kt">bool</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">enc</span><span class="p">,</span> <span class="n">ok</span> <span class="o">:=</span> <span class="n">drivers</span><span class="p">[</span><span class="n">name</span><span class="p">]</span>
    <span class="k">return</span> <span class="n">enc</span><span class="p">,</span> <span class="n">ok</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Individual driver packages implement the <code class="language-plaintext highlighter-rouge">Encoder</code> interface and register themselves via <code class="language-plaintext highlighter-rouge">init()</code>:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="rouge-code"><pre><span class="k">package</span> <span class="n">json</span>

<span class="k">import</span> <span class="p">(</span>
    <span class="s">"encoding/json"</span>
    <span class="s">"go-learn/ex15_packages/registry"</span>
<span class="p">)</span>

<span class="k">type</span> <span class="n">encoder</span> <span class="k">struct</span><span class="p">{}</span>

<span class="k">func</span> <span class="p">(</span><span class="n">encoder</span><span class="p">)</span> <span class="n">Encode</span><span class="p">(</span><span class="n">v</span> <span class="n">any</span><span class="p">)</span> <span class="p">([]</span><span class="kt">byte</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">json</span><span class="o">.</span><span class="n">Marshal</span><span class="p">(</span><span class="n">v</span><span class="p">)</span> <span class="p">}</span>

<span class="k">func</span> <span class="n">init</span><span class="p">()</span> <span class="p">{</span> <span class="n">registry</span><span class="o">.</span><span class="n">Register</span><span class="p">(</span><span class="s">"json"</span><span class="p">,</span> <span class="n">encoder</span><span class="p">{})</span> <span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="rouge-code"><pre><span class="k">package</span> <span class="n">xml</span>

<span class="k">import</span> <span class="p">(</span>
    <span class="s">"encoding/xml"</span>
    <span class="s">"go-learn/ex15_packages/registry"</span>
<span class="p">)</span>

<span class="k">type</span> <span class="n">encoder</span> <span class="k">struct</span><span class="p">{}</span>

<span class="k">func</span> <span class="p">(</span><span class="n">encoder</span><span class="p">)</span> <span class="n">Encode</span><span class="p">(</span><span class="n">v</span> <span class="n">any</span><span class="p">)</span> <span class="p">([]</span><span class="kt">byte</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">xml</span><span class="o">.</span><span class="n">Marshal</span><span class="p">(</span><span class="n">v</span><span class="p">)</span> <span class="p">}</span>

<span class="k">func</span> <span class="n">init</span><span class="p">()</span> <span class="p">{</span> <span class="n">registry</span><span class="o">.</span><span class="n">Register</span><span class="p">(</span><span class="s">"xml"</span><span class="p">,</span> <span class="n">encoder</span><span class="p">{})</span> <span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Each driver package has zero coupling to the other drivers. The registry does not import the driver packages. The only thing that ties them together is the <code class="language-plaintext highlighter-rouge">Encoder</code> interface.</p>

<h3 id="blank-imports">Blank Imports</h3>

<p>The driver packages above register themselves through <code class="language-plaintext highlighter-rouge">init()</code>, but nothing in the main program directly calls any function from them. Normally, Go would reject an unused import. The blank import syntax — <code class="language-plaintext highlighter-rouge">_ "package/path"</code> — tells the compiler “import this package for its side effects only”:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
</pre></td><td class="rouge-code"><pre><span class="k">package</span> <span class="n">main</span>

<span class="k">import</span> <span class="p">(</span>
    <span class="s">"fmt"</span>
    <span class="s">"go-learn/ex15_packages/calculator"</span>
    <span class="s">"go-learn/ex15_packages/registry"</span>

    <span class="n">_</span> <span class="s">"go-learn/ex15_packages/drivers/json"</span>
    <span class="n">_</span> <span class="s">"go-learn/ex15_packages/drivers/xml"</span>
<span class="p">)</span>

<span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
    <span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"Version:"</span><span class="p">,</span> <span class="n">calculator</span><span class="o">.</span><span class="n">Version</span><span class="p">)</span>
    <span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"2 + 3 ="</span><span class="p">,</span> <span class="n">calculator</span><span class="o">.</span><span class="n">Add</span><span class="p">(</span><span class="m">2</span><span class="p">,</span> <span class="m">3</span><span class="p">))</span>

    <span class="n">result</span> <span class="o">:=</span> <span class="n">calculator</span><span class="o">.</span><span class="n">NewResult</span><span class="p">(</span><span class="m">42</span><span class="p">,</span> <span class="s">"answer"</span><span class="p">)</span>
    <span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="n">result</span><span class="p">)</span>

    <span class="n">drivers</span> <span class="o">:=</span> <span class="n">registry</span><span class="o">.</span><span class="n">List</span><span class="p">()</span>
    <span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"registered drivers:"</span><span class="p">,</span> <span class="n">drivers</span><span class="p">)</span>

    <span class="k">if</span> <span class="n">enc</span><span class="p">,</span> <span class="n">ok</span> <span class="o">:=</span> <span class="n">registry</span><span class="o">.</span><span class="n">Get</span><span class="p">(</span><span class="s">"json"</span><span class="p">);</span> <span class="n">ok</span> <span class="p">{</span>
        <span class="n">data</span><span class="p">,</span> <span class="n">_</span> <span class="o">:=</span> <span class="n">enc</span><span class="o">.</span><span class="n">Encode</span><span class="p">(</span><span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">int</span><span class="p">{</span><span class="s">"x"</span><span class="o">:</span> <span class="m">1</span><span class="p">,</span> <span class="s">"y"</span><span class="o">:</span> <span class="m">2</span><span class="p">})</span>
        <span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"json output:"</span><span class="p">,</span> <span class="kt">string</span><span class="p">(</span><span class="n">data</span><span class="p">))</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>When Go loads this program, the import graph triggers <code class="language-plaintext highlighter-rouge">init()</code> functions in dependency order: first the <code class="language-plaintext highlighter-rouge">registry</code> package, then the <code class="language-plaintext highlighter-rouge">json</code> and <code class="language-plaintext highlighter-rouge">xml</code> driver packages (which call <code class="language-plaintext highlighter-rouge">registry.Register</code>), then the <code class="language-plaintext highlighter-rouge">calculator</code> package, and finally <code class="language-plaintext highlighter-rouge">main</code>. By the time <code class="language-plaintext highlighter-rouge">main()</code> runs, both drivers are registered and ready to use.</p>

<p>This pattern — a registry interface plus blank-imported driver packages — is used throughout Go’s standard library. The <code class="language-plaintext highlighter-rouge">database/sql</code> package works exactly this way: you import a driver like <code class="language-plaintext highlighter-rouge">_ "github.com/lib/pq"</code> and the driver registers itself with <code class="language-plaintext highlighter-rouge">sql.Register</code> during <code class="language-plaintext highlighter-rouge">init()</code>. The <code class="language-plaintext highlighter-rouge">image</code> package uses the same pattern for image format decoders.</p>

<h2 id="ioreader-and-iowriter">io.Reader and io.Writer</h2>

<p>Go’s <code class="language-plaintext highlighter-rouge">io</code> package defines two small interfaces that underpin almost all I/O in the language. <code class="language-plaintext highlighter-rouge">io.Reader</code> reads bytes from a source; <code class="language-plaintext highlighter-rouge">io.Writer</code> writes bytes to a destination. Because they are interfaces, any type that implements the right method signature qualifies — files, network connections, in-memory buffers, compressors, encryptors, and anything else you can imagine. This composability is what makes Go’s I/O system so powerful.</p>

<h3 id="the-reader-interface">The Reader Interface</h3>

<p><code class="language-plaintext highlighter-rouge">io.Reader</code> has a single method:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="k">type</span> <span class="n">Reader</span> <span class="k">interface</span> <span class="p">{</span>
    <span class="n">Read</span><span class="p">(</span><span class="n">p</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">)</span> <span class="p">(</span><span class="n">n</span> <span class="kt">int</span><span class="p">,</span> <span class="n">err</span> <span class="kt">error</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The caller provides a byte slice <code class="language-plaintext highlighter-rouge">p</code>, and the reader fills it with up to <code class="language-plaintext highlighter-rouge">len(p)</code> bytes. It returns the number of bytes read and an error. When the data source is exhausted, it returns <code class="language-plaintext highlighter-rouge">io.EOF</code>. The key insight is that <code class="language-plaintext highlighter-rouge">Read</code> may return fewer bytes than requested — you typically call it in a loop.</p>

<p>Here is how to read from a <code class="language-plaintext highlighter-rouge">strings.Reader</code> in small chunks:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
</pre></td><td class="rouge-code"><pre><span class="n">reader</span> <span class="o">:=</span> <span class="n">strings</span><span class="o">.</span><span class="n">NewReader</span><span class="p">(</span><span class="s">"Hello, Go io.Reader!"</span><span class="p">)</span>
<span class="n">buf</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="kt">byte</span><span class="p">,</span> <span class="m">5</span><span class="p">)</span>

<span class="k">for</span> <span class="p">{</span>
    <span class="n">n</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">reader</span><span class="o">.</span><span class="n">Read</span><span class="p">(</span><span class="n">buf</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">n</span> <span class="o">&gt;</span> <span class="m">0</span> <span class="p">{</span>
        <span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  read %d bytes: %q</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">n</span><span class="p">,</span> <span class="n">buf</span><span class="p">[</span><span class="o">:</span><span class="n">n</span><span class="p">])</span>
    <span class="p">}</span>
    <span class="k">if</span> <span class="n">err</span> <span class="o">==</span> <span class="n">io</span><span class="o">.</span><span class="n">EOF</span> <span class="p">{</span>
        <span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"  reached EOF"</span><span class="p">)</span>
        <span class="k">break</span>
    <span class="p">}</span>
    <span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
        <span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"  error:"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
        <span class="k">break</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The 5-byte buffer means each <code class="language-plaintext highlighter-rouge">Read</code> call gets at most 5 bytes. The string “Hello, Go io.Reader!” is 20 bytes, so this loop will make four full reads and one final read that returns <code class="language-plaintext highlighter-rouge">io.EOF</code>. Notice that we check <code class="language-plaintext highlighter-rouge">n &gt; 0</code> before checking <code class="language-plaintext highlighter-rouge">err</code> — a <code class="language-plaintext highlighter-rouge">Read</code> can return both data and <code class="language-plaintext highlighter-rouge">io.EOF</code> on the same call.</p>

<h3 id="the-writer-interface">The Writer Interface</h3>

<p><code class="language-plaintext highlighter-rouge">io.Writer</code> is the mirror of <code class="language-plaintext highlighter-rouge">io.Reader</code>:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="k">type</span> <span class="n">Writer</span> <span class="k">interface</span> <span class="p">{</span>
    <span class="n">Write</span><span class="p">(</span><span class="n">p</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">)</span> <span class="p">(</span><span class="n">n</span> <span class="kt">int</span><span class="p">,</span> <span class="n">err</span> <span class="kt">error</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>You pass in a byte slice, and the writer consumes it. Two of the most common writers are <code class="language-plaintext highlighter-rouge">os.Stdout</code> (standard output) and <code class="language-plaintext highlighter-rouge">bytes.Buffer</code> (an in-memory buffer that grows as needed):</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="k">var</span> <span class="n">buf</span> <span class="n">bytes</span><span class="o">.</span><span class="n">Buffer</span>
<span class="n">buf</span><span class="o">.</span><span class="n">Write</span><span class="p">([]</span><span class="kt">byte</span><span class="p">(</span><span class="s">"Hello "</span><span class="p">))</span>
<span class="n">buf</span><span class="o">.</span><span class="n">Write</span><span class="p">([]</span><span class="kt">byte</span><span class="p">(</span><span class="s">"Buffer!"</span><span class="p">))</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"buffer contents:"</span><span class="p">,</span> <span class="n">buf</span><span class="o">.</span><span class="n">String</span><span class="p">())</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"buffer length:"</span><span class="p">,</span> <span class="n">buf</span><span class="o">.</span><span class="n">Len</span><span class="p">())</span>

<span class="n">fmt</span><span class="o">.</span><span class="n">Fprintln</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">Stdout</span><span class="p">,</span> <span class="s">"this goes to stdout via io.Writer"</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">bytes.Buffer</code> implements <code class="language-plaintext highlighter-rouge">io.Writer</code>, so you can pass it anywhere a writer is expected. <code class="language-plaintext highlighter-rouge">fmt.Fprintln</code> takes any <code class="language-plaintext highlighter-rouge">io.Writer</code> as its first argument — passing <code class="language-plaintext highlighter-rouge">os.Stdout</code> writes to the terminal, but you could just as easily pass a file, a network connection, or a buffer.</p>

<h3 id="iocopy">io.Copy</h3>

<p><code class="language-plaintext highlighter-rouge">io.Copy</code> connects a reader to a writer. It reads from the source and writes to the destination until it hits <code class="language-plaintext highlighter-rouge">io.EOF</code> or an error:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="n">src</span> <span class="o">:=</span> <span class="n">strings</span><span class="o">.</span><span class="n">NewReader</span><span class="p">(</span><span class="s">"streaming data from reader to writer"</span><span class="p">)</span>
<span class="n">dst</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="n">bytes</span><span class="o">.</span><span class="n">Buffer</span><span class="p">{}</span>
<span class="n">n</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">io</span><span class="o">.</span><span class="n">Copy</span><span class="p">(</span><span class="n">dst</span><span class="p">,</span> <span class="n">src</span><span class="p">)</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"copied %d bytes: %q</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">n</span><span class="p">,</span> <span class="n">dst</span><span class="o">.</span><span class="n">String</span><span class="p">())</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>There is also <code class="language-plaintext highlighter-rouge">io.CopyN</code> for copying a specific number of bytes:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="n">src2</span> <span class="o">:=</span> <span class="n">strings</span><span class="o">.</span><span class="n">NewReader</span><span class="p">(</span><span class="s">"only first 10 bytes please"</span><span class="p">)</span>
<span class="n">dst2</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="n">bytes</span><span class="o">.</span><span class="n">Buffer</span><span class="p">{}</span>
<span class="n">n2</span><span class="p">,</span> <span class="n">_</span> <span class="o">:=</span> <span class="n">io</span><span class="o">.</span><span class="n">CopyN</span><span class="p">(</span><span class="n">dst2</span><span class="p">,</span> <span class="n">src2</span><span class="p">,</span> <span class="m">10</span><span class="p">)</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"copied %d bytes: %q</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">n2</span><span class="p">,</span> <span class="n">dst2</span><span class="o">.</span><span class="n">String</span><span class="p">())</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">io.Copy</code> is the idiomatic way to transfer data between any reader and writer. It handles the read loop, buffering, and EOF detection for you.</p>

<h3 id="composing-readers-and-writers">Composing Readers and Writers</h3>

<p>The <code class="language-plaintext highlighter-rouge">io</code> package provides several functions that wrap readers and writers to add behavior. This is composition over inheritance — you build complex I/O pipelines by layering simple wrappers.</p>

<p><code class="language-plaintext highlighter-rouge">io.TeeReader</code> creates a reader that writes everything it reads to a writer, like the Unix <code class="language-plaintext highlighter-rouge">tee</code> command:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="n">original</span> <span class="o">:=</span> <span class="n">strings</span><span class="o">.</span><span class="n">NewReader</span><span class="p">(</span><span class="s">"data to tee"</span><span class="p">)</span>
<span class="k">var</span> <span class="n">log</span> <span class="n">bytes</span><span class="o">.</span><span class="n">Buffer</span>
<span class="n">tee</span> <span class="o">:=</span> <span class="n">io</span><span class="o">.</span><span class="n">TeeReader</span><span class="p">(</span><span class="n">original</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">log</span><span class="p">)</span>

<span class="n">result</span><span class="p">,</span> <span class="n">_</span> <span class="o">:=</span> <span class="n">io</span><span class="o">.</span><span class="n">ReadAll</span><span class="p">(</span><span class="n">tee</span><span class="p">)</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"read: %q</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="kt">string</span><span class="p">(</span><span class="n">result</span><span class="p">))</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"log captured: %q</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">log</span><span class="o">.</span><span class="n">String</span><span class="p">())</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">io.MultiReader</code> concatenates multiple readers into one. Reads drain each reader in order:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="n">r1</span> <span class="o">:=</span> <span class="n">strings</span><span class="o">.</span><span class="n">NewReader</span><span class="p">(</span><span class="s">"Hello "</span><span class="p">)</span>
<span class="n">r2</span> <span class="o">:=</span> <span class="n">strings</span><span class="o">.</span><span class="n">NewReader</span><span class="p">(</span><span class="s">"from "</span><span class="p">)</span>
<span class="n">r3</span> <span class="o">:=</span> <span class="n">strings</span><span class="o">.</span><span class="n">NewReader</span><span class="p">(</span><span class="s">"multiple readers!"</span><span class="p">)</span>
<span class="n">multi</span> <span class="o">:=</span> <span class="n">io</span><span class="o">.</span><span class="n">MultiReader</span><span class="p">(</span><span class="n">r1</span><span class="p">,</span> <span class="n">r2</span><span class="p">,</span> <span class="n">r3</span><span class="p">)</span>

<span class="n">combined</span><span class="p">,</span> <span class="n">_</span> <span class="o">:=</span> <span class="n">io</span><span class="o">.</span><span class="n">ReadAll</span><span class="p">(</span><span class="n">multi</span><span class="p">)</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"combined: %q</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="kt">string</span><span class="p">(</span><span class="n">combined</span><span class="p">))</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">io.LimitReader</code> wraps a reader and stops after a fixed number of bytes:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="n">unlimited</span> <span class="o">:=</span> <span class="n">strings</span><span class="o">.</span><span class="n">NewReader</span><span class="p">(</span><span class="s">"this is a very long string that we want to limit"</span><span class="p">)</span>
<span class="n">limited</span> <span class="o">:=</span> <span class="n">io</span><span class="o">.</span><span class="n">LimitReader</span><span class="p">(</span><span class="n">unlimited</span><span class="p">,</span> <span class="m">20</span><span class="p">)</span>
<span class="n">data</span><span class="p">,</span> <span class="n">_</span> <span class="o">:=</span> <span class="n">io</span><span class="o">.</span><span class="n">ReadAll</span><span class="p">(</span><span class="n">limited</span><span class="p">)</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"limited read: %q</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="kt">string</span><span class="p">(</span><span class="n">data</span><span class="p">))</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>These building blocks compose freely. You could create a <code class="language-plaintext highlighter-rouge">TeeReader</code> that wraps a <code class="language-plaintext highlighter-rouge">LimitReader</code> that wraps a <code class="language-plaintext highlighter-rouge">MultiReader</code> — each layer adds one piece of behavior, and the <code class="language-plaintext highlighter-rouge">io.Reader</code> interface is the glue that holds them together.</p>

<h3 id="custom-reader">Custom Reader</h3>

<p>Because <code class="language-plaintext highlighter-rouge">io.Reader</code> is just an interface with one method, you can create your own readers. Here is a <code class="language-plaintext highlighter-rouge">CountingReader</code> that wraps any reader and tracks how many bytes have been read:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="rouge-code"><pre><span class="k">type</span> <span class="n">CountingReader</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">reader</span>    <span class="n">io</span><span class="o">.</span><span class="n">Reader</span>
    <span class="n">BytesRead</span> <span class="kt">int</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">NewCountingReader</span><span class="p">(</span><span class="n">r</span> <span class="n">io</span><span class="o">.</span><span class="n">Reader</span><span class="p">)</span> <span class="o">*</span><span class="n">CountingReader</span> <span class="p">{</span>
    <span class="k">return</span> <span class="o">&amp;</span><span class="n">CountingReader</span><span class="p">{</span><span class="n">reader</span><span class="o">:</span> <span class="n">r</span><span class="p">}</span>
<span class="p">}</span>

<span class="k">func</span> <span class="p">(</span><span class="n">cr</span> <span class="o">*</span><span class="n">CountingReader</span><span class="p">)</span> <span class="n">Read</span><span class="p">(</span><span class="n">p</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">)</span> <span class="p">(</span><span class="kt">int</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">n</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">cr</span><span class="o">.</span><span class="n">reader</span><span class="o">.</span><span class="n">Read</span><span class="p">(</span><span class="n">p</span><span class="p">)</span>
    <span class="n">cr</span><span class="o">.</span><span class="n">BytesRead</span> <span class="o">+=</span> <span class="n">n</span>
    <span class="k">return</span> <span class="n">n</span><span class="p">,</span> <span class="n">err</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The implementation delegates to the wrapped reader and adds up the byte count. Because <code class="language-plaintext highlighter-rouge">CountingReader</code> satisfies <code class="language-plaintext highlighter-rouge">io.Reader</code>, it can be used anywhere a reader is expected:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="n">source</span> <span class="o">:=</span> <span class="n">strings</span><span class="o">.</span><span class="n">NewReader</span><span class="p">(</span><span class="s">"count these bytes"</span><span class="p">)</span>
<span class="n">counter</span> <span class="o">:=</span> <span class="n">NewCountingReader</span><span class="p">(</span><span class="n">source</span><span class="p">)</span>
<span class="n">io</span><span class="o">.</span><span class="n">ReadAll</span><span class="p">(</span><span class="n">counter</span><span class="p">)</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"total bytes read: %d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">counter</span><span class="o">.</span><span class="n">BytesRead</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Here is a more involved example — a <code class="language-plaintext highlighter-rouge">RepeatReader</code> that produces the same text a given number of times:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
</pre></td><td class="rouge-code"><pre><span class="k">type</span> <span class="n">RepeatReader</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">text</span>      <span class="kt">string</span>
    <span class="n">remaining</span> <span class="kt">int</span>
    <span class="n">current</span>   <span class="o">*</span><span class="n">strings</span><span class="o">.</span><span class="n">Reader</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">NewRepeatReader</span><span class="p">(</span><span class="n">text</span> <span class="kt">string</span><span class="p">,</span> <span class="n">times</span> <span class="kt">int</span><span class="p">)</span> <span class="o">*</span><span class="n">RepeatReader</span> <span class="p">{</span>
    <span class="k">return</span> <span class="o">&amp;</span><span class="n">RepeatReader</span><span class="p">{</span><span class="n">text</span><span class="o">:</span> <span class="n">text</span><span class="p">,</span> <span class="n">remaining</span><span class="o">:</span> <span class="n">times</span><span class="p">}</span>
<span class="p">}</span>

<span class="k">func</span> <span class="p">(</span><span class="n">rr</span> <span class="o">*</span><span class="n">RepeatReader</span><span class="p">)</span> <span class="n">Read</span><span class="p">(</span><span class="n">p</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">)</span> <span class="p">(</span><span class="kt">int</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">for</span> <span class="p">{</span>
        <span class="k">if</span> <span class="n">rr</span><span class="o">.</span><span class="n">current</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
            <span class="n">n</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">rr</span><span class="o">.</span><span class="n">current</span><span class="o">.</span><span class="n">Read</span><span class="p">(</span><span class="n">p</span><span class="p">)</span>
            <span class="k">if</span> <span class="n">n</span> <span class="o">&gt;</span> <span class="m">0</span> <span class="p">{</span>
                <span class="k">return</span> <span class="n">n</span><span class="p">,</span> <span class="no">nil</span>
            <span class="p">}</span>
            <span class="k">if</span> <span class="n">err</span> <span class="o">==</span> <span class="n">io</span><span class="o">.</span><span class="n">EOF</span> <span class="p">{</span>
                <span class="n">rr</span><span class="o">.</span><span class="n">current</span> <span class="o">=</span> <span class="no">nil</span>
                <span class="k">continue</span>
            <span class="p">}</span>
            <span class="k">return</span> <span class="n">n</span><span class="p">,</span> <span class="n">err</span>
        <span class="p">}</span>
        <span class="k">if</span> <span class="n">rr</span><span class="o">.</span><span class="n">remaining</span> <span class="o">&lt;=</span> <span class="m">0</span> <span class="p">{</span>
            <span class="k">return</span> <span class="m">0</span><span class="p">,</span> <span class="n">io</span><span class="o">.</span><span class="n">EOF</span>
        <span class="p">}</span>
        <span class="n">rr</span><span class="o">.</span><span class="n">remaining</span><span class="o">--</span>
        <span class="n">rr</span><span class="o">.</span><span class="n">current</span> <span class="o">=</span> <span class="n">strings</span><span class="o">.</span><span class="n">NewReader</span><span class="p">(</span><span class="n">rr</span><span class="o">.</span><span class="n">text</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Each time the current <code class="language-plaintext highlighter-rouge">strings.Reader</code> is exhausted, <code class="language-plaintext highlighter-rouge">RepeatReader</code> creates a fresh one from the same text — until the repeat count reaches zero, at which point it returns <code class="language-plaintext highlighter-rouge">io.EOF</code>. This pattern of lazily creating underlying readers is common in Go I/O code.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="n">repeater</span> <span class="o">:=</span> <span class="n">NewRepeatReader</span><span class="p">(</span><span class="s">"Go! "</span><span class="p">,</span> <span class="m">3</span><span class="p">)</span>
<span class="n">data</span><span class="p">,</span> <span class="n">_</span> <span class="o">:=</span> <span class="n">io</span><span class="o">.</span><span class="n">ReadAll</span><span class="p">(</span><span class="n">repeater</span><span class="p">)</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"repeated: %q</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="kt">string</span><span class="p">(</span><span class="n">data</span><span class="p">))</span>
<span class="c">// Output: "Go! Go! Go! "</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h2 id="json">JSON</h2>

<p>Go’s <code class="language-plaintext highlighter-rouge">encoding/json</code> package handles serialization (Go values to JSON) and deserialization (JSON to Go values). It uses struct tags to control how struct fields map to JSON keys, and it provides both high-level functions (<code class="language-plaintext highlighter-rouge">Marshal</code>/<code class="language-plaintext highlighter-rouge">Unmarshal</code>) and a streaming decoder for working with JSON data.</p>

<h3 id="struct-tags">Struct Tags</h3>

<p>Struct tags are metadata strings attached to struct fields. The <code class="language-plaintext highlighter-rouge">json</code> tag tells the JSON encoder and decoder what key name to use for each field. Without tags, the JSON key matches the Go field name exactly. Unexported fields (lowercase) are always ignored by the JSON package.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="k">type</span> <span class="n">Person</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">FirstName</span> <span class="kt">string</span> <span class="s">`json:"first_name"`</span>
    <span class="n">LastName</span>  <span class="kt">string</span> <span class="s">`json:"last_name"`</span>
    <span class="n">Age</span>       <span class="kt">int</span>    <span class="s">`json:"age"`</span>
    <span class="n">Email</span>     <span class="kt">string</span> <span class="s">`json:"email"`</span>
    <span class="n">password</span>  <span class="kt">string</span> <span class="c">// unexported — json package cannot see this</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>When you marshal a <code class="language-plaintext highlighter-rouge">Person</code>, the JSON keys will be <code class="language-plaintext highlighter-rouge">first_name</code>, <code class="language-plaintext highlighter-rouge">last_name</code>, <code class="language-plaintext highlighter-rouge">age</code>, and <code class="language-plaintext highlighter-rouge">email</code>. The <code class="language-plaintext highlighter-rouge">password</code> field is invisible to the JSON encoder because it starts with a lowercase letter:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="n">p</span> <span class="o">:=</span> <span class="n">Person</span><span class="p">{</span>
    <span class="n">FirstName</span><span class="o">:</span> <span class="s">"Alice"</span><span class="p">,</span>
    <span class="n">LastName</span><span class="o">:</span>  <span class="s">"Smith"</span><span class="p">,</span>
    <span class="n">Age</span><span class="o">:</span>       <span class="m">30</span><span class="p">,</span>
    <span class="n">Email</span><span class="o">:</span>     <span class="s">"alice@example.com"</span><span class="p">,</span>
    <span class="n">password</span><span class="o">:</span>  <span class="s">"secret123"</span><span class="p">,</span>
<span class="p">}</span>
<span class="n">data</span><span class="p">,</span> <span class="n">_</span> <span class="o">:=</span> <span class="n">json</span><span class="o">.</span><span class="n">Marshal</span><span class="p">(</span><span class="n">p</span><span class="p">)</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="kt">string</span><span class="p">(</span><span class="n">data</span><span class="p">))</span>
<span class="c">// {"first_name":"Alice","last_name":"Smith","age":30,"email":"alice@example.com"}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">password</code> field is excluded entirely. This is a natural consequence of Go’s export rules — the JSON package is in a different package from your struct, so it can only see exported fields.</p>

<h3 id="marshal--go-to-json">Marshal — Go to JSON</h3>

<p><code class="language-plaintext highlighter-rouge">json.Marshal</code> converts any Go value to a JSON byte slice. It works with structs, slices, maps, and primitive types:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
</pre></td><td class="rouge-code"><pre><span class="k">type</span> <span class="n">Point</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">X</span> <span class="kt">int</span> <span class="s">`json:"x"`</span>
    <span class="n">Y</span> <span class="kt">int</span> <span class="s">`json:"y"`</span>
<span class="p">}</span>

<span class="c">// Struct</span>
<span class="n">p</span> <span class="o">:=</span> <span class="n">Point</span><span class="p">{</span><span class="n">X</span><span class="o">:</span> <span class="m">1</span><span class="p">,</span> <span class="n">Y</span><span class="o">:</span> <span class="m">2</span><span class="p">}</span>
<span class="n">data</span><span class="p">,</span> <span class="n">_</span> <span class="o">:=</span> <span class="n">json</span><span class="o">.</span><span class="n">Marshal</span><span class="p">(</span><span class="n">p</span><span class="p">)</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="kt">string</span><span class="p">(</span><span class="n">data</span><span class="p">))</span>
<span class="c">// {"x":1,"y":2}</span>

<span class="c">// Slice</span>
<span class="n">points</span> <span class="o">:=</span> <span class="p">[]</span><span class="n">Point</span><span class="p">{{</span><span class="n">X</span><span class="o">:</span> <span class="m">1</span><span class="p">,</span> <span class="n">Y</span><span class="o">:</span> <span class="m">2</span><span class="p">},</span> <span class="p">{</span><span class="n">X</span><span class="o">:</span> <span class="m">3</span><span class="p">,</span> <span class="n">Y</span><span class="o">:</span> <span class="m">4</span><span class="p">}}</span>
<span class="n">data</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">Marshal</span><span class="p">(</span><span class="n">points</span><span class="p">)</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="kt">string</span><span class="p">(</span><span class="n">data</span><span class="p">))</span>
<span class="c">// [{"x":1,"y":2},{"x":3,"y":4}]</span>

<span class="c">// Map</span>
<span class="n">m</span> <span class="o">:=</span> <span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">int</span><span class="p">{</span><span class="s">"width"</span><span class="o">:</span> <span class="m">100</span><span class="p">,</span> <span class="s">"height"</span><span class="o">:</span> <span class="m">200</span><span class="p">}</span>
<span class="n">data</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">Marshal</span><span class="p">(</span><span class="n">m</span><span class="p">)</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="kt">string</span><span class="p">(</span><span class="n">data</span><span class="p">))</span>
<span class="c">// {"height":200,"width":100}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>For human-readable output, use <code class="language-plaintext highlighter-rouge">json.MarshalIndent</code>:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="n">data</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">MarshalIndent</span><span class="p">(</span><span class="n">p</span><span class="p">,</span> <span class="s">""</span><span class="p">,</span> <span class="s">"  "</span><span class="p">)</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="kt">string</span><span class="p">(</span><span class="n">data</span><span class="p">))</span>
<span class="c">// {</span>
<span class="c">//   "x": 1,</span>
<span class="c">//   "y": 2</span>
<span class="c">// }</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The second argument is a prefix for each line, and the third is the indent string. Using an empty prefix and two spaces is the most common convention.</p>

<h3 id="unmarshal--json-to-go">Unmarshal — JSON to Go</h3>

<p><code class="language-plaintext highlighter-rouge">json.Unmarshal</code> takes a JSON byte slice and populates a Go value. You pass a pointer to the target value so the function can modify it:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="n">jsonStr</span> <span class="o">:=</span> <span class="s">`{"first_name":"Bob","last_name":"Jones","age":25,"email":"bob@example.com"}`</span>

<span class="k">var</span> <span class="n">person</span> <span class="n">Person</span>
<span class="n">err</span> <span class="o">:=</span> <span class="n">json</span><span class="o">.</span><span class="n">Unmarshal</span><span class="p">([]</span><span class="kt">byte</span><span class="p">(</span><span class="n">jsonStr</span><span class="p">),</span> <span class="o">&amp;</span><span class="n">person</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
    <span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"error:"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"%+v</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">person</span><span class="p">)</span>
<span class="c">// {FirstName:Bob LastName:Jones Age:25 Email:bob@example.com password:}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Two important behaviors of <code class="language-plaintext highlighter-rouge">Unmarshal</code>: unknown fields in the JSON are silently ignored, and missing fields get Go’s zero values. If the JSON contains a key <code class="language-plaintext highlighter-rouge">phone</code> that has no matching struct field, nothing happens. If the JSON is missing the <code class="language-plaintext highlighter-rouge">age</code> key, the struct’s <code class="language-plaintext highlighter-rouge">Age</code> field stays at 0.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="n">partial</span> <span class="o">:=</span> <span class="s">`{"first_name":"Charlie"}`</span>
<span class="k">var</span> <span class="n">p2</span> <span class="n">Person</span>
<span class="n">json</span><span class="o">.</span><span class="n">Unmarshal</span><span class="p">([]</span><span class="kt">byte</span><span class="p">(</span><span class="n">partial</span><span class="p">),</span> <span class="o">&amp;</span><span class="n">p2</span><span class="p">)</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"%+v</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">p2</span><span class="p">)</span>
<span class="c">// {FirstName:Charlie LastName: Age:0 Email: password:}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="tag-options">Tag Options</h3>

<p>Struct tags support several options that control marshaling behavior. The most useful ones are <code class="language-plaintext highlighter-rouge">omitempty</code>, <code class="language-plaintext highlighter-rouge">-</code>, and <code class="language-plaintext highlighter-rouge">string</code>.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="k">type</span> <span class="n">Config</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">Name</span>     <span class="kt">string</span>  <span class="s">`json:"name"`</span>
    <span class="n">Debug</span>    <span class="kt">bool</span>    <span class="s">`json:"debug,omitempty"`</span>
    <span class="n">Verbose</span>  <span class="kt">bool</span>    <span class="s">`json:"verbose,omitempty"`</span>
    <span class="n">Secret</span>   <span class="kt">string</span>  <span class="s">`json:"-"`</span>
    <span class="n">Count</span>    <span class="kt">int</span>     <span class="s">`json:"count,string"`</span>
    <span class="n">Timeout</span>  <span class="o">*</span><span class="kt">int</span>    <span class="s">`json:"timeout,omitempty"`</span>
    <span class="n">MaxRetry</span> <span class="o">*</span><span class="kt">int</span>    <span class="s">`json:"max_retry,omitempty"`</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">omitempty</code> option tells the encoder to skip the field if it has its zero value (empty string, 0, false, nil pointer, empty slice, or empty map). This keeps the JSON output compact by omitting fields that carry no information.</p>

<p>The <code class="language-plaintext highlighter-rouge">"-"</code> tag tells the encoder to always skip this field, regardless of its value. Use this for sensitive data or internal state that should never appear in JSON output.</p>

<p>The <code class="language-plaintext highlighter-rouge">"string"</code> option encodes a numeric or boolean field as a JSON string. This is useful when interacting with APIs that represent numbers as strings.</p>

<p>Pointer fields interact interestingly with <code class="language-plaintext highlighter-rouge">omitempty</code>. A nil pointer is omitted, but a pointer to a zero value is included:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre></td><td class="rouge-code"><pre><span class="n">timeout</span> <span class="o">:=</span> <span class="m">0</span>
<span class="n">maxRetry</span> <span class="o">:=</span> <span class="m">0</span>

<span class="n">c1</span> <span class="o">:=</span> <span class="n">Config</span><span class="p">{</span>
    <span class="n">Name</span><span class="o">:</span>     <span class="s">"app"</span><span class="p">,</span>
    <span class="n">Secret</span><span class="o">:</span>   <span class="s">"password"</span><span class="p">,</span>
    <span class="n">Count</span><span class="o">:</span>    <span class="m">42</span><span class="p">,</span>
    <span class="n">Timeout</span><span class="o">:</span>  <span class="o">&amp;</span><span class="n">timeout</span><span class="p">,</span>
    <span class="n">MaxRetry</span><span class="o">:</span> <span class="no">nil</span><span class="p">,</span>
<span class="p">}</span>

<span class="n">data</span><span class="p">,</span> <span class="n">_</span> <span class="o">:=</span> <span class="n">json</span><span class="o">.</span><span class="n">MarshalIndent</span><span class="p">(</span><span class="n">c1</span><span class="p">,</span> <span class="s">""</span><span class="p">,</span> <span class="s">"  "</span><span class="p">)</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="kt">string</span><span class="p">(</span><span class="n">data</span><span class="p">))</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>In this output, <code class="language-plaintext highlighter-rouge">Debug</code> and <code class="language-plaintext highlighter-rouge">Verbose</code> are omitted (zero booleans with <code class="language-plaintext highlighter-rouge">omitempty</code>), <code class="language-plaintext highlighter-rouge">Secret</code> is omitted (tag is <code class="language-plaintext highlighter-rouge">"-"</code>), <code class="language-plaintext highlighter-rouge">Count</code> appears as <code class="language-plaintext highlighter-rouge">"42"</code> (a string, not a number), <code class="language-plaintext highlighter-rouge">Timeout</code> appears as <code class="language-plaintext highlighter-rouge">0</code> (pointer to zero is not nil, so it is included), and <code class="language-plaintext highlighter-rouge">MaxRetry</code> is omitted (nil pointer with <code class="language-plaintext highlighter-rouge">omitempty</code>). This distinction between nil and zero is the main reason to use pointer fields with <code class="language-plaintext highlighter-rouge">omitempty</code>.</p>

<h3 id="dynamic-json">Dynamic JSON</h3>

<p>Sometimes you do not know the JSON structure at compile time. Go provides several tools for working with dynamic JSON.</p>

<p>The first is <code class="language-plaintext highlighter-rouge">map[string]any</code>. Unmarshaling into a map gives you a dynamic key-value structure where the JSON decoder chooses Go types for you (strings become <code class="language-plaintext highlighter-rouge">string</code>, numbers become <code class="language-plaintext highlighter-rouge">float64</code>, booleans become <code class="language-plaintext highlighter-rouge">bool</code>, objects become <code class="language-plaintext highlighter-rouge">map[string]any</code>, and arrays become <code class="language-plaintext highlighter-rouge">[]any</code>):</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="n">dynamic</span> <span class="o">:=</span> <span class="s">`{"name":"test","count":42,"active":true,"tags":["a","b"]}`</span>
<span class="k">var</span> <span class="n">m</span> <span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="n">any</span>
<span class="n">json</span><span class="o">.</span><span class="n">Unmarshal</span><span class="p">([]</span><span class="kt">byte</span><span class="p">(</span><span class="n">dynamic</span><span class="p">),</span> <span class="o">&amp;</span><span class="n">m</span><span class="p">)</span>

<span class="k">for</span> <span class="n">key</span><span class="p">,</span> <span class="n">val</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">m</span> <span class="p">{</span>
    <span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  %s: %v (%T)</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">key</span><span class="p">,</span> <span class="n">val</span><span class="p">,</span> <span class="n">val</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The second tool is <code class="language-plaintext highlighter-rouge">json.RawMessage</code>. It lets you defer parsing part of a JSON document. The raw bytes are stored as-is, and you decode them later once you know what type to expect. This is ideal for envelope patterns where a <code class="language-plaintext highlighter-rouge">type</code> field determines the schema of a <code class="language-plaintext highlighter-rouge">payload</code> field:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
</pre></td><td class="rouge-code"><pre><span class="k">type</span> <span class="n">Envelope</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">Type</span>    <span class="kt">string</span>          <span class="s">`json:"type"`</span>
    <span class="n">Payload</span> <span class="n">json</span><span class="o">.</span><span class="n">RawMessage</span> <span class="s">`json:"payload"`</span>
<span class="p">}</span>

<span class="k">type</span> <span class="n">TextMsg</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">Body</span> <span class="kt">string</span> <span class="s">`json:"body"`</span>
<span class="p">}</span>

<span class="k">type</span> <span class="n">ImageMsg</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">URL</span>    <span class="kt">string</span> <span class="s">`json:"url"`</span>
    <span class="n">Width</span>  <span class="kt">int</span>    <span class="s">`json:"width"`</span>
    <span class="n">Height</span> <span class="kt">int</span>    <span class="s">`json:"height"`</span>
<span class="p">}</span>

<span class="n">messages</span> <span class="o">:=</span> <span class="p">[]</span><span class="kt">string</span><span class="p">{</span>
    <span class="s">`{"type":"text","payload":{"body":"Hello!"}}`</span><span class="p">,</span>
    <span class="s">`{"type":"image","payload":{"url":"pic.png","width":800,"height":600}}`</span><span class="p">,</span>
<span class="p">}</span>

<span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">raw</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">messages</span> <span class="p">{</span>
    <span class="k">var</span> <span class="n">env</span> <span class="n">Envelope</span>
    <span class="n">json</span><span class="o">.</span><span class="n">Unmarshal</span><span class="p">([]</span><span class="kt">byte</span><span class="p">(</span><span class="n">raw</span><span class="p">),</span> <span class="o">&amp;</span><span class="n">env</span><span class="p">)</span>

    <span class="k">switch</span> <span class="n">env</span><span class="o">.</span><span class="n">Type</span> <span class="p">{</span>
    <span class="k">case</span> <span class="s">"text"</span><span class="o">:</span>
        <span class="k">var</span> <span class="n">msg</span> <span class="n">TextMsg</span>
        <span class="n">json</span><span class="o">.</span><span class="n">Unmarshal</span><span class="p">(</span><span class="n">env</span><span class="o">.</span><span class="n">Payload</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">msg</span><span class="p">)</span>
        <span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  text message: %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">msg</span><span class="o">.</span><span class="n">Body</span><span class="p">)</span>
    <span class="k">case</span> <span class="s">"image"</span><span class="o">:</span>
        <span class="k">var</span> <span class="n">msg</span> <span class="n">ImageMsg</span>
        <span class="n">json</span><span class="o">.</span><span class="n">Unmarshal</span><span class="p">(</span><span class="n">env</span><span class="o">.</span><span class="n">Payload</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">msg</span><span class="p">)</span>
        <span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  image: %s (%dx%d)</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">msg</span><span class="o">.</span><span class="n">URL</span><span class="p">,</span> <span class="n">msg</span><span class="o">.</span><span class="n">Width</span><span class="p">,</span> <span class="n">msg</span><span class="o">.</span><span class="n">Height</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">Payload</code> field stays as raw JSON bytes until we inspect <code class="language-plaintext highlighter-rouge">Type</code> and know which struct to decode into. Without <code class="language-plaintext highlighter-rouge">json.RawMessage</code>, you would need to unmarshal twice or use a map.</p>

<p>The third tool is <code class="language-plaintext highlighter-rouge">json.Decoder</code>, which reads JSON values from an <code class="language-plaintext highlighter-rouge">io.Reader</code> stream. This is useful for processing multiple JSON objects from a single source (a file, a network connection, or any reader) without loading everything into memory at once:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="n">stream</span> <span class="o">:=</span> <span class="s">`{"name":"Alice"}{"name":"Bob"}{"name":"Charlie"}`</span>
<span class="n">decoder</span> <span class="o">:=</span> <span class="n">json</span><span class="o">.</span><span class="n">NewDecoder</span><span class="p">(</span><span class="n">strings</span><span class="o">.</span><span class="n">NewReader</span><span class="p">(</span><span class="n">stream</span><span class="p">))</span>

<span class="k">for</span> <span class="n">decoder</span><span class="o">.</span><span class="n">More</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">var</span> <span class="n">obj</span> <span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">string</span>
    <span class="k">if</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">decoder</span><span class="o">.</span><span class="n">Decode</span><span class="p">(</span><span class="o">&amp;</span><span class="n">obj</span><span class="p">);</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
        <span class="k">break</span>
    <span class="p">}</span>
    <span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  decoded: %v</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">obj</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">decoder.More()</code> returns true as long as there is another JSON value in the stream. Each call to <code class="language-plaintext highlighter-rouge">Decode</code> reads exactly one value, advances the stream position, and populates the target. This is more memory-efficient than reading the entire input and splitting it yourself.</p>

<h2 id="testing">Testing</h2>

<p>Go has a built-in testing framework in the <code class="language-plaintext highlighter-rouge">testing</code> package. Test files live next to the code they test, with a <code class="language-plaintext highlighter-rouge">_test.go</code> suffix. Test functions start with <code class="language-plaintext highlighter-rouge">Test</code> and take a single <code class="language-plaintext highlighter-rouge">*testing.T</code> argument. You run them with <code class="language-plaintext highlighter-rouge">go test</code>.</p>

<h3 id="basic-tests">Basic Tests</h3>

<p>The simplest test calls a function and checks the result. If the result is wrong, you call <code class="language-plaintext highlighter-rouge">t.Errorf</code> with a descriptive message. There is no assertion library — just conditional checks and error reporting:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="rouge-code"><pre><span class="c">// math_test.go</span>
<span class="k">package</span> <span class="n">main</span>

<span class="k">import</span> <span class="s">"testing"</span>

<span class="k">func</span> <span class="n">TestAdd</span><span class="p">(</span><span class="n">t</span> <span class="o">*</span><span class="n">testing</span><span class="o">.</span><span class="n">T</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">got</span> <span class="o">:=</span> <span class="n">Add</span><span class="p">(</span><span class="m">2</span><span class="p">,</span> <span class="m">3</span><span class="p">)</span>
    <span class="n">want</span> <span class="o">:=</span> <span class="m">5</span>
    <span class="k">if</span> <span class="n">got</span> <span class="o">!=</span> <span class="n">want</span> <span class="p">{</span>
        <span class="n">t</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"Add(2, 3) = %d, want %d"</span><span class="p">,</span> <span class="n">got</span><span class="p">,</span> <span class="n">want</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">got</code>/<code class="language-plaintext highlighter-rouge">want</code> naming convention is idiomatic Go. If <code class="language-plaintext highlighter-rouge">got</code> does not equal <code class="language-plaintext highlighter-rouge">want</code>, <code class="language-plaintext highlighter-rouge">t.Errorf</code> logs the failure and the test continues (unlike <code class="language-plaintext highlighter-rouge">t.Fatalf</code>, which stops the test immediately). A test with no errors passes.</p>

<h3 id="table-driven-tests">Table-Driven Tests</h3>

<p>When you need to test a function with many inputs, Go developers use table-driven tests. You define a slice of test cases — each with a name, inputs, and expected outputs — and loop over them with <code class="language-plaintext highlighter-rouge">t.Run</code>. This pattern is clean, easy to extend, and produces well-labeled output when a test fails:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">TestDivide</span><span class="p">(</span><span class="n">t</span> <span class="o">*</span><span class="n">testing</span><span class="o">.</span><span class="n">T</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">tests</span> <span class="o">:=</span> <span class="p">[]</span><span class="k">struct</span> <span class="p">{</span>
        <span class="n">name</span>      <span class="kt">string</span>
        <span class="n">a</span><span class="p">,</span> <span class="n">b</span>      <span class="kt">float64</span>
        <span class="n">want</span>      <span class="kt">float64</span>
        <span class="n">wantError</span> <span class="kt">bool</span>
    <span class="p">}{</span>
        <span class="p">{</span><span class="n">name</span><span class="o">:</span> <span class="s">"positive"</span><span class="p">,</span> <span class="n">a</span><span class="o">:</span> <span class="m">10</span><span class="p">,</span> <span class="n">b</span><span class="o">:</span> <span class="m">2</span><span class="p">,</span> <span class="n">want</span><span class="o">:</span> <span class="m">5</span><span class="p">,</span> <span class="n">wantError</span><span class="o">:</span> <span class="no">false</span><span class="p">},</span>
        <span class="p">{</span><span class="n">name</span><span class="o">:</span> <span class="s">"negative result"</span><span class="p">,</span> <span class="n">a</span><span class="o">:</span> <span class="o">-</span><span class="m">10</span><span class="p">,</span> <span class="n">b</span><span class="o">:</span> <span class="m">2</span><span class="p">,</span> <span class="n">want</span><span class="o">:</span> <span class="o">-</span><span class="m">5</span><span class="p">,</span> <span class="n">wantError</span><span class="o">:</span> <span class="no">false</span><span class="p">},</span>
        <span class="p">{</span><span class="n">name</span><span class="o">:</span> <span class="s">"divide by zero"</span><span class="p">,</span> <span class="n">a</span><span class="o">:</span> <span class="m">5</span><span class="p">,</span> <span class="n">b</span><span class="o">:</span> <span class="m">0</span><span class="p">,</span> <span class="n">want</span><span class="o">:</span> <span class="m">0</span><span class="p">,</span> <span class="n">wantError</span><span class="o">:</span> <span class="no">true</span><span class="p">},</span>
    <span class="p">}</span>

    <span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">tt</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">tests</span> <span class="p">{</span>
        <span class="n">t</span><span class="o">.</span><span class="n">Run</span><span class="p">(</span><span class="n">tt</span><span class="o">.</span><span class="n">name</span><span class="p">,</span> <span class="k">func</span><span class="p">(</span><span class="n">t</span> <span class="o">*</span><span class="n">testing</span><span class="o">.</span><span class="n">T</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">got</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">Divide</span><span class="p">(</span><span class="n">tt</span><span class="o">.</span><span class="n">a</span><span class="p">,</span> <span class="n">tt</span><span class="o">.</span><span class="n">b</span><span class="p">)</span>
            <span class="k">if</span> <span class="n">tt</span><span class="o">.</span><span class="n">wantError</span> <span class="p">{</span>
                <span class="k">if</span> <span class="n">err</span> <span class="o">==</span> <span class="no">nil</span> <span class="p">{</span>
                    <span class="n">t</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"expected error"</span><span class="p">)</span>
                <span class="p">}</span>
                <span class="k">return</span>
            <span class="p">}</span>
            <span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
                <span class="n">t</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"unexpected error: %v"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
                <span class="k">return</span>
            <span class="p">}</span>
            <span class="k">if</span> <span class="n">got</span> <span class="o">!=</span> <span class="n">tt</span><span class="o">.</span><span class="n">want</span> <span class="p">{</span>
                <span class="n">t</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"got %v, want %v"</span><span class="p">,</span> <span class="n">got</span><span class="p">,</span> <span class="n">tt</span><span class="o">.</span><span class="n">want</span><span class="p">)</span>
            <span class="p">}</span>
        <span class="p">})</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Each call to <code class="language-plaintext highlighter-rouge">t.Run</code> creates a subtest with the given name. When you run <code class="language-plaintext highlighter-rouge">go test -v</code>, you see output like <code class="language-plaintext highlighter-rouge">TestDivide/positive</code>, <code class="language-plaintext highlighter-rouge">TestDivide/negative_result</code>, and <code class="language-plaintext highlighter-rouge">TestDivide/divide_by_zero</code>. If the “divide by zero” case fails, the name tells you exactly which case went wrong without you having to count indices.</p>

<h3 id="edge-cases">Edge Cases</h3>

<p>Table-driven tests make it natural to include edge cases alongside the happy path. Here is a primality check test that covers zero, one, negative numbers, and a large prime:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">TestIsPrime</span><span class="p">(</span><span class="n">t</span> <span class="o">*</span><span class="n">testing</span><span class="o">.</span><span class="n">T</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">tests</span> <span class="o">:=</span> <span class="p">[]</span><span class="k">struct</span> <span class="p">{</span>
        <span class="n">name</span> <span class="kt">string</span>
        <span class="n">n</span>    <span class="kt">int</span>
        <span class="n">want</span> <span class="kt">bool</span>
    <span class="p">}{</span>
        <span class="p">{</span><span class="s">"zero"</span><span class="p">,</span> <span class="m">0</span><span class="p">,</span> <span class="no">false</span><span class="p">},</span>
        <span class="p">{</span><span class="s">"one"</span><span class="p">,</span> <span class="m">1</span><span class="p">,</span> <span class="no">false</span><span class="p">},</span>
        <span class="p">{</span><span class="s">"two"</span><span class="p">,</span> <span class="m">2</span><span class="p">,</span> <span class="no">true</span><span class="p">},</span>
        <span class="p">{</span><span class="s">"four"</span><span class="p">,</span> <span class="m">4</span><span class="p">,</span> <span class="no">false</span><span class="p">},</span>
        <span class="p">{</span><span class="s">"negative"</span><span class="p">,</span> <span class="o">-</span><span class="m">7</span><span class="p">,</span> <span class="no">false</span><span class="p">},</span>
        <span class="p">{</span><span class="s">"large prime"</span><span class="p">,</span> <span class="m">97</span><span class="p">,</span> <span class="no">true</span><span class="p">},</span>
    <span class="p">}</span>

    <span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">tt</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">tests</span> <span class="p">{</span>
        <span class="n">t</span><span class="o">.</span><span class="n">Run</span><span class="p">(</span><span class="n">tt</span><span class="o">.</span><span class="n">name</span><span class="p">,</span> <span class="k">func</span><span class="p">(</span><span class="n">t</span> <span class="o">*</span><span class="n">testing</span><span class="o">.</span><span class="n">T</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">if</span> <span class="n">got</span> <span class="o">:=</span> <span class="n">IsPrime</span><span class="p">(</span><span class="n">tt</span><span class="o">.</span><span class="n">n</span><span class="p">);</span> <span class="n">got</span> <span class="o">!=</span> <span class="n">tt</span><span class="o">.</span><span class="n">want</span> <span class="p">{</span>
                <span class="n">t</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"IsPrime(%d) = %v, want %v"</span><span class="p">,</span> <span class="n">tt</span><span class="o">.</span><span class="n">n</span><span class="p">,</span> <span class="n">got</span><span class="p">,</span> <span class="n">tt</span><span class="o">.</span><span class="n">want</span><span class="p">)</span>
            <span class="p">}</span>
        <span class="p">})</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The test covers the boundary conditions that are most likely to expose bugs: numbers below 2, the smallest prime, a composite number, and a negative number. Adding a new edge case is just adding one more entry to the slice.</p>

<h3 id="test-helpers-with-thelper">Test Helpers with t.Helper()</h3>

<p>When you find yourself repeating the same assertion logic across multiple tests, extract it into a helper function. Calling <code class="language-plaintext highlighter-rouge">t.Helper()</code> at the top of the helper tells the testing framework to report failures at the caller’s line number, not inside the helper. Without <code class="language-plaintext highlighter-rouge">t.Helper()</code>, every failure would point to the same line inside the helper function, making it hard to tell which test case failed:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">assertEqual</span><span class="p">(</span><span class="n">t</span> <span class="o">*</span><span class="n">testing</span><span class="o">.</span><span class="n">T</span><span class="p">,</span> <span class="n">got</span><span class="p">,</span> <span class="n">want</span> <span class="kt">string</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">t</span><span class="o">.</span><span class="n">Helper</span><span class="p">()</span>
    <span class="k">if</span> <span class="n">got</span> <span class="o">!=</span> <span class="n">want</span> <span class="p">{</span>
        <span class="n">t</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"got %q, want %q"</span><span class="p">,</span> <span class="n">got</span><span class="p">,</span> <span class="n">want</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">TestFizzBuzz</span><span class="p">(</span><span class="n">t</span> <span class="o">*</span><span class="n">testing</span><span class="o">.</span><span class="n">T</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">assertEqual</span><span class="p">(</span><span class="n">t</span><span class="p">,</span> <span class="n">FizzBuzz</span><span class="p">(</span><span class="m">1</span><span class="p">),</span> <span class="s">"1"</span><span class="p">)</span>
    <span class="n">assertEqual</span><span class="p">(</span><span class="n">t</span><span class="p">,</span> <span class="n">FizzBuzz</span><span class="p">(</span><span class="m">3</span><span class="p">),</span> <span class="s">"Fizz"</span><span class="p">)</span>
    <span class="n">assertEqual</span><span class="p">(</span><span class="n">t</span><span class="p">,</span> <span class="n">FizzBuzz</span><span class="p">(</span><span class="m">5</span><span class="p">),</span> <span class="s">"Buzz"</span><span class="p">)</span>
    <span class="n">assertEqual</span><span class="p">(</span><span class="n">t</span><span class="p">,</span> <span class="n">FizzBuzz</span><span class="p">(</span><span class="m">15</span><span class="p">),</span> <span class="s">"FizzBuzz"</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>If <code class="language-plaintext highlighter-rouge">FizzBuzz(5)</code> returned the wrong value, the failure message would point to the <code class="language-plaintext highlighter-rouge">assertEqual(t, FizzBuzz(5), "Buzz")</code> line in <code class="language-plaintext highlighter-rouge">TestFizzBuzz</code>, not to the <code class="language-plaintext highlighter-rouge">t.Errorf</code> line inside <code class="language-plaintext highlighter-rouge">assertEqual</code>. This is the difference <code class="language-plaintext highlighter-rouge">t.Helper()</code> makes.</p>

<h3 id="testing-unicode">Testing Unicode</h3>

<p>Go strings are UTF-8 encoded, and operations that work on bytes can break multi-byte characters. Testing with unicode input catches these issues. Here is a <code class="language-plaintext highlighter-rouge">Reverse</code> function tested with ASCII, unicode, and emoji:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">TestReverse</span><span class="p">(</span><span class="n">t</span> <span class="o">*</span><span class="n">testing</span><span class="o">.</span><span class="n">T</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">tests</span> <span class="o">:=</span> <span class="p">[]</span><span class="k">struct</span> <span class="p">{</span>
        <span class="n">name</span><span class="p">,</span> <span class="n">input</span><span class="p">,</span> <span class="n">want</span> <span class="kt">string</span>
    <span class="p">}{</span>
        <span class="p">{</span><span class="s">"ascii"</span><span class="p">,</span> <span class="s">"hello"</span><span class="p">,</span> <span class="s">"olleh"</span><span class="p">},</span>
        <span class="p">{</span><span class="s">"empty"</span><span class="p">,</span> <span class="s">""</span><span class="p">,</span> <span class="s">""</span><span class="p">},</span>
        <span class="p">{</span><span class="s">"unicode"</span><span class="p">,</span> <span class="s">"Hello, 世界"</span><span class="p">,</span> <span class="s">"界世 ,olleH"</span><span class="p">},</span>
        <span class="p">{</span><span class="s">"emoji"</span><span class="p">,</span> <span class="s">"Go🚀"</span><span class="p">,</span> <span class="s">"🚀oG"</span><span class="p">},</span>
    <span class="p">}</span>

    <span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">tt</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">tests</span> <span class="p">{</span>
        <span class="n">t</span><span class="o">.</span><span class="n">Run</span><span class="p">(</span><span class="n">tt</span><span class="o">.</span><span class="n">name</span><span class="p">,</span> <span class="k">func</span><span class="p">(</span><span class="n">t</span> <span class="o">*</span><span class="n">testing</span><span class="o">.</span><span class="n">T</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">if</span> <span class="n">got</span> <span class="o">:=</span> <span class="n">Reverse</span><span class="p">(</span><span class="n">tt</span><span class="o">.</span><span class="n">input</span><span class="p">);</span> <span class="n">got</span> <span class="o">!=</span> <span class="n">tt</span><span class="o">.</span><span class="n">want</span> <span class="p">{</span>
                <span class="n">t</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"Reverse(%q) = %q, want %q"</span><span class="p">,</span> <span class="n">tt</span><span class="o">.</span><span class="n">input</span><span class="p">,</span> <span class="n">got</span><span class="p">,</span> <span class="n">tt</span><span class="o">.</span><span class="n">want</span><span class="p">)</span>
            <span class="p">}</span>
        <span class="p">})</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">Reverse</code> function must work on runes (Unicode code points), not bytes. If it reversed bytes instead of runes, the multi-byte characters in “Hello, 世界” would be corrupted. Including unicode test cases in the table is a simple way to verify correct behavior:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">Reverse</span><span class="p">(</span><span class="n">s</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">string</span> <span class="p">{</span>
    <span class="n">runes</span> <span class="o">:=</span> <span class="p">[]</span><span class="kt">rune</span><span class="p">(</span><span class="n">s</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">j</span> <span class="o">:=</span> <span class="m">0</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">runes</span><span class="p">)</span><span class="o">-</span><span class="m">1</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">j</span><span class="p">;</span> <span class="n">i</span><span class="p">,</span> <span class="n">j</span> <span class="o">=</span> <span class="n">i</span><span class="o">+</span><span class="m">1</span><span class="p">,</span> <span class="n">j</span><span class="o">-</span><span class="m">1</span> <span class="p">{</span>
        <span class="n">runes</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">runes</span><span class="p">[</span><span class="n">j</span><span class="p">]</span> <span class="o">=</span> <span class="n">runes</span><span class="p">[</span><span class="n">j</span><span class="p">],</span> <span class="n">runes</span><span class="p">[</span><span class="n">i</span><span class="p">]</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="kt">string</span><span class="p">(</span><span class="n">runes</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Converting to <code class="language-plaintext highlighter-rouge">[]rune</code> ensures each element is a full Unicode code point. The swap loop works on runes, not bytes, so multi-byte characters like <code class="language-plaintext highlighter-rouge">世</code>, <code class="language-plaintext highlighter-rouge">界</code>, and <code class="language-plaintext highlighter-rouge">🚀</code> are reversed correctly. The test table makes it trivial to add more unicode edge cases as you discover them.</p>]]></content><author><name>kimserey</name></author><category term="go" /><summary type="html"><![CDATA[This post covers four day-to-day essentials for writing Go programs: organizing code into packages and modules, streaming data through the io.Reader and io.Writer interfaces, serializing and deserializing JSON, and writing tests. These are the building blocks you will reach for in nearly every Go project — packages keep code organized, the I/O interfaces let you compose data pipelines, JSON handles serialization, and the testing package gives you a lightweight framework for verifying everything works.]]></summary></entry><entry><title type="html">Go Concurrency — Goroutines, Channels and Sync</title><link href="https://www.kimsereylam.com/go/2026/09/05/go-concurrency-goroutines-channels-and-sync.html" rel="alternate" type="text/html" title="Go Concurrency — Goroutines, Channels and Sync" /><published>2026-09-05T00:00:00-05:00</published><updated>2026-09-05T00:00:00-05:00</updated><id>https://www.kimsereylam.com/go/2026/09/05/go-concurrency-goroutines-channels-and-sync</id><content type="html" xml:base="https://www.kimsereylam.com/go/2026/09/05/go-concurrency-goroutines-channels-and-sync.html"><![CDATA[<p>Go was designed with concurrency as a first-class feature. Rather than relying on OS threads and shared-memory locking as the primary model, Go provides goroutines (lightweight threads managed by the Go runtime), channels (typed conduits for communication between goroutines), and a <code class="language-plaintext highlighter-rouge">select</code> statement for multiplexing channel operations. The standard library rounds this out with a <code class="language-plaintext highlighter-rouge">context</code> package for cancellation and deadlines, and a <code class="language-plaintext highlighter-rouge">sync</code> package for traditional mutual exclusion when channels are not the right fit. This tutorial walks through all five building blocks, starting from basic goroutines and working up to a concurrent cache protected by read-write locks and atomics.</p>

<!--more-->

<h2 id="goroutines-and-waitgroup">Goroutines and WaitGroup</h2>

<p>A goroutine is a function that runs concurrently with other goroutines in the same address space. You start one with the <code class="language-plaintext highlighter-rouge">go</code> keyword followed by a function call. Goroutines are multiplexed onto a small number of OS threads by the Go runtime scheduler, so spawning thousands of them is cheap — each one starts with a stack of only a few kilobytes.</p>

<h3 id="starting-a-goroutine">Starting a Goroutine</h3>

<p>The simplest way to launch a goroutine is with an anonymous function. Here, the main goroutine creates a channel, launches a background goroutine that sends a value, and then blocks on a receive:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">part1_basicGoroutine</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">done</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="k">chan</span> <span class="kt">bool</span><span class="p">)</span>
	<span class="k">go</span> <span class="k">func</span><span class="p">()</span> <span class="p">{</span>
		<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"  hello from a goroutine!"</span><span class="p">)</span>
		<span class="n">done</span> <span class="o">&lt;-</span> <span class="no">true</span>
	<span class="p">}()</span>
	<span class="o">&lt;-</span><span class="n">done</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">go func() { ... }()</code> syntax launches the anonymous function as a new goroutine. The channel receive <code class="language-plaintext highlighter-rouge">&lt;-done</code> blocks <code class="language-plaintext highlighter-rouge">main</code> until the goroutine sends a value, ensuring we see the printed message before the program exits.</p>

<h3 id="the-broken-version--no-synchronization">The Broken Version — No Synchronization</h3>

<p>If you launch goroutines and do not wait for them, the main function may exit before they finish. This is a common mistake:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">part2_brokenNoSync</span><span class="p">()</span> <span class="p">{</span>
	<span class="k">for</span> <span class="n">i</span> <span class="o">:=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="m">3</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span> <span class="p">{</span>
		<span class="k">go</span> <span class="k">func</span><span class="p">(</span><span class="n">id</span> <span class="kt">int</span><span class="p">)</span> <span class="p">{</span>
			<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  worker %d: processing (might not print!)</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">id</span><span class="p">)</span>
		<span class="p">}(</span><span class="n">i</span><span class="p">)</span>
	<span class="p">}</span>
	<span class="n">time</span><span class="o">.</span><span class="n">Sleep</span><span class="p">(</span><span class="m">10</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Millisecond</span><span class="p">)</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"  main continued immediately — goroutines may or may not have finished"</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">time.Sleep</code> gives the goroutines a brief window to run, but there is no guarantee. On a fast machine they might finish; on a loaded machine they might not. Sleeping is never a correct synchronization mechanism.</p>

<h3 id="waitgroup">WaitGroup</h3>

<p>A <code class="language-plaintext highlighter-rouge">sync.WaitGroup</code> is the standard tool for waiting on a set of goroutines to complete. You call <code class="language-plaintext highlighter-rouge">Add</code> to register work, <code class="language-plaintext highlighter-rouge">Done</code> to signal completion, and <code class="language-plaintext highlighter-rouge">Wait</code> to block until all work is done:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">part3_waitGroup</span><span class="p">()</span> <span class="p">{</span>
	<span class="k">var</span> <span class="n">wg</span> <span class="n">sync</span><span class="o">.</span><span class="n">WaitGroup</span>
	<span class="k">for</span> <span class="n">i</span> <span class="o">:=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="m">3</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span> <span class="p">{</span>
		<span class="n">wg</span><span class="o">.</span><span class="n">Add</span><span class="p">(</span><span class="m">1</span><span class="p">)</span>
		<span class="k">go</span> <span class="k">func</span><span class="p">(</span><span class="n">id</span> <span class="kt">int</span><span class="p">)</span> <span class="p">{</span>
			<span class="k">defer</span> <span class="n">wg</span><span class="o">.</span><span class="n">Done</span><span class="p">()</span>
			<span class="n">time</span><span class="o">.</span><span class="n">Sleep</span><span class="p">(</span><span class="n">time</span><span class="o">.</span><span class="n">Duration</span><span class="p">(</span><span class="n">id</span><span class="o">*</span><span class="m">50</span><span class="p">)</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Millisecond</span><span class="p">)</span>
			<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  worker %d: done</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">id</span><span class="p">)</span>
		<span class="p">}(</span><span class="n">i</span><span class="p">)</span>
	<span class="p">}</span>
	<span class="n">wg</span><span class="o">.</span><span class="n">Wait</span><span class="p">()</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"  all workers finished!"</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">wg.Add(1)</code> is called before launching each goroutine, not inside it — this avoids a race where <code class="language-plaintext highlighter-rouge">Wait</code> could return before all goroutines have registered. <code class="language-plaintext highlighter-rouge">defer wg.Done()</code> ensures the counter is decremented even if the goroutine panics. After <code class="language-plaintext highlighter-rouge">wg.Wait()</code> returns, all three goroutines have completed.</p>

<h3 id="closure-variable-capture">Closure Variable Capture</h3>

<p>When launching goroutines in a loop, the loop variable must be passed explicitly as a function argument. Otherwise, all goroutines share the same variable and may all see the final value:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">part4_closureCapture</span><span class="p">()</span> <span class="p">{</span>
	<span class="k">var</span> <span class="n">wg</span> <span class="n">sync</span><span class="o">.</span><span class="n">WaitGroup</span>
	<span class="k">for</span> <span class="n">i</span> <span class="o">:=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="m">3</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span> <span class="p">{</span>
		<span class="n">wg</span><span class="o">.</span><span class="n">Add</span><span class="p">(</span><span class="m">1</span><span class="p">)</span>
		<span class="k">go</span> <span class="k">func</span><span class="p">(</span><span class="n">id</span> <span class="kt">int</span><span class="p">)</span> <span class="p">{</span>
			<span class="k">defer</span> <span class="n">wg</span><span class="o">.</span><span class="n">Done</span><span class="p">()</span>
			<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"    worker id=%d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">id</span><span class="p">)</span>
		<span class="p">}(</span><span class="n">i</span><span class="p">)</span>
	<span class="p">}</span>
	<span class="n">wg</span><span class="o">.</span><span class="n">Wait</span><span class="p">()</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The expression <code class="language-plaintext highlighter-rouge">go func(id int) { ... }(i)</code> copies the current value of <code class="language-plaintext highlighter-rouge">i</code> into the parameter <code class="language-plaintext highlighter-rouge">id</code>. Each goroutine gets its own copy. If you wrote <code class="language-plaintext highlighter-rouge">go func() { fmt.Println(i) }()</code> instead, all three goroutines would close over the same <code class="language-plaintext highlighter-rouge">i</code> variable, and you would likely see <code class="language-plaintext highlighter-rouge">3, 3, 3</code> instead of <code class="language-plaintext highlighter-rouge">0, 1, 2</code>.</p>

<h3 id="concurrent-workers">Concurrent Workers</h3>

<p>A common pattern is to fan out work across multiple goroutines and collect results. Since each goroutine writes to a distinct index in the results slice, no lock is needed:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">part5_concurrentWorkers</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">batches</span> <span class="o">:=</span> <span class="p">[][]</span><span class="kt">string</span><span class="p">{</span>
		<span class="p">{</span><span class="s">"INSERT users alice"</span><span class="p">,</span> <span class="s">"INSERT users bob"</span><span class="p">},</span>
		<span class="p">{</span><span class="s">"UPDATE orders ord-1"</span><span class="p">,</span> <span class="s">"DELETE orders ord-2"</span><span class="p">},</span>
		<span class="p">{</span><span class="s">"INSERT events evt-1"</span><span class="p">,</span> <span class="s">"INSERT events evt-2"</span><span class="p">,</span> <span class="s">"INSERT events evt-3"</span><span class="p">},</span>
	<span class="p">}</span>
	<span class="k">var</span> <span class="n">wg</span> <span class="n">sync</span><span class="o">.</span><span class="n">WaitGroup</span>
	<span class="n">results</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="kt">int</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">batches</span><span class="p">))</span>
	<span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">batch</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">batches</span> <span class="p">{</span>
		<span class="n">wg</span><span class="o">.</span><span class="n">Add</span><span class="p">(</span><span class="m">1</span><span class="p">)</span>
		<span class="k">go</span> <span class="k">func</span><span class="p">(</span><span class="n">id</span> <span class="kt">int</span><span class="p">,</span> <span class="n">items</span> <span class="p">[]</span><span class="kt">string</span><span class="p">)</span> <span class="p">{</span>
			<span class="k">defer</span> <span class="n">wg</span><span class="o">.</span><span class="n">Done</span><span class="p">()</span>
			<span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">item</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">items</span> <span class="p">{</span>
				<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"    worker %d: %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">id</span><span class="p">,</span> <span class="n">item</span><span class="p">)</span>
			<span class="p">}</span>
			<span class="n">results</span><span class="p">[</span><span class="n">id</span><span class="p">]</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">items</span><span class="p">)</span>
		<span class="p">}(</span><span class="n">i</span><span class="p">,</span> <span class="n">batch</span><span class="p">)</span>
	<span class="p">}</span>
	<span class="n">wg</span><span class="o">.</span><span class="n">Wait</span><span class="p">()</span>
	<span class="n">total</span> <span class="o">:=</span> <span class="m">0</span>
	<span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">count</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">results</span> <span class="p">{</span> <span class="n">total</span> <span class="o">+=</span> <span class="n">count</span> <span class="p">}</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  all workers done. total items processed: %d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">total</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Each goroutine receives its own <code class="language-plaintext highlighter-rouge">id</code> and <code class="language-plaintext highlighter-rouge">items</code> slice. The <code class="language-plaintext highlighter-rouge">results</code> slice is pre-allocated with one slot per worker, so concurrent writes to different indices are safe without synchronization. After <code class="language-plaintext highlighter-rouge">wg.Wait()</code>, the main goroutine aggregates the counts.</p>

<h2 id="channels">Channels</h2>

<p>Channels are Go’s primary mechanism for communication between goroutines. A channel is a typed conduit — you send values into it and receive values out of it. Channels enforce synchronization: a send on an unbuffered channel blocks until another goroutine receives, and vice versa. This “communication by sharing” model is the foundation of Go’s concurrency philosophy.</p>

<h3 id="unbuffered-channels">Unbuffered Channels</h3>

<p>An unbuffered channel has no internal storage. A send blocks until a receiver is ready, and a receive blocks until a sender is ready. This provides a synchronization point between two goroutines:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">part1_unbuffered</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">ch</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="k">chan</span> <span class="kt">string</span><span class="p">)</span>
	<span class="k">go</span> <span class="k">func</span><span class="p">()</span> <span class="p">{</span>
		<span class="n">ch</span> <span class="o">&lt;-</span> <span class="s">"hello from goroutine"</span>
	<span class="p">}()</span>
	<span class="n">time</span><span class="o">.</span><span class="n">Sleep</span><span class="p">(</span><span class="m">50</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Millisecond</span><span class="p">)</span>
	<span class="n">msg</span> <span class="o">:=</span> <span class="o">&lt;-</span><span class="n">ch</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  receiver: got %q</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">msg</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The goroutine’s <code class="language-plaintext highlighter-rouge">ch &lt;- "hello from goroutine"</code> blocks until <code class="language-plaintext highlighter-rouge">main</code> executes <code class="language-plaintext highlighter-rouge">msg := &lt;-ch</code>. The sleep in main demonstrates that the sender will patiently wait — there is no data loss.</p>

<h3 id="buffered-channels">Buffered Channels</h3>

<p>A buffered channel has internal capacity. Sends do not block until the buffer is full, and receives do not block as long as the buffer is non-empty:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">part2_buffered</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">ch</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="k">chan</span> <span class="kt">int</span><span class="p">,</span> <span class="m">3</span><span class="p">)</span>
	<span class="n">ch</span> <span class="o">&lt;-</span> <span class="m">10</span>
	<span class="n">ch</span> <span class="o">&lt;-</span> <span class="m">20</span>
	<span class="n">ch</span> <span class="o">&lt;-</span> <span class="m">30</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  sent 3 values without blocking (len=%d, cap=%d)</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">ch</span><span class="p">),</span> <span class="nb">cap</span><span class="p">(</span><span class="n">ch</span><span class="p">))</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  received: %d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="o">&lt;-</span><span class="n">ch</span><span class="p">)</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  received: %d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="o">&lt;-</span><span class="n">ch</span><span class="p">)</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  received: %d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="o">&lt;-</span><span class="n">ch</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">make(chan int, 3)</code> creates a channel with a buffer of three. All three sends succeed without blocking because the buffer has room. The <code class="language-plaintext highlighter-rouge">len</code> function returns how many values are currently in the buffer, and <code class="language-plaintext highlighter-rouge">cap</code> returns the total capacity.</p>

<h3 id="directional-channels">Directional Channels</h3>

<p>Channel parameters can be restricted to send-only or receive-only. This makes the intended data flow explicit and catches mistakes at compile time:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">produce</span><span class="p">(</span><span class="n">out</span> <span class="k">chan</span><span class="o">&lt;-</span> <span class="kt">string</span><span class="p">)</span> <span class="p">{</span>
	<span class="n">changes</span> <span class="o">:=</span> <span class="p">[]</span><span class="kt">string</span><span class="p">{</span><span class="s">"INSERT alice"</span><span class="p">,</span> <span class="s">"UPDATE bob"</span><span class="p">,</span> <span class="s">"DELETE charlie"</span><span class="p">}</span>
	<span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">c</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">changes</span> <span class="p">{</span>
		<span class="n">out</span> <span class="o">&lt;-</span> <span class="n">c</span>
	<span class="p">}</span>
	<span class="nb">close</span><span class="p">(</span><span class="n">out</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">consume</span><span class="p">(</span><span class="n">in</span> <span class="o">&lt;-</span><span class="k">chan</span> <span class="kt">string</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">for</span> <span class="n">msg</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">in</span> <span class="p">{</span>
		<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  consumer received: %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">msg</span><span class="p">)</span>
	<span class="p">}</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">part3_directional</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">ch</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="k">chan</span> <span class="kt">string</span><span class="p">,</span> <span class="m">5</span><span class="p">)</span>
	<span class="k">go</span> <span class="n">produce</span><span class="p">(</span><span class="n">ch</span><span class="p">)</span>
	<span class="n">consume</span><span class="p">(</span><span class="n">ch</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">chan&lt;-</code> type means the function can only send to the channel. The <code class="language-plaintext highlighter-rouge">&lt;-chan</code> type means the function can only receive. A bidirectional <code class="language-plaintext highlighter-rouge">chan string</code> is automatically convertible to either direction when passed as an argument.</p>

<h3 id="close-and-range">Close and Range</h3>

<p>Closing a channel signals that no more values will be sent. A <code class="language-plaintext highlighter-rouge">for range</code> loop over a channel receives values until the channel is closed:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">part4_closeAndRange</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">ch</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="k">chan</span> <span class="kt">int</span><span class="p">,</span> <span class="m">5</span><span class="p">)</span>
	<span class="k">for</span> <span class="n">i</span> <span class="o">:=</span> <span class="m">1</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;=</span> <span class="m">5</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span> <span class="p">{</span> <span class="n">ch</span> <span class="o">&lt;-</span> <span class="n">i</span> <span class="o">*</span> <span class="m">10</span> <span class="p">}</span>
	<span class="nb">close</span><span class="p">(</span><span class="n">ch</span><span class="p">)</span>
	<span class="k">for</span> <span class="n">val</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">ch</span> <span class="p">{</span>
		<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"    got: %d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">val</span><span class="p">)</span>
	<span class="p">}</span>
	<span class="n">val</span><span class="p">,</span> <span class="n">ok</span> <span class="o">:=</span> <span class="o">&lt;-</span><span class="n">ch</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  receive after close: val=%d, ok=%t  &lt;- zero value, ok=false</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">val</span><span class="p">,</span> <span class="n">ok</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>After <code class="language-plaintext highlighter-rouge">close(ch)</code>, the <code class="language-plaintext highlighter-rouge">for range</code> loop drains all remaining values and then exits. A receive on a closed, empty channel returns the zero value immediately with <code class="language-plaintext highlighter-rouge">ok=false</code>. Only the sender should close a channel — closing a channel that has already been closed, or sending on a closed channel, will panic.</p>

<h3 id="producer-consumer-pipeline">Producer-Consumer Pipeline</h3>

<p>Combining goroutines, channels, and close gives you a clean producer-consumer pattern. The producer sends structured data, closes the channel when done, and the consumer processes everything until the channel is drained:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
</pre></td><td class="rouge-code"><pre><span class="k">type</span> <span class="n">TxBatch</span> <span class="k">struct</span> <span class="p">{</span>
	<span class="n">Table</span>   <span class="kt">string</span>
	<span class="n">Changes</span> <span class="p">[]</span><span class="kt">string</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">part5_producerConsumer</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">ch</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="k">chan</span> <span class="n">TxBatch</span><span class="p">,</span> <span class="m">5</span><span class="p">)</span>
	<span class="k">var</span> <span class="n">wg</span> <span class="n">sync</span><span class="o">.</span><span class="n">WaitGroup</span>

	<span class="n">wg</span><span class="o">.</span><span class="n">Add</span><span class="p">(</span><span class="m">1</span><span class="p">)</span>
	<span class="k">go</span> <span class="k">func</span><span class="p">()</span> <span class="p">{</span>
		<span class="k">defer</span> <span class="n">wg</span><span class="o">.</span><span class="n">Done</span><span class="p">()</span>
		<span class="n">batches</span> <span class="o">:=</span> <span class="p">[]</span><span class="n">TxBatch</span><span class="p">{</span>
			<span class="p">{</span><span class="n">Table</span><span class="o">:</span> <span class="s">"users"</span><span class="p">,</span> <span class="n">Changes</span><span class="o">:</span> <span class="p">[]</span><span class="kt">string</span><span class="p">{</span><span class="s">"INSERT alice"</span><span class="p">,</span> <span class="s">"INSERT bob"</span><span class="p">}},</span>
			<span class="p">{</span><span class="n">Table</span><span class="o">:</span> <span class="s">"orders"</span><span class="p">,</span> <span class="n">Changes</span><span class="o">:</span> <span class="p">[]</span><span class="kt">string</span><span class="p">{</span><span class="s">"INSERT ord-1"</span><span class="p">}},</span>
			<span class="p">{</span><span class="n">Table</span><span class="o">:</span> <span class="s">"users"</span><span class="p">,</span> <span class="n">Changes</span><span class="o">:</span> <span class="p">[]</span><span class="kt">string</span><span class="p">{</span><span class="s">"UPDATE alice"</span><span class="p">}},</span>
			<span class="p">{</span><span class="n">Table</span><span class="o">:</span> <span class="s">"events"</span><span class="p">,</span> <span class="n">Changes</span><span class="o">:</span> <span class="p">[]</span><span class="kt">string</span><span class="p">{</span><span class="s">"INSERT evt-1"</span><span class="p">,</span> <span class="s">"INSERT evt-2"</span><span class="p">}},</span>
		<span class="p">}</span>
		<span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">b</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">batches</span> <span class="p">{</span> <span class="n">ch</span> <span class="o">&lt;-</span> <span class="n">b</span> <span class="p">}</span>
		<span class="nb">close</span><span class="p">(</span><span class="n">ch</span><span class="p">)</span>
	<span class="p">}()</span>

	<span class="n">wg</span><span class="o">.</span><span class="n">Add</span><span class="p">(</span><span class="m">1</span><span class="p">)</span>
	<span class="k">go</span> <span class="k">func</span><span class="p">()</span> <span class="p">{</span>
		<span class="k">defer</span> <span class="n">wg</span><span class="o">.</span><span class="n">Done</span><span class="p">()</span>
		<span class="n">total</span> <span class="o">:=</span> <span class="m">0</span>
		<span class="k">for</span> <span class="n">batch</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">ch</span> <span class="p">{</span>
			<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  consumer: processing %s batch (%d changes)</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">batch</span><span class="o">.</span><span class="n">Table</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">batch</span><span class="o">.</span><span class="n">Changes</span><span class="p">))</span>
			<span class="n">total</span> <span class="o">+=</span> <span class="nb">len</span><span class="p">(</span><span class="n">batch</span><span class="o">.</span><span class="n">Changes</span><span class="p">)</span>
		<span class="p">}</span>
		<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  consumer: done, processed %d total changes</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">total</span><span class="p">)</span>
	<span class="p">}()</span>

	<span class="n">wg</span><span class="o">.</span><span class="n">Wait</span><span class="p">()</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The producer goroutine sends four batches and then closes the channel. The consumer goroutine ranges over the channel, processing each batch as it arrives. The <code class="language-plaintext highlighter-rouge">WaitGroup</code> ensures <code class="language-plaintext highlighter-rouge">main</code> waits for both goroutines to finish. This pattern scales naturally — you can add more consumers by launching additional goroutines that range over the same channel.</p>

<h2 id="select-and-ticker">Select and Ticker</h2>

<p>The <code class="language-plaintext highlighter-rouge">select</code> statement lets a goroutine wait on multiple channel operations simultaneously. It looks like a <code class="language-plaintext highlighter-rouge">switch</code>, but each case is a channel send or receive. When multiple cases are ready, Go picks one at random — this prevents starvation. Combined with tickers and timeouts, <code class="language-plaintext highlighter-rouge">select</code> is the building block for event loops, polling, and graceful shutdown.</p>

<h3 id="basic-select">Basic Select</h3>

<p>A <code class="language-plaintext highlighter-rouge">select</code> with multiple cases executes whichever channel operation is ready. If multiple are ready, one is chosen at random:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">part1_basicSelect</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">ch1</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="k">chan</span> <span class="kt">string</span><span class="p">,</span> <span class="m">1</span><span class="p">)</span>
	<span class="n">ch2</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="k">chan</span> <span class="kt">string</span><span class="p">,</span> <span class="m">1</span><span class="p">)</span>
	<span class="n">ch2</span> <span class="o">&lt;-</span> <span class="s">"hello from ch2"</span>
	<span class="k">select</span> <span class="p">{</span>
	<span class="k">case</span> <span class="n">msg</span> <span class="o">:=</span> <span class="o">&lt;-</span><span class="n">ch1</span><span class="o">:</span>
		<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  received from ch1: %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">msg</span><span class="p">)</span>
	<span class="k">case</span> <span class="n">msg</span> <span class="o">:=</span> <span class="o">&lt;-</span><span class="n">ch2</span><span class="o">:</span>
		<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  received from ch2: %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">msg</span><span class="p">)</span>
	<span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Only <code class="language-plaintext highlighter-rouge">ch2</code> has a value ready, so the second case executes. If both channels had values, either case could run — the runtime makes a pseudo-random choice.</p>

<h3 id="ticker">Ticker</h3>

<p>A <code class="language-plaintext highlighter-rouge">time.Ticker</code> delivers ticks at regular intervals on its <code class="language-plaintext highlighter-rouge">C</code> channel. Combined with <code class="language-plaintext highlighter-rouge">select</code>, it creates a periodic loop that can also respond to other events:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">part2_ticker</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">ticker</span> <span class="o">:=</span> <span class="n">time</span><span class="o">.</span><span class="n">NewTicker</span><span class="p">(</span><span class="m">100</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Millisecond</span><span class="p">)</span>
	<span class="k">defer</span> <span class="n">ticker</span><span class="o">.</span><span class="n">Stop</span><span class="p">()</span>
	<span class="n">done</span> <span class="o">:=</span> <span class="n">time</span><span class="o">.</span><span class="n">After</span><span class="p">(</span><span class="m">350</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Millisecond</span><span class="p">)</span>
	<span class="n">count</span> <span class="o">:=</span> <span class="m">0</span>
	<span class="k">for</span> <span class="p">{</span>
		<span class="k">select</span> <span class="p">{</span>
		<span class="k">case</span> <span class="n">t</span> <span class="o">:=</span> <span class="o">&lt;-</span><span class="n">ticker</span><span class="o">.</span><span class="n">C</span><span class="o">:</span>
			<span class="n">count</span><span class="o">++</span>
			<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  tick %d at %v</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">count</span><span class="p">,</span> <span class="n">t</span><span class="o">.</span><span class="n">Format</span><span class="p">(</span><span class="s">"15:04:05.000"</span><span class="p">))</span>
		<span class="k">case</span> <span class="o">&lt;-</span><span class="n">done</span><span class="o">:</span>
			<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  ticker stopped after %d ticks</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">count</span><span class="p">)</span>
			<span class="k">return</span>
		<span class="p">}</span>
	<span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The ticker fires every 100ms, and the <code class="language-plaintext highlighter-rouge">time.After</code> channel fires once after 350ms to shut the loop down. Always call <code class="language-plaintext highlighter-rouge">ticker.Stop()</code> to release the ticker’s resources. Without the <code class="language-plaintext highlighter-rouge">done</code> channel, this loop would run forever.</p>

<h3 id="timeout-with-timeafter">Timeout with time.After</h3>

<p><code class="language-plaintext highlighter-rouge">time.After</code> returns a channel that receives a single value after the specified duration. Inside a <code class="language-plaintext highlighter-rouge">select</code>, it acts as a deadline for any other channel operation:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">part3_timeout</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">slowCh</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="k">chan</span> <span class="kt">string</span><span class="p">)</span>
	<span class="k">go</span> <span class="k">func</span><span class="p">()</span> <span class="p">{</span>
		<span class="n">time</span><span class="o">.</span><span class="n">Sleep</span><span class="p">(</span><span class="m">500</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Millisecond</span><span class="p">)</span>
		<span class="n">slowCh</span> <span class="o">&lt;-</span> <span class="s">"slow result"</span>
	<span class="p">}()</span>
	<span class="k">select</span> <span class="p">{</span>
	<span class="k">case</span> <span class="n">result</span> <span class="o">:=</span> <span class="o">&lt;-</span><span class="n">slowCh</span><span class="o">:</span>
		<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  got result: %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">result</span><span class="p">)</span>
	<span class="k">case</span> <span class="o">&lt;-</span><span class="n">time</span><span class="o">.</span><span class="n">After</span><span class="p">(</span><span class="m">100</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Millisecond</span><span class="p">)</span><span class="o">:</span>
		<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"  timed out! (100ms elapsed before result arrived)"</span><span class="p">)</span>
	<span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The goroutine takes 500ms but the timeout is 100ms, so the timeout case wins. This pattern is useful for one-off operations, but for anything more complex, the <code class="language-plaintext highlighter-rouge">context</code> package (covered below) provides better cancellation semantics.</p>

<h3 id="non-blocking-select-with-default">Non-Blocking Select with Default</h3>

<p>Adding a <code class="language-plaintext highlighter-rouge">default</code> case makes a <code class="language-plaintext highlighter-rouge">select</code> non-blocking. If no channel operation is immediately ready, the <code class="language-plaintext highlighter-rouge">default</code> case runs:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">part4_nonBlocking</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">ch</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="k">chan</span> <span class="kt">string</span><span class="p">,</span> <span class="m">1</span><span class="p">)</span>
	<span class="k">select</span> <span class="p">{</span>
	<span class="k">case</span> <span class="n">msg</span> <span class="o">:=</span> <span class="o">&lt;-</span><span class="n">ch</span><span class="o">:</span>
		<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  received: %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">msg</span><span class="p">)</span>
	<span class="k">default</span><span class="o">:</span>
		<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"  no message ready (default executed)"</span><span class="p">)</span>
	<span class="p">}</span>
	<span class="n">ch</span> <span class="o">&lt;-</span> <span class="s">"buffered message"</span>
	<span class="k">select</span> <span class="p">{</span>
	<span class="k">case</span> <span class="n">msg</span> <span class="o">:=</span> <span class="o">&lt;-</span><span class="n">ch</span><span class="o">:</span>
		<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  received: %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">msg</span><span class="p">)</span>
	<span class="k">default</span><span class="o">:</span>
		<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"  no message ready"</span><span class="p">)</span>
	<span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The first <code class="language-plaintext highlighter-rouge">select</code> finds an empty channel, so <code class="language-plaintext highlighter-rouge">default</code> runs. After sending a value, the second <code class="language-plaintext highlighter-rouge">select</code> succeeds on the receive case. Non-blocking selects are useful for polling or try-send patterns where you want to make progress without waiting.</p>

<h3 id="event-loop-pattern">Event Loop Pattern</h3>

<p>Combining <code class="language-plaintext highlighter-rouge">select</code> with multiple channels creates an event loop — a goroutine that reacts to different kinds of events as they arrive. This is the backbone of many server and pipeline designs:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">part5_eventLoop</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">ctx</span><span class="p">,</span> <span class="n">cancel</span> <span class="o">:=</span> <span class="n">context</span><span class="o">.</span><span class="n">WithTimeout</span><span class="p">(</span><span class="n">context</span><span class="o">.</span><span class="n">Background</span><span class="p">(),</span> <span class="m">350</span><span class="o">*</span><span class="n">time</span><span class="o">.</span><span class="n">Millisecond</span><span class="p">)</span>
	<span class="k">defer</span> <span class="n">cancel</span><span class="p">()</span>
	<span class="n">input</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="k">chan</span> <span class="kt">string</span><span class="p">,</span> <span class="m">10</span><span class="p">)</span>
	<span class="k">go</span> <span class="k">func</span><span class="p">()</span> <span class="p">{</span>
		<span class="n">changes</span> <span class="o">:=</span> <span class="p">[]</span><span class="kt">string</span><span class="p">{</span><span class="s">"INSERT users alice"</span><span class="p">,</span> <span class="s">"UPDATE users bob"</span><span class="p">,</span> <span class="s">"DELETE sessions old"</span><span class="p">}</span>
		<span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">c</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">changes</span> <span class="p">{</span>
			<span class="n">time</span><span class="o">.</span><span class="n">Sleep</span><span class="p">(</span><span class="m">80</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Millisecond</span><span class="p">)</span>
			<span class="n">input</span> <span class="o">&lt;-</span> <span class="n">c</span>
		<span class="p">}</span>
	<span class="p">}()</span>
	<span class="n">ticker</span> <span class="o">:=</span> <span class="n">time</span><span class="o">.</span><span class="n">NewTicker</span><span class="p">(</span><span class="m">150</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Millisecond</span><span class="p">)</span>
	<span class="k">defer</span> <span class="n">ticker</span><span class="o">.</span><span class="n">Stop</span><span class="p">()</span>
	<span class="k">var</span> <span class="n">buffer</span> <span class="p">[]</span><span class="kt">string</span>
	<span class="k">for</span> <span class="p">{</span>
		<span class="k">select</span> <span class="p">{</span>
		<span class="k">case</span> <span class="o">&lt;-</span><span class="n">ctx</span><span class="o">.</span><span class="n">Done</span><span class="p">()</span><span class="o">:</span>
			<span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">buffer</span><span class="p">)</span> <span class="o">&gt;</span> <span class="m">0</span> <span class="p">{</span>
				<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  [shutdown] flushing %d remaining items</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">buffer</span><span class="p">))</span>
			<span class="p">}</span>
			<span class="k">return</span>
		<span class="k">case</span> <span class="n">change</span> <span class="o">:=</span> <span class="o">&lt;-</span><span class="n">input</span><span class="o">:</span>
			<span class="n">buffer</span> <span class="o">=</span> <span class="nb">append</span><span class="p">(</span><span class="n">buffer</span><span class="p">,</span> <span class="n">change</span><span class="p">)</span>
			<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  [input] buffered: %s (buffer size: %d)</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">change</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">buffer</span><span class="p">))</span>
		<span class="k">case</span> <span class="o">&lt;-</span><span class="n">ticker</span><span class="o">.</span><span class="n">C</span><span class="o">:</span>
			<span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">buffer</span><span class="p">)</span> <span class="o">&gt;</span> <span class="m">0</span> <span class="p">{</span>
				<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  [ticker] flushing %d items</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">buffer</span><span class="p">))</span>
				<span class="n">buffer</span> <span class="o">=</span> <span class="n">buffer</span><span class="p">[</span><span class="o">:</span><span class="m">0</span><span class="p">]</span>
			<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
				<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"  [ticker] nothing to flush"</span><span class="p">)</span>
			<span class="p">}</span>
		<span class="p">}</span>
	<span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>This loop handles three kinds of events: incoming data (buffered for batch processing), periodic flushes (driven by the ticker), and shutdown (driven by the context timeout). Each iteration of the loop blocks in <code class="language-plaintext highlighter-rouge">select</code> until one of the three channels is ready. The context timeout ensures the loop eventually terminates, and the shutdown case flushes any remaining buffered items.</p>

<h2 id="context">Context</h2>

<p>The <code class="language-plaintext highlighter-rouge">context</code> package provides a standard way to carry deadlines, cancellation signals, and request-scoped values across API boundaries and between goroutines. Almost every Go program that does I/O or spawns goroutines should use contexts. The core idea is simple: a parent creates a context, passes it to child goroutines, and can cancel it at any time — all children see the cancellation immediately.</p>

<h3 id="withcancel">WithCancel</h3>

<p><code class="language-plaintext highlighter-rouge">context.WithCancel</code> returns a new context and a <code class="language-plaintext highlighter-rouge">cancel</code> function. Calling <code class="language-plaintext highlighter-rouge">cancel</code> closes the context’s <code class="language-plaintext highlighter-rouge">Done</code> channel, which unblocks any goroutine waiting on it:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">part1_withCancel</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">ctx</span><span class="p">,</span> <span class="n">cancel</span> <span class="o">:=</span> <span class="n">context</span><span class="o">.</span><span class="n">WithCancel</span><span class="p">(</span><span class="n">context</span><span class="o">.</span><span class="n">Background</span><span class="p">())</span>
	<span class="k">go</span> <span class="k">func</span><span class="p">()</span> <span class="p">{</span>
		<span class="o">&lt;-</span><span class="n">ctx</span><span class="o">.</span><span class="n">Done</span><span class="p">()</span>
		<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  goroutine: context cancelled (%v)</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">ctx</span><span class="o">.</span><span class="n">Err</span><span class="p">())</span>
	<span class="p">}()</span>
	<span class="n">time</span><span class="o">.</span><span class="n">Sleep</span><span class="p">(</span><span class="m">50</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Millisecond</span><span class="p">)</span>
	<span class="n">cancel</span><span class="p">()</span>
	<span class="n">time</span><span class="o">.</span><span class="n">Sleep</span><span class="p">(</span><span class="m">10</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Millisecond</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The goroutine blocks on <code class="language-plaintext highlighter-rouge">&lt;-ctx.Done()</code> until the main goroutine calls <code class="language-plaintext highlighter-rouge">cancel()</code>. After cancellation, <code class="language-plaintext highlighter-rouge">ctx.Err()</code> returns <code class="language-plaintext highlighter-rouge">context.Canceled</code>. This is the simplest cancellation mechanism — the parent decides when to stop, and the children listen.</p>

<h3 id="withtimeout">WithTimeout</h3>

<p><code class="language-plaintext highlighter-rouge">context.WithTimeout</code> creates a context that cancels itself automatically after a duration. If the work finishes before the deadline, the deferred <code class="language-plaintext highlighter-rouge">cancel</code> call releases resources early:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">part2_withTimeout</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">ctx</span><span class="p">,</span> <span class="n">cancel</span> <span class="o">:=</span> <span class="n">context</span><span class="o">.</span><span class="n">WithTimeout</span><span class="p">(</span><span class="n">context</span><span class="o">.</span><span class="n">Background</span><span class="p">(),</span> <span class="m">100</span><span class="o">*</span><span class="n">time</span><span class="o">.</span><span class="n">Millisecond</span><span class="p">)</span>
	<span class="k">defer</span> <span class="n">cancel</span><span class="p">()</span>
	<span class="k">select</span> <span class="p">{</span>
	<span class="k">case</span> <span class="o">&lt;-</span><span class="n">ctx</span><span class="o">.</span><span class="n">Done</span><span class="p">()</span><span class="o">:</span>
		<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  context done: %v</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">ctx</span><span class="o">.</span><span class="n">Err</span><span class="p">())</span>
	<span class="p">}</span>

	<span class="n">ctx2</span><span class="p">,</span> <span class="n">cancel2</span> <span class="o">:=</span> <span class="n">context</span><span class="o">.</span><span class="n">WithTimeout</span><span class="p">(</span><span class="n">context</span><span class="o">.</span><span class="n">Background</span><span class="p">(),</span> <span class="m">200</span><span class="o">*</span><span class="n">time</span><span class="o">.</span><span class="n">Millisecond</span><span class="p">)</span>
	<span class="k">defer</span> <span class="n">cancel2</span><span class="p">()</span>
	<span class="n">result</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="k">chan</span> <span class="kt">string</span><span class="p">,</span> <span class="m">1</span><span class="p">)</span>
	<span class="k">go</span> <span class="k">func</span><span class="p">()</span> <span class="p">{</span>
		<span class="n">time</span><span class="o">.</span><span class="n">Sleep</span><span class="p">(</span><span class="m">50</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Millisecond</span><span class="p">)</span>
		<span class="n">result</span> <span class="o">&lt;-</span> <span class="s">"computation complete"</span>
	<span class="p">}()</span>
	<span class="k">select</span> <span class="p">{</span>
	<span class="k">case</span> <span class="n">r</span> <span class="o">:=</span> <span class="o">&lt;-</span><span class="n">result</span><span class="o">:</span>
		<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  got result before timeout: %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">r</span><span class="p">)</span>
	<span class="k">case</span> <span class="o">&lt;-</span><span class="n">ctx2</span><span class="o">.</span><span class="n">Done</span><span class="p">()</span><span class="o">:</span>
		<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  timed out: %v</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">ctx2</span><span class="o">.</span><span class="n">Err</span><span class="p">())</span>
	<span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The first context times out after 100ms because nothing else happens — <code class="language-plaintext highlighter-rouge">ctx.Err()</code> returns <code class="language-plaintext highlighter-rouge">context.DeadlineExceeded</code>. The second context has a 200ms deadline but the goroutine completes in 50ms, so the result arrives in time. Always <code class="language-plaintext highlighter-rouge">defer cancel()</code> even with timeouts — it frees internal timers immediately instead of waiting for the deadline.</p>

<h3 id="cancellation-cascade">Cancellation Cascade</h3>

<p>Contexts form a tree. When a parent context is cancelled, all of its children and grandchildren are cancelled too. This is how you propagate shutdown signals through a hierarchy of goroutines:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">part3_cascading</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">parent</span><span class="p">,</span> <span class="n">parentCancel</span> <span class="o">:=</span> <span class="n">context</span><span class="o">.</span><span class="n">WithCancel</span><span class="p">(</span><span class="n">context</span><span class="o">.</span><span class="n">Background</span><span class="p">())</span>
	<span class="n">child1</span><span class="p">,</span> <span class="n">child1Cancel</span> <span class="o">:=</span> <span class="n">context</span><span class="o">.</span><span class="n">WithCancel</span><span class="p">(</span><span class="n">parent</span><span class="p">)</span>
	<span class="k">defer</span> <span class="n">child1Cancel</span><span class="p">()</span>
	<span class="n">child2</span><span class="p">,</span> <span class="n">child2Cancel</span> <span class="o">:=</span> <span class="n">context</span><span class="o">.</span><span class="n">WithCancel</span><span class="p">(</span><span class="n">parent</span><span class="p">)</span>
	<span class="k">defer</span> <span class="n">child2Cancel</span><span class="p">()</span>
	<span class="n">grandchild</span><span class="p">,</span> <span class="n">grandchildCancel</span> <span class="o">:=</span> <span class="n">context</span><span class="o">.</span><span class="n">WithCancel</span><span class="p">(</span><span class="n">child1</span><span class="p">)</span>
	<span class="k">defer</span> <span class="n">grandchildCancel</span><span class="p">()</span>
	<span class="n">parentCancel</span><span class="p">()</span>
	<span class="n">time</span><span class="o">.</span><span class="n">Sleep</span><span class="p">(</span><span class="m">10</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Millisecond</span><span class="p">)</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  parent err:     %v</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">parent</span><span class="o">.</span><span class="n">Err</span><span class="p">())</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  child1 err:     %v</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">child1</span><span class="o">.</span><span class="n">Err</span><span class="p">())</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  child2 err:     %v</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">child2</span><span class="o">.</span><span class="n">Err</span><span class="p">())</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  grandchild err: %v</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">grandchild</span><span class="o">.</span><span class="n">Err</span><span class="p">())</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>After <code class="language-plaintext highlighter-rouge">parentCancel()</code>, all four contexts report <code class="language-plaintext highlighter-rouge">context.Canceled</code>. Cancelling <code class="language-plaintext highlighter-rouge">child1Cancel()</code> would only affect <code class="language-plaintext highlighter-rouge">child1</code> and <code class="language-plaintext highlighter-rouge">grandchild</code>, leaving <code class="language-plaintext highlighter-rouge">parent</code> and <code class="language-plaintext highlighter-rouge">child2</code> untouched. This tree structure mirrors the natural structure of request handling — an HTTP handler creates a context, passes it to a database call, which passes it to a retry loop, and cancelling the handler cancels everything downstream.</p>

<h3 id="ctxerr--cancellation-vs-timeout">ctx.Err() — Cancellation vs Timeout</h3>

<p>The <code class="language-plaintext highlighter-rouge">Err()</code> method on a context tells you why it was cancelled. There are exactly two possible non-nil values:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">part4_ctxErr</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">ctx1</span><span class="p">,</span> <span class="n">cancel1</span> <span class="o">:=</span> <span class="n">context</span><span class="o">.</span><span class="n">WithCancel</span><span class="p">(</span><span class="n">context</span><span class="o">.</span><span class="n">Background</span><span class="p">())</span>
	<span class="n">cancel1</span><span class="p">()</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  after cancel(): ctx.Err() = %v</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">ctx1</span><span class="o">.</span><span class="n">Err</span><span class="p">())</span>
	<span class="n">ctx2</span><span class="p">,</span> <span class="n">cancel2</span> <span class="o">:=</span> <span class="n">context</span><span class="o">.</span><span class="n">WithTimeout</span><span class="p">(</span><span class="n">context</span><span class="o">.</span><span class="n">Background</span><span class="p">(),</span> <span class="m">1</span><span class="o">*</span><span class="n">time</span><span class="o">.</span><span class="n">Millisecond</span><span class="p">)</span>
	<span class="k">defer</span> <span class="n">cancel2</span><span class="p">()</span>
	<span class="n">time</span><span class="o">.</span><span class="n">Sleep</span><span class="p">(</span><span class="m">10</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Millisecond</span><span class="p">)</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  after timeout:  ctx.Err() = %v</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">ctx2</span><span class="o">.</span><span class="n">Err</span><span class="p">())</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  context.Canceled == ctx1.Err()? %t</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">ctx1</span><span class="o">.</span><span class="n">Err</span><span class="p">()</span> <span class="o">==</span> <span class="n">context</span><span class="o">.</span><span class="n">Canceled</span><span class="p">)</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  DeadlineExceeded == ctx2.Err()? %t</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">ctx2</span><span class="o">.</span><span class="n">Err</span><span class="p">()</span> <span class="o">==</span> <span class="n">context</span><span class="o">.</span><span class="n">DeadlineExceeded</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">context.Canceled</code> means someone called the cancel function explicitly. <code class="language-plaintext highlighter-rouge">context.DeadlineExceeded</code> means the timeout or deadline elapsed. This distinction is important in practice — a cancellation usually means “the caller no longer cares about the result,” while a deadline exceeded usually means “the operation was too slow and should be retried or reported as an error.”</p>

<h3 id="per-attempt-deadlines">Per-Attempt Deadlines</h3>

<p>A powerful pattern combines a long-lived parent context with short-lived per-attempt contexts. Each attempt gets its own timeout, but the parent context enforces an overall deadline:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">part5_perAttemptDeadline</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">ctx</span><span class="p">,</span> <span class="n">cancel</span> <span class="o">:=</span> <span class="n">context</span><span class="o">.</span><span class="n">WithTimeout</span><span class="p">(</span><span class="n">context</span><span class="o">.</span><span class="n">Background</span><span class="p">(),</span> <span class="m">500</span><span class="o">*</span><span class="n">time</span><span class="o">.</span><span class="n">Millisecond</span><span class="p">)</span>
	<span class="k">defer</span> <span class="n">cancel</span><span class="p">()</span>
	<span class="k">for</span> <span class="n">attempt</span> <span class="o">:=</span> <span class="m">1</span><span class="p">;</span> <span class="p">;</span> <span class="n">attempt</span><span class="o">++</span> <span class="p">{</span>
		<span class="n">attemptCtx</span><span class="p">,</span> <span class="n">attemptCancel</span> <span class="o">:=</span> <span class="n">context</span><span class="o">.</span><span class="n">WithTimeout</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="m">120</span><span class="o">*</span><span class="n">time</span><span class="o">.</span><span class="n">Millisecond</span><span class="p">)</span>
		<span class="n">result</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">simulateReceive</span><span class="p">(</span><span class="n">attemptCtx</span><span class="p">,</span> <span class="n">attempt</span><span class="p">)</span>
		<span class="n">attemptCancel</span><span class="p">()</span>
		<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
			<span class="k">if</span> <span class="n">ctx</span><span class="o">.</span><span class="n">Err</span><span class="p">()</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
				<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  attempt %d: parent context done, shutting down</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">attempt</span><span class="p">)</span>
				<span class="k">break</span>
			<span class="p">}</span>
			<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  attempt %d: timed out, retrying...</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">attempt</span><span class="p">)</span>
			<span class="k">continue</span>
		<span class="p">}</span>
		<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  attempt %d: received %q</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">attempt</span><span class="p">,</span> <span class="n">result</span><span class="p">)</span>
	<span class="p">}</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">simulateReceive</span><span class="p">(</span><span class="n">ctx</span> <span class="n">context</span><span class="o">.</span><span class="n">Context</span><span class="p">,</span> <span class="n">attempt</span> <span class="kt">int</span><span class="p">)</span> <span class="p">(</span><span class="kt">string</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
	<span class="n">delay</span> <span class="o">:=</span> <span class="n">time</span><span class="o">.</span><span class="n">Duration</span><span class="p">(</span><span class="n">attempt</span><span class="o">*</span><span class="m">80</span><span class="p">)</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Millisecond</span>
	<span class="k">select</span> <span class="p">{</span>
	<span class="k">case</span> <span class="o">&lt;-</span><span class="n">time</span><span class="o">.</span><span class="n">After</span><span class="p">(</span><span class="n">delay</span><span class="p">)</span><span class="o">:</span>
		<span class="k">return</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"WAL message #%d"</span><span class="p">,</span> <span class="n">attempt</span><span class="p">),</span> <span class="no">nil</span>
	<span class="k">case</span> <span class="o">&lt;-</span><span class="n">ctx</span><span class="o">.</span><span class="n">Done</span><span class="p">()</span><span class="o">:</span>
		<span class="k">return</span> <span class="s">""</span><span class="p">,</span> <span class="n">ctx</span><span class="o">.</span><span class="n">Err</span><span class="p">()</span>
	<span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The parent context has a 500ms deadline. Each attempt gets 120ms. Early attempts complete quickly and succeed. As the simulated delay grows, individual attempts start timing out. Eventually the parent context expires, and the loop exits. The key check is <code class="language-plaintext highlighter-rouge">ctx.Err() != nil</code> — this distinguishes “this attempt timed out but we can retry” from “the overall operation is done.”</p>

<h2 id="sync-primitives-and-atomics">Sync Primitives and Atomics</h2>

<p>Channels are the preferred communication mechanism in Go, but sometimes you need shared mutable state. The <code class="language-plaintext highlighter-rouge">sync</code> package provides mutual exclusion locks, and the <code class="language-plaintext highlighter-rouge">sync/atomic</code> package provides lock-free operations on individual values. Both are lower-level than channels and should be used when the overhead of a channel is too high or when the access pattern does not fit the message-passing model.</p>

<h3 id="mutex">Mutex</h3>

<p>A <code class="language-plaintext highlighter-rouge">sync.Mutex</code> provides mutual exclusion. Only one goroutine can hold the lock at a time. All others block on <code class="language-plaintext highlighter-rouge">Lock()</code> until the holder calls <code class="language-plaintext highlighter-rouge">Unlock()</code>:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">part1_mutex</span><span class="p">()</span> <span class="p">{</span>
	<span class="k">var</span> <span class="n">mu</span> <span class="n">sync</span><span class="o">.</span><span class="n">Mutex</span>
	<span class="n">counter</span> <span class="o">:=</span> <span class="m">0</span>
	<span class="k">var</span> <span class="n">wg</span> <span class="n">sync</span><span class="o">.</span><span class="n">WaitGroup</span>
	<span class="k">for</span> <span class="n">i</span> <span class="o">:=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="m">100</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span> <span class="p">{</span>
		<span class="n">wg</span><span class="o">.</span><span class="n">Add</span><span class="p">(</span><span class="m">1</span><span class="p">)</span>
		<span class="k">go</span> <span class="k">func</span><span class="p">()</span> <span class="p">{</span>
			<span class="k">defer</span> <span class="n">wg</span><span class="o">.</span><span class="n">Done</span><span class="p">()</span>
			<span class="n">mu</span><span class="o">.</span><span class="n">Lock</span><span class="p">()</span>
			<span class="n">counter</span><span class="o">++</span>
			<span class="n">mu</span><span class="o">.</span><span class="n">Unlock</span><span class="p">()</span>
		<span class="p">}()</span>
	<span class="p">}</span>
	<span class="n">wg</span><span class="o">.</span><span class="n">Wait</span><span class="p">()</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  counter after 100 goroutines: %d (expected 100)</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">counter</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Without the mutex, concurrent <code class="language-plaintext highlighter-rouge">counter++</code> operations would race — <code class="language-plaintext highlighter-rouge">counter++</code> is not atomic; it reads, increments, and writes, and two goroutines could read the same value and both write the same incremented value, losing an update. The mutex serializes access so every increment is visible.</p>

<h3 id="rwmutex">RWMutex</h3>

<p>A <code class="language-plaintext highlighter-rouge">sync.RWMutex</code> distinguishes between readers and writers. Multiple goroutines can hold a read lock simultaneously, but a write lock is exclusive — no readers or other writers can proceed while it is held. This is ideal for data structures that are read far more often than they are written:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
</pre></td><td class="rouge-code"><pre><span class="k">type</span> <span class="n">SchemaCache</span> <span class="k">struct</span> <span class="p">{</span>
	<span class="n">mu</span>      <span class="n">sync</span><span class="o">.</span><span class="n">RWMutex</span>
	<span class="n">schemas</span> <span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">][]</span><span class="kt">string</span>
	<span class="n">ops</span>     <span class="n">atomic</span><span class="o">.</span><span class="n">Uint64</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">NewSchemaCache</span><span class="p">()</span> <span class="o">*</span><span class="n">SchemaCache</span> <span class="p">{</span>
	<span class="k">return</span> <span class="o">&amp;</span><span class="n">SchemaCache</span><span class="p">{</span><span class="n">schemas</span><span class="o">:</span> <span class="nb">make</span><span class="p">(</span><span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">][]</span><span class="kt">string</span><span class="p">)}</span>
<span class="p">}</span>

<span class="k">func</span> <span class="p">(</span><span class="n">c</span> <span class="o">*</span><span class="n">SchemaCache</span><span class="p">)</span> <span class="n">Get</span><span class="p">(</span><span class="n">table</span> <span class="kt">string</span><span class="p">)</span> <span class="p">([]</span><span class="kt">string</span><span class="p">,</span> <span class="kt">bool</span><span class="p">)</span> <span class="p">{</span>
	<span class="n">c</span><span class="o">.</span><span class="n">mu</span><span class="o">.</span><span class="n">RLock</span><span class="p">()</span>
	<span class="k">defer</span> <span class="n">c</span><span class="o">.</span><span class="n">mu</span><span class="o">.</span><span class="n">RUnlock</span><span class="p">()</span>
	<span class="n">c</span><span class="o">.</span><span class="n">ops</span><span class="o">.</span><span class="n">Add</span><span class="p">(</span><span class="m">1</span><span class="p">)</span>
	<span class="n">cols</span><span class="p">,</span> <span class="n">ok</span> <span class="o">:=</span> <span class="n">c</span><span class="o">.</span><span class="n">schemas</span><span class="p">[</span><span class="n">table</span><span class="p">]</span>
	<span class="k">return</span> <span class="n">cols</span><span class="p">,</span> <span class="n">ok</span>
<span class="p">}</span>

<span class="k">func</span> <span class="p">(</span><span class="n">c</span> <span class="o">*</span><span class="n">SchemaCache</span><span class="p">)</span> <span class="n">Set</span><span class="p">(</span><span class="n">table</span> <span class="kt">string</span><span class="p">,</span> <span class="n">columns</span> <span class="p">[]</span><span class="kt">string</span><span class="p">)</span> <span class="p">{</span>
	<span class="n">c</span><span class="o">.</span><span class="n">mu</span><span class="o">.</span><span class="n">Lock</span><span class="p">()</span>
	<span class="k">defer</span> <span class="n">c</span><span class="o">.</span><span class="n">mu</span><span class="o">.</span><span class="n">Unlock</span><span class="p">()</span>
	<span class="n">c</span><span class="o">.</span><span class="n">ops</span><span class="o">.</span><span class="n">Add</span><span class="p">(</span><span class="m">1</span><span class="p">)</span>
	<span class="n">c</span><span class="o">.</span><span class="n">schemas</span><span class="p">[</span><span class="n">table</span><span class="p">]</span> <span class="o">=</span> <span class="n">columns</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">Get</code> uses <code class="language-plaintext highlighter-rouge">RLock</code>/<code class="language-plaintext highlighter-rouge">RUnlock</code> — multiple readers can call <code class="language-plaintext highlighter-rouge">Get</code> concurrently without blocking each other. <code class="language-plaintext highlighter-rouge">Set</code> uses <code class="language-plaintext highlighter-rouge">Lock</code>/<code class="language-plaintext highlighter-rouge">Unlock</code> — it waits for all readers to finish and then holds exclusive access while writing. This is significantly more efficient than a plain <code class="language-plaintext highlighter-rouge">Mutex</code> when reads vastly outnumber writes.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">part2_rwMutex</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">cache</span> <span class="o">:=</span> <span class="n">NewSchemaCache</span><span class="p">()</span>
	<span class="n">cache</span><span class="o">.</span><span class="n">Set</span><span class="p">(</span><span class="s">"users"</span><span class="p">,</span> <span class="p">[]</span><span class="kt">string</span><span class="p">{</span><span class="s">"id"</span><span class="p">,</span> <span class="s">"name"</span><span class="p">,</span> <span class="s">"email"</span><span class="p">})</span>
	<span class="n">cache</span><span class="o">.</span><span class="n">Set</span><span class="p">(</span><span class="s">"orders"</span><span class="p">,</span> <span class="p">[]</span><span class="kt">string</span><span class="p">{</span><span class="s">"id"</span><span class="p">,</span> <span class="s">"user_id"</span><span class="p">,</span> <span class="s">"total"</span><span class="p">})</span>
	<span class="k">var</span> <span class="n">wg</span> <span class="n">sync</span><span class="o">.</span><span class="n">WaitGroup</span>
	<span class="k">for</span> <span class="n">i</span> <span class="o">:=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="m">10</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span> <span class="p">{</span>
		<span class="n">wg</span><span class="o">.</span><span class="n">Add</span><span class="p">(</span><span class="m">1</span><span class="p">)</span>
		<span class="k">go</span> <span class="k">func</span><span class="p">(</span><span class="n">id</span> <span class="kt">int</span><span class="p">)</span> <span class="p">{</span>
			<span class="k">defer</span> <span class="n">wg</span><span class="o">.</span><span class="n">Done</span><span class="p">()</span>
			<span class="k">if</span> <span class="n">cols</span><span class="p">,</span> <span class="n">ok</span> <span class="o">:=</span> <span class="n">cache</span><span class="o">.</span><span class="n">Get</span><span class="p">(</span><span class="s">"users"</span><span class="p">);</span> <span class="n">ok</span> <span class="p">{</span>
				<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  reader %d: got users schema (%d columns)</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">id</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">cols</span><span class="p">))</span>
			<span class="p">}</span>
		<span class="p">}(</span><span class="n">i</span><span class="p">)</span>
	<span class="p">}</span>
	<span class="n">wg</span><span class="o">.</span><span class="n">Add</span><span class="p">(</span><span class="m">1</span><span class="p">)</span>
	<span class="k">go</span> <span class="k">func</span><span class="p">()</span> <span class="p">{</span>
		<span class="k">defer</span> <span class="n">wg</span><span class="o">.</span><span class="n">Done</span><span class="p">()</span>
		<span class="n">cache</span><span class="o">.</span><span class="n">Set</span><span class="p">(</span><span class="s">"events"</span><span class="p">,</span> <span class="p">[]</span><span class="kt">string</span><span class="p">{</span><span class="s">"id"</span><span class="p">,</span> <span class="s">"type"</span><span class="p">,</span> <span class="s">"payload"</span><span class="p">,</span> <span class="s">"timestamp"</span><span class="p">})</span>
	<span class="p">}()</span>
	<span class="n">wg</span><span class="o">.</span><span class="n">Wait</span><span class="p">()</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  total cache ops: %d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">cache</span><span class="o">.</span><span class="n">TotalOps</span><span class="p">())</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Ten reader goroutines and one writer goroutine run concurrently. The readers can all proceed in parallel, and the writer waits its turn. The atomic operation counter tracks total operations without needing the mutex.</p>

<h3 id="atomics">Atomics</h3>

<p>The <code class="language-plaintext highlighter-rouge">sync/atomic</code> package provides lock-free operations on integer and pointer types. Atomics are faster than a mutex for simple counters or flags because they use hardware-level atomic instructions instead of OS-level locking:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">part3_atomics</span><span class="p">()</span> <span class="p">{</span>
	<span class="k">var</span> <span class="n">lsn</span> <span class="n">atomic</span><span class="o">.</span><span class="n">Uint64</span>
	<span class="n">lsn</span><span class="o">.</span><span class="n">Store</span><span class="p">(</span><span class="m">0</span><span class="p">)</span>
	<span class="k">var</span> <span class="n">wg</span> <span class="n">sync</span><span class="o">.</span><span class="n">WaitGroup</span>
	<span class="n">wg</span><span class="o">.</span><span class="n">Add</span><span class="p">(</span><span class="m">1</span><span class="p">)</span>
	<span class="k">go</span> <span class="k">func</span><span class="p">()</span> <span class="p">{</span>
		<span class="k">defer</span> <span class="n">wg</span><span class="o">.</span><span class="n">Done</span><span class="p">()</span>
		<span class="k">for</span> <span class="n">i</span> <span class="o">:=</span> <span class="kt">uint64</span><span class="p">(</span><span class="m">100</span><span class="p">);</span> <span class="n">i</span> <span class="o">&lt;=</span> <span class="m">500</span><span class="p">;</span> <span class="n">i</span> <span class="o">+=</span> <span class="m">100</span> <span class="p">{</span>
			<span class="n">lsn</span><span class="o">.</span><span class="n">Store</span><span class="p">(</span><span class="n">i</span><span class="p">)</span>
			<span class="n">time</span><span class="o">.</span><span class="n">Sleep</span><span class="p">(</span><span class="m">10</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Millisecond</span><span class="p">)</span>
		<span class="p">}</span>
	<span class="p">}()</span>
	<span class="n">wg</span><span class="o">.</span><span class="n">Add</span><span class="p">(</span><span class="m">1</span><span class="p">)</span>
	<span class="k">go</span> <span class="k">func</span><span class="p">()</span> <span class="p">{</span>
		<span class="k">defer</span> <span class="n">wg</span><span class="o">.</span><span class="n">Done</span><span class="p">()</span>
		<span class="k">for</span> <span class="n">i</span> <span class="o">:=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="m">5</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span> <span class="p">{</span>
			<span class="n">time</span><span class="o">.</span><span class="n">Sleep</span><span class="p">(</span><span class="m">15</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Millisecond</span><span class="p">)</span>
			<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  consumer: read LSN = %d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">lsn</span><span class="o">.</span><span class="n">Load</span><span class="p">())</span>
		<span class="p">}</span>
	<span class="p">}()</span>
	<span class="n">wg</span><span class="o">.</span><span class="n">Wait</span><span class="p">()</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  final LSN: %d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">lsn</span><span class="o">.</span><span class="n">Load</span><span class="p">())</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>One goroutine writes increasing values with <code class="language-plaintext highlighter-rouge">Store</code>, and another reads them with <code class="language-plaintext highlighter-rouge">Load</code>. These operations are individually atomic — the reader will never see a partially written value. However, atomics only protect individual reads and writes; if you need to read-modify-write multiple fields together, use a mutex.</p>

<p>The <code class="language-plaintext highlighter-rouge">Add</code> method performs an atomic increment, which is the most common use case:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre><span class="k">var</span> <span class="n">counter</span> <span class="n">atomic</span><span class="o">.</span><span class="n">Uint64</span>
<span class="k">var</span> <span class="n">wg2</span> <span class="n">sync</span><span class="o">.</span><span class="n">WaitGroup</span>
<span class="k">for</span> <span class="n">i</span> <span class="o">:=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="m">1000</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span> <span class="p">{</span>
	<span class="n">wg2</span><span class="o">.</span><span class="n">Add</span><span class="p">(</span><span class="m">1</span><span class="p">)</span>
	<span class="k">go</span> <span class="k">func</span><span class="p">()</span> <span class="p">{</span>
		<span class="k">defer</span> <span class="n">wg2</span><span class="o">.</span><span class="n">Done</span><span class="p">()</span>
		<span class="n">counter</span><span class="o">.</span><span class="n">Add</span><span class="p">(</span><span class="m">1</span><span class="p">)</span>
	<span class="p">}()</span>
<span class="p">}</span>
<span class="n">wg2</span><span class="o">.</span><span class="n">Wait</span><span class="p">()</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  atomic counter after 1000 increments: %d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">counter</span><span class="o">.</span><span class="n">Load</span><span class="p">())</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>All 1000 goroutines increment the counter concurrently, and the final value is exactly 1000 — no lock needed.</p>

<h3 id="cache-with-full-concurrency">Cache with Full Concurrency</h3>

<p>Putting it all together, here is the <code class="language-plaintext highlighter-rouge">SchemaCache</code> under realistic concurrent load — one writer populating the cache while multiple readers query it simultaneously:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">part4_cacheWithConcurrency</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">cache</span> <span class="o">:=</span> <span class="n">NewSchemaCache</span><span class="p">()</span>
	<span class="k">var</span> <span class="n">wg</span> <span class="n">sync</span><span class="o">.</span><span class="n">WaitGroup</span>
	<span class="n">wg</span><span class="o">.</span><span class="n">Add</span><span class="p">(</span><span class="m">1</span><span class="p">)</span>
	<span class="k">go</span> <span class="k">func</span><span class="p">()</span> <span class="p">{</span>
		<span class="k">defer</span> <span class="n">wg</span><span class="o">.</span><span class="n">Done</span><span class="p">()</span>
		<span class="n">tables</span> <span class="o">:=</span> <span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">][]</span><span class="kt">string</span><span class="p">{</span>
			<span class="s">"users"</span><span class="o">:</span>    <span class="p">{</span><span class="s">"id"</span><span class="p">,</span> <span class="s">"name"</span><span class="p">,</span> <span class="s">"email"</span><span class="p">,</span> <span class="s">"created_at"</span><span class="p">},</span>
			<span class="s">"orders"</span><span class="o">:</span>   <span class="p">{</span><span class="s">"id"</span><span class="p">,</span> <span class="s">"user_id"</span><span class="p">,</span> <span class="s">"total"</span><span class="p">,</span> <span class="s">"status"</span><span class="p">},</span>
			<span class="s">"events"</span><span class="o">:</span>   <span class="p">{</span><span class="s">"id"</span><span class="p">,</span> <span class="s">"type"</span><span class="p">,</span> <span class="s">"payload"</span><span class="p">},</span>
			<span class="s">"sessions"</span><span class="o">:</span> <span class="p">{</span><span class="s">"id"</span><span class="p">,</span> <span class="s">"user_id"</span><span class="p">,</span> <span class="s">"token"</span><span class="p">,</span> <span class="s">"expires_at"</span><span class="p">},</span>
		<span class="p">}</span>
		<span class="k">for</span> <span class="n">table</span><span class="p">,</span> <span class="n">cols</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">tables</span> <span class="p">{</span>
			<span class="n">cache</span><span class="o">.</span><span class="n">Set</span><span class="p">(</span><span class="n">table</span><span class="p">,</span> <span class="n">cols</span><span class="p">)</span>
			<span class="n">time</span><span class="o">.</span><span class="n">Sleep</span><span class="p">(</span><span class="m">10</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Millisecond</span><span class="p">)</span>
		<span class="p">}</span>
	<span class="p">}()</span>
	<span class="k">for</span> <span class="n">i</span> <span class="o">:=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="m">5</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span> <span class="p">{</span>
		<span class="n">wg</span><span class="o">.</span><span class="n">Add</span><span class="p">(</span><span class="m">1</span><span class="p">)</span>
		<span class="k">go</span> <span class="k">func</span><span class="p">(</span><span class="n">id</span> <span class="kt">int</span><span class="p">)</span> <span class="p">{</span>
			<span class="k">defer</span> <span class="n">wg</span><span class="o">.</span><span class="n">Done</span><span class="p">()</span>
			<span class="n">tables</span> <span class="o">:=</span> <span class="p">[]</span><span class="kt">string</span><span class="p">{</span><span class="s">"users"</span><span class="p">,</span> <span class="s">"orders"</span><span class="p">,</span> <span class="s">"events"</span><span class="p">,</span> <span class="s">"sessions"</span><span class="p">,</span> <span class="s">"missing"</span><span class="p">}</span>
			<span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">t</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">tables</span> <span class="p">{</span>
				<span class="n">time</span><span class="o">.</span><span class="n">Sleep</span><span class="p">(</span><span class="m">8</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Millisecond</span><span class="p">)</span>
				<span class="k">if</span> <span class="n">cols</span><span class="p">,</span> <span class="n">ok</span> <span class="o">:=</span> <span class="n">cache</span><span class="o">.</span><span class="n">Get</span><span class="p">(</span><span class="n">t</span><span class="p">);</span> <span class="n">ok</span> <span class="p">{</span>
					<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  reader %d: %s has %d columns</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">id</span><span class="p">,</span> <span class="n">t</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">cols</span><span class="p">))</span>
				<span class="p">}</span>
			<span class="p">}</span>
		<span class="p">}(</span><span class="n">i</span><span class="p">)</span>
	<span class="p">}</span>
	<span class="n">wg</span><span class="o">.</span><span class="n">Wait</span><span class="p">()</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  total ops: %d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">cache</span><span class="o">.</span><span class="n">TotalOps</span><span class="p">())</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The writer goroutine adds tables one at a time with a small delay between each. Five reader goroutines query for tables concurrently — some queries will find the table (if the writer has added it already), and some will miss (if the writer has not reached it yet or if the table does not exist). The <code class="language-plaintext highlighter-rouge">RWMutex</code> ensures that reads never see a partially written map entry, while the <code class="language-plaintext highlighter-rouge">atomic.Uint64</code> counter tracks total operations without requiring the lock. This pattern — a read-heavy cache protected by an <code class="language-plaintext highlighter-rouge">RWMutex</code> with atomic counters for metrics — is a common building block in Go services.</p>]]></content><author><name>kimserey</name></author><category term="go" /><summary type="html"><![CDATA[Go was designed with concurrency as a first-class feature. Rather than relying on OS threads and shared-memory locking as the primary model, Go provides goroutines (lightweight threads managed by the Go runtime), channels (typed conduits for communication between goroutines), and a select statement for multiplexing channel operations. The standard library rounds this out with a context package for cancellation and deadlines, and a sync package for traditional mutual exclusion when channels are not the right fit. This tutorial walks through all five building blocks, starting from basic goroutines and working up to a concurrent cache protected by read-write locks and atomics.]]></summary></entry><entry><title type="html">Go Error Handling and Resource Management</title><link href="https://www.kimsereylam.com/go/2026/09/02/go-error-handling-and-resource-management.html" rel="alternate" type="text/html" title="Go Error Handling and Resource Management" /><published>2026-09-02T00:00:00-05:00</published><updated>2026-09-02T00:00:00-05:00</updated><id>https://www.kimsereylam.com/go/2026/09/02/go-error-handling-and-resource-management</id><content type="html" xml:base="https://www.kimsereylam.com/go/2026/09/02/go-error-handling-and-resource-management.html"><![CDATA[<p>Go treats errors as ordinary values — there are no exceptions, no try/catch, and no hidden control flow. Every function that can fail returns an <code class="language-plaintext highlighter-rouge">error</code> alongside its result, and the caller decides what to do with it. For cleanup, Go provides <code class="language-plaintext highlighter-rouge">defer</code>, which guarantees a function call runs when the enclosing function returns, regardless of how it returns. Together, explicit error returns and <code class="language-plaintext highlighter-rouge">defer</code> give you predictable error propagation and deterministic resource management without the complexity of exception hierarchies or finalizers. In this post we cover Go’s error handling patterns — from basic checks through wrapping, sentinel errors, and context-aware error classification — then move to <code class="language-plaintext highlighter-rouge">defer</code> patterns including LIFO ordering, loop pitfalls, closure capture semantics, named return manipulation, and panic recovery.</p>

<!--more-->

<h2 id="error-handling">Error Handling</h2>

<h3 id="basic-errors">Basic Errors</h3>

<p>The simplest way to create an error in Go is <code class="language-plaintext highlighter-rouge">errors.New</code>, which returns a value that implements the <code class="language-plaintext highlighter-rouge">error</code> interface (any type with an <code class="language-plaintext highlighter-rouge">Error() string</code> method). For formatted messages, <code class="language-plaintext highlighter-rouge">fmt.Errorf</code> works like <code class="language-plaintext highlighter-rouge">fmt.Sprintf</code> but returns an <code class="language-plaintext highlighter-rouge">error</code>. The convention is to return a zero value alongside the error, and <code class="language-plaintext highlighter-rouge">nil</code> for the error on success.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
</pre></td><td class="rouge-code"><pre><span class="k">package</span> <span class="n">main</span>

<span class="k">import</span> <span class="p">(</span>
	<span class="s">"errors"</span>
	<span class="s">"fmt"</span>
<span class="p">)</span>

<span class="k">func</span> <span class="n">divide</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span> <span class="kt">int</span><span class="p">)</span> <span class="p">(</span><span class="kt">int</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">if</span> <span class="n">b</span> <span class="o">==</span> <span class="m">0</span> <span class="p">{</span>
		<span class="k">return</span> <span class="m">0</span><span class="p">,</span> <span class="n">errors</span><span class="o">.</span><span class="n">New</span><span class="p">(</span><span class="s">"division by zero"</span><span class="p">)</span>
	<span class="p">}</span>
	<span class="k">return</span> <span class="n">a</span> <span class="o">/</span> <span class="n">b</span><span class="p">,</span> <span class="no">nil</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">err</span> <span class="o">:=</span> <span class="n">errors</span><span class="o">.</span><span class="n">New</span><span class="p">(</span><span class="s">"connection refused"</span><span class="p">)</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"errors.New: %v</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>

	<span class="n">err2</span> <span class="o">:=</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"failed to connect to %s:%d"</span><span class="p">,</span> <span class="s">"localhost"</span><span class="p">,</span> <span class="m">5432</span><span class="p">)</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"fmt.Errorf: %v</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">err2</span><span class="p">)</span>

	<span class="n">result</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">divide</span><span class="p">(</span><span class="m">10</span><span class="p">,</span> <span class="m">0</span><span class="p">)</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"divide error: %v</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
	<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
		<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"result: %d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">result</span><span class="p">)</span>
	<span class="p">}</span>

	<span class="n">result</span><span class="p">,</span> <span class="n">err</span> <span class="o">=</span> <span class="n">divide</span><span class="p">(</span><span class="m">10</span><span class="p">,</span> <span class="m">3</span><span class="p">)</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"divide error: %v</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
	<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
		<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"10 / 3 = %d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">result</span><span class="p">)</span>
	<span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">if err != nil</code> pattern is the most common construct in Go code. There is no shorthand — every error is checked explicitly at the call site. This verbosity is intentional: it makes the error path visible and forces the developer to think about what should happen when something fails.</p>

<h3 id="error-wrapping-with-w">Error Wrapping with %w</h3>

<p>When an error passes through multiple layers of a program, you want to add context (which function failed, what it was trying to do) without losing the original error. Go 1.13 introduced the <code class="language-plaintext highlighter-rouge">%w</code> verb in <code class="language-plaintext highlighter-rouge">fmt.Errorf</code> for this. It wraps the original error so that downstream code can still match it, while prepending a descriptive message.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="k">var</span> <span class="n">ErrNotFound</span> <span class="o">=</span> <span class="n">errors</span><span class="o">.</span><span class="n">New</span><span class="p">(</span><span class="s">"not found"</span><span class="p">)</span>

<span class="n">original</span> <span class="o">:=</span> <span class="n">ErrNotFound</span>
<span class="n">wrapped</span> <span class="o">:=</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"load user: %w"</span><span class="p">,</span> <span class="n">original</span><span class="p">)</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"wrapped: %v</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">wrapped</span><span class="p">)</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"errors.Is(wrapped, ErrNotFound)? %t</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span>
	<span class="n">errors</span><span class="o">.</span><span class="n">Is</span><span class="p">(</span><span class="n">wrapped</span><span class="p">,</span> <span class="n">ErrNotFound</span><span class="p">))</span> <span class="c">// true — chain preserved</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The critical distinction is between <code class="language-plaintext highlighter-rouge">%w</code> and <code class="language-plaintext highlighter-rouge">%v</code>. Using <code class="language-plaintext highlighter-rouge">%v</code> formats the error’s message into a new string but breaks the chain — the resulting error is just a string, not a wrapper around the original.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="n">broken</span> <span class="o">:=</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"load user: %v"</span><span class="p">,</span> <span class="n">original</span><span class="p">)</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"errors.Is(broken, ErrNotFound)? %t</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span>
	<span class="n">errors</span><span class="o">.</span><span class="n">Is</span><span class="p">(</span><span class="n">broken</span><span class="p">,</span> <span class="n">ErrNotFound</span><span class="p">))</span> <span class="c">// false — chain lost</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Use <code class="language-plaintext highlighter-rouge">%w</code> when you want callers to be able to inspect the underlying error. Use <code class="language-plaintext highlighter-rouge">%v</code> only when you intentionally want to hide the original error from programmatic inspection (for example, when crossing an API boundary where you do not want to expose internal error types).</p>

<h3 id="sentinel-errors">Sentinel Errors</h3>

<p>A sentinel error is a package-level variable that represents a specific, well-known error condition. Callers compare against it using <code class="language-plaintext highlighter-rouge">errors.Is</code>, which walks the entire wrap chain looking for a match.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
</pre></td><td class="rouge-code"><pre><span class="k">var</span> <span class="p">(</span>
	<span class="n">ErrNotFound</span>      <span class="o">=</span> <span class="n">errors</span><span class="o">.</span><span class="n">New</span><span class="p">(</span><span class="s">"not found"</span><span class="p">)</span>
	<span class="n">ErrInvalidFormat</span> <span class="o">=</span> <span class="n">errors</span><span class="o">.</span><span class="n">New</span><span class="p">(</span><span class="s">"invalid format"</span><span class="p">)</span>
<span class="p">)</span>

<span class="k">func</span> <span class="n">loadConfig</span><span class="p">(</span><span class="n">path</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
	<span class="k">switch</span> <span class="n">path</span> <span class="p">{</span>
	<span class="k">case</span> <span class="s">"missing.yaml"</span><span class="o">:</span>
		<span class="k">return</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"loadConfig(%s): %w"</span><span class="p">,</span> <span class="n">path</span><span class="p">,</span> <span class="n">ErrNotFound</span><span class="p">)</span>
	<span class="k">case</span> <span class="s">"bad.yaml"</span><span class="o">:</span>
		<span class="k">return</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"loadConfig(%s): %w"</span><span class="p">,</span> <span class="n">path</span><span class="p">,</span> <span class="n">ErrInvalidFormat</span><span class="p">)</span>
	<span class="k">default</span><span class="o">:</span>
		<span class="k">return</span> <span class="no">nil</span>
	<span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The caller does not need to know how many layers of wrapping sit between it and the sentinel — <code class="language-plaintext highlighter-rouge">errors.Is</code> unwraps automatically:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="n">err</span> <span class="o">:=</span> <span class="n">loadConfig</span><span class="p">(</span><span class="s">"missing.yaml"</span><span class="p">)</span>
<span class="k">if</span> <span class="n">errors</span><span class="o">.</span><span class="n">Is</span><span class="p">(</span><span class="n">err</span><span class="p">,</span> <span class="n">ErrNotFound</span><span class="p">)</span> <span class="p">{</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"config not found — using defaults"</span><span class="p">)</span>
<span class="p">}</span>

<span class="n">err</span> <span class="o">=</span> <span class="n">loadConfig</span><span class="p">(</span><span class="s">"bad.yaml"</span><span class="p">)</span>
<span class="k">if</span> <span class="n">errors</span><span class="o">.</span><span class="n">Is</span><span class="p">(</span><span class="n">err</span><span class="p">,</span> <span class="n">ErrInvalidFormat</span><span class="p">)</span> <span class="p">{</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"config format invalid — check syntax"</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Sentinel errors are best suited for conditions that callers genuinely need to branch on. Do not create sentinels for every possible failure — most errors should just be returned with context and eventually logged or displayed.</p>

<h3 id="errorsas--matching-by-type">errors.As — Matching by Type</h3>

<p>While <code class="language-plaintext highlighter-rouge">errors.Is</code> checks whether an error in the chain matches a specific value, <code class="language-plaintext highlighter-rouge">errors.As</code> checks whether any error in the chain matches a specific type. This is useful when the error carries structured data beyond a message string.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
</pre></td><td class="rouge-code"><pre><span class="k">type</span> <span class="n">ValidationError</span> <span class="k">struct</span> <span class="p">{</span>
	<span class="n">Field</span>   <span class="kt">string</span>
	<span class="n">Message</span> <span class="kt">string</span>
<span class="p">}</span>

<span class="k">func</span> <span class="p">(</span><span class="n">e</span> <span class="o">*</span><span class="n">ValidationError</span><span class="p">)</span> <span class="n">Error</span><span class="p">()</span> <span class="kt">string</span> <span class="p">{</span>
	<span class="k">return</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"validation failed on %s: %s"</span><span class="p">,</span> <span class="n">e</span><span class="o">.</span><span class="n">Field</span><span class="p">,</span> <span class="n">e</span><span class="o">.</span><span class="n">Message</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">validateChange</span><span class="p">(</span><span class="n">table</span><span class="p">,</span> <span class="n">action</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
	<span class="k">if</span> <span class="n">table</span> <span class="o">==</span> <span class="s">""</span> <span class="p">{</span>
		<span class="k">return</span> <span class="o">&amp;</span><span class="n">ValidationError</span><span class="p">{</span><span class="n">Field</span><span class="o">:</span> <span class="s">"table"</span><span class="p">,</span> <span class="n">Message</span><span class="o">:</span> <span class="s">"cannot be empty"</span><span class="p">}</span>
	<span class="p">}</span>
	<span class="k">return</span> <span class="no">nil</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>To extract the typed error from a (possibly wrapped) chain, declare a variable of the target type and pass its address to <code class="language-plaintext highlighter-rouge">errors.As</code>:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre><span class="n">err</span> <span class="o">:=</span> <span class="n">validateChange</span><span class="p">(</span><span class="s">""</span><span class="p">,</span> <span class="s">"INSERT"</span><span class="p">)</span>
<span class="k">var</span> <span class="n">ve</span> <span class="o">*</span><span class="n">ValidationError</span>
<span class="k">if</span> <span class="n">errors</span><span class="o">.</span><span class="n">As</span><span class="p">(</span><span class="n">err</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">ve</span><span class="p">)</span> <span class="p">{</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"validation error: field=%s, message=%s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">ve</span><span class="o">.</span><span class="n">Field</span><span class="p">,</span> <span class="n">ve</span><span class="o">.</span><span class="n">Message</span><span class="p">)</span>
<span class="p">}</span>

<span class="c">// Works through wrapping too</span>
<span class="n">wrapped</span> <span class="o">:=</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"process change: %w"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
<span class="k">if</span> <span class="n">errors</span><span class="o">.</span><span class="n">As</span><span class="p">(</span><span class="n">wrapped</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">ve</span><span class="p">)</span> <span class="p">{</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"errors.As through wrap: field=%s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">ve</span><span class="o">.</span><span class="n">Field</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Use <code class="language-plaintext highlighter-rouge">errors.Is</code> when you are checking against a known value (sentinel). Use <code class="language-plaintext highlighter-rouge">errors.As</code> when you need to extract a specific error type to read its fields.</p>

<h3 id="context-errors">Context Errors</h3>

<p>When a function takes a <code class="language-plaintext highlighter-rouge">context.Context</code>, the operation might fail because the context was cancelled or because a deadline expired. These are not real application errors — they are intentional signals from the caller that it no longer wants the result. The pattern is to check <code class="language-plaintext highlighter-rouge">ctx.Err()</code> after receiving an error to determine whether the failure is genuine or just a cancellation.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">receive</span><span class="p">(</span><span class="n">ctx</span> <span class="n">context</span><span class="o">.</span><span class="n">Context</span><span class="p">,</span> <span class="n">simulateError</span> <span class="kt">bool</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
	<span class="k">if</span> <span class="n">ctx</span><span class="o">.</span><span class="n">Err</span><span class="p">()</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="k">return</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"receive: %w"</span><span class="p">,</span> <span class="n">ctx</span><span class="o">.</span><span class="n">Err</span><span class="p">())</span>
	<span class="p">}</span>
	<span class="k">if</span> <span class="n">simulateError</span> <span class="p">{</span>
		<span class="k">return</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"receive: %w"</span><span class="p">,</span> <span class="n">errors</span><span class="o">.</span><span class="n">New</span><span class="p">(</span><span class="s">"connection reset"</span><span class="p">))</span>
	<span class="p">}</span>
	<span class="k">return</span> <span class="no">nil</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>At the call site, the distinction matters for how you respond:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
</pre></td><td class="rouge-code"><pre><span class="c">// Real error — context is fine</span>
<span class="n">err</span> <span class="o">:=</span> <span class="n">receive</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="no">true</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="o">&amp;&amp;</span> <span class="n">ctx</span><span class="o">.</span><span class="n">Err</span><span class="p">()</span> <span class="o">==</span> <span class="no">nil</span> <span class="p">{</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"real error: %v</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
<span class="p">}</span>

<span class="c">// Context cancelled — not a real error</span>
<span class="n">cancel</span><span class="p">()</span> <span class="c">// cancel the context</span>
<span class="n">err</span> <span class="o">=</span> <span class="n">receive</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="no">true</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="o">&amp;&amp;</span> <span class="n">ctx</span><span class="o">.</span><span class="n">Err</span><span class="p">()</span> <span class="o">==</span> <span class="no">nil</span> <span class="p">{</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"real error: %v</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"context cancelled — ignoring error: %v</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
<span class="p">}</span>

<span class="c">// Timeout — deadline exceeded</span>
<span class="n">timeoutCtx</span><span class="p">,</span> <span class="n">timeoutCancel</span> <span class="o">:=</span> <span class="n">context</span><span class="o">.</span><span class="n">WithTimeout</span><span class="p">(</span><span class="n">context</span><span class="o">.</span><span class="n">Background</span><span class="p">(),</span> <span class="m">1</span><span class="o">*</span><span class="n">time</span><span class="o">.</span><span class="n">Millisecond</span><span class="p">)</span>
<span class="k">defer</span> <span class="n">timeoutCancel</span><span class="p">()</span>
<span class="n">time</span><span class="o">.</span><span class="n">Sleep</span><span class="p">(</span><span class="m">5</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Millisecond</span><span class="p">)</span>
<span class="n">err</span> <span class="o">=</span> <span class="n">receive</span><span class="p">(</span><span class="n">timeoutCtx</span><span class="p">,</span> <span class="no">false</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="o">&amp;&amp;</span> <span class="n">timeoutCtx</span><span class="o">.</span><span class="n">Err</span><span class="p">()</span> <span class="o">==</span> <span class="no">nil</span> <span class="p">{</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"real error: %v</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"timed out — not a real failure: %v</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>This pattern is essential in server code, background workers, and anything using gRPC or HTTP handlers where contexts carry deadlines. Without the <code class="language-plaintext highlighter-rouge">ctx.Err()</code> check, you would log cancellations as failures, cluttering your error monitoring.</p>

<h3 id="multi-layer-error-chains">Multi-Layer Error Chains</h3>

<p>In real applications, errors flow up through multiple layers — a database call fails, the repository wraps it, the service wraps it again, and the handler wraps it once more. Each layer adds context with <code class="language-plaintext highlighter-rouge">%w</code>, building a chain you can inspect with <code class="language-plaintext highlighter-rouge">errors.Is</code>, <code class="language-plaintext highlighter-rouge">errors.As</code>, or <code class="language-plaintext highlighter-rouge">errors.Unwrap</code>.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">bottomLayer</span><span class="p">()</span> <span class="kt">error</span> <span class="p">{</span>
	<span class="k">return</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"bottom (table lookup): %w"</span><span class="p">,</span> <span class="n">ErrNotFound</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">middleLayer</span><span class="p">()</span> <span class="kt">error</span> <span class="p">{</span>
	<span class="n">err</span> <span class="o">:=</span> <span class="n">bottomLayer</span><span class="p">()</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="k">return</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"middle (processing batch): %w"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
	<span class="p">}</span>
	<span class="k">return</span> <span class="no">nil</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">topLevel</span><span class="p">()</span> <span class="kt">error</span> <span class="p">{</span>
	<span class="n">err</span> <span class="o">:=</span> <span class="n">middleLayer</span><span class="p">()</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="k">return</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"top: %w"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
	<span class="p">}</span>
	<span class="k">return</span> <span class="no">nil</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The caller at the top gets a richly annotated error that still matches the original sentinel:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="n">err</span> <span class="o">:=</span> <span class="n">topLevel</span><span class="p">()</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"full chain: %v</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
	<span class="c">// Output: top: middle (processing batch): bottom (table lookup): not found</span>

	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"is ErrNotFound? %t</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">errors</span><span class="o">.</span><span class="n">Is</span><span class="p">(</span><span class="n">err</span><span class="p">,</span> <span class="n">ErrNotFound</span><span class="p">))</span>
	<span class="c">// Output: true</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>You can also walk the chain manually with <code class="language-plaintext highlighter-rouge">errors.Unwrap</code> to see each layer:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="n">current</span> <span class="o">:=</span> <span class="n">err</span>
<span class="k">for</span> <span class="n">current</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  -&gt; %v</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">current</span><span class="p">)</span>
	<span class="n">current</span> <span class="o">=</span> <span class="n">errors</span><span class="o">.</span><span class="n">Unwrap</span><span class="p">(</span><span class="n">current</span><span class="p">)</span>
<span class="p">}</span>
<span class="c">// -&gt; top: middle (processing batch): bottom (table lookup): not found</span>
<span class="c">// -&gt; middle (processing batch): bottom (table lookup): not found</span>
<span class="c">// -&gt; bottom (table lookup): not found</span>
<span class="c">// -&gt; not found</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The rule of thumb is: wrap at every layer boundary, use sentinel errors for conditions that callers branch on, and use <code class="language-plaintext highlighter-rouge">errors.Is</code>/<code class="language-plaintext highlighter-rouge">errors.As</code> to inspect the chain without breaking encapsulation.</p>

<h2 id="defer-patterns">Defer Patterns</h2>

<h3 id="lifo-order">LIFO Order</h3>

<p>Deferred function calls execute in last-in, first-out order when the enclosing function returns. This means the last <code class="language-plaintext highlighter-rouge">defer</code> registered runs first. The LIFO order is deliberate — it mirrors the typical pattern of acquiring resources in sequence and releasing them in reverse order (open A, open B, close B, close A).</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">part1_lifoOrder</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"registering defers 1, 2, 3..."</span><span class="p">)</span>
	<span class="k">defer</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"defer 1 (registered first — runs last)"</span><span class="p">)</span>
	<span class="k">defer</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"defer 2"</span><span class="p">)</span>
	<span class="k">defer</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"defer 3 (registered last — runs first)"</span><span class="p">)</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"function body done — defers fire in reverse:"</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Output:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre>registering defers 1, 2, 3...
function body done — defers fire in reverse:
defer 3 (registered last — runs first)
defer 2
defer 1 (registered first — runs last)
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="defer-in-loops">Defer in Loops</h3>

<p>A common mistake is deferring a close call inside a loop. Because <code class="language-plaintext highlighter-rouge">defer</code> is scoped to the enclosing function (not the loop iteration), all the deferred calls pile up and only execute when the function returns. If you are opening files or database connections in a loop, they all stay open simultaneously until the function exits.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="c">// BAD — all defers pile up until function returns</span>
<span class="n">filenames</span> <span class="o">:=</span> <span class="p">[]</span><span class="kt">string</span><span class="p">{</span><span class="s">"a.txt"</span><span class="p">,</span> <span class="s">"b.txt"</span><span class="p">,</span> <span class="s">"c.txt"</span><span class="p">}</span>
<span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">name</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">filenames</span> <span class="p">{</span>
	<span class="n">f</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">os</span><span class="o">.</span><span class="n">Open</span><span class="p">(</span><span class="n">name</span><span class="p">)</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span> <span class="k">continue</span> <span class="p">}</span>
	<span class="k">defer</span> <span class="n">f</span><span class="o">.</span><span class="n">Close</span><span class="p">()</span> <span class="c">// won't run until the surrounding function returns</span>
	<span class="c">// process f...</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The fix is to wrap the loop body in its own function (either a named function or an anonymous closure), so <code class="language-plaintext highlighter-rouge">defer</code> fires at the end of each iteration:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="c">// FIX — wrap in a function so defer runs each iteration</span>
<span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">name</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">filenames</span> <span class="p">{</span>
	<span class="k">func</span><span class="p">(</span><span class="n">n</span> <span class="kt">string</span><span class="p">)</span> <span class="p">{</span>
		<span class="n">f</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">os</span><span class="o">.</span><span class="n">Open</span><span class="p">(</span><span class="n">n</span><span class="p">)</span>
		<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span>
		<span class="k">defer</span> <span class="n">f</span><span class="o">.</span><span class="n">Close</span><span class="p">()</span>
		<span class="c">// process f...</span>
	<span class="p">}(</span><span class="n">name</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Alternatively, extract the body into a named function like <code class="language-plaintext highlighter-rouge">processFile(name)</code>. Either approach ensures resources are released promptly.</p>

<h3 id="defer-with-closures">Defer with Closures</h3>

<p>Deferred closures capture variables by reference, not by value. This means a deferred closure sees the final value of any variable from the enclosing scope, not the value at the time <code class="language-plaintext highlighter-rouge">defer</code> was called.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">part3_deferWithClosures</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">x</span> <span class="o">:=</span> <span class="m">0</span>
	<span class="k">defer</span> <span class="k">func</span><span class="p">()</span> <span class="p">{</span>
		<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"deferred closure sees x = %d (final value, not 0)</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">x</span><span class="p">)</span>
	<span class="p">}()</span>
	<span class="n">x</span> <span class="o">=</span> <span class="m">42</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"x set to %d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">x</span><span class="p">)</span>
<span class="p">}</span>
<span class="c">// Output:</span>
<span class="c">// x set to 42</span>
<span class="c">// deferred closure sees x = 42 (final value, not 0)</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>If you need to capture the value at the time of the <code class="language-plaintext highlighter-rouge">defer</code>, pass it as a parameter. Function parameters are evaluated immediately when <code class="language-plaintext highlighter-rouge">defer</code> is executed, so the value is frozen:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="n">y</span> <span class="o">:=</span> <span class="m">0</span>
<span class="k">defer</span> <span class="k">func</span><span class="p">(</span><span class="n">val</span> <span class="kt">int</span><span class="p">)</span> <span class="p">{</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"deferred closure(val) sees val = %d (frozen at defer time)</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">val</span><span class="p">)</span>
<span class="p">}(</span><span class="n">y</span><span class="p">)</span> <span class="c">// y is evaluated now, val = 0</span>
<span class="n">y</span> <span class="o">=</span> <span class="m">99</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"y set to %d, but deferred param already captured 0</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span>
<span class="c">// Output:</span>
<span class="c">// y set to 99, but deferred param already captured 0</span>
<span class="c">// deferred closure(val) sees val = 0 (frozen at defer time)</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The reference-capture behaviour is what makes named-return error wrapping work (covered next), but it can also be a source of bugs when you unintentionally capture a loop variable.</p>

<h3 id="named-returns-with-defer">Named Returns with Defer</h3>

<p>One of the more powerful <code class="language-plaintext highlighter-rouge">defer</code> patterns uses named return values to modify the return result in a deferred function. Because the deferred closure captures the named return by reference, it can inspect and change the value that the caller ultimately receives. This is especially useful for wrapping errors with context or for flushing buffers on the way out.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">readConfig</span><span class="p">(</span><span class="n">path</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="n">content</span> <span class="kt">string</span><span class="p">,</span> <span class="n">err</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">defer</span> <span class="k">func</span><span class="p">()</span> <span class="p">{</span>
		<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
			<span class="n">err</span> <span class="o">=</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"readConfig(%s): %w"</span><span class="p">,</span> <span class="n">path</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
		<span class="p">}</span>
	<span class="p">}()</span>

	<span class="k">if</span> <span class="n">path</span> <span class="o">==</span> <span class="s">"missing.conf"</span> <span class="p">{</span>
		<span class="k">return</span> <span class="s">""</span><span class="p">,</span> <span class="n">os</span><span class="o">.</span><span class="n">ErrNotExist</span>
	<span class="p">}</span>
	<span class="k">return</span> <span class="s">"key=value"</span><span class="p">,</span> <span class="no">nil</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>When <code class="language-plaintext highlighter-rouge">readConfig("missing.conf")</code> returns <code class="language-plaintext highlighter-rouge">os.ErrNotExist</code>, the deferred function wraps it into <code class="language-plaintext highlighter-rouge">readConfig(missing.conf): file does not exist</code> before the caller sees it. This avoids duplicating the wrapping logic at every <code class="language-plaintext highlighter-rouge">return</code> statement in the function.</p>

<p>The same technique works for capturing flush errors that would otherwise be silently ignored:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">writeWithFlush</span><span class="p">()</span> <span class="p">(</span><span class="n">n</span> <span class="kt">int</span><span class="p">,</span> <span class="n">err</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
	<span class="n">n</span> <span class="o">=</span> <span class="m">128</span>
	<span class="k">defer</span> <span class="k">func</span><span class="p">()</span> <span class="p">{</span>
		<span class="n">flushErr</span> <span class="o">:=</span> <span class="n">simulateFlush</span><span class="p">()</span>
		<span class="k">if</span> <span class="n">err</span> <span class="o">==</span> <span class="no">nil</span> <span class="p">{</span>
			<span class="n">err</span> <span class="o">=</span> <span class="n">flushErr</span>
		<span class="p">}</span>
	<span class="p">}()</span>
	<span class="k">return</span> <span class="n">n</span><span class="p">,</span> <span class="no">nil</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">simulateFlush</span><span class="p">()</span> <span class="kt">error</span> <span class="p">{</span>
	<span class="k">return</span> <span class="n">errors</span><span class="o">.</span><span class="n">New</span><span class="p">(</span><span class="s">"flush: disk full"</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Here the write itself succeeds, but the deferred flush fails. Without the named return pattern, the flush error would be lost because <code class="language-plaintext highlighter-rouge">return n, nil</code> has already been executed. The deferred function overwrites <code class="language-plaintext highlighter-rouge">err</code> before the caller receives it.</p>

<h3 id="panic-and-recover">Panic and Recover</h3>

<p>Go’s <code class="language-plaintext highlighter-rouge">panic</code> is not for normal error handling — it is reserved for truly unrecoverable situations (programming errors, violated invariants, corrupted state). When a panic occurs, the runtime begins unwinding the call stack, executing deferred functions in each frame. If no deferred function calls <code class="language-plaintext highlighter-rouge">recover()</code>, the program crashes with a stack trace.</p>

<p><code class="language-plaintext highlighter-rouge">recover()</code> stops the panic and returns the value that was passed to <code class="language-plaintext highlighter-rouge">panic</code>. It only works inside a deferred function — calling <code class="language-plaintext highlighter-rouge">recover()</code> in normal code always returns <code class="language-plaintext highlighter-rouge">nil</code>.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">safeDivide</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span> <span class="kt">int</span><span class="p">)</span> <span class="p">(</span><span class="n">result</span> <span class="kt">int</span><span class="p">,</span> <span class="n">err</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">defer</span> <span class="k">func</span><span class="p">()</span> <span class="p">{</span>
		<span class="k">if</span> <span class="n">r</span> <span class="o">:=</span> <span class="nb">recover</span><span class="p">();</span> <span class="n">r</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
			<span class="n">err</span> <span class="o">=</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"caught panic: %v"</span><span class="p">,</span> <span class="n">r</span><span class="p">)</span>
		<span class="p">}</span>
	<span class="p">}()</span>
	<span class="k">return</span> <span class="n">a</span> <span class="o">/</span> <span class="n">b</span><span class="p">,</span> <span class="no">nil</span> <span class="c">// panics if b == 0 (integer division by zero)</span>
<span class="p">}</span>

<span class="n">result</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">safeDivide</span><span class="p">(</span><span class="m">10</span><span class="p">,</span> <span class="m">0</span><span class="p">)</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"result=%d, err=%v</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">result</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
<span class="c">// result=0, err=caught panic: runtime error: integer division by zero</span>

<span class="n">result</span><span class="p">,</span> <span class="n">err</span> <span class="o">=</span> <span class="n">safeDivide</span><span class="p">(</span><span class="m">10</span><span class="p">,</span> <span class="m">3</span><span class="p">)</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"result=%d, err=%v</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">result</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
<span class="c">// result=3, err=&lt;nil&gt;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>During a panic unwind, all deferred functions in the call stack still run in LIFO order. This is important — it means cleanup code (closing files, releasing locks) still executes even during a panic:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">panicChain</span><span class="p">()</span> <span class="p">{</span>
	<span class="k">defer</span> <span class="k">func</span><span class="p">()</span> <span class="p">{</span>
		<span class="k">if</span> <span class="n">r</span> <span class="o">:=</span> <span class="nb">recover</span><span class="p">();</span> <span class="n">r</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
			<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"defer 1 (outermost): recovered panic: %v</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">r</span><span class="p">)</span>
		<span class="p">}</span>
	<span class="p">}()</span>
	<span class="k">defer</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"defer 2: I still run during panic unwind"</span><span class="p">)</span>
	<span class="k">defer</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"defer 3: I run first during panic unwind"</span><span class="p">)</span>

	<span class="nb">panic</span><span class="p">(</span><span class="s">"something went wrong"</span><span class="p">)</span>
<span class="p">}</span>
<span class="c">// Output:</span>
<span class="c">// defer 3: I run first during panic unwind</span>
<span class="c">// defer 2: I still run during panic unwind</span>
<span class="c">// defer 1 (outermost): recovered panic: something went wrong</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The general guidance is: return errors, do not panic. Use <code class="language-plaintext highlighter-rouge">panic</code> only for programming bugs (like indexing out of bounds or an impossible state). Use <code class="language-plaintext highlighter-rouge">recover</code> at API boundaries (HTTP handlers, goroutine entry points) to convert panics into errors so that one bad request does not bring down the entire server.</p>]]></content><author><name>kimserey</name></author><category term="go" /><summary type="html"><![CDATA[Go treats errors as ordinary values — there are no exceptions, no try/catch, and no hidden control flow. Every function that can fail returns an error alongside its result, and the caller decides what to do with it. For cleanup, Go provides defer, which guarantees a function call runs when the enclosing function returns, regardless of how it returns. Together, explicit error returns and defer give you predictable error propagation and deterministic resource management without the complexity of exception hierarchies or finalizers. In this post we cover Go’s error handling patterns — from basic checks through wrapping, sentinel errors, and context-aware error classification — then move to defer patterns including LIFO ordering, loop pitfalls, closure capture semantics, named return manipulation, and panic recovery.]]></summary></entry><entry><title type="html">Go Type System — Interfaces, Embedding and Generics</title><link href="https://www.kimsereylam.com/go/2026/08/29/go-type-system-interfaces-embedding-and-generics.html" rel="alternate" type="text/html" title="Go Type System — Interfaces, Embedding and Generics" /><published>2026-08-29T00:00:00-05:00</published><updated>2026-08-29T00:00:00-05:00</updated><id>https://www.kimsereylam.com/go/2026/08/29/go-type-system-interfaces-embedding-and-generics</id><content type="html" xml:base="https://www.kimsereylam.com/go/2026/08/29/go-type-system-interfaces-embedding-and-generics.html"><![CDATA[<p>Go takes a distinctive approach to polymorphism and code reuse. There are no classes, no inheritance hierarchies, and no explicit <code class="language-plaintext highlighter-rouge">implements</code> declarations. Instead, Go provides three orthogonal mechanisms: interfaces for abstraction, embedding for composition, and generics for type-safe parameterization. Each one is simple on its own, and together they cover the same ground that class hierarchies cover in other languages — with less coupling. In this post, we’ll work through all three, starting with how Go interfaces are satisfied implicitly, then moving to struct and interface embedding, and finishing with generic functions and types.</p>

<!--more-->

<h2 id="interfaces-and-type-assertions">Interfaces and Type Assertions</h2>

<h3 id="implicit-satisfaction">Implicit Satisfaction</h3>

<p>In Go, a type satisfies an interface simply by implementing all of its methods. There is no <code class="language-plaintext highlighter-rouge">implements</code> keyword and no explicit declaration binding a type to an interface. If the methods match, the type qualifies.</p>

<p>Consider a <code class="language-plaintext highlighter-rouge">Message</code> interface with two methods:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="k">type</span> <span class="n">Message</span> <span class="k">interface</span> <span class="p">{</span>
    <span class="n">Type</span><span class="p">()</span> <span class="kt">string</span>
    <span class="n">String</span><span class="p">()</span> <span class="kt">string</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Any struct that has both a <code class="language-plaintext highlighter-rouge">Type()</code> method and a <code class="language-plaintext highlighter-rouge">String()</code> method satisfies <code class="language-plaintext highlighter-rouge">Message</code> automatically. Here are three structs that each represent a different kind of database operation:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
</pre></td><td class="rouge-code"><pre><span class="k">type</span> <span class="n">Insert</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">Table</span> <span class="kt">string</span>
    <span class="n">Data</span>  <span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">string</span>
<span class="p">}</span>

<span class="k">func</span> <span class="p">(</span><span class="n">i</span> <span class="n">Insert</span><span class="p">)</span> <span class="n">Type</span><span class="p">()</span> <span class="kt">string</span>   <span class="p">{</span> <span class="k">return</span> <span class="s">"INSERT"</span> <span class="p">}</span>
<span class="k">func</span> <span class="p">(</span><span class="n">i</span> <span class="n">Insert</span><span class="p">)</span> <span class="n">String</span><span class="p">()</span> <span class="kt">string</span> <span class="p">{</span> <span class="k">return</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"INSERT into %s: %v"</span><span class="p">,</span> <span class="n">i</span><span class="o">.</span><span class="n">Table</span><span class="p">,</span> <span class="n">i</span><span class="o">.</span><span class="n">Data</span><span class="p">)</span> <span class="p">}</span>

<span class="k">type</span> <span class="n">Update</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">Table</span> <span class="kt">string</span>
    <span class="n">Key</span>   <span class="kt">string</span>
    <span class="n">Data</span>  <span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">string</span>
<span class="p">}</span>

<span class="k">func</span> <span class="p">(</span><span class="n">u</span> <span class="n">Update</span><span class="p">)</span> <span class="n">Type</span><span class="p">()</span> <span class="kt">string</span>   <span class="p">{</span> <span class="k">return</span> <span class="s">"UPDATE"</span> <span class="p">}</span>
<span class="k">func</span> <span class="p">(</span><span class="n">u</span> <span class="n">Update</span><span class="p">)</span> <span class="n">String</span><span class="p">()</span> <span class="kt">string</span> <span class="p">{</span> <span class="k">return</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"UPDATE %s where key=%s: %v"</span><span class="p">,</span> <span class="n">u</span><span class="o">.</span><span class="n">Table</span><span class="p">,</span> <span class="n">u</span><span class="o">.</span><span class="n">Key</span><span class="p">,</span> <span class="n">u</span><span class="o">.</span><span class="n">Data</span><span class="p">)</span> <span class="p">}</span>

<span class="k">type</span> <span class="n">Delete</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">Table</span> <span class="kt">string</span>
    <span class="n">Key</span>   <span class="kt">string</span>
<span class="p">}</span>

<span class="k">func</span> <span class="p">(</span><span class="n">d</span> <span class="n">Delete</span><span class="p">)</span> <span class="n">Type</span><span class="p">()</span> <span class="kt">string</span>   <span class="p">{</span> <span class="k">return</span> <span class="s">"DELETE"</span> <span class="p">}</span>
<span class="k">func</span> <span class="p">(</span><span class="n">d</span> <span class="n">Delete</span><span class="p">)</span> <span class="n">String</span><span class="p">()</span> <span class="kt">string</span> <span class="p">{</span> <span class="k">return</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"DELETE from %s where key=%s"</span><span class="p">,</span> <span class="n">d</span><span class="o">.</span><span class="n">Table</span><span class="p">,</span> <span class="n">d</span><span class="o">.</span><span class="n">Key</span><span class="p">)</span> <span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>None of these types mention <code class="language-plaintext highlighter-rouge">Message</code> anywhere. Yet all three satisfy it, and you can store them together in a slice:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="k">var</span> <span class="n">msg</span> <span class="n">Message</span> <span class="o">=</span> <span class="n">Insert</span><span class="p">{</span><span class="n">Table</span><span class="o">:</span> <span class="s">"users"</span><span class="p">,</span> <span class="n">Data</span><span class="o">:</span> <span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">string</span><span class="p">{</span><span class="s">"name"</span><span class="o">:</span> <span class="s">"alice"</span><span class="p">}}</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"msg.Type() = %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">msg</span><span class="o">.</span><span class="n">Type</span><span class="p">())</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"msg.String() = %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">msg</span><span class="p">)</span>

<span class="n">messages</span> <span class="o">:=</span> <span class="p">[]</span><span class="n">Message</span><span class="p">{</span>
    <span class="n">Insert</span><span class="p">{</span><span class="n">Table</span><span class="o">:</span> <span class="s">"users"</span><span class="p">,</span> <span class="n">Data</span><span class="o">:</span> <span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">string</span><span class="p">{</span><span class="s">"name"</span><span class="o">:</span> <span class="s">"alice"</span><span class="p">}},</span>
    <span class="n">Update</span><span class="p">{</span><span class="n">Table</span><span class="o">:</span> <span class="s">"users"</span><span class="p">,</span> <span class="n">Key</span><span class="o">:</span> <span class="s">"alice"</span><span class="p">,</span> <span class="n">Data</span><span class="o">:</span> <span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">string</span><span class="p">{</span><span class="s">"email"</span><span class="o">:</span> <span class="s">"a@b.com"</span><span class="p">}},</span>
    <span class="n">Delete</span><span class="p">{</span><span class="n">Table</span><span class="o">:</span> <span class="s">"sessions"</span><span class="p">,</span> <span class="n">Key</span><span class="o">:</span> <span class="s">"expired-123"</span><span class="p">},</span>
<span class="p">}</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"%d messages in slice, all satisfy Message interface</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">messages</span><span class="p">))</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>This implicit satisfaction is what makes Go interfaces so powerful for decoupling. A package can define an interface, and types from completely unrelated packages can satisfy it without importing or even knowing about the interface definition.</p>

<h3 id="type-switches">Type Switches</h3>

<p>When you have an interface value and need to branch on the concrete type behind it, Go provides the type switch:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">dispatch</span><span class="p">(</span><span class="n">msg</span> <span class="n">Message</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">switch</span> <span class="n">m</span> <span class="o">:=</span> <span class="n">msg</span><span class="o">.</span><span class="p">(</span><span class="k">type</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">case</span> <span class="o">*</span><span class="n">Insert</span><span class="o">:</span>
        <span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"[DISPATCH] Insert into %s: %v</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">m</span><span class="o">.</span><span class="n">Table</span><span class="p">,</span> <span class="n">m</span><span class="o">.</span><span class="n">Data</span><span class="p">)</span>
    <span class="k">case</span> <span class="o">*</span><span class="n">Update</span><span class="o">:</span>
        <span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"[DISPATCH] Update %s (key=%s): %v</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">m</span><span class="o">.</span><span class="n">Table</span><span class="p">,</span> <span class="n">m</span><span class="o">.</span><span class="n">Key</span><span class="p">,</span> <span class="n">m</span><span class="o">.</span><span class="n">Data</span><span class="p">)</span>
    <span class="k">case</span> <span class="o">*</span><span class="n">Delete</span><span class="o">:</span>
        <span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"[DISPATCH] Delete from %s (key=%s)</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">m</span><span class="o">.</span><span class="n">Table</span><span class="p">,</span> <span class="n">m</span><span class="o">.</span><span class="n">Key</span><span class="p">)</span>
    <span class="k">default</span><span class="o">:</span>
        <span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"[DISPATCH] Unknown message type: %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">m</span><span class="o">.</span><span class="n">Type</span><span class="p">())</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The syntax <code class="language-plaintext highlighter-rouge">msg.(type)</code> extracts the concrete type. In each <code class="language-plaintext highlighter-rouge">case</code> branch, the variable <code class="language-plaintext highlighter-rouge">m</code> is already narrowed to the matched type — you can access <code class="language-plaintext highlighter-rouge">m.Table</code>, <code class="language-plaintext highlighter-rouge">m.Key</code>, or <code class="language-plaintext highlighter-rouge">m.Data</code> directly without any additional casting. The <code class="language-plaintext highlighter-rouge">default</code> branch handles any <code class="language-plaintext highlighter-rouge">Message</code> implementation that was not explicitly listed.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="n">messages</span> <span class="o">:=</span> <span class="p">[]</span><span class="n">Message</span><span class="p">{</span>
    <span class="o">&amp;</span><span class="n">Insert</span><span class="p">{</span><span class="n">Table</span><span class="o">:</span> <span class="s">"users"</span><span class="p">,</span> <span class="n">Data</span><span class="o">:</span> <span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">string</span><span class="p">{</span><span class="s">"name"</span><span class="o">:</span> <span class="s">"bob"</span><span class="p">}},</span>
    <span class="o">&amp;</span><span class="n">Delete</span><span class="p">{</span><span class="n">Table</span><span class="o">:</span> <span class="s">"sessions"</span><span class="p">,</span> <span class="n">Key</span><span class="o">:</span> <span class="s">"sess-456"</span><span class="p">},</span>
    <span class="o">&amp;</span><span class="n">Update</span><span class="p">{</span><span class="n">Table</span><span class="o">:</span> <span class="s">"users"</span><span class="p">,</span> <span class="n">Key</span><span class="o">:</span> <span class="s">"bob"</span><span class="p">,</span> <span class="n">Data</span><span class="o">:</span> <span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">string</span><span class="p">{</span><span class="s">"role"</span><span class="o">:</span> <span class="s">"admin"</span><span class="p">}},</span>
<span class="p">}</span>
<span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">msg</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">messages</span> <span class="p">{</span>
    <span class="n">dispatch</span><span class="p">(</span><span class="n">msg</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="type-assertion-with-comma-ok">Type Assertion with Comma-Ok</h3>

<p>A type switch is useful when you need to handle multiple types. When you only care about one specific type, use a type assertion with the comma-ok pattern:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">tryExtractInsert</span><span class="p">(</span><span class="n">msg</span> <span class="n">Message</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">ins</span><span class="p">,</span> <span class="n">ok</span> <span class="o">:=</span> <span class="n">msg</span><span class="o">.</span><span class="p">(</span><span class="n">Insert</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">ok</span> <span class="p">{</span>
        <span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"type assertion succeeded: Insert into %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">ins</span><span class="o">.</span><span class="n">Table</span><span class="p">)</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"type assertion failed: message is %s, not Insert</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">msg</span><span class="o">.</span><span class="n">Type</span><span class="p">())</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The expression <code class="language-plaintext highlighter-rouge">msg.(Insert)</code> attempts to extract the concrete <code class="language-plaintext highlighter-rouge">Insert</code> value from the interface. If the underlying type matches, <code class="language-plaintext highlighter-rouge">ok</code> is <code class="language-plaintext highlighter-rouge">true</code> and <code class="language-plaintext highlighter-rouge">ins</code> holds the value. If it does not match, <code class="language-plaintext highlighter-rouge">ok</code> is <code class="language-plaintext highlighter-rouge">false</code> and <code class="language-plaintext highlighter-rouge">ins</code> is the zero value of <code class="language-plaintext highlighter-rouge">Insert</code>. This two-value form is safe — it never panics. If you omit the second return value (<code class="language-plaintext highlighter-rouge">ins := msg.(Insert)</code>), a failed assertion will panic at runtime.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="k">var</span> <span class="n">msg</span> <span class="n">Message</span> <span class="o">=</span> <span class="n">Insert</span><span class="p">{</span><span class="n">Table</span><span class="o">:</span> <span class="s">"orders"</span><span class="p">,</span> <span class="n">Data</span><span class="o">:</span> <span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">string</span><span class="p">{</span><span class="s">"id"</span><span class="o">:</span> <span class="s">"100"</span><span class="p">}}</span>
<span class="n">tryExtractInsert</span><span class="p">(</span><span class="n">msg</span><span class="p">)</span> <span class="c">// succeeds</span>

<span class="k">var</span> <span class="n">msg2</span> <span class="n">Message</span> <span class="o">=</span> <span class="n">Delete</span><span class="p">{</span><span class="n">Table</span><span class="o">:</span> <span class="s">"orders"</span><span class="p">,</span> <span class="n">Key</span><span class="o">:</span> <span class="s">"100"</span><span class="p">}</span>
<span class="n">tryExtractInsert</span><span class="p">(</span><span class="n">msg2</span><span class="p">)</span> <span class="c">// fails gracefully</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="error-as-an-interface">Error as an Interface</h3>

<p>Go’s built-in <code class="language-plaintext highlighter-rouge">error</code> type is itself an interface with a single method:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="k">type</span> <span class="kt">error</span> <span class="k">interface</span> <span class="p">{</span>
    <span class="n">Error</span><span class="p">()</span> <span class="kt">string</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Any type that has an <code class="language-plaintext highlighter-rouge">Error() string</code> method satisfies the <code class="language-plaintext highlighter-rouge">error</code> interface. This means you can create custom error types that carry structured data beyond a simple message string:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="k">type</span> <span class="n">ParseError</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">Code</span>    <span class="kt">int</span>
    <span class="n">Message</span> <span class="kt">string</span>
<span class="p">}</span>

<span class="k">func</span> <span class="p">(</span><span class="n">e</span> <span class="o">*</span><span class="n">ParseError</span><span class="p">)</span> <span class="n">Error</span><span class="p">()</span> <span class="kt">string</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"parse error %d: %s"</span><span class="p">,</span> <span class="n">e</span><span class="o">.</span><span class="n">Code</span><span class="p">,</span> <span class="n">e</span><span class="o">.</span><span class="n">Message</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Because <code class="language-plaintext highlighter-rouge">*ParseError</code> has an <code class="language-plaintext highlighter-rouge">Error() string</code> method, it satisfies the <code class="language-plaintext highlighter-rouge">error</code> interface. You can return it wherever an <code class="language-plaintext highlighter-rouge">error</code> is expected, and callers can use a type assertion to extract the richer information:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="k">var</span> <span class="n">err</span> <span class="kt">error</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">ParseError</span><span class="p">{</span><span class="n">Code</span><span class="o">:</span> <span class="m">422</span><span class="p">,</span> <span class="n">Message</span><span class="o">:</span> <span class="s">"invalid WAL format"</span><span class="p">}</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"err.Error() = %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">err</span><span class="o">.</span><span class="n">Error</span><span class="p">())</span>

<span class="k">if</span> <span class="n">pe</span><span class="p">,</span> <span class="n">ok</span> <span class="o">:=</span> <span class="n">err</span><span class="o">.</span><span class="p">(</span><span class="o">*</span><span class="n">ParseError</span><span class="p">);</span> <span class="n">ok</span> <span class="p">{</span>
    <span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"extracted ParseError: code=%d, message=%s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">pe</span><span class="o">.</span><span class="n">Code</span><span class="p">,</span> <span class="n">pe</span><span class="o">.</span><span class="n">Message</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Note the assertion is against <code class="language-plaintext highlighter-rouge">*ParseError</code> (a pointer), because the <code class="language-plaintext highlighter-rouge">Error()</code> method has a pointer receiver. The receiver type matters — <code class="language-plaintext highlighter-rouge">ParseError</code> (value) would not match here.</p>

<h3 id="the-any-type">The <code class="language-plaintext highlighter-rouge">any</code> Type</h3>

<p>Go 1.18 introduced <code class="language-plaintext highlighter-rouge">any</code> as an alias for <code class="language-plaintext highlighter-rouge">interface{}</code>, the empty interface. Since every type satisfies an interface with zero methods, <code class="language-plaintext highlighter-rouge">any</code> can hold a value of any type. A type switch lets you recover the concrete type:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">processAny</span><span class="p">(</span><span class="n">val</span> <span class="n">any</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">switch</span> <span class="n">v</span> <span class="o">:=</span> <span class="n">val</span><span class="o">.</span><span class="p">(</span><span class="k">type</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">case</span> <span class="kt">string</span><span class="o">:</span>
        <span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"any is string: %q</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">v</span><span class="p">)</span>
    <span class="k">case</span> <span class="kt">int</span><span class="o">:</span>
        <span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"any is int: %d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">v</span><span class="p">)</span>
    <span class="k">case</span> <span class="n">Message</span><span class="o">:</span>
        <span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"any is Message: %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">v</span><span class="p">)</span>
    <span class="k">default</span><span class="o">:</span>
        <span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"any is unknown: %T</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">v</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="n">processAny</span><span class="p">(</span><span class="s">"hello"</span><span class="p">)</span>                              <span class="c">// string</span>
<span class="n">processAny</span><span class="p">(</span><span class="m">42</span><span class="p">)</span>                                    <span class="c">// int</span>
<span class="n">processAny</span><span class="p">(</span><span class="n">Insert</span><span class="p">{</span><span class="n">Table</span><span class="o">:</span> <span class="s">"t"</span><span class="p">,</span> <span class="n">Data</span><span class="o">:</span> <span class="no">nil</span><span class="p">})</span>         <span class="c">// Message</span>
<span class="n">processAny</span><span class="p">(</span><span class="m">3.14</span><span class="p">)</span>                                  <span class="c">// unknown: float64</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">any</code> type is useful for truly generic containers and APIs. However, it erases type information at compile time. For most cases where you want type-safe parameterization, generics (covered below) are the better tool.</p>

<h2 id="embedding">Embedding</h2>

<h3 id="struct-embedding">Struct Embedding</h3>

<p>Go does not have inheritance. Instead, it has embedding — you place one struct type inside another without giving it a field name, and its fields and methods are “promoted” to the outer struct.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="rouge-code"><pre><span class="k">type</span> <span class="n">Base</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">ID</span>        <span class="kt">int</span>
    <span class="n">CreatedAt</span> <span class="kt">string</span>
<span class="p">}</span>

<span class="k">func</span> <span class="p">(</span><span class="n">b</span> <span class="n">Base</span><span class="p">)</span> <span class="n">Summary</span><span class="p">()</span> <span class="kt">string</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"id=%d, created=%s"</span><span class="p">,</span> <span class="n">b</span><span class="o">.</span><span class="n">ID</span><span class="p">,</span> <span class="n">b</span><span class="o">.</span><span class="n">CreatedAt</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">type</span> <span class="n">User</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">Base</span>
    <span class="n">Name</span>  <span class="kt">string</span>
    <span class="n">Email</span> <span class="kt">string</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">User</code> struct embeds <code class="language-plaintext highlighter-rouge">Base</code>. There is no field name — just the type. This is different from a named field like <code class="language-plaintext highlighter-rouge">base Base</code>; embedding promotes all of <code class="language-plaintext highlighter-rouge">Base</code>’s fields and methods onto <code class="language-plaintext highlighter-rouge">User</code>.</p>

<h3 id="field-and-method-promotion">Field and Method Promotion</h3>

<p>Because <code class="language-plaintext highlighter-rouge">Base</code> is embedded, you can access its fields directly on a <code class="language-plaintext highlighter-rouge">User</code> value:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="n">u</span> <span class="o">:=</span> <span class="n">User</span><span class="p">{</span>
    <span class="n">Base</span><span class="o">:</span>  <span class="n">Base</span><span class="p">{</span><span class="n">ID</span><span class="o">:</span> <span class="m">1</span><span class="p">,</span> <span class="n">CreatedAt</span><span class="o">:</span> <span class="s">"2025-01-15"</span><span class="p">},</span>
    <span class="n">Name</span><span class="o">:</span>  <span class="s">"Alice"</span><span class="p">,</span>
    <span class="n">Email</span><span class="o">:</span> <span class="s">"alice@example.com"</span><span class="p">,</span>
<span class="p">}</span>

<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"user.ID: %d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">u</span><span class="o">.</span><span class="n">ID</span><span class="p">)</span>           <span class="c">// promoted from Base</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"user.Base.ID: %d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">u</span><span class="o">.</span><span class="n">Base</span><span class="o">.</span><span class="n">ID</span><span class="p">)</span> <span class="c">// explicit path also works</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Both <code class="language-plaintext highlighter-rouge">u.ID</code> and <code class="language-plaintext highlighter-rouge">u.Base.ID</code> refer to the same field. The promoted path is syntactic convenience — the compiler rewrites <code class="language-plaintext highlighter-rouge">u.ID</code> to <code class="language-plaintext highlighter-rouge">u.Base.ID</code> behind the scenes.</p>

<p>Methods are promoted the same way. Since <code class="language-plaintext highlighter-rouge">Base</code> has a <code class="language-plaintext highlighter-rouge">Summary()</code> method, <code class="language-plaintext highlighter-rouge">User</code> also has <code class="language-plaintext highlighter-rouge">Summary()</code> without writing any additional code:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"u.Summary() = %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">u</span><span class="o">.</span><span class="n">Summary</span><span class="p">())</span> <span class="c">// calls Base.Summary()</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>This also means <code class="language-plaintext highlighter-rouge">User</code> satisfies any interface that <code class="language-plaintext highlighter-rouge">Base</code> satisfies. If there is a <code class="language-plaintext highlighter-rouge">Summarizer</code> interface requiring a <code class="language-plaintext highlighter-rouge">Summary() string</code> method, <code class="language-plaintext highlighter-rouge">User</code> satisfies it through promotion:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="k">type</span> <span class="n">Summarizer</span> <span class="k">interface</span> <span class="p">{</span>
    <span class="n">Summary</span><span class="p">()</span> <span class="kt">string</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">printSummary</span><span class="p">(</span><span class="n">s</span> <span class="n">Summarizer</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"summary: %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">s</span><span class="o">.</span><span class="n">Summary</span><span class="p">())</span>
<span class="p">}</span>

<span class="n">printSummary</span><span class="p">(</span><span class="n">u</span><span class="p">)</span> <span class="c">// works — User has Summary() via Base</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="method-override">Method Override</h3>

<p>An outer type can define its own version of a promoted method, effectively overriding it:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="k">type</span> <span class="n">Order</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">Base</span>
    <span class="n">UserID</span> <span class="kt">int</span>
    <span class="n">Total</span>  <span class="kt">float64</span>
<span class="p">}</span>

<span class="k">func</span> <span class="p">(</span><span class="n">o</span> <span class="n">Order</span><span class="p">)</span> <span class="n">Summary</span><span class="p">()</span> <span class="kt">string</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"order %d: $%.2f (user %d)"</span><span class="p">,</span> <span class="n">o</span><span class="o">.</span><span class="n">ID</span><span class="p">,</span> <span class="n">o</span><span class="o">.</span><span class="n">Total</span><span class="p">,</span> <span class="n">o</span><span class="o">.</span><span class="n">UserID</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Now <code class="language-plaintext highlighter-rouge">Order</code> has its own <code class="language-plaintext highlighter-rouge">Summary()</code> that takes priority over the promoted one from <code class="language-plaintext highlighter-rouge">Base</code>. The original is still accessible through the explicit path:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="n">o</span> <span class="o">:=</span> <span class="n">Order</span><span class="p">{</span><span class="n">Base</span><span class="o">:</span> <span class="n">Base</span><span class="p">{</span><span class="n">ID</span><span class="o">:</span> <span class="m">42</span><span class="p">,</span> <span class="n">CreatedAt</span><span class="o">:</span> <span class="s">"2025-03-01"</span><span class="p">},</span> <span class="n">UserID</span><span class="o">:</span> <span class="m">1</span><span class="p">,</span> <span class="n">Total</span><span class="o">:</span> <span class="m">99.95</span><span class="p">}</span>

<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"o.Summary() = %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">o</span><span class="o">.</span><span class="n">Summary</span><span class="p">())</span>      <span class="c">// Order.Summary()</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"o.Base.Summary() = %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">o</span><span class="o">.</span><span class="n">Base</span><span class="o">.</span><span class="n">Summary</span><span class="p">())</span> <span class="c">// Base.Summary()</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="interface-embedding">Interface Embedding</h3>

<p>Embedding also works with interfaces. You can compose larger interfaces from smaller ones:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="rouge-code"><pre><span class="k">type</span> <span class="n">Reader</span> <span class="k">interface</span> <span class="p">{</span>
    <span class="n">Read</span><span class="p">()</span> <span class="kt">string</span>
<span class="p">}</span>

<span class="k">type</span> <span class="n">Writer</span> <span class="k">interface</span> <span class="p">{</span>
    <span class="n">Write</span><span class="p">(</span><span class="n">data</span> <span class="kt">string</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">type</span> <span class="n">ReadWriter</span> <span class="k">interface</span> <span class="p">{</span>
    <span class="n">Reader</span>
    <span class="n">Writer</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">ReadWriter</code> embeds both <code class="language-plaintext highlighter-rouge">Reader</code> and <code class="language-plaintext highlighter-rouge">Writer</code>, so any type that satisfies <code class="language-plaintext highlighter-rouge">ReadWriter</code> must implement both <code class="language-plaintext highlighter-rouge">Read()</code> and <code class="language-plaintext highlighter-rouge">Write()</code>. A concrete type can satisfy all three interfaces at once:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="k">type</span> <span class="n">File</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">Name</span>    <span class="kt">string</span>
    <span class="n">content</span> <span class="kt">string</span>
<span class="p">}</span>

<span class="k">func</span> <span class="p">(</span><span class="n">f</span> <span class="n">File</span><span class="p">)</span> <span class="n">Read</span><span class="p">()</span> <span class="kt">string</span>       <span class="p">{</span> <span class="k">return</span> <span class="n">f</span><span class="o">.</span><span class="n">content</span> <span class="p">}</span>
<span class="k">func</span> <span class="p">(</span><span class="n">f</span> <span class="o">*</span><span class="n">File</span><span class="p">)</span> <span class="n">Write</span><span class="p">(</span><span class="n">data</span> <span class="kt">string</span><span class="p">)</span> <span class="p">{</span> <span class="n">f</span><span class="o">.</span><span class="n">content</span> <span class="o">+=</span> <span class="n">data</span> <span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="n">f</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="n">File</span><span class="p">{</span><span class="n">Name</span><span class="o">:</span> <span class="s">"data.txt"</span><span class="p">,</span> <span class="n">content</span><span class="o">:</span> <span class="s">"hello"</span><span class="p">}</span>

<span class="k">var</span> <span class="n">r</span> <span class="n">Reader</span> <span class="o">=</span> <span class="n">f</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"Reader: %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">r</span><span class="o">.</span><span class="n">Read</span><span class="p">())</span>

<span class="k">var</span> <span class="n">w</span> <span class="n">Writer</span> <span class="o">=</span> <span class="n">f</span>
<span class="n">w</span><span class="o">.</span><span class="n">Write</span><span class="p">(</span><span class="s">" world"</span><span class="p">)</span>

<span class="k">var</span> <span class="n">rw</span> <span class="n">ReadWriter</span> <span class="o">=</span> <span class="n">f</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"ReadWriter: %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">rw</span><span class="o">.</span><span class="n">Read</span><span class="p">())</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>This is the same pattern the standard library uses. <code class="language-plaintext highlighter-rouge">io.ReadWriter</code> embeds <code class="language-plaintext highlighter-rouge">io.Reader</code> and <code class="language-plaintext highlighter-rouge">io.Writer</code>, and types like <code class="language-plaintext highlighter-rouge">os.File</code> and <code class="language-plaintext highlighter-rouge">bytes.Buffer</code> satisfy all three.</p>

<h3 id="embedding-vs-inheritance">Embedding vs Inheritance</h3>

<p>Embedding looks superficially like inheritance, but it has a fundamental difference: the embedded type has no knowledge of the outer type. In classical inheritance, a base class method can call an overridden method on the subclass (dynamic dispatch). With Go embedding, <code class="language-plaintext highlighter-rouge">Base.Summary()</code> will always run <code class="language-plaintext highlighter-rouge">Base</code>’s logic — it cannot “see” that it is embedded inside <code class="language-plaintext highlighter-rouge">User</code> or <code class="language-plaintext highlighter-rouge">Order</code>.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="n">u</span> <span class="o">:=</span> <span class="n">User</span><span class="p">{</span><span class="n">Base</span><span class="o">:</span> <span class="n">Base</span><span class="p">{</span><span class="n">ID</span><span class="o">:</span> <span class="m">1</span><span class="p">,</span> <span class="n">CreatedAt</span><span class="o">:</span> <span class="s">"2025-01-15"</span><span class="p">},</span> <span class="n">Name</span><span class="o">:</span> <span class="s">"Alice"</span><span class="p">}</span>
<span class="k">var</span> <span class="n">s</span> <span class="n">Summarizer</span> <span class="o">=</span> <span class="n">u</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"Summarizer: %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">s</span><span class="o">.</span><span class="n">Summary</span><span class="p">())</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The key differences:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Base</code> has no idea <code class="language-plaintext highlighter-rouge">User</code> exists.</li>
  <li>There is no polymorphism on the embedded type — <code class="language-plaintext highlighter-rouge">Base</code> methods do not dispatch to <code class="language-plaintext highlighter-rouge">User</code> methods.</li>
  <li><code class="language-plaintext highlighter-rouge">User</code> “has a” <code class="language-plaintext highlighter-rouge">Base</code>, not “is a” <code class="language-plaintext highlighter-rouge">Base</code>.</li>
  <li>It is composition with promoted access, not inheritance.</li>
</ul>

<h2 id="generics">Generics</h2>

<p>Go 1.18 introduced generics (type parameters), letting you write functions and types that work across multiple types without sacrificing compile-time type safety.</p>

<h3 id="generic-functions">Generic Functions</h3>

<p>The most common use of generics is writing utility functions that operate on slices of any type. Here is a generic <code class="language-plaintext highlighter-rouge">Filter</code> function:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">Filter</span><span class="p">[</span><span class="n">T</span> <span class="n">any</span><span class="p">](</span><span class="n">slice</span> <span class="p">[]</span><span class="n">T</span><span class="p">,</span> <span class="n">predicate</span> <span class="k">func</span><span class="p">(</span><span class="n">T</span><span class="p">)</span> <span class="kt">bool</span><span class="p">)</span> <span class="p">[]</span><span class="n">T</span> <span class="p">{</span>
    <span class="k">var</span> <span class="n">result</span> <span class="p">[]</span><span class="n">T</span>
    <span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">v</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">slice</span> <span class="p">{</span>
        <span class="k">if</span> <span class="n">predicate</span><span class="p">(</span><span class="n">v</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">result</span> <span class="o">=</span> <span class="nb">append</span><span class="p">(</span><span class="n">result</span><span class="p">,</span> <span class="n">v</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="n">result</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">[T any]</code> after the function name declares a type parameter <code class="language-plaintext highlighter-rouge">T</code> with the constraint <code class="language-plaintext highlighter-rouge">any</code> — meaning <code class="language-plaintext highlighter-rouge">T</code> can be any type. The compiler generates specialized code for each concrete type used at call sites, so there is no runtime overhead.</p>

<p>A <code class="language-plaintext highlighter-rouge">Map</code> function follows the same pattern but transforms each element:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">Map</span><span class="p">[</span><span class="n">T</span> <span class="n">any</span><span class="p">,</span> <span class="n">U</span> <span class="n">any</span><span class="p">](</span><span class="n">slice</span> <span class="p">[]</span><span class="n">T</span><span class="p">,</span> <span class="n">transform</span> <span class="k">func</span><span class="p">(</span><span class="n">T</span><span class="p">)</span> <span class="n">U</span><span class="p">)</span> <span class="p">[]</span><span class="n">U</span> <span class="p">{</span>
    <span class="n">result</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="n">U</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">slice</span><span class="p">))</span>
    <span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">v</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">slice</span> <span class="p">{</span>
        <span class="n">result</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">transform</span><span class="p">(</span><span class="n">v</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="n">result</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Using them:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="n">nums</span> <span class="o">:=</span> <span class="p">[]</span><span class="kt">int</span><span class="p">{</span><span class="m">1</span><span class="p">,</span> <span class="m">2</span><span class="p">,</span> <span class="m">3</span><span class="p">,</span> <span class="m">4</span><span class="p">,</span> <span class="m">5</span><span class="p">,</span> <span class="m">6</span><span class="p">,</span> <span class="m">7</span><span class="p">,</span> <span class="m">8</span><span class="p">,</span> <span class="m">9</span><span class="p">,</span> <span class="m">10</span><span class="p">}</span>
<span class="n">evens</span> <span class="o">:=</span> <span class="n">Filter</span><span class="p">(</span><span class="n">nums</span><span class="p">,</span> <span class="k">func</span><span class="p">(</span><span class="n">n</span> <span class="kt">int</span><span class="p">)</span> <span class="kt">bool</span> <span class="p">{</span> <span class="k">return</span> <span class="n">n</span><span class="o">%</span><span class="m">2</span> <span class="o">==</span> <span class="m">0</span> <span class="p">})</span>
<span class="c">// evens = [2, 4, 6, 8, 10]</span>

<span class="n">words</span> <span class="o">:=</span> <span class="p">[]</span><span class="kt">string</span><span class="p">{</span><span class="s">"hello"</span><span class="p">,</span> <span class="s">"world"</span><span class="p">,</span> <span class="s">"go"</span><span class="p">}</span>
<span class="n">upper</span> <span class="o">:=</span> <span class="n">Map</span><span class="p">(</span><span class="n">words</span><span class="p">,</span> <span class="n">strings</span><span class="o">.</span><span class="n">ToUpper</span><span class="p">)</span>
<span class="c">// upper = ["HELLO", "WORLD", "GO"]</span>

<span class="n">lengths</span> <span class="o">:=</span> <span class="n">Map</span><span class="p">(</span><span class="n">words</span><span class="p">,</span> <span class="k">func</span><span class="p">(</span><span class="n">s</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">int</span> <span class="p">{</span> <span class="k">return</span> <span class="nb">len</span><span class="p">(</span><span class="n">s</span><span class="p">)</span> <span class="p">})</span>
<span class="c">// lengths = [5, 5, 2]</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Notice that you do not need to specify the type parameters explicitly at the call site — Go infers them from the arguments.</p>

<h3 id="type-constraints">Type Constraints</h3>

<p>The <code class="language-plaintext highlighter-rouge">any</code> constraint is maximally permissive, but it only lets you do things that work on every type (pass values around, store them in slices). If you need to perform operations like addition, you need a narrower constraint.</p>

<p>A type constraint is an interface that lists the allowed types using the union syntax:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre><span class="k">type</span> <span class="n">Number</span> <span class="k">interface</span> <span class="p">{</span>
    <span class="kt">int</span> <span class="o">|</span> <span class="kt">float64</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">Sum</span><span class="p">[</span><span class="n">T</span> <span class="n">Number</span><span class="p">](</span><span class="n">values</span> <span class="p">[]</span><span class="n">T</span><span class="p">)</span> <span class="n">T</span> <span class="p">{</span>
    <span class="k">var</span> <span class="n">total</span> <span class="n">T</span>
    <span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">v</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">values</span> <span class="p">{</span>
        <span class="n">total</span> <span class="o">+=</span> <span class="n">v</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="n">total</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">Number</code> constraint restricts <code class="language-plaintext highlighter-rouge">T</code> to <code class="language-plaintext highlighter-rouge">int</code> or <code class="language-plaintext highlighter-rouge">float64</code>. The <code class="language-plaintext highlighter-rouge">+=</code> operator is valid because both types support it. The compiler rejects any call that passes a type not in the union:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="n">Sum</span><span class="p">([]</span><span class="kt">int</span><span class="p">{</span><span class="m">1</span><span class="p">,</span> <span class="m">2</span><span class="p">,</span> <span class="m">3</span><span class="p">,</span> <span class="m">4</span><span class="p">,</span> <span class="m">5</span><span class="p">}))</span>          <span class="c">// 15</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="n">Sum</span><span class="p">([]</span><span class="kt">float64</span><span class="p">{</span><span class="m">1.1</span><span class="p">,</span> <span class="m">2.2</span><span class="p">,</span> <span class="m">3.3</span><span class="p">}))</span>       <span class="c">// 6.6</span>
<span class="c">// Sum([]string{"a", "b"})                        // compile error</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The standard library provides common constraints in the <code class="language-plaintext highlighter-rouge">golang.org/x/exp/constraints</code> package (e.g., <code class="language-plaintext highlighter-rouge">constraints.Ordered</code> for any type that supports <code class="language-plaintext highlighter-rouge">&lt;</code>, <code class="language-plaintext highlighter-rouge">&gt;</code>, <code class="language-plaintext highlighter-rouge">&lt;=</code>, <code class="language-plaintext highlighter-rouge">&gt;=</code>).</p>

<h3 id="generic-structs">Generic Structs</h3>

<p>Type parameters work on structs too. Here is a generic stack:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
</pre></td><td class="rouge-code"><pre><span class="k">type</span> <span class="n">Stack</span><span class="p">[</span><span class="n">T</span> <span class="n">any</span><span class="p">]</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">items</span> <span class="p">[]</span><span class="n">T</span>
<span class="p">}</span>

<span class="k">func</span> <span class="p">(</span><span class="n">s</span> <span class="o">*</span><span class="n">Stack</span><span class="p">[</span><span class="n">T</span><span class="p">])</span> <span class="n">Push</span><span class="p">(</span><span class="n">v</span> <span class="n">T</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">s</span><span class="o">.</span><span class="n">items</span> <span class="o">=</span> <span class="nb">append</span><span class="p">(</span><span class="n">s</span><span class="o">.</span><span class="n">items</span><span class="p">,</span> <span class="n">v</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">func</span> <span class="p">(</span><span class="n">s</span> <span class="o">*</span><span class="n">Stack</span><span class="p">[</span><span class="n">T</span><span class="p">])</span> <span class="n">Pop</span><span class="p">()</span> <span class="p">(</span><span class="n">T</span><span class="p">,</span> <span class="kt">bool</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">var</span> <span class="n">zero</span> <span class="n">T</span>
    <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">s</span><span class="o">.</span><span class="n">items</span><span class="p">)</span> <span class="o">==</span> <span class="m">0</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">zero</span><span class="p">,</span> <span class="no">false</span>
    <span class="p">}</span>
    <span class="n">v</span> <span class="o">:=</span> <span class="n">s</span><span class="o">.</span><span class="n">items</span><span class="p">[</span><span class="nb">len</span><span class="p">(</span><span class="n">s</span><span class="o">.</span><span class="n">items</span><span class="p">)</span><span class="o">-</span><span class="m">1</span><span class="p">]</span>
    <span class="n">s</span><span class="o">.</span><span class="n">items</span> <span class="o">=</span> <span class="n">s</span><span class="o">.</span><span class="n">items</span><span class="p">[</span><span class="o">:</span><span class="nb">len</span><span class="p">(</span><span class="n">s</span><span class="o">.</span><span class="n">items</span><span class="p">)</span><span class="o">-</span><span class="m">1</span><span class="p">]</span>
    <span class="k">return</span> <span class="n">v</span><span class="p">,</span> <span class="no">true</span>
<span class="p">}</span>

<span class="k">func</span> <span class="p">(</span><span class="n">s</span> <span class="o">*</span><span class="n">Stack</span><span class="p">[</span><span class="n">T</span><span class="p">])</span> <span class="n">Peek</span><span class="p">()</span> <span class="p">(</span><span class="n">T</span><span class="p">,</span> <span class="kt">bool</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">var</span> <span class="n">zero</span> <span class="n">T</span>
    <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">s</span><span class="o">.</span><span class="n">items</span><span class="p">)</span> <span class="o">==</span> <span class="m">0</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">zero</span><span class="p">,</span> <span class="no">false</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="n">s</span><span class="o">.</span><span class="n">items</span><span class="p">[</span><span class="nb">len</span><span class="p">(</span><span class="n">s</span><span class="o">.</span><span class="n">items</span><span class="p">)</span><span class="o">-</span><span class="m">1</span><span class="p">],</span> <span class="no">true</span>
<span class="p">}</span>

<span class="k">func</span> <span class="p">(</span><span class="n">s</span> <span class="o">*</span><span class="n">Stack</span><span class="p">[</span><span class="n">T</span><span class="p">])</span> <span class="n">IsEmpty</span><span class="p">()</span> <span class="kt">bool</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nb">len</span><span class="p">(</span><span class="n">s</span><span class="o">.</span><span class="n">items</span><span class="p">)</span> <span class="o">==</span> <span class="m">0</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>You specify the type parameter when creating an instance:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre></td><td class="rouge-code"><pre><span class="n">intStack</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="n">Stack</span><span class="p">[</span><span class="kt">int</span><span class="p">]{}</span>
<span class="n">intStack</span><span class="o">.</span><span class="n">Push</span><span class="p">(</span><span class="m">10</span><span class="p">)</span>
<span class="n">intStack</span><span class="o">.</span><span class="n">Push</span><span class="p">(</span><span class="m">20</span><span class="p">)</span>
<span class="n">intStack</span><span class="o">.</span><span class="n">Push</span><span class="p">(</span><span class="m">30</span><span class="p">)</span>

<span class="n">v</span><span class="p">,</span> <span class="n">ok</span> <span class="o">:=</span> <span class="n">intStack</span><span class="o">.</span><span class="n">Pop</span><span class="p">()</span>
<span class="c">// v = 30, ok = true</span>

<span class="n">strStack</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="n">Stack</span><span class="p">[</span><span class="kt">string</span><span class="p">]{}</span>
<span class="n">strStack</span><span class="o">.</span><span class="n">Push</span><span class="p">(</span><span class="s">"a"</span><span class="p">)</span>
<span class="n">strStack</span><span class="o">.</span><span class="n">Push</span><span class="p">(</span><span class="s">"b"</span><span class="p">)</span>
<span class="n">top</span><span class="p">,</span> <span class="n">_</span> <span class="o">:=</span> <span class="n">strStack</span><span class="o">.</span><span class="n">Peek</span><span class="p">()</span>
<span class="c">// top = "b"</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">var zero T</code> pattern is worth noting — it is the idiomatic way to get the zero value of a generic type. For <code class="language-plaintext highlighter-rouge">int</code> it is <code class="language-plaintext highlighter-rouge">0</code>, for <code class="language-plaintext highlighter-rouge">string</code> it is <code class="language-plaintext highlighter-rouge">""</code>, for pointers it is <code class="language-plaintext highlighter-rouge">nil</code>.</p>

<h3 id="multiple-type-parameters">Multiple Type Parameters</h3>

<p>A generic type can have more than one type parameter. Here is a <code class="language-plaintext highlighter-rouge">Pair</code> that holds two values of different types:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="k">type</span> <span class="n">Pair</span><span class="p">[</span><span class="n">K</span> <span class="n">any</span><span class="p">,</span> <span class="n">V</span> <span class="n">any</span><span class="p">]</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">Key</span>   <span class="n">K</span>
    <span class="n">Value</span> <span class="n">V</span>
<span class="p">}</span>

<span class="k">func</span> <span class="p">(</span><span class="n">p</span> <span class="n">Pair</span><span class="p">[</span><span class="n">K</span><span class="p">,</span> <span class="n">V</span><span class="p">])</span> <span class="n">String</span><span class="p">()</span> <span class="kt">string</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"(%v, %v)"</span><span class="p">,</span> <span class="n">p</span><span class="o">.</span><span class="n">Key</span><span class="p">,</span> <span class="n">p</span><span class="o">.</span><span class="n">Value</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>A <code class="language-plaintext highlighter-rouge">Zip</code> function combines two slices into a slice of pairs:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">Zip</span><span class="p">[</span><span class="n">K</span> <span class="n">any</span><span class="p">,</span> <span class="n">V</span> <span class="n">any</span><span class="p">](</span><span class="n">keys</span> <span class="p">[]</span><span class="n">K</span><span class="p">,</span> <span class="n">values</span> <span class="p">[]</span><span class="n">V</span><span class="p">)</span> <span class="p">[]</span><span class="n">Pair</span><span class="p">[</span><span class="n">K</span><span class="p">,</span> <span class="n">V</span><span class="p">]</span> <span class="p">{</span>
    <span class="n">n</span> <span class="o">:=</span> <span class="nb">len</span><span class="p">(</span><span class="n">keys</span><span class="p">)</span>
    <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">values</span><span class="p">)</span> <span class="o">&lt;</span> <span class="n">n</span> <span class="p">{</span>
        <span class="n">n</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">values</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="n">pairs</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="n">Pair</span><span class="p">[</span><span class="n">K</span><span class="p">,</span> <span class="n">V</span><span class="p">],</span> <span class="n">n</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">i</span> <span class="o">:=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">n</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span> <span class="p">{</span>
        <span class="n">pairs</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">Pair</span><span class="p">[</span><span class="n">K</span><span class="p">,</span> <span class="n">V</span><span class="p">]{</span><span class="n">Key</span><span class="o">:</span> <span class="n">keys</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">Value</span><span class="o">:</span> <span class="n">values</span><span class="p">[</span><span class="n">i</span><span class="p">]}</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="n">pairs</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>And <code class="language-plaintext highlighter-rouge">GroupBy</code> uses a <code class="language-plaintext highlighter-rouge">comparable</code> constraint on the key type, since map keys must be comparable:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">GroupBy</span><span class="p">[</span><span class="n">T</span> <span class="n">any</span><span class="p">,</span> <span class="n">K</span> <span class="n">comparable</span><span class="p">](</span><span class="n">items</span> <span class="p">[]</span><span class="n">T</span><span class="p">,</span> <span class="n">keyFn</span> <span class="k">func</span><span class="p">(</span><span class="n">T</span><span class="p">)</span> <span class="n">K</span><span class="p">)</span> <span class="k">map</span><span class="p">[</span><span class="n">K</span><span class="p">][]</span><span class="n">T</span> <span class="p">{</span>
    <span class="n">groups</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="k">map</span><span class="p">[</span><span class="n">K</span><span class="p">][]</span><span class="n">T</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">item</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">items</span> <span class="p">{</span>
        <span class="n">k</span> <span class="o">:=</span> <span class="n">keyFn</span><span class="p">(</span><span class="n">item</span><span class="p">)</span>
        <span class="n">groups</span><span class="p">[</span><span class="n">k</span><span class="p">]</span> <span class="o">=</span> <span class="nb">append</span><span class="p">(</span><span class="n">groups</span><span class="p">[</span><span class="n">k</span><span class="p">],</span> <span class="n">item</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="n">groups</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="n">pairs</span> <span class="o">:=</span> <span class="n">Zip</span><span class="p">([]</span><span class="kt">string</span><span class="p">{</span><span class="s">"a"</span><span class="p">,</span> <span class="s">"b"</span><span class="p">,</span> <span class="s">"c"</span><span class="p">},</span> <span class="p">[]</span><span class="kt">int</span><span class="p">{</span><span class="m">1</span><span class="p">,</span> <span class="m">2</span><span class="p">,</span> <span class="m">3</span><span class="p">})</span>
<span class="c">// [(a, 1), (b, 2), (c, 3)]</span>

<span class="n">words</span> <span class="o">:=</span> <span class="p">[]</span><span class="kt">string</span><span class="p">{</span><span class="s">"apple"</span><span class="p">,</span> <span class="s">"avocado"</span><span class="p">,</span> <span class="s">"banana"</span><span class="p">,</span> <span class="s">"blueberry"</span><span class="p">,</span> <span class="s">"cherry"</span><span class="p">}</span>
<span class="n">grouped</span> <span class="o">:=</span> <span class="n">GroupBy</span><span class="p">(</span><span class="n">words</span><span class="p">,</span> <span class="k">func</span><span class="p">(</span><span class="n">s</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">string</span> <span class="p">{</span> <span class="k">return</span> <span class="kt">string</span><span class="p">(</span><span class="n">s</span><span class="p">[</span><span class="m">0</span><span class="p">])</span> <span class="p">})</span>
<span class="c">// {"a": ["apple", "avocado"], "b": ["banana", "blueberry"], "c": ["cherry"]}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">comparable</code> constraint is a built-in that permits any type supporting <code class="language-plaintext highlighter-rouge">==</code> and <code class="language-plaintext highlighter-rouge">!=</code>. It is required for map keys and is narrower than <code class="language-plaintext highlighter-rouge">any</code> but broader than specific type unions.</p>

<h3 id="interface-constraints">Interface Constraints</h3>

<p>You can use any interface as a type constraint, not just unions of primitive types. This lets you write generic functions that call methods on their type parameters:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre><span class="k">type</span> <span class="n">Stringer</span> <span class="k">interface</span> <span class="p">{</span>
    <span class="n">String</span><span class="p">()</span> <span class="kt">string</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">JoinStrings</span><span class="p">[</span><span class="n">T</span> <span class="n">Stringer</span><span class="p">](</span><span class="n">items</span> <span class="p">[]</span><span class="n">T</span><span class="p">,</span> <span class="n">sep</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">string</span> <span class="p">{</span>
    <span class="n">parts</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="kt">string</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">items</span><span class="p">))</span>
    <span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">item</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">items</span> <span class="p">{</span>
        <span class="n">parts</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">item</span><span class="o">.</span><span class="n">String</span><span class="p">()</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="n">strings</span><span class="o">.</span><span class="n">Join</span><span class="p">(</span><span class="n">parts</span><span class="p">,</span> <span class="n">sep</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Any type that has a <code class="language-plaintext highlighter-rouge">String() string</code> method can be used with <code class="language-plaintext highlighter-rouge">JoinStrings</code>:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre><span class="k">type</span> <span class="n">Color</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">Name</span> <span class="kt">string</span>
    <span class="n">Hex</span>  <span class="kt">string</span>
<span class="p">}</span>
<span class="k">func</span> <span class="p">(</span><span class="n">c</span> <span class="n">Color</span><span class="p">)</span> <span class="n">String</span><span class="p">()</span> <span class="kt">string</span> <span class="p">{</span> <span class="k">return</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"%s(%s)"</span><span class="p">,</span> <span class="n">c</span><span class="o">.</span><span class="n">Name</span><span class="p">,</span> <span class="n">c</span><span class="o">.</span><span class="n">Hex</span><span class="p">)</span> <span class="p">}</span>

<span class="k">type</span> <span class="n">City</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">Name</span>    <span class="kt">string</span>
    <span class="n">Country</span> <span class="kt">string</span>
<span class="p">}</span>
<span class="k">func</span> <span class="p">(</span><span class="n">c</span> <span class="n">City</span><span class="p">)</span> <span class="n">String</span><span class="p">()</span> <span class="kt">string</span> <span class="p">{</span> <span class="k">return</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"%s, %s"</span><span class="p">,</span> <span class="n">c</span><span class="o">.</span><span class="n">Name</span><span class="p">,</span> <span class="n">c</span><span class="o">.</span><span class="n">Country</span><span class="p">)</span> <span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre></td><td class="rouge-code"><pre><span class="n">colors</span> <span class="o">:=</span> <span class="p">[]</span><span class="n">Color</span><span class="p">{</span>
    <span class="p">{</span><span class="n">Name</span><span class="o">:</span> <span class="s">"Red"</span><span class="p">,</span> <span class="n">Hex</span><span class="o">:</span> <span class="s">"#FF0000"</span><span class="p">},</span>
    <span class="p">{</span><span class="n">Name</span><span class="o">:</span> <span class="s">"Green"</span><span class="p">,</span> <span class="n">Hex</span><span class="o">:</span> <span class="s">"#00FF00"</span><span class="p">},</span>
<span class="p">}</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="n">JoinStrings</span><span class="p">(</span><span class="n">colors</span><span class="p">,</span> <span class="s">", "</span><span class="p">))</span>
<span class="c">// Red(#FF0000), Green(#00FF00)</span>

<span class="n">cities</span> <span class="o">:=</span> <span class="p">[]</span><span class="n">City</span><span class="p">{</span>
    <span class="p">{</span><span class="n">Name</span><span class="o">:</span> <span class="s">"Tokyo"</span><span class="p">,</span> <span class="n">Country</span><span class="o">:</span> <span class="s">"Japan"</span><span class="p">},</span>
    <span class="p">{</span><span class="n">Name</span><span class="o">:</span> <span class="s">"Paris"</span><span class="p">,</span> <span class="n">Country</span><span class="o">:</span> <span class="s">"France"</span><span class="p">},</span>
<span class="p">}</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="n">JoinStrings</span><span class="p">(</span><span class="n">cities</span><span class="p">,</span> <span class="s">" | "</span><span class="p">))</span>
<span class="c">// Tokyo, Japan | Paris, France</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The difference between an interface constraint and <code class="language-plaintext highlighter-rouge">any</code> is enforcement at compile time. With <code class="language-plaintext highlighter-rouge">any</code>, you cannot call <code class="language-plaintext highlighter-rouge">.String()</code> on the type parameter because the compiler does not know it exists. With <code class="language-plaintext highlighter-rouge">Stringer</code>, the compiler guarantees every type passed to <code class="language-plaintext highlighter-rouge">JoinStrings</code> has that method, so the call is safe. This is the same implicit satisfaction mechanism from the interfaces section — the constraint is an interface, and the concrete type must satisfy it.</p>]]></content><author><name>kimserey</name></author><category term="go" /><summary type="html"><![CDATA[Go takes a distinctive approach to polymorphism and code reuse. There are no classes, no inheritance hierarchies, and no explicit implements declarations. Instead, Go provides three orthogonal mechanisms: interfaces for abstraction, embedding for composition, and generics for type-safe parameterization. Each one is simple on its own, and together they cover the same ground that class hierarchies cover in other languages — with less coupling. In this post, we’ll work through all three, starting with how Go interfaces are satisfied implicitly, then moving to struct and interface embedding, and finishing with generic functions and types.]]></summary></entry><entry><title type="html">Go Basics — Types, Memory and Data Structures</title><link href="https://www.kimsereylam.com/go/2026/08/26/go-basics-types-memory-and-data-structures.html" rel="alternate" type="text/html" title="Go Basics — Types, Memory and Data Structures" /><published>2026-08-26T00:00:00-05:00</published><updated>2026-08-26T00:00:00-05:00</updated><id>https://www.kimsereylam.com/go/2026/08/26/go-basics-types-memory-and-data-structures</id><content type="html" xml:base="https://www.kimsereylam.com/go/2026/08/26/go-basics-types-memory-and-data-structures.html"><![CDATA[<p>This is the first tutorial in a series on Go. It covers three foundational topics: pointers and references (how Go passes data around and when mutations are visible), structs with methods and receivers (how Go models data and behavior without classes), and slices and maps (the two built-in data structures you will use constantly). Each section builds on the previous one — pointers explain why receiver types matter, and receiver types explain how slice and map patterns work in practice.</p>

<!--more-->

<h2 id="pointers-and-references">Pointers and References</h2>

<p>Go is a pass-by-value language. When you pass a variable to a function, Go copies the value. The function works on its own independent copy, and the caller never sees changes. This is safe and predictable, but sometimes you need the function to modify the original. That is what pointers are for.</p>

<h3 id="value-semantics">Value Semantics</h3>

<p>A function that takes a plain <code class="language-plaintext highlighter-rouge">int</code> gets a copy. Incrementing the copy has no effect on the caller’s variable:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">incrementBroken</span><span class="p">(</span><span class="n">counter</span> <span class="kt">int</span><span class="p">)</span> <span class="p">{</span>
	<span class="n">counter</span><span class="o">++</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  inside incrementBroken: counter = %d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">counter</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">counter</span> <span class="o">:=</span> <span class="m">0</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  before: counter = %d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">counter</span><span class="p">)</span>
	<span class="n">incrementBroken</span><span class="p">(</span><span class="n">counter</span><span class="p">)</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  after:  counter = %d  &lt;- still 0! the function got a copy</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">counter</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Inside <code class="language-plaintext highlighter-rouge">incrementBroken</code>, <code class="language-plaintext highlighter-rouge">counter</code> is 1. Back in <code class="language-plaintext highlighter-rouge">main</code>, it is still 0. The function modified its own copy and the original was untouched.</p>

<h3 id="pointer-semantics">Pointer Semantics</h3>

<p>A pointer holds the memory address of a value. The <code class="language-plaintext highlighter-rouge">&amp;</code> operator takes the address of a variable, and the <code class="language-plaintext highlighter-rouge">*</code> operator dereferences a pointer to access the value it points to. When you pass a pointer to a function, both the caller and the function are looking at the same memory:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">incrementFixed</span><span class="p">(</span><span class="n">counter</span> <span class="o">*</span><span class="kt">int</span><span class="p">)</span> <span class="p">{</span>
	<span class="o">*</span><span class="n">counter</span><span class="o">++</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  inside incrementFixed: *counter = %d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="o">*</span><span class="n">counter</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">counter</span> <span class="o">:=</span> <span class="m">0</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  before: counter = %d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">counter</span><span class="p">)</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  address of counter: %p</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">counter</span><span class="p">)</span>
	<span class="n">incrementFixed</span><span class="p">(</span><span class="o">&amp;</span><span class="n">counter</span><span class="p">)</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  after:  counter = %d  &lt;- it changed! both see the same memory</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">counter</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">&amp;counter</code> produces a <code class="language-plaintext highlighter-rouge">*int</code> — a pointer to an int. Inside <code class="language-plaintext highlighter-rouge">incrementFixed</code>, <code class="language-plaintext highlighter-rouge">*counter++</code> follows the pointer to the original variable and increments it. After the call, <code class="language-plaintext highlighter-rouge">counter</code> in <code class="language-plaintext highlighter-rouge">main</code> is 1.</p>

<h3 id="shared-state-via-pointers">Shared State via Pointers</h3>

<p>Pointers let multiple functions share and coordinate through the same variable. Here, a producer writes values and a consumer reads the result — both operate on the same <code class="language-plaintext highlighter-rouge">uint64</code> through a pointer:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">producer</span><span class="p">(</span><span class="n">lsn</span> <span class="o">*</span><span class="kt">uint64</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">for</span> <span class="n">i</span> <span class="o">:=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="m">5</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span> <span class="p">{</span>
		<span class="o">*</span><span class="n">lsn</span> <span class="o">=</span> <span class="kt">uint64</span><span class="p">((</span><span class="n">i</span> <span class="o">+</span> <span class="m">1</span><span class="p">)</span> <span class="o">*</span> <span class="m">100</span><span class="p">)</span>
		<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  producer set LSN to %d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="o">*</span><span class="n">lsn</span><span class="p">)</span>
	<span class="p">}</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">consumer</span><span class="p">(</span><span class="n">lsn</span> <span class="o">*</span><span class="kt">uint64</span><span class="p">)</span> <span class="p">{</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  consumer reads LSN = %d  &lt;- sees the producer's last write</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="o">*</span><span class="n">lsn</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
	<span class="k">var</span> <span class="n">confirmedLSN</span> <span class="kt">uint64</span> <span class="o">=</span> <span class="m">0</span>
	<span class="n">producer</span><span class="p">(</span><span class="o">&amp;</span><span class="n">confirmedLSN</span><span class="p">)</span>
	<span class="n">consumer</span><span class="p">(</span><span class="o">&amp;</span><span class="n">confirmedLSN</span><span class="p">)</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  final LSN:   %d  &lt;- everyone sees the same value</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">confirmedLSN</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Both <code class="language-plaintext highlighter-rouge">producer</code> and <code class="language-plaintext highlighter-rouge">consumer</code> receive a <code class="language-plaintext highlighter-rouge">*uint64</code> pointing to the same <code class="language-plaintext highlighter-rouge">confirmedLSN</code> variable. The producer’s writes are immediately visible to anyone else who holds a pointer to that address.</p>

<h3 id="pointers-to-structs">Pointers to Structs</h3>

<p>Pointers become essential with structs. If you pass a struct by value to a function, the function gets a full copy — modifications are lost. A method with a pointer receiver, on the other hand, mutates the original:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
</pre></td><td class="rouge-code"><pre><span class="k">type</span> <span class="n">Writer</span> <span class="k">struct</span> <span class="p">{</span>
	<span class="n">Path</span>      <span class="kt">string</span>
	<span class="n">BytesUsed</span> <span class="kt">int</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">NewWriter</span><span class="p">(</span><span class="n">path</span> <span class="kt">string</span><span class="p">)</span> <span class="o">*</span><span class="n">Writer</span> <span class="p">{</span>
	<span class="k">return</span> <span class="o">&amp;</span><span class="n">Writer</span><span class="p">{</span><span class="n">Path</span><span class="o">:</span> <span class="n">path</span><span class="p">,</span> <span class="n">BytesUsed</span><span class="o">:</span> <span class="m">0</span><span class="p">}</span>
<span class="p">}</span>

<span class="k">func</span> <span class="p">(</span><span class="n">w</span> <span class="o">*</span><span class="n">Writer</span><span class="p">)</span> <span class="n">Write</span><span class="p">(</span><span class="n">data</span> <span class="kt">string</span><span class="p">)</span> <span class="p">{</span>
	<span class="n">w</span><span class="o">.</span><span class="n">BytesUsed</span> <span class="o">+=</span> <span class="nb">len</span><span class="p">(</span><span class="n">data</span><span class="p">)</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  wrote %d bytes to %s (total: %d)</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">data</span><span class="p">),</span> <span class="n">w</span><span class="o">.</span><span class="n">Path</span><span class="p">,</span> <span class="n">w</span><span class="o">.</span><span class="n">BytesUsed</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">writeBroken</span><span class="p">(</span><span class="n">w</span> <span class="n">Writer</span><span class="p">,</span> <span class="n">data</span> <span class="kt">string</span><span class="p">)</span> <span class="p">{</span>
	<span class="n">w</span><span class="o">.</span><span class="n">BytesUsed</span> <span class="o">+=</span> <span class="nb">len</span><span class="p">(</span><span class="n">data</span><span class="p">)</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  (broken) wrote %d bytes (total in copy: %d)</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">data</span><span class="p">),</span> <span class="n">w</span><span class="o">.</span><span class="n">BytesUsed</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">w</span> <span class="o">:=</span> <span class="n">NewWriter</span><span class="p">(</span><span class="s">"/data/output.parquet"</span><span class="p">)</span>
	<span class="n">w</span><span class="o">.</span><span class="n">Write</span><span class="p">(</span><span class="s">"hello"</span><span class="p">)</span>
	<span class="n">w</span><span class="o">.</span><span class="n">Write</span><span class="p">(</span><span class="s">"world"</span><span class="p">)</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  after writes: BytesUsed = %d &lt;- pointer receiver mutated it</span><span class="se">\n\n</span><span class="s">"</span><span class="p">,</span> <span class="n">w</span><span class="o">.</span><span class="n">BytesUsed</span><span class="p">)</span>

	<span class="n">w2</span> <span class="o">:=</span> <span class="n">Writer</span><span class="p">{</span><span class="n">Path</span><span class="o">:</span> <span class="s">"/data/broken.parquet"</span><span class="p">,</span> <span class="n">BytesUsed</span><span class="o">:</span> <span class="m">0</span><span class="p">}</span>
	<span class="n">writeBroken</span><span class="p">(</span><span class="n">w2</span><span class="p">,</span> <span class="s">"hello"</span><span class="p">)</span>
	<span class="n">writeBroken</span><span class="p">(</span><span class="n">w2</span><span class="p">,</span> <span class="s">"world"</span><span class="p">)</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  after broken writes: BytesUsed = %d &lt;- still 0, copies were modified</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">w2</span><span class="o">.</span><span class="n">BytesUsed</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">NewWriter</code> returns a <code class="language-plaintext highlighter-rouge">*Writer</code> — a pointer to a heap-allocated struct. The <code class="language-plaintext highlighter-rouge">Write</code> method has a pointer receiver <code class="language-plaintext highlighter-rouge">(w *Writer)</code>, so it modifies the same struct the caller holds. By contrast, <code class="language-plaintext highlighter-rouge">writeBroken</code> takes a <code class="language-plaintext highlighter-rouge">Writer</code> by value. Each call gets a fresh copy, increments the copy’s <code class="language-plaintext highlighter-rouge">BytesUsed</code>, and throws it away. The caller’s <code class="language-plaintext highlighter-rouge">w2</code> never changes.</p>

<h3 id="nil-pointers">Nil Pointers</h3>

<p>A pointer that has not been assigned points to nothing — its zero value is <code class="language-plaintext highlighter-rouge">nil</code>. Calling a method on a nil pointer will panic at runtime. Always check before dereferencing:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
	<span class="k">var</span> <span class="n">w</span> <span class="o">*</span><span class="n">Writer</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  w == nil? %t</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">w</span> <span class="o">==</span> <span class="no">nil</span><span class="p">)</span>
	<span class="k">if</span> <span class="n">w</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="n">w</span><span class="o">.</span><span class="n">Write</span><span class="p">(</span><span class="s">"data"</span><span class="p">)</span>
	<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
		<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"  skipped write: w is nil (would panic if we called w.Write)"</span><span class="p">)</span>
	<span class="p">}</span>
	<span class="n">w</span> <span class="o">=</span> <span class="n">NewWriter</span><span class="p">(</span><span class="s">"/data/safe.parquet"</span><span class="p">)</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  w == nil? %t  &lt;- initialized now</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">w</span> <span class="o">==</span> <span class="no">nil</span><span class="p">)</span>
	<span class="n">w</span><span class="o">.</span><span class="n">Write</span><span class="p">(</span><span class="s">"safe data"</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">var w *Writer</code> declaration gives <code class="language-plaintext highlighter-rouge">w</code> a zero value of <code class="language-plaintext highlighter-rouge">nil</code>. The nil check prevents the panic. After calling <code class="language-plaintext highlighter-rouge">NewWriter</code>, <code class="language-plaintext highlighter-rouge">w</code> points to a valid struct and methods work normally.</p>

<h2 id="structs-methods-and-receivers">Structs, Methods and Receivers</h2>

<p>Go does not have classes. Instead, you define structs to hold data and attach methods to types using receivers. The receiver type — value or pointer — determines whether the method can mutate the struct.</p>

<h3 id="struct-initialization">Struct Initialization</h3>

<p>A struct groups fields into a single type. You can initialize it with named fields, rely on zero values, or provide only the fields you care about:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
</pre></td><td class="rouge-code"><pre><span class="k">type</span> <span class="n">Duration</span> <span class="n">time</span><span class="o">.</span><span class="n">Duration</span>

<span class="k">type</span> <span class="n">Config</span> <span class="k">struct</span> <span class="p">{</span>
	<span class="n">Host</span>     <span class="kt">string</span>
	<span class="n">Port</span>     <span class="kt">int</span>
	<span class="n">Interval</span> <span class="n">Duration</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">c1</span> <span class="o">:=</span> <span class="n">Config</span><span class="p">{</span><span class="n">Host</span><span class="o">:</span> <span class="s">"localhost"</span><span class="p">,</span> <span class="n">Port</span><span class="o">:</span> <span class="m">5432</span><span class="p">,</span> <span class="n">Interval</span><span class="o">:</span> <span class="n">Duration</span><span class="p">(</span><span class="m">5</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Second</span><span class="p">)}</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  named fields:  %+v</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">c1</span><span class="p">)</span>
	<span class="k">var</span> <span class="n">c2</span> <span class="n">Config</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  zero value:    %+v</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">c2</span><span class="p">)</span>
	<span class="n">c3</span> <span class="o">:=</span> <span class="n">Config</span><span class="p">{</span><span class="n">Host</span><span class="o">:</span> <span class="s">"prod-db"</span><span class="p">}</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  partial init:  %+v</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">c3</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">c1</code> sets every field explicitly. <code class="language-plaintext highlighter-rouge">c2</code> uses Go’s zero values — empty string for <code class="language-plaintext highlighter-rouge">Host</code>, 0 for <code class="language-plaintext highlighter-rouge">Port</code>, 0 for <code class="language-plaintext highlighter-rouge">Interval</code>. <code class="language-plaintext highlighter-rouge">c3</code> sets only <code class="language-plaintext highlighter-rouge">Host</code>; the rest default to zero values. The <code class="language-plaintext highlighter-rouge">%+v</code> format verb prints field names alongside values, which is useful for debugging.</p>

<h3 id="custom-types">Custom Types</h3>

<p>Go lets you define a new named type based on an existing type. The new type is distinct — you cannot mix them without an explicit conversion — but it can carry its own methods:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre></td><td class="rouge-code"><pre><span class="k">type</span> <span class="n">Duration</span> <span class="n">time</span><span class="o">.</span><span class="n">Duration</span>

<span class="k">func</span> <span class="p">(</span><span class="n">d</span> <span class="n">Duration</span><span class="p">)</span> <span class="n">Std</span><span class="p">()</span> <span class="n">time</span><span class="o">.</span><span class="n">Duration</span> <span class="p">{</span>
	<span class="k">return</span> <span class="n">time</span><span class="o">.</span><span class="n">Duration</span><span class="p">(</span><span class="n">d</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">d</span> <span class="o">:=</span> <span class="n">Duration</span><span class="p">(</span><span class="m">10</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Second</span><span class="p">)</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  Duration value: %v</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">d</span><span class="p">)</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  As time.Duration: %v</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">d</span><span class="o">.</span><span class="n">Std</span><span class="p">())</span>
	<span class="n">td</span> <span class="o">:=</span> <span class="n">time</span><span class="o">.</span><span class="n">Duration</span><span class="p">(</span><span class="n">d</span><span class="p">)</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  Explicit conversion: %v</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">td</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">Duration</code> is a new type with the same underlying representation as <code class="language-plaintext highlighter-rouge">time.Duration</code>, but Go treats them as separate types. You cannot pass a <code class="language-plaintext highlighter-rouge">Duration</code> where a <code class="language-plaintext highlighter-rouge">time.Duration</code> is expected without converting. The <code class="language-plaintext highlighter-rouge">Std()</code> method provides a convenient way to convert back. This pattern is useful for adding domain-specific methods to standard library types.</p>

<h3 id="value-receivers-vs-pointer-receivers">Value Receivers vs Pointer Receivers</h3>

<p>A value receiver gets a copy of the struct. It can read fields but any changes are lost when the method returns. A pointer receiver gets the address of the original, so changes persist:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="p">(</span><span class="n">d</span> <span class="n">Duration</span><span class="p">)</span> <span class="n">Std</span><span class="p">()</span> <span class="n">time</span><span class="o">.</span><span class="n">Duration</span> <span class="p">{</span>
	<span class="k">return</span> <span class="n">time</span><span class="o">.</span><span class="n">Duration</span><span class="p">(</span><span class="n">d</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">func</span> <span class="p">(</span><span class="n">d</span> <span class="o">*</span><span class="n">Duration</span><span class="p">)</span> <span class="n">Set</span><span class="p">(</span><span class="n">v</span> <span class="n">time</span><span class="o">.</span><span class="n">Duration</span><span class="p">)</span> <span class="p">{</span>
	<span class="o">*</span><span class="n">d</span> <span class="o">=</span> <span class="n">Duration</span><span class="p">(</span><span class="n">v</span><span class="p">)</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  duration set to %v</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">d</span><span class="o">.</span><span class="n">Std</span><span class="p">())</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">d</span> <span class="o">:=</span> <span class="n">Duration</span><span class="p">(</span><span class="m">3</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Second</span><span class="p">)</span>
	<span class="n">std</span> <span class="o">:=</span> <span class="n">d</span><span class="o">.</span><span class="n">Std</span><span class="p">()</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  d.Std() = %v (d is unchanged: %v)</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">std</span><span class="p">,</span> <span class="n">time</span><span class="o">.</span><span class="n">Duration</span><span class="p">(</span><span class="n">d</span><span class="p">))</span>

	<span class="n">d</span> <span class="o">=</span> <span class="n">Duration</span><span class="p">(</span><span class="m">1</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Second</span><span class="p">)</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  before Set: %v</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">d</span><span class="o">.</span><span class="n">Std</span><span class="p">())</span>
	<span class="n">d</span><span class="o">.</span><span class="n">Set</span><span class="p">(</span><span class="m">5</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Second</span><span class="p">)</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  after Set:  %v  &lt;- mutated via pointer receiver</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">d</span><span class="o">.</span><span class="n">Std</span><span class="p">())</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">Std()</code> uses a value receiver — it reads <code class="language-plaintext highlighter-rouge">d</code> but does not change it. <code class="language-plaintext highlighter-rouge">Set()</code> uses a pointer receiver — it overwrites the value that <code class="language-plaintext highlighter-rouge">d</code> points to. The rule of thumb: use a pointer receiver if the method needs to modify the receiver, or if the struct is large enough that copying it would be wasteful. Use a value receiver for small, immutable reads.</p>

<h3 id="the-constructor-pattern">The Constructor Pattern</h3>

<p>Go does not have constructors, but the convention is to write a <code class="language-plaintext highlighter-rouge">NewXxx</code> function that returns a pointer to an initialized struct. This lets you set up internal state (like pre-allocated buffers) that callers should not need to think about:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
</pre></td><td class="rouge-code"><pre><span class="k">type</span> <span class="n">Writer</span> <span class="k">struct</span> <span class="p">{</span>
	<span class="n">Path</span>         <span class="kt">string</span>
	<span class="n">BatchSize</span>    <span class="kt">int</span>
	<span class="n">BytesWritten</span> <span class="kt">int</span>
	<span class="n">buffer</span>       <span class="p">[]</span><span class="kt">string</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">NewWriter</span><span class="p">(</span><span class="n">path</span> <span class="kt">string</span><span class="p">,</span> <span class="n">batchSize</span> <span class="kt">int</span><span class="p">)</span> <span class="o">*</span><span class="n">Writer</span> <span class="p">{</span>
	<span class="k">return</span> <span class="o">&amp;</span><span class="n">Writer</span><span class="p">{</span>
		<span class="n">Path</span><span class="o">:</span>      <span class="n">path</span><span class="p">,</span>
		<span class="n">BatchSize</span><span class="o">:</span> <span class="n">batchSize</span><span class="p">,</span>
		<span class="n">buffer</span><span class="o">:</span>    <span class="nb">make</span><span class="p">([]</span><span class="kt">string</span><span class="p">,</span> <span class="m">0</span><span class="p">,</span> <span class="n">batchSize</span><span class="p">),</span>
	<span class="p">}</span>
<span class="p">}</span>

<span class="k">func</span> <span class="p">(</span><span class="n">w</span> <span class="o">*</span><span class="n">Writer</span><span class="p">)</span> <span class="n">Write</span><span class="p">(</span><span class="n">data</span> <span class="kt">string</span><span class="p">)</span> <span class="p">{</span>
	<span class="n">w</span><span class="o">.</span><span class="n">buffer</span> <span class="o">=</span> <span class="nb">append</span><span class="p">(</span><span class="n">w</span><span class="o">.</span><span class="n">buffer</span><span class="p">,</span> <span class="n">data</span><span class="p">)</span>
	<span class="n">w</span><span class="o">.</span><span class="n">BytesWritten</span> <span class="o">+=</span> <span class="nb">len</span><span class="p">(</span><span class="n">data</span><span class="p">)</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  wrote %q (buffer: %d/%d, total bytes: %d)</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span>
		<span class="n">data</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">w</span><span class="o">.</span><span class="n">buffer</span><span class="p">),</span> <span class="n">w</span><span class="o">.</span><span class="n">BatchSize</span><span class="p">,</span> <span class="n">w</span><span class="o">.</span><span class="n">BytesWritten</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">func</span> <span class="p">(</span><span class="n">w</span> <span class="o">*</span><span class="n">Writer</span><span class="p">)</span> <span class="n">Flush</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  flushing %d items from buffer</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">w</span><span class="o">.</span><span class="n">buffer</span><span class="p">))</span>
	<span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">item</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">w</span><span class="o">.</span><span class="n">buffer</span> <span class="p">{</span>
		<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"    -&gt; %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">item</span><span class="p">)</span>
	<span class="p">}</span>
	<span class="n">w</span><span class="o">.</span><span class="n">buffer</span> <span class="o">=</span> <span class="n">w</span><span class="o">.</span><span class="n">buffer</span><span class="p">[</span><span class="o">:</span><span class="m">0</span><span class="p">]</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  buffer cleared (len=%d, cap=%d)</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">w</span><span class="o">.</span><span class="n">buffer</span><span class="p">),</span> <span class="nb">cap</span><span class="p">(</span><span class="n">w</span><span class="o">.</span><span class="n">buffer</span><span class="p">))</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">w</span> <span class="o">:=</span> <span class="n">NewWriter</span><span class="p">(</span><span class="s">"/data/output.parquet"</span><span class="p">,</span> <span class="m">3</span><span class="p">)</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  created: %+v</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="o">*</span><span class="n">w</span><span class="p">)</span>
	<span class="n">w</span><span class="o">.</span><span class="n">Write</span><span class="p">(</span><span class="s">"INSERT users alice"</span><span class="p">)</span>
	<span class="n">w</span><span class="o">.</span><span class="n">Write</span><span class="p">(</span><span class="s">"INSERT users bob"</span><span class="p">)</span>
	<span class="n">w</span><span class="o">.</span><span class="n">Write</span><span class="p">(</span><span class="s">"UPDATE users alice"</span><span class="p">)</span>
	<span class="n">w</span><span class="o">.</span><span class="n">Flush</span><span class="p">()</span>
	<span class="n">w</span><span class="o">.</span><span class="n">Write</span><span class="p">(</span><span class="s">"DELETE users bob"</span><span class="p">)</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  after more writes: buffer=%d, total bytes=%d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span>
		<span class="nb">len</span><span class="p">(</span><span class="n">w</span><span class="o">.</span><span class="n">buffer</span><span class="p">),</span> <span class="n">w</span><span class="o">.</span><span class="n">BytesWritten</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">NewWriter</code> allocates the struct on the heap (returning <code class="language-plaintext highlighter-rouge">&amp;Writer{...}</code> ensures the struct escapes the function’s stack frame) and pre-allocates the internal buffer with <code class="language-plaintext highlighter-rouge">make([]string, 0, batchSize)</code>. The lowercase <code class="language-plaintext highlighter-rouge">buffer</code> field is unexported — callers outside the package cannot access it directly. Both <code class="language-plaintext highlighter-rouge">Write</code> and <code class="language-plaintext highlighter-rouge">Flush</code> use pointer receivers because they mutate the struct. Notice how <code class="language-plaintext highlighter-rouge">Flush</code> resets the buffer with <code class="language-plaintext highlighter-rouge">w.buffer[:0]</code> — this keeps the allocated memory and just sets the length to zero, which we will look at more closely in the next section.</p>

<h2 id="slices-and-maps">Slices and Maps</h2>

<p>Slices and maps are Go’s two workhorse collection types. A slice is a dynamically-sized view into an underlying array. A map is a hash table. Both are reference types — they contain internal pointers, so passing them to a function does not copy the underlying data.</p>

<h3 id="slice-internals-length-vs-capacity">Slice Internals: Length vs Capacity</h3>

<p>A slice header has three fields: a pointer to the underlying array, a length (how many elements are in use), and a capacity (how many elements the array can hold before a new allocation is needed). Understanding this distinction is key to writing efficient Go:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">s</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="kt">int</span><span class="p">,</span> <span class="m">0</span><span class="p">,</span> <span class="m">5</span><span class="p">)</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  make([]int, 0, 5): len=%d, cap=%d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">s</span><span class="p">),</span> <span class="nb">cap</span><span class="p">(</span><span class="n">s</span><span class="p">))</span>
	<span class="k">for</span> <span class="n">i</span> <span class="o">:=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="m">8</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span> <span class="p">{</span>
		<span class="n">s</span> <span class="o">=</span> <span class="nb">append</span><span class="p">(</span><span class="n">s</span><span class="p">,</span> <span class="n">i</span><span class="p">)</span>
		<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  append(%d): len=%d, cap=%d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">i</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">s</span><span class="p">),</span> <span class="nb">cap</span><span class="p">(</span><span class="n">s</span><span class="p">))</span>
	<span class="p">}</span>
	<span class="k">var</span> <span class="n">nilSlice</span> <span class="p">[]</span><span class="kt">int</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"</span><span class="se">\n</span><span class="s">  nil slice: len=%d, cap=%d, is nil=%t</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span>
		<span class="nb">len</span><span class="p">(</span><span class="n">nilSlice</span><span class="p">),</span> <span class="nb">cap</span><span class="p">(</span><span class="n">nilSlice</span><span class="p">),</span> <span class="n">nilSlice</span> <span class="o">==</span> <span class="no">nil</span><span class="p">)</span>
	<span class="n">nilSlice</span> <span class="o">=</span> <span class="nb">append</span><span class="p">(</span><span class="n">nilSlice</span><span class="p">,</span> <span class="m">1</span><span class="p">)</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  after append: len=%d, cap=%d, is nil=%t</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span>
		<span class="nb">len</span><span class="p">(</span><span class="n">nilSlice</span><span class="p">),</span> <span class="nb">cap</span><span class="p">(</span><span class="n">nilSlice</span><span class="p">),</span> <span class="n">nilSlice</span> <span class="o">==</span> <span class="no">nil</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">make([]int, 0, 5)</code> creates a slice with length 0 and capacity 5. The first five appends fit without allocating. On the sixth append, Go allocates a new, larger array (typically doubling the capacity), copies the existing elements, and updates the slice header. This is why <code class="language-plaintext highlighter-rouge">append</code> returns a new slice — the pointer inside the header may have changed.</p>

<p>A nil slice (<code class="language-plaintext highlighter-rouge">var nilSlice []int</code>) has length 0, capacity 0, and is equal to <code class="language-plaintext highlighter-rouge">nil</code>. But <code class="language-plaintext highlighter-rouge">append</code> works on nil slices — it allocates on the first call. This means you do not need to initialize a slice before appending to it.</p>

<h3 id="append-and-spread">Append and Spread</h3>

<p>The <code class="language-plaintext highlighter-rouge">...</code> operator spreads a slice into individual arguments. This is how you merge slices:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
</pre></td><td class="rouge-code"><pre><span class="k">type</span> <span class="n">Change</span> <span class="k">struct</span> <span class="p">{</span>
	<span class="n">Table</span>  <span class="kt">string</span>
	<span class="n">Action</span> <span class="kt">string</span>
	<span class="n">Key</span>    <span class="kt">string</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">batch1</span> <span class="o">:=</span> <span class="p">[]</span><span class="n">Change</span><span class="p">{</span>
		<span class="p">{</span><span class="n">Table</span><span class="o">:</span> <span class="s">"users"</span><span class="p">,</span> <span class="n">Action</span><span class="o">:</span> <span class="s">"INSERT"</span><span class="p">,</span> <span class="n">Key</span><span class="o">:</span> <span class="s">"alice"</span><span class="p">},</span>
		<span class="p">{</span><span class="n">Table</span><span class="o">:</span> <span class="s">"users"</span><span class="p">,</span> <span class="n">Action</span><span class="o">:</span> <span class="s">"UPDATE"</span><span class="p">,</span> <span class="n">Key</span><span class="o">:</span> <span class="s">"bob"</span><span class="p">},</span>
	<span class="p">}</span>
	<span class="n">batch2</span> <span class="o">:=</span> <span class="p">[]</span><span class="n">Change</span><span class="p">{</span>
		<span class="p">{</span><span class="n">Table</span><span class="o">:</span> <span class="s">"orders"</span><span class="p">,</span> <span class="n">Action</span><span class="o">:</span> <span class="s">"INSERT"</span><span class="p">,</span> <span class="n">Key</span><span class="o">:</span> <span class="s">"ord-1"</span><span class="p">},</span>
	<span class="p">}</span>
	<span class="k">var</span> <span class="n">all</span> <span class="p">[]</span><span class="n">Change</span>
	<span class="n">all</span> <span class="o">=</span> <span class="nb">append</span><span class="p">(</span><span class="n">all</span><span class="p">,</span> <span class="n">batch1</span><span class="o">...</span><span class="p">)</span>
	<span class="n">all</span> <span class="o">=</span> <span class="nb">append</span><span class="p">(</span><span class="n">all</span><span class="p">,</span> <span class="n">batch2</span><span class="o">...</span><span class="p">)</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  merged %d + %d = %d changes</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">batch1</span><span class="p">),</span> <span class="nb">len</span><span class="p">(</span><span class="n">batch2</span><span class="p">),</span> <span class="nb">len</span><span class="p">(</span><span class="n">all</span><span class="p">))</span>
	<span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">c</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">all</span> <span class="p">{</span>
		<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"    %s %s (key=%s)</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">c</span><span class="o">.</span><span class="n">Action</span><span class="p">,</span> <span class="n">c</span><span class="o">.</span><span class="n">Table</span><span class="p">,</span> <span class="n">c</span><span class="o">.</span><span class="n">Key</span><span class="p">)</span>
	<span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">append(all, batch1...)</code> is equivalent to appending each element of <code class="language-plaintext highlighter-rouge">batch1</code> individually. Without the <code class="language-plaintext highlighter-rouge">...</code>, the compiler would complain — <code class="language-plaintext highlighter-rouge">append</code> expects individual elements of the slice’s element type, not another slice.</p>

<h3 id="efficient-reset-with-0">Efficient Reset with <code class="language-plaintext highlighter-rouge">[:0]</code></h3>

<p>When you need to reuse a buffer without reallocating, slice it back to zero length. The capacity (and the underlying array) are preserved:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">buffer</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="n">Change</span><span class="p">,</span> <span class="m">0</span><span class="p">,</span> <span class="m">10</span><span class="p">)</span>
	<span class="n">buffer</span> <span class="o">=</span> <span class="nb">append</span><span class="p">(</span><span class="n">buffer</span><span class="p">,</span>
		<span class="n">Change</span><span class="p">{</span><span class="n">Table</span><span class="o">:</span> <span class="s">"users"</span><span class="p">,</span> <span class="n">Action</span><span class="o">:</span> <span class="s">"INSERT"</span><span class="p">,</span> <span class="n">Key</span><span class="o">:</span> <span class="s">"alice"</span><span class="p">},</span>
		<span class="n">Change</span><span class="p">{</span><span class="n">Table</span><span class="o">:</span> <span class="s">"users"</span><span class="p">,</span> <span class="n">Action</span><span class="o">:</span> <span class="s">"INSERT"</span><span class="p">,</span> <span class="n">Key</span><span class="o">:</span> <span class="s">"bob"</span><span class="p">},</span>
		<span class="n">Change</span><span class="p">{</span><span class="n">Table</span><span class="o">:</span> <span class="s">"orders"</span><span class="p">,</span> <span class="n">Action</span><span class="o">:</span> <span class="s">"INSERT"</span><span class="p">,</span> <span class="n">Key</span><span class="o">:</span> <span class="s">"ord-1"</span><span class="p">},</span>
	<span class="p">)</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  before reset: len=%d, cap=%d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">buffer</span><span class="p">),</span> <span class="nb">cap</span><span class="p">(</span><span class="n">buffer</span><span class="p">))</span>
	<span class="n">buffer</span> <span class="o">=</span> <span class="n">buffer</span><span class="p">[</span><span class="o">:</span><span class="m">0</span><span class="p">]</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  after [:0]:   len=%d, cap=%d  &lt;- capacity retained!</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">buffer</span><span class="p">),</span> <span class="nb">cap</span><span class="p">(</span><span class="n">buffer</span><span class="p">))</span>
	<span class="n">buffer</span> <span class="o">=</span> <span class="nb">append</span><span class="p">(</span><span class="n">buffer</span><span class="p">,</span> <span class="n">Change</span><span class="p">{</span><span class="n">Table</span><span class="o">:</span> <span class="s">"events"</span><span class="p">,</span> <span class="n">Action</span><span class="o">:</span> <span class="s">"INSERT"</span><span class="p">,</span> <span class="n">Key</span><span class="o">:</span> <span class="s">"evt-1"</span><span class="p">})</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  after reuse:  len=%d, cap=%d  &lt;- no new allocation</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">buffer</span><span class="p">),</span> <span class="nb">cap</span><span class="p">(</span><span class="n">buffer</span><span class="p">))</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">buffer[:0]</code> creates a new slice header with length 0 but the same capacity and underlying array. The next <code class="language-plaintext highlighter-rouge">append</code> writes into the existing array instead of allocating a new one. This pattern is common in hot loops where you process batches repeatedly — allocate once, reset between iterations.</p>

<h3 id="maps-and-the-comma-ok-pattern">Maps and the Comma-Ok Pattern</h3>

<p>A map is an unordered collection of key-value pairs. Accessing a missing key returns the zero value for the value type, which can be ambiguous — is the value actually zero, or does the key not exist? The comma-ok idiom resolves this:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">schemas</span> <span class="o">:=</span> <span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">string</span><span class="p">{</span>
		<span class="s">"users"</span><span class="o">:</span>  <span class="s">"id, name, email"</span><span class="p">,</span>
		<span class="s">"orders"</span><span class="o">:</span> <span class="s">"id, user_id, total"</span><span class="p">,</span>
	<span class="p">}</span>
	<span class="k">if</span> <span class="n">cols</span><span class="p">,</span> <span class="n">ok</span> <span class="o">:=</span> <span class="n">schemas</span><span class="p">[</span><span class="s">"users"</span><span class="p">];</span> <span class="n">ok</span> <span class="p">{</span>
		<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  users columns: %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">cols</span><span class="p">)</span>
	<span class="p">}</span>
	<span class="k">if</span> <span class="n">_</span><span class="p">,</span> <span class="n">ok</span> <span class="o">:=</span> <span class="n">schemas</span><span class="p">[</span><span class="s">"missing"</span><span class="p">];</span> <span class="o">!</span><span class="n">ok</span> <span class="p">{</span>
		<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"  'missing' table not found (comma-ok prevented silent zero value)"</span><span class="p">)</span>
	<span class="p">}</span>
	<span class="k">for</span> <span class="n">table</span><span class="p">,</span> <span class="n">cols</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">schemas</span> <span class="p">{</span>
		<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"    %s -&gt; %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">table</span><span class="p">,</span> <span class="n">cols</span><span class="p">)</span>
	<span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The two-value assignment <code class="language-plaintext highlighter-rouge">cols, ok := schemas["users"]</code> returns the value and a boolean. If the key exists, <code class="language-plaintext highlighter-rouge">ok</code> is true. If not, <code class="language-plaintext highlighter-rouge">ok</code> is false and <code class="language-plaintext highlighter-rouge">cols</code> is the zero value (empty string for <code class="language-plaintext highlighter-rouge">string</code>). Always use the comma-ok form when the distinction between “missing” and “zero” matters.</p>

<h3 id="grouping-with-maps-of-slices">Grouping with Maps of Slices</h3>

<p>A common pattern is grouping items by some key. Since accessing a missing map key returns the zero value — and the zero value of a slice is <code class="language-plaintext highlighter-rouge">nil</code> — you can <code class="language-plaintext highlighter-rouge">append</code> directly without initializing:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">changes</span> <span class="o">:=</span> <span class="p">[]</span><span class="n">Change</span><span class="p">{</span>
		<span class="p">{</span><span class="n">Table</span><span class="o">:</span> <span class="s">"users"</span><span class="p">,</span> <span class="n">Action</span><span class="o">:</span> <span class="s">"INSERT"</span><span class="p">,</span> <span class="n">Key</span><span class="o">:</span> <span class="s">"alice"</span><span class="p">},</span>
		<span class="p">{</span><span class="n">Table</span><span class="o">:</span> <span class="s">"orders"</span><span class="p">,</span> <span class="n">Action</span><span class="o">:</span> <span class="s">"INSERT"</span><span class="p">,</span> <span class="n">Key</span><span class="o">:</span> <span class="s">"ord-1"</span><span class="p">},</span>
		<span class="p">{</span><span class="n">Table</span><span class="o">:</span> <span class="s">"users"</span><span class="p">,</span> <span class="n">Action</span><span class="o">:</span> <span class="s">"UPDATE"</span><span class="p">,</span> <span class="n">Key</span><span class="o">:</span> <span class="s">"bob"</span><span class="p">},</span>
		<span class="p">{</span><span class="n">Table</span><span class="o">:</span> <span class="s">"orders"</span><span class="p">,</span> <span class="n">Action</span><span class="o">:</span> <span class="s">"DELETE"</span><span class="p">,</span> <span class="n">Key</span><span class="o">:</span> <span class="s">"ord-2"</span><span class="p">},</span>
		<span class="p">{</span><span class="n">Table</span><span class="o">:</span> <span class="s">"users"</span><span class="p">,</span> <span class="n">Action</span><span class="o">:</span> <span class="s">"INSERT"</span><span class="p">,</span> <span class="n">Key</span><span class="o">:</span> <span class="s">"charlie"</span><span class="p">},</span>
	<span class="p">}</span>
	<span class="n">groups</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">][]</span><span class="n">Change</span><span class="p">)</span>
	<span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">c</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">changes</span> <span class="p">{</span>
		<span class="n">groups</span><span class="p">[</span><span class="n">c</span><span class="o">.</span><span class="n">Table</span><span class="p">]</span> <span class="o">=</span> <span class="nb">append</span><span class="p">(</span><span class="n">groups</span><span class="p">[</span><span class="n">c</span><span class="o">.</span><span class="n">Table</span><span class="p">],</span> <span class="n">c</span><span class="p">)</span>
	<span class="p">}</span>
	<span class="k">for</span> <span class="n">table</span><span class="p">,</span> <span class="n">tableChanges</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">groups</span> <span class="p">{</span>
		<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  %s: %d changes</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">table</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">tableChanges</span><span class="p">))</span>
		<span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">c</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">tableChanges</span> <span class="p">{</span>
			<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"    %s key=%s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">c</span><span class="o">.</span><span class="n">Action</span><span class="p">,</span> <span class="n">c</span><span class="o">.</span><span class="n">Key</span><span class="p">)</span>
		<span class="p">}</span>
	<span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The first time <code class="language-plaintext highlighter-rouge">groups["users"]</code> is accessed, it returns <code class="language-plaintext highlighter-rouge">nil</code>. But <code class="language-plaintext highlighter-rouge">append(nil, item)</code> works — it allocates a new slice. On subsequent accesses, it appends to the existing slice. This means you never need to check whether a key exists before appending. The pattern works because Go’s zero values are designed to be useful, not just empty.</p>

<h3 id="deduplication-with-a-seen-map">Deduplication with a Seen Map</h3>

<p>A <code class="language-plaintext highlighter-rouge">map[string]bool</code> is the standard way to track whether you have already seen a value. Combined with a slice to preserve order, it gives you a deduplicated list:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
</pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">changes</span> <span class="o">:=</span> <span class="p">[]</span><span class="n">Change</span><span class="p">{</span>
		<span class="p">{</span><span class="n">Table</span><span class="o">:</span> <span class="s">"users"</span><span class="p">,</span> <span class="n">Action</span><span class="o">:</span> <span class="s">"INSERT"</span><span class="p">,</span> <span class="n">Key</span><span class="o">:</span> <span class="s">"alice"</span><span class="p">},</span>
		<span class="p">{</span><span class="n">Table</span><span class="o">:</span> <span class="s">"orders"</span><span class="p">,</span> <span class="n">Action</span><span class="o">:</span> <span class="s">"INSERT"</span><span class="p">,</span> <span class="n">Key</span><span class="o">:</span> <span class="s">"ord-1"</span><span class="p">},</span>
		<span class="p">{</span><span class="n">Table</span><span class="o">:</span> <span class="s">"users"</span><span class="p">,</span> <span class="n">Action</span><span class="o">:</span> <span class="s">"UPDATE"</span><span class="p">,</span> <span class="n">Key</span><span class="o">:</span> <span class="s">"bob"</span><span class="p">},</span>
		<span class="p">{</span><span class="n">Table</span><span class="o">:</span> <span class="s">"events"</span><span class="p">,</span> <span class="n">Action</span><span class="o">:</span> <span class="s">"INSERT"</span><span class="p">,</span> <span class="n">Key</span><span class="o">:</span> <span class="s">"evt-1"</span><span class="p">},</span>
		<span class="p">{</span><span class="n">Table</span><span class="o">:</span> <span class="s">"orders"</span><span class="p">,</span> <span class="n">Action</span><span class="o">:</span> <span class="s">"DELETE"</span><span class="p">,</span> <span class="n">Key</span><span class="o">:</span> <span class="s">"ord-2"</span><span class="p">},</span>
	<span class="p">}</span>
	<span class="n">seen</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">bool</span><span class="p">)</span>
	<span class="k">var</span> <span class="n">uniqueTables</span> <span class="p">[]</span><span class="kt">string</span>
	<span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">c</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">changes</span> <span class="p">{</span>
		<span class="k">if</span> <span class="o">!</span><span class="n">seen</span><span class="p">[</span><span class="n">c</span><span class="o">.</span><span class="n">Table</span><span class="p">]</span> <span class="p">{</span>
			<span class="n">seen</span><span class="p">[</span><span class="n">c</span><span class="o">.</span><span class="n">Table</span><span class="p">]</span> <span class="o">=</span> <span class="no">true</span>
			<span class="n">uniqueTables</span> <span class="o">=</span> <span class="nb">append</span><span class="p">(</span><span class="n">uniqueTables</span><span class="p">,</span> <span class="n">c</span><span class="o">.</span><span class="n">Table</span><span class="p">)</span>
		<span class="p">}</span>
	<span class="p">}</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"  %d changes across %d unique tables: %v</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span>
		<span class="nb">len</span><span class="p">(</span><span class="n">changes</span><span class="p">),</span> <span class="nb">len</span><span class="p">(</span><span class="n">uniqueTables</span><span class="p">),</span> <span class="n">uniqueTables</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">seen[c.Table]</code> returns <code class="language-plaintext highlighter-rouge">false</code> (the zero value for <code class="language-plaintext highlighter-rouge">bool</code>) if the key has never been set. The first time a table name appears, it passes the <code class="language-plaintext highlighter-rouge">!seen</code> check, gets added to both the map and the slice. On subsequent appearances, the map returns <code class="language-plaintext highlighter-rouge">true</code> and the item is skipped. The slice preserves insertion order, which the map alone does not guarantee.</p>

<p>This pattern generalizes to any deduplication task — swap <code class="language-plaintext highlighter-rouge">string</code> for whatever type you are deduplicating by, and the logic stays the same.</p>]]></content><author><name>kimserey</name></author><category term="go" /><summary type="html"><![CDATA[This is the first tutorial in a series on Go. It covers three foundational topics: pointers and references (how Go passes data around and when mutations are visible), structs with methods and receivers (how Go models data and behavior without classes), and slices and maps (the two built-in data structures you will use constantly). Each section builds on the previous one — pointers explain why receiver types matter, and receiver types explain how slice and map patterns work in practice.]]></summary></entry><entry><title type="html">DuckLake — Open Lakehouse with DuckDB</title><link href="https://www.kimsereylam.com/duckdb/2026/08/22/ducklake-open-lakehouse-with-duckdb.html" rel="alternate" type="text/html" title="DuckLake — Open Lakehouse with DuckDB" /><published>2026-08-22T00:00:00-05:00</published><updated>2026-08-22T00:00:00-05:00</updated><id>https://www.kimsereylam.com/duckdb/2026/08/22/ducklake-open-lakehouse-with-duckdb</id><content type="html" xml:base="https://www.kimsereylam.com/duckdb/2026/08/22/ducklake-open-lakehouse-with-duckdb.html"><![CDATA[<p>DuckLake is a lakehouse format built as a DuckDB extension. It separates metadata from data: a catalog (a local <code class="language-plaintext highlighter-rouge">.ducklake</code> file or a PostgreSQL database) tracks table schemas, snapshots, and file pointers, while Parquet files hold the actual data on a local filesystem or in S3-compatible object storage. This separation is the central design decision — the catalog knows what files exist, what columns they contain, and what value ranges they hold, but it never stores the data itself. In this post, we’ll go from running DuckLake locally to running it with PostgreSQL and MinIO, look at how partitioning organizes data in S3, and see how DuckLake’s query engine prunes files and pushes predicates down to avoid reading data it doesn’t need.</p>

<!--more-->

<h2 id="what-is-ducklake">What Is DuckLake</h2>

<p>Traditional databases store metadata and data together. Lakehouse formats like Iceberg, Delta Lake, and Hudi split them apart — they use a metadata layer (manifests, commit logs) on top of Parquet files in object storage. DuckLake follows the same philosophy but uses a relational database as the catalog instead of JSON/Avro manifest files.</p>

<p>The catalog stores:</p>

<ul>
  <li><strong>Table definitions</strong> — schemas, column types, partition keys.</li>
  <li><strong>Data file registry</strong> — which Parquet files belong to which table, their row counts, and file sizes.</li>
  <li><strong>Column statistics</strong> — min/max values per column per file, used for pruning at query time.</li>
  <li><strong>Snapshots</strong> — a complete version history of every change, enabling time travel.</li>
</ul>

<p>The data files are Parquet, stored wherever you point <code class="language-plaintext highlighter-rouge">DATA_PATH</code> — a local directory, an S3 bucket, or any S3-compatible object store like MinIO or SeaweedFS.</p>

<p>Because the catalog is a regular database, you get transactions, concurrent access (with PostgreSQL), and the ability to query metadata with SQL. And because the data files are plain Parquet, any tool that reads Parquet can access them independently of DuckLake.</p>

<h2 id="running-locally">Running Locally</h2>

<p>The simplest setup uses a local <code class="language-plaintext highlighter-rouge">.ducklake</code> file for metadata and a local directory for data. No Docker, no object storage, no external dependencies.</p>

<p>Install and load the extension:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="n">INSTALL</span> <span class="n">ducklake</span><span class="p">;</span>
<span class="k">LOAD</span> <span class="n">ducklake</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>DuckLake inlines small tables (10 rows or fewer) directly into the catalog to avoid creating tiny Parquet files. For testing, disable this so all data goes to Parquet:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="k">SET</span> <span class="n">ducklake_default_data_inlining_row_limit</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Attach a DuckLake catalog. The first argument is the metadata path (prefixed with <code class="language-plaintext highlighter-rouge">ducklake:</code>), and <code class="language-plaintext highlighter-rouge">DATA_PATH</code> is where Parquet files will be written:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="n">ATTACH</span> <span class="s1">'ducklake:step1_metadata.ducklake'</span> <span class="k">AS</span> <span class="n">lake</span> <span class="p">(</span><span class="n">DATA_PATH</span> <span class="s1">'step1_data/'</span><span class="p">);</span>
<span class="n">USE</span> <span class="n">lake</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Create a table and insert data:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
</pre></td><td class="rouge-code"><pre><span class="k">CREATE</span> <span class="k">OR</span> <span class="k">REPLACE</span> <span class="k">TABLE</span> <span class="n">sensors</span> <span class="p">(</span>
    <span class="n">tenant_id</span>   <span class="nb">VARCHAR</span><span class="p">,</span>
    <span class="n">sensor_id</span>   <span class="nb">INTEGER</span><span class="p">,</span>
    <span class="n">ts</span>          <span class="nb">TIMESTAMP</span><span class="p">,</span>
    <span class="n">reading</span>     <span class="nb">DOUBLE</span><span class="p">,</span>
    <span class="k">location</span>    <span class="nb">VARCHAR</span>
<span class="p">);</span>

<span class="k">INSERT</span> <span class="k">INTO</span> <span class="n">sensors</span>
<span class="k">SELECT</span>
    <span class="k">CASE</span> <span class="k">WHEN</span> <span class="n">i</span> <span class="o">%</span> <span class="mi">2</span> <span class="o">=</span> <span class="mi">0</span> <span class="k">THEN</span> <span class="s1">'tenant_a'</span> <span class="k">ELSE</span> <span class="s1">'tenant_b'</span> <span class="k">END</span><span class="p">,</span>
    <span class="n">i</span><span class="p">,</span>
    <span class="nb">TIMESTAMP</span> <span class="s1">'2025-01-01'</span> <span class="o">+</span> <span class="n">INTERVAL</span> <span class="p">(</span><span class="n">i</span><span class="p">)</span> <span class="k">MINUTE</span><span class="p">,</span>
    <span class="n">round</span><span class="p">(</span><span class="n">random</span><span class="p">()</span> <span class="o">*</span> <span class="mi">100</span><span class="p">,</span> <span class="mi">2</span><span class="p">),</span>
    <span class="k">CASE</span> <span class="k">WHEN</span> <span class="n">i</span> <span class="o">%</span> <span class="mi">3</span> <span class="o">=</span> <span class="mi">0</span> <span class="k">THEN</span> <span class="s1">'NYC'</span> <span class="k">WHEN</span> <span class="n">i</span> <span class="o">%</span> <span class="mi">3</span> <span class="o">=</span> <span class="mi">1</span> <span class="k">THEN</span> <span class="s1">'SF'</span> <span class="k">ELSE</span> <span class="s1">'CHI'</span> <span class="k">END</span>
<span class="k">FROM</span> <span class="k">range</span><span class="p">(</span><span class="mi">10000</span><span class="p">)</span> <span class="n">t</span><span class="p">(</span><span class="n">i</span><span class="p">);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>After the insert, <code class="language-plaintext highlighter-rouge">step1_data/</code> contains the Parquet files:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre>step1_data/
  main/sensors/
    ducklake-&lt;uuid&gt;.parquet
</pre></td></tr></tbody></table></code></pre></div></div>

<p>And <code class="language-plaintext highlighter-rouge">step1_metadata.ducklake</code> is a DuckDB file that holds the catalog tables — table definitions, column stats, snapshot history, and pointers to the Parquet files.</p>

<p>You can inspect the catalog with built-in functions:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">ducklake_snapshots</span><span class="p">(</span><span class="s1">'lake'</span><span class="p">);</span>
<span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">ducklake_table_info</span><span class="p">(</span><span class="s1">'lake'</span><span class="p">);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>This setup works for local development and single-user workflows. But it has a limitation: the <code class="language-plaintext highlighter-rouge">.ducklake</code> file uses an exclusive file lock. If two processes try to write at the same time, the second one gets an error immediately:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre>Could not set lock on file "step1_metadata.ducklake":
Conflicting lock is held in duckdb (PID 46829)
</pre></td></tr></tbody></table></code></pre></div></div>

<p>For concurrent writes, you need PostgreSQL as the catalog backend.</p>

<h2 id="running-with-postgresql-and-minio">Running with PostgreSQL and MinIO</h2>

<p>A production-like setup replaces the local <code class="language-plaintext highlighter-rouge">.ducklake</code> file with PostgreSQL (for concurrent metadata access) and the local directory with MinIO (an S3-compatible object store for data).</p>

<p>A docker compose file brings both services up:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
</pre></td><td class="rouge-code"><pre><span class="na">services</span><span class="pi">:</span>
  <span class="na">minio</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">quay.io/minio/minio:latest</span>
    <span class="na">command</span><span class="pi">:</span> <span class="s">server /data --console-address ":9001"</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">9000:9000"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">9001:9001"</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="na">MINIO_ROOT_USER</span><span class="pi">:</span> <span class="s">minioadmin</span>
      <span class="na">MINIO_ROOT_PASSWORD</span><span class="pi">:</span> <span class="s">minioadmin</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">minio_data:/data</span>
    <span class="na">healthcheck</span><span class="pi">:</span>
      <span class="na">test</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">CMD"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">mc"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">ready"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">local"</span><span class="pi">]</span>
      <span class="na">interval</span><span class="pi">:</span> <span class="s">5s</span>
      <span class="na">timeout</span><span class="pi">:</span> <span class="s">5s</span>
      <span class="na">retries</span><span class="pi">:</span> <span class="m">5</span>

  <span class="na">postgres</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">postgres:16</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">5432:5432"</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="na">POSTGRES_USER</span><span class="pi">:</span> <span class="s">ducklake</span>
      <span class="na">POSTGRES_PASSWORD</span><span class="pi">:</span> <span class="s">ducklake</span>
      <span class="na">POSTGRES_DB</span><span class="pi">:</span> <span class="s">ducklake</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">pg_data:/var/lib/postgresql/data</span>
    <span class="na">healthcheck</span><span class="pi">:</span>
      <span class="na">test</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">CMD-SHELL"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">pg_isready</span><span class="nv"> </span><span class="s">-U</span><span class="nv"> </span><span class="s">ducklake"</span><span class="pi">]</span>
      <span class="na">interval</span><span class="pi">:</span> <span class="s">5s</span>
      <span class="na">timeout</span><span class="pi">:</span> <span class="s">5s</span>
      <span class="na">retries</span><span class="pi">:</span> <span class="m">5</span>

<span class="na">volumes</span><span class="pi">:</span>
  <span class="na">minio_data</span><span class="pi">:</span>
  <span class="na">pg_data</span><span class="pi">:</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Start the services and create a bucket for DuckLake data:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre>docker compose up <span class="nt">-d</span>
docker compose <span class="nb">exec </span>minio mc mb <span class="nb">local</span>/ducklake-data
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Now load the required extensions — <code class="language-plaintext highlighter-rouge">ducklake</code> for the lakehouse logic, <code class="language-plaintext highlighter-rouge">httpfs</code> for S3 access, and <code class="language-plaintext highlighter-rouge">postgres</code> for the PostgreSQL catalog backend:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="n">INSTALL</span> <span class="n">ducklake</span><span class="p">;</span> <span class="k">LOAD</span> <span class="n">ducklake</span><span class="p">;</span>
<span class="n">INSTALL</span> <span class="n">httpfs</span><span class="p">;</span>   <span class="k">LOAD</span> <span class="n">httpfs</span><span class="p">;</span>
<span class="n">INSTALL</span> <span class="n">postgres</span><span class="p">;</span> <span class="k">LOAD</span> <span class="n">postgres</span><span class="p">;</span>

<span class="k">SET</span> <span class="n">ducklake_default_data_inlining_row_limit</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Create an S3 secret pointing at MinIO:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="k">CREATE</span> <span class="n">SECRET</span> <span class="p">(</span>
    <span class="k">TYPE</span> <span class="n">s3</span><span class="p">,</span>
    <span class="n">KEY_ID</span> <span class="s1">'minioadmin'</span><span class="p">,</span>
    <span class="n">SECRET</span> <span class="s1">'minioadmin'</span><span class="p">,</span>
    <span class="n">ENDPOINT</span> <span class="s1">'localhost:9000'</span><span class="p">,</span>
    <span class="n">USE_SSL</span> <span class="k">false</span><span class="p">,</span>
    <span class="n">URL_STYLE</span> <span class="s1">'path'</span>
<span class="p">);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Attach the DuckLake catalog using PostgreSQL for metadata and MinIO for data:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="n">ATTACH</span> <span class="s1">'ducklake:postgres:dbname=ducklake user=ducklake password=ducklake host=localhost port=5432'</span>
    <span class="k">AS</span> <span class="n">lake</span> <span class="p">(</span><span class="n">DATA_PATH</span> <span class="s1">'s3://ducklake-data/'</span><span class="p">);</span>
<span class="n">USE</span> <span class="n">lake</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>From here, creating tables and inserting data is identical to the local setup. The difference is where things go: metadata rows land in PostgreSQL tables (<code class="language-plaintext highlighter-rouge">ducklake_table</code>, <code class="language-plaintext highlighter-rouge">ducklake_column</code>, <code class="language-plaintext highlighter-rouge">ducklake_data_file</code>, <code class="language-plaintext highlighter-rouge">ducklake_snapshot</code>), and Parquet files land in the MinIO bucket.</p>

<p>With PostgreSQL as the catalog, concurrent writes work. Two DuckDB processes can insert into the same table simultaneously — PostgreSQL handles the row-level locking internally. The data files in S3 are immutable (each insert creates new Parquet files), so there are no conflicts on the storage side either.</p>

<h2 id="partitioning">Partitioning</h2>

<p>Partitioning tells DuckLake to organize Parquet files into a directory hierarchy based on column values. Instead of writing all rows into a single file, DuckLake creates separate files for each distinct combination of partition key values.</p>

<p>Define a table and set the partition key:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">sensors</span> <span class="p">(</span>
    <span class="n">tenant_id</span>   <span class="nb">VARCHAR</span><span class="p">,</span>
    <span class="n">sensor_id</span>   <span class="nb">INTEGER</span><span class="p">,</span>
    <span class="n">event_date</span>  <span class="nb">DATE</span><span class="p">,</span>
    <span class="n">ts</span>          <span class="nb">TIMESTAMP</span><span class="p">,</span>
    <span class="n">reading</span>     <span class="nb">DOUBLE</span><span class="p">,</span>
    <span class="k">location</span>    <span class="nb">VARCHAR</span>
<span class="p">);</span>

<span class="k">ALTER</span> <span class="k">TABLE</span> <span class="n">sensors</span> <span class="k">SET</span> <span class="n">PARTITIONED</span> <span class="k">BY</span> <span class="p">(</span><span class="n">tenant_id</span><span class="p">,</span> <span class="n">event_date</span><span class="p">);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Insert 30,000 rows spanning 3 tenants and 10 dates:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre></td><td class="rouge-code"><pre><span class="k">INSERT</span> <span class="k">INTO</span> <span class="n">sensors</span>
<span class="k">SELECT</span>
    <span class="k">CASE</span> <span class="k">WHEN</span> <span class="n">i</span> <span class="o">%</span> <span class="mi">3</span> <span class="o">=</span> <span class="mi">0</span> <span class="k">THEN</span> <span class="s1">'tenant_a'</span>
         <span class="k">WHEN</span> <span class="n">i</span> <span class="o">%</span> <span class="mi">3</span> <span class="o">=</span> <span class="mi">1</span> <span class="k">THEN</span> <span class="s1">'tenant_b'</span>
         <span class="k">ELSE</span> <span class="s1">'tenant_c'</span> <span class="k">END</span> <span class="k">AS</span> <span class="n">tenant_id</span><span class="p">,</span>
    <span class="n">i</span> <span class="k">AS</span> <span class="n">sensor_id</span><span class="p">,</span>
    <span class="nb">DATE</span> <span class="s1">'2025-01-01'</span> <span class="o">+</span> <span class="n">INTERVAL</span> <span class="p">(</span><span class="n">i</span> <span class="o">%</span> <span class="mi">10</span><span class="p">)</span> <span class="k">DAY</span> <span class="k">AS</span> <span class="n">event_date</span><span class="p">,</span>
    <span class="nb">TIMESTAMP</span> <span class="s1">'2025-01-01'</span> <span class="o">+</span> <span class="n">INTERVAL</span> <span class="p">(</span><span class="n">i</span><span class="p">)</span> <span class="k">MINUTE</span> <span class="k">AS</span> <span class="n">ts</span><span class="p">,</span>
    <span class="n">round</span><span class="p">(</span><span class="n">random</span><span class="p">()</span> <span class="o">*</span> <span class="mi">100</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span> <span class="k">AS</span> <span class="n">reading</span><span class="p">,</span>
    <span class="k">CASE</span> <span class="k">WHEN</span> <span class="n">i</span> <span class="o">%</span> <span class="mi">3</span> <span class="o">=</span> <span class="mi">0</span> <span class="k">THEN</span> <span class="s1">'NYC'</span>
         <span class="k">WHEN</span> <span class="n">i</span> <span class="o">%</span> <span class="mi">3</span> <span class="o">=</span> <span class="mi">1</span> <span class="k">THEN</span> <span class="s1">'SF'</span>
         <span class="k">ELSE</span> <span class="s1">'CHI'</span> <span class="k">END</span> <span class="k">AS</span> <span class="k">location</span>
<span class="k">FROM</span> <span class="k">range</span><span class="p">(</span><span class="mi">30000</span><span class="p">)</span> <span class="n">t</span><span class="p">(</span><span class="n">i</span><span class="p">);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>This produces 30 Parquet files — one for each (tenant, date) combination, each holding 1,000 rows.</p>

<h2 id="how-data-is-organized-in-s3">How Data Is Organized in S3</h2>

<p>The directory layout in S3 follows the Hive partitioning convention: <code class="language-plaintext highlighter-rouge">column=value/</code> directories nested according to the partition key order.</p>

<p>For an <strong>unpartitioned</strong> table, all files sit in a flat directory:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre>s3://ducklake-data/
  main/sensors/
    ducklake-&lt;uuid-1&gt;.parquet
    ducklake-&lt;uuid-2&gt;.parquet
</pre></td></tr></tbody></table></code></pre></div></div>

<p>For a table <strong>partitioned by a single column</strong> (<code class="language-plaintext highlighter-rouge">location</code>):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre>s3://ducklake-data/
  main/sensors_by_location/
    location=CHI/
      ducklake-&lt;uuid&gt;.parquet
    location=NYC/
      ducklake-&lt;uuid&gt;.parquet
    location=SF/
      ducklake-&lt;uuid&gt;.parquet
</pre></td></tr></tbody></table></code></pre></div></div>

<p>For a table <strong>partitioned by two columns</strong> (<code class="language-plaintext highlighter-rouge">tenant_id</code>, <code class="language-plaintext highlighter-rouge">event_date</code>):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="rouge-code"><pre>s3://ducklake-data/
  main/sensors/
    tenant_id=tenant_a/
      event_date=2025-01-01/
        ducklake-&lt;uuid&gt;.parquet
      event_date=2025-01-02/
        ducklake-&lt;uuid&gt;.parquet
      ...
    tenant_id=tenant_b/
      event_date=2025-01-01/
        ducklake-&lt;uuid&gt;.parquet
      ...
    tenant_id=tenant_c/
      ...
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The path structure is <code class="language-plaintext highlighter-rouge">{DATA_PATH}/{schema}/{table}/{partition_key=value}/.../{file}.parquet</code>. Each Parquet file is a standard columnar file with row groups, column chunks, and a metadata footer containing min/max statistics and byte offsets.</p>

<p>The catalog in PostgreSQL (or the <code class="language-plaintext highlighter-rouge">.ducklake</code> file) tracks every one of these files: their paths, row counts, file sizes, and per-column statistics. DuckLake never needs to list the S3 bucket to find files — it queries the catalog instead. This is what makes pruning fast: the catalog is a regular database table, and filtering it is a local operation that doesn’t touch object storage at all.</p>

<h2 id="partition-pruning">Partition Pruning</h2>

<p>Partition pruning is the first and most impactful optimization. When a query includes a <code class="language-plaintext highlighter-rouge">WHERE</code> clause on a partition column, DuckLake consults the catalog to determine which Parquet files could possibly match. Files that can’t match are eliminated before any S3 request is made.</p>

<p>Using the 30-file dataset (3 tenants x 10 dates), <code class="language-plaintext highlighter-rouge">EXPLAIN ANALYZE</code> shows exactly how many files DuckLake reads for each query:</p>

<p><strong>Full scan (no filter) — 30 files:</strong></p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="k">EXPLAIN</span> <span class="k">ANALYZE</span>
<span class="k">SELECT</span> <span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">),</span> <span class="k">AVG</span><span class="p">(</span><span class="n">reading</span><span class="p">)</span> <span class="k">FROM</span> <span class="n">sensors</span><span class="p">;</span>
<span class="c1">-- Total Files Read: 30</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>Both partition keys — 1 file:</strong></p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="k">EXPLAIN</span> <span class="k">ANALYZE</span>
<span class="k">SELECT</span> <span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">),</span> <span class="k">AVG</span><span class="p">(</span><span class="n">reading</span><span class="p">)</span> <span class="k">FROM</span> <span class="n">sensors</span>
<span class="k">WHERE</span> <span class="n">tenant_id</span> <span class="o">=</span> <span class="s1">'tenant_a'</span> <span class="k">AND</span> <span class="n">event_date</span> <span class="o">=</span> <span class="nb">DATE</span> <span class="s1">'2025-01-03'</span><span class="p">;</span>
<span class="c1">-- Total Files Read: 1</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>First partition key only — 10 files:</strong></p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="k">EXPLAIN</span> <span class="k">ANALYZE</span>
<span class="k">SELECT</span> <span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">),</span> <span class="k">AVG</span><span class="p">(</span><span class="n">reading</span><span class="p">)</span> <span class="k">FROM</span> <span class="n">sensors</span>
<span class="k">WHERE</span> <span class="n">tenant_id</span> <span class="o">=</span> <span class="s1">'tenant_a'</span><span class="p">;</span>
<span class="c1">-- Total Files Read: 10</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>Second partition key only (no tenant filter) — 3 files:</strong></p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="k">EXPLAIN</span> <span class="k">ANALYZE</span>
<span class="k">SELECT</span> <span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">),</span> <span class="k">AVG</span><span class="p">(</span><span class="n">reading</span><span class="p">)</span> <span class="k">FROM</span> <span class="n">sensors</span>
<span class="k">WHERE</span> <span class="n">event_date</span> <span class="o">=</span> <span class="nb">DATE</span> <span class="s1">'2025-01-03'</span><span class="p">;</span>
<span class="c1">-- Total Files Read: 3</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>This last query is significant. Filtering on <code class="language-plaintext highlighter-rouge">event_date</code> without specifying <code class="language-plaintext highlighter-rouge">tenant_id</code> still prunes down to 3 files (one per tenant for that date). DuckLake prunes on the second partition key independently of the first. It doesn’t need the leading key to be present — it evaluates each partition column separately against the catalog.</p>

<p><strong>Date range — 9 files:</strong></p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="k">EXPLAIN</span> <span class="k">ANALYZE</span>
<span class="k">SELECT</span> <span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">),</span> <span class="k">AVG</span><span class="p">(</span><span class="n">reading</span><span class="p">)</span> <span class="k">FROM</span> <span class="n">sensors</span>
<span class="k">WHERE</span> <span class="n">event_date</span> <span class="k">BETWEEN</span> <span class="nb">DATE</span> <span class="s1">'2025-01-02'</span> <span class="k">AND</span> <span class="nb">DATE</span> <span class="s1">'2025-01-04'</span><span class="p">;</span>
<span class="c1">-- Total Files Read: 9</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Range predicates prune the same way. Three dates across three tenants: 9 files.</p>

<p>The pruning stack has three layers, each narrowing the data further:</p>

<ol>
  <li><strong>Partition pruning</strong> — skip entire Parquet files using partition column values in the catalog. No S3 requests.</li>
  <li><strong>Row group pruning</strong> — within a selected file, skip row groups using min/max stats from the Parquet footer.</li>
  <li><strong>Filter evaluation</strong> — scan the remaining rows and discard non-matches.</li>
</ol>

<p>In <code class="language-plaintext highlighter-rouge">EXPLAIN ANALYZE</code> output, the key number to look for is <code class="language-plaintext highlighter-rouge">Total Files Read</code> in the <code class="language-plaintext highlighter-rouge">TABLE_SCAN</code> node:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre>┌───────────────────────────┐
│         TABLE_SCAN        │
│       Table: sensors      │
│          Filters:         │
│  event_date='2025-01-03'  │
│    Total Files Read: 3    │
│         3,000 rows        │
└───────────────────────────┘
</pre></td></tr></tbody></table></code></pre></div></div>

<h2 id="predicate-pushdown-through-views">Predicate Pushdown Through Views</h2>

<p>Partition pruning only helps if the filter reaches the table scan. In real applications, queries often go through layers of views. Does DuckLake still prune when the filter is applied to a view several layers above the base table?</p>

<p>Stack three views on top of the partitioned table:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre><span class="k">CREATE</span> <span class="k">OR</span> <span class="k">REPLACE</span> <span class="k">VIEW</span> <span class="n">v_base</span> <span class="k">AS</span>
<span class="k">SELECT</span> <span class="n">tenant_id</span><span class="p">,</span> <span class="n">event_date</span><span class="p">,</span> <span class="n">sensor_id</span><span class="p">,</span> <span class="n">reading</span><span class="p">,</span> <span class="k">location</span> <span class="k">FROM</span> <span class="n">sensors</span><span class="p">;</span>

<span class="k">CREATE</span> <span class="k">OR</span> <span class="k">REPLACE</span> <span class="k">VIEW</span> <span class="n">v_enriched</span> <span class="k">AS</span>
<span class="k">SELECT</span> <span class="o">*</span><span class="p">,</span> <span class="n">reading</span> <span class="o">*</span> <span class="mi">1</span><span class="p">.</span><span class="mi">1</span> <span class="k">AS</span> <span class="n">adjusted_reading</span> <span class="k">FROM</span> <span class="n">v_base</span><span class="p">;</span>

<span class="k">CREATE</span> <span class="k">OR</span> <span class="k">REPLACE</span> <span class="k">VIEW</span> <span class="n">v_summary</span> <span class="k">AS</span>
<span class="k">SELECT</span>
    <span class="n">v</span><span class="p">.</span><span class="n">tenant_id</span><span class="p">,</span> <span class="n">v</span><span class="p">.</span><span class="n">event_date</span><span class="p">,</span> <span class="n">v</span><span class="p">.</span><span class="n">adjusted_reading</span><span class="p">,</span> <span class="n">v</span><span class="p">.</span><span class="k">location</span><span class="p">,</span>
    <span class="k">AVG</span><span class="p">(</span><span class="n">v</span><span class="p">.</span><span class="n">adjusted_reading</span><span class="p">)</span> <span class="n">OVER</span> <span class="p">(</span><span class="k">PARTITION</span> <span class="k">BY</span> <span class="n">v</span><span class="p">.</span><span class="n">tenant_id</span><span class="p">)</span> <span class="k">AS</span> <span class="n">tenant_avg</span>
<span class="k">FROM</span> <span class="n">v_enriched</span> <span class="n">v</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Query the top-level view with a date filter:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="k">EXPLAIN</span> <span class="k">ANALYZE</span>
<span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">v_summary</span>
<span class="k">WHERE</span> <span class="n">event_date</span> <span class="o">=</span> <span class="nb">DATE</span> <span class="s1">'2025-01-03'</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The filter on <code class="language-plaintext highlighter-rouge">event_date</code> pushes all the way through <code class="language-plaintext highlighter-rouge">v_summary</code> → <code class="language-plaintext highlighter-rouge">v_enriched</code> → <code class="language-plaintext highlighter-rouge">v_base</code> → <code class="language-plaintext highlighter-rouge">sensors</code> and triggers partition pruning at the <code class="language-plaintext highlighter-rouge">TABLE_SCAN</code> node. Only 3 files are read, not 30. The views add zero overhead to the pruning — DuckDB’s optimizer traces the filter down to the underlying table before execution begins.</p>

<p>This also works through joins. A view that joins <code class="language-plaintext highlighter-rouge">v_base</code> to itself still benefits from pushdown:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="rouge-code"><pre><span class="k">CREATE</span> <span class="k">OR</span> <span class="k">REPLACE</span> <span class="k">VIEW</span> <span class="n">v_with_join</span> <span class="k">AS</span>
<span class="k">SELECT</span> <span class="n">a</span><span class="p">.</span><span class="n">tenant_id</span><span class="p">,</span> <span class="n">a</span><span class="p">.</span><span class="n">event_date</span><span class="p">,</span> <span class="n">a</span><span class="p">.</span><span class="n">reading</span><span class="p">,</span> <span class="n">b</span><span class="p">.</span><span class="n">reading</span> <span class="k">AS</span> <span class="n">other_reading</span>
<span class="k">FROM</span> <span class="n">v_base</span> <span class="n">a</span>
<span class="k">JOIN</span> <span class="n">v_base</span> <span class="n">b</span>
  <span class="k">ON</span> <span class="n">a</span><span class="p">.</span><span class="n">tenant_id</span> <span class="o">=</span> <span class="n">b</span><span class="p">.</span><span class="n">tenant_id</span>
 <span class="k">AND</span> <span class="n">a</span><span class="p">.</span><span class="n">event_date</span> <span class="o">=</span> <span class="n">b</span><span class="p">.</span><span class="n">event_date</span>
 <span class="k">AND</span> <span class="n">a</span><span class="p">.</span><span class="n">sensor_id</span> <span class="o">!=</span> <span class="n">b</span><span class="p">.</span><span class="n">sensor_id</span><span class="p">;</span>

<span class="k">EXPLAIN</span> <span class="k">ANALYZE</span>
<span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">v_with_join</span>
<span class="k">WHERE</span> <span class="n">event_date</span> <span class="o">=</span> <span class="nb">DATE</span> <span class="s1">'2025-01-03'</span>
<span class="k">LIMIT</span> <span class="mi">10</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Both sides of the join prune to 3 files each. The optimizer pushes the predicate into both branches independently.</p>

<h2 id="external-sources">External Sources</h2>

<ul>
  <li><a href="https://ducklake.select/">DuckLake Official Website</a></li>
  <li><a href="https://github.com/duckdb/ducklake">DuckLake GitHub Repository</a></li>
  <li><a href="https://duckdb.org/2025/06/03/ducklake.html">DuckLake Announcement Blog Post</a></li>
  <li><a href="https://duckdb.org/docs/">DuckDB Documentation</a></li>
</ul>]]></content><author><name>kimserey</name></author><category term="duckdb" /><summary type="html"><![CDATA[DuckLake is a lakehouse format built as a DuckDB extension. It separates metadata from data: a catalog (a local .ducklake file or a PostgreSQL database) tracks table schemas, snapshots, and file pointers, while Parquet files hold the actual data on a local filesystem or in S3-compatible object storage. This separation is the central design decision — the catalog knows what files exist, what columns they contain, and what value ranges they hold, but it never stores the data itself. In this post, we’ll go from running DuckLake locally to running it with PostgreSQL and MinIO, look at how partitioning organizes data in S3, and see how DuckLake’s query engine prunes files and pushes predicates down to avoid reading data it doesn’t need.]]></summary></entry><entry><title type="html">AWS Networking for Aurora RDS PostgreSQL</title><link href="https://www.kimsereylam.com/aws/2026/08/19/aws-networking-for-aurora-rds-postgresql.html" rel="alternate" type="text/html" title="AWS Networking for Aurora RDS PostgreSQL" /><published>2026-08-19T00:00:00-05:00</published><updated>2026-08-19T00:00:00-05:00</updated><id>https://www.kimsereylam.com/aws/2026/08/19/aws-networking-for-aurora-rds-postgresql</id><content type="html" xml:base="https://www.kimsereylam.com/aws/2026/08/19/aws-networking-for-aurora-rds-postgresql.html"><![CDATA[<p>Running PostgreSQL in AWS isn’t just about creating the database — you first build the network it sits in. With Aurora RDS, you are the network engineer: you decide the IP range, which parts are reachable from the internet, what traffic is allowed between resources, and which data centers your database runs in. This post walks through the core networking concepts — VPC, subnets, gateways, route tables, security groups, and DB subnet groups — that you need to understand before provisioning Aurora RDS PostgreSQL.</p>

<!--more-->

<h2 id="ip-addressing-fundamentals">IP Addressing Fundamentals</h2>

<p>Before diving into AWS-specific concepts, a quick refresher on the addressing system everything is built on.</p>

<h3 id="cidr-notation">CIDR Notation</h3>

<p>CIDR notation defines a range of IP addresses. The number after the slash is how many bits are fixed — more fixed bits means fewer addresses:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre>10.0.0.0/16  = 10.0.*.* = 65,536 IPs
10.0.1.0/24  = 10.0.1.* = 256 IPs
10.0.2.0/24  = 10.0.2.* = 256 IPs
</pre></td></tr></tbody></table></code></pre></div></div>

<ul>
  <li><code class="language-plaintext highlighter-rouge">/16</code> = first 16 bits fixed (first two octets) = 65,536 IPs</li>
  <li><code class="language-plaintext highlighter-rouge">/24</code> = first 24 bits fixed (first three octets) = 256 IPs</li>
  <li><code class="language-plaintext highlighter-rouge">/32</code> = all bits fixed = exactly 1 IP</li>
</ul>

<h3 id="private-ip-ranges">Private IP Ranges</h3>

<p>Three ranges are globally reserved for private networks (RFC 1918). Every router and OS knows these will never appear on the public internet:</p>

<table>
  <thead>
    <tr>
      <th>Range</th>
      <th>Size</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">10.0.0.0/8</code></td>
      <td>16.7 million IPs</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">172.16.0.0/12</code></td>
      <td>~1 million IPs</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">192.168.0.0/16</code></td>
      <td>65,536 IPs</td>
    </tr>
  </tbody>
</table>

<p>Using a public range internally (say <code class="language-plaintext highlighter-rouge">142.250.80.0</code>, which belongs to Google) would work — until you need to reach the real owner of that range. Internal traffic would route locally instead of reaching the real destination. This is called IP shadowing and it’s why you always use private ranges for internal networks.</p>

<h3 id="ingress-and-egress">Ingress and Egress</h3>

<p>Two terms that appear everywhere in AWS docs and Terraform configs: <strong>ingress</strong> means inbound traffic (coming in), <strong>egress</strong> means outbound traffic (going out). The plain English equivalents — inbound and outbound — mean the same thing.</p>

<h2 id="vpc--virtual-private-cloud">VPC — Virtual Private Cloud</h2>

<p>A VPC is your own isolated network inside AWS. Think of it as your private data center’s network, built on the same private IP ranges described above.</p>

<p>When you create a VPC you assign it a CIDR block — for example, <code class="language-plaintext highlighter-rouge">10.0.0.0/16</code> gives you 65,536 addresses. AWS requires VPC CIDR blocks to be between <code class="language-plaintext highlighter-rouge">/16</code> and <code class="language-plaintext highlighter-rouge">/28</code>.</p>

<p>The CIDR block is just the starting point. A VPC is the container for everything else: subnets, gateways, route tables, and security groups. The IP range defines the address space; the rest defines what can talk to what and how.</p>

<h3 id="which-private-range-to-use">Which Private Range to Use?</h3>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">10.0.0.0/8</code></strong> — most common for cloud and corporate networks. AWS defaults to this. Plenty of room to carve out many VPCs without overlapping.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">172.16.0.0/12</code></strong> — sometimes used as a second range when <code class="language-plaintext highlighter-rouge">10.x</code> is already taken.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">192.168.0.0/16</code></strong> — home routers default to this. Rarely used in cloud because it’s small and likely to collide if you ever connect a VPC to an office network.</li>
</ul>

<p>The practical concern is overlap. If a VPC uses <code class="language-plaintext highlighter-rouge">10.0.0.0/16</code> and an office network also uses <code class="language-plaintext highlighter-rouge">10.0.0.0/16</code>, they can’t be connected via VPC peering or VPN because the ranges collide. Companies often plan CIDR allocations across environments to avoid this — for example, <code class="language-plaintext highlighter-rouge">10.0.0.0/16</code> for prod, <code class="language-plaintext highlighter-rouge">10.1.0.0/16</code> for staging, <code class="language-plaintext highlighter-rouge">10.2.0.0/16</code> for dev.</p>

<h2 id="subnets">Subnets</h2>

<p>A subnet is a subdivision of the VPC tied to a specific <strong>Availability Zone</strong> (AZ). A VPC spans a whole region (e.g., <code class="language-plaintext highlighter-rouge">us-east-1</code>), but each subnet lives in exactly one AZ (e.g., <code class="language-plaintext highlighter-rouge">us-east-1a</code>). AZs are physically separate data centers.</p>

<h3 id="public-vs-private-subnets">Public vs Private Subnets</h3>

<p>This distinction is not an inherent property — it’s a consequence of routing. A subnet is “public” because its route table has a route to an Internet Gateway. A subnet is “private” because it doesn’t. The labels are conventions based on how traffic flows.</p>

<p>Even in a “public” subnet, resources aren’t automatically exposed. A NAT Gateway in a public subnet has a public IP but only allows outbound traffic — nobody can initiate a connection to it from outside. A bastion host accepts inbound SSH but only from a specific IP, locked down by its security group.</p>

<h3 id="why-aurora-rds-needs-multiple-subnets">Why Aurora RDS Needs Multiple Subnets</h3>

<p>AWS requires the DB subnet group to span at least two AZs. This is for high availability — if one data center goes down, the database can failover to the other AZ. Aurora takes this further: the storage layer automatically replicates data six ways across three AZs, so having subnets in multiple AZs is essential.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre>VPC: 10.0.0.0/16
├── Subnet A (private): 10.0.1.0/24 in us-east-1a
├── Subnet B (private): 10.0.2.0/24 in us-east-1b   ← Aurora needs both
├── Subnet C (public):  10.0.101.0/24 in us-east-1a  ← for bastion/NAT
└── Subnet D (public):  10.0.102.0/24 in us-east-1b
</pre></td></tr></tbody></table></code></pre></div></div>

<h2 id="internet-gateway-and-nat-gateway">Internet Gateway and NAT Gateway</h2>

<p><strong>Internet Gateway (IGW)</strong> attaches to the VPC and allows resources in public subnets to communicate with the internet — both inbound and outbound.</p>

<p><strong>NAT Gateway</strong> sits in a public subnet and allows resources in private subnets to make outbound requests (download patches, call external APIs) without being reachable from the internet. It’s a one-way door.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre>Internet
   │
   ▼
Internet Gateway ─── Public Subnet ─── NAT Gateway
                                            │
                                            ▼
                                       Private Subnet ─── Aurora RDS
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Aurora RDS should never be in a public subnet. If it needs to reach the internet (which is rare — AWS handles engine updates through its own management plane), it goes through the NAT Gateway.</p>

<h3 id="do-you-always-need-public-subnets">Do You Always Need Public Subnets?</h3>

<p>No. The bastion-plus-public-subnet pattern is the simplest way to get started, but if you use alternatives like a VPN, AWS Systems Manager Session Manager, or an application server in the same VPC, you don’t need a bastion or any public subnets at all. The entire VPC can be private — you still need at least two private subnets in different AZs for the DB subnet group, but nothing needs to be public.</p>

<h2 id="route-tables">Route Tables</h2>

<p>Route tables are the rules that tell traffic where to go. Each subnet is associated with exactly one route table.</p>

<p><strong>Public subnet route table</strong>:</p>

<table>
  <thead>
    <tr>
      <th>Destination</th>
      <th>Target</th>
      <th>Meaning</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">10.0.0.0/16</code></td>
      <td><code class="language-plaintext highlighter-rouge">local</code></td>
      <td>Traffic within VPC stays local</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">0.0.0.0/0</code></td>
      <td><code class="language-plaintext highlighter-rouge">igw-xxxxx</code></td>
      <td>Everything else goes to the internet</td>
    </tr>
  </tbody>
</table>

<p><strong>Private subnet route table</strong>:</p>

<table>
  <thead>
    <tr>
      <th>Destination</th>
      <th>Target</th>
      <th>Meaning</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">10.0.0.0/16</code></td>
      <td><code class="language-plaintext highlighter-rouge">local</code></td>
      <td>Traffic within VPC stays local</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">0.0.0.0/0</code></td>
      <td><code class="language-plaintext highlighter-rouge">nat-xxxxx</code></td>
      <td>Outbound goes through NAT (no inbound)</td>
    </tr>
  </tbody>
</table>

<p>The only difference is one line: <code class="language-plaintext highlighter-rouge">igw</code> vs <code class="language-plaintext highlighter-rouge">nat</code> for the default route.</p>

<h2 id="security-groups">Security Groups</h2>

<p>A security group is a virtual firewall attached to a resource — an EC2 instance, an Aurora RDS cluster, etc. It controls which traffic is allowed in (ingress) and out (egress).</p>

<p>Security groups are a separate layer from routing. Routing controls whether traffic can physically reach a resource; security groups control whether the resource accepts that traffic. Both must allow the traffic for it to get through.</p>

<h3 id="key-behaviors">Key Behaviors</h3>

<ul>
  <li><strong>Stateful</strong>: if you allow inbound traffic, the response is automatically allowed out (unlike NACLs, which are stateless).</li>
  <li><strong>Default deny inbound</strong>: nothing can reach the resource unless you add a rule.</li>
  <li><strong>Default allow outbound</strong>: the resource can talk to anything unless you restrict it.</li>
  <li><strong>Source can be another security group</strong>: this is the powerful part.</li>
</ul>

<h3 id="referencing-security-groups-instead-of-ips">Referencing Security Groups Instead of IPs</h3>

<p>When the DB security group says “allow ingress from <code class="language-plaintext highlighter-rouge">sg-app</code>,” it means “allow ingress from any resource that has <code class="language-plaintext highlighter-rouge">sg-app</code> attached.” You don’t need to track IPs — security group references don’t break when IPs change:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
</pre></td><td class="rouge-code"><pre><span class="nx">resource</span> <span class="s2">"aws_security_group"</span> <span class="s2">"app"</span> <span class="p">{</span>
  <span class="nx">vpc_id</span> <span class="p">=</span> <span class="nx">aws_vpc</span><span class="err">.</span><span class="nx">main</span><span class="err">.</span><span class="nx">id</span>
  <span class="nx">name</span>   <span class="p">=</span> <span class="s2">"app"</span>
<span class="p">}</span>

<span class="nx">resource</span> <span class="s2">"aws_security_group"</span> <span class="s2">"db"</span> <span class="p">{</span>
  <span class="nx">vpc_id</span> <span class="p">=</span> <span class="nx">aws_vpc</span><span class="err">.</span><span class="nx">main</span><span class="err">.</span><span class="nx">id</span>
  <span class="nx">name</span>   <span class="p">=</span> <span class="s2">"database"</span>

  <span class="nx">ingress</span> <span class="p">{</span>
    <span class="nx">from_port</span>       <span class="p">=</span> <span class="mi">5432</span>
    <span class="nx">to_port</span>         <span class="p">=</span> <span class="mi">5432</span>
    <span class="nx">security_groups</span> <span class="p">=</span> <span class="p">[</span><span class="nx">aws_security_group</span><span class="err">.</span><span class="nx">app</span><span class="err">.</span><span class="nx">id</span><span class="p">]</span>
  <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The app server gets <code class="language-plaintext highlighter-rouge">aws_security_group.app</code> attached, the Aurora cluster gets <code class="language-plaintext highlighter-rouge">aws_security_group.db</code>. Only resources in the <code class="language-plaintext highlighter-rouge">app</code> group can reach the database on port 5432.</p>

<h2 id="db-subnet-group">DB Subnet Group</h2>

<p>A DB subnet group is an RDS-specific concept — a named collection of subnets where RDS is allowed to place database instances. When you create an Aurora cluster, you don’t pick a subnet directly. You pick a DB subnet group, and Aurora chooses which subnets to use. For Multi-AZ deployments it places the writer in one subnet and readers in others.</p>

<p>The rule: the group must include subnets in at least two different AZs.</p>

<h2 id="putting-it-all-together">Putting It All Together</h2>

<p>Here’s the complete picture for an Aurora RDS PostgreSQL deployment:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
</pre></td><td class="rouge-code"><pre>┌─────────────────────── VPC (10.0.0.0/16) ────────────────────────┐
│                                                                  │
│  ┌── Public Subnet (10.0.101.0/24, us-east-1a) ────┐             │
│  │  Bastion Host (EC2)                             │             │
│  │  SG: allow SSH (22) from your IP                │             │
│  └─────────────────────────────────────────────────┘             │
│         │ connects via private IP on port 5432                   │
│         ▼                                                        │
│  ┌── Private Subnet (10.0.1.0/24, us-east-1a) ─────┐             │
│  │  Aurora PostgreSQL (writer)                     │             │
│  │  SG: allow 5432 from bastion SG                 │             │
│  └─────────────────────────────────────────────────┘             │
│                                                                  │
│  ┌── Private Subnet (10.0.2.0/24, us-east-1b) ───────┐           │
│  │  Aurora PostgreSQL (reader replica)               │           │
│  │  Same SG                                          │           │
│  └───────────────────────────────────────────────────┘           │
│                                                                  │
│  DB Subnet Group = [private-subnet-1a, private-subnet-1b]        │
└──────────────────────────────────────────────────────────────────┘
</pre></td></tr></tbody></table></code></pre></div></div>

<p>You SSH into the bastion, then from there <code class="language-plaintext highlighter-rouge">psql</code> into the Aurora endpoint. The security group on Aurora only allows connections from the bastion’s security group on port 5432. The database is never directly reachable from the internet.</p>

<p>Each layer has a distinct job: the VPC defines the address space, subnets partition it across AZs, route tables control where traffic flows, security groups decide what gets through, and the DB subnet group tells Aurora which subnets it can use. All of these must be in place before you create the first Aurora cluster.</p>]]></content><author><name>kimserey</name></author><category term="aws" /><summary type="html"><![CDATA[Running PostgreSQL in AWS isn’t just about creating the database — you first build the network it sits in. With Aurora RDS, you are the network engineer: you decide the IP range, which parts are reachable from the internet, what traffic is allowed between resources, and which data centers your database runs in. This post walks through the core networking concepts — VPC, subnets, gateways, route tables, security groups, and DB subnet groups — that you need to understand before provisioning Aurora RDS PostgreSQL.]]></summary></entry><entry><title type="html">Debezium PostgreSQL Connector — Failure Modes and Slot Management</title><link href="https://www.kimsereylam.com/postgres/2026/08/15/debezium-postgresql-connector-failure-modes-and-slot-management.html" rel="alternate" type="text/html" title="Debezium PostgreSQL Connector — Failure Modes and Slot Management" /><published>2026-08-15T00:00:00-05:00</published><updated>2026-08-15T00:00:00-05:00</updated><id>https://www.kimsereylam.com/postgres/2026/08/15/debezium-postgresql-connector-failure-modes-and-slot-management</id><content type="html" xml:base="https://www.kimsereylam.com/postgres/2026/08/15/debezium-postgresql-connector-failure-modes-and-slot-management.html"><![CDATA[<p>Debezium’s reliability comes from PostgreSQL’s replication slots — they guarantee that no WAL is discarded before the connector has consumed it, so a crashed connector can catch up without data loss. But that same guarantee creates the biggest operational risk: an abandoned slot tells PostgreSQL to hold WAL indefinitely, and on a busy database the WAL can fill the disk. This post covers connector failure and recovery, leaked slots, and how Debezium handles schema evolution.</p>

<!--more-->

<h2 id="connector-failure-and-wal-accumulation">Connector Failure and WAL Accumulation</h2>

<p>A replication slot is a server-side bookmark. While the connector is running, it continuously reads from the WAL and confirms its position back to PostgreSQL, which allows PostgreSQL to recycle old WAL segments. When the connector goes down, the slot stays — PostgreSQL holds all WAL from the slot’s last confirmed position forward.</p>

<h3 id="seeing-it">Seeing It</h3>

<p>Check the current slot state while the connector is running:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="k">SELECT</span> <span class="n">slot_name</span><span class="p">,</span> <span class="n">active</span><span class="p">,</span> <span class="n">restart_lsn</span><span class="p">,</span> <span class="n">confirmed_flush_lsn</span>
<span class="k">FROM</span> <span class="n">pg_replication_slots</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">active</code> column is <code class="language-plaintext highlighter-rouge">true</code> and the lag is small — Debezium is keeping up. Now delete the connector:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>curl <span class="nt">-X</span> DELETE http://localhost:8083/connectors/learn-cdc-connector
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The connector is gone, but the slot is still there:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="k">SELECT</span> <span class="n">slot_name</span><span class="p">,</span> <span class="n">active</span><span class="p">,</span> <span class="n">restart_lsn</span> <span class="k">FROM</span> <span class="n">pg_replication_slots</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">active</code> is now <code class="language-plaintext highlighter-rouge">false</code>. PostgreSQL will keep all WAL from this point forward because the slot indicates its consumer has not read past here.</p>

<h3 id="wal-growth-while-down">WAL Growth While Down</h3>

<p>Make some changes while the connector is offline:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="k">INSERT</span> <span class="k">INTO</span> <span class="n">orders</span> <span class="p">(</span><span class="n">customer</span><span class="p">,</span> <span class="n">product</span><span class="p">,</span> <span class="n">amount</span><span class="p">)</span>
<span class="k">VALUES</span> <span class="p">(</span><span class="s1">'eve'</span><span class="p">,</span> <span class="s1">'Widget E'</span><span class="p">,</span> <span class="mi">59</span><span class="p">.</span><span class="mi">99</span><span class="p">);</span>

<span class="k">INSERT</span> <span class="k">INTO</span> <span class="n">orders</span> <span class="p">(</span><span class="n">customer</span><span class="p">,</span> <span class="n">product</span><span class="p">,</span> <span class="n">amount</span><span class="p">)</span>
<span class="k">VALUES</span> <span class="p">(</span><span class="s1">'frank'</span><span class="p">,</span> <span class="s1">'Widget F'</span><span class="p">,</span> <span class="mi">14</span><span class="p">.</span><span class="mi">99</span><span class="p">);</span>

<span class="k">UPDATE</span> <span class="n">customers</span> <span class="k">SET</span> <span class="n">tier</span> <span class="o">=</span> <span class="s1">'premium'</span> <span class="k">WHERE</span> <span class="n">id</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Check the lag:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="k">SELECT</span> <span class="n">slot_name</span><span class="p">,</span>
       <span class="n">pg_size_pretty</span><span class="p">(</span><span class="n">pg_current_wal_lsn</span><span class="p">()</span> <span class="o">-</span> <span class="n">restart_lsn</span><span class="p">)</span> <span class="k">AS</span> <span class="n">wal_retained</span>
<span class="k">FROM</span> <span class="n">pg_replication_slots</span>
<span class="k">WHERE</span> <span class="n">slot_name</span> <span class="o">=</span> <span class="s1">'learn_cdc_slot'</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The retained WAL is growing. On a production database with high write throughput, this can fill the disk within hours.</p>

<h3 id="recovery">Recovery</h3>

<p>Re-register the connector with the same <code class="language-plaintext highlighter-rouge">slot.name</code>. It reconnects to the existing slot, reads all the WAL that accumulated while it was down, and produces the missed events to Kafka. No data is lost.</p>

<p>This is the key guarantee: as long as the replication slot exists, PostgreSQL will not discard WAL that has not been consumed. The connector can go down and come back, and it picks up exactly where it left off.</p>

<h2 id="leaked-slots">Leaked Slots</h2>

<p>A leaked slot is a replication slot whose connector has been permanently removed but the slot was never dropped. It is the single biggest operational risk when running Debezium.</p>

<h3 id="how-it-happens">How It Happens</h3>

<p>Deleting a Debezium connector through the Kafka Connect REST API removes the connector process, but it does not drop the replication slot in PostgreSQL. The slot stays, <code class="language-plaintext highlighter-rouge">active = false</code>, telling PostgreSQL to hold WAL indefinitely.</p>

<p>In a lab environment this is harmless — a few extra kilobytes of WAL. In production with continuous writes, the WAL grows without bound because PostgreSQL will not recycle any segment past the slot’s LSN:</p>

<ol>
  <li>WAL grows unbounded</li>
  <li>Disk fills up</li>
  <li>PostgreSQL stops accepting writes</li>
  <li>Outage</li>
</ol>

<h3 id="detecting-leaked-slots">Detecting Leaked Slots</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="k">SELECT</span> <span class="n">slot_name</span><span class="p">,</span> <span class="n">active</span><span class="p">,</span>
       <span class="n">pg_size_pretty</span><span class="p">(</span><span class="n">pg_current_wal_lsn</span><span class="p">()</span> <span class="o">-</span> <span class="n">restart_lsn</span><span class="p">)</span> <span class="k">AS</span> <span class="n">wal_retained</span><span class="p">,</span>
       <span class="n">pg_size_pretty</span><span class="p">(</span><span class="n">pg_current_wal_lsn</span><span class="p">()</span> <span class="o">-</span> <span class="n">confirmed_flush_lsn</span><span class="p">)</span> <span class="k">AS</span> <span class="n">consumer_lag</span>
<span class="k">FROM</span> <span class="n">pg_replication_slots</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Alert if:</p>
<ul>
  <li>Any slot has <code class="language-plaintext highlighter-rouge">active = false</code> for more than a few minutes</li>
  <li><code class="language-plaintext highlighter-rouge">wal_retained</code> exceeds a threshold (e.g., 1 GB)</li>
  <li><code class="language-plaintext highlighter-rouge">consumer_lag</code> is growing steadily (connector falling behind)</li>
</ul>

<h3 id="cleaning-up">Cleaning Up</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="k">SELECT</span> <span class="n">pg_drop_replication_slot</span><span class="p">(</span><span class="s1">'learn_cdc_slot'</span><span class="p">);</span>

<span class="c1">-- Verify</span>
<span class="k">SELECT</span> <span class="n">slot_name</span><span class="p">,</span> <span class="n">active</span> <span class="k">FROM</span> <span class="n">pg_replication_slots</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Name slots explicitly (e.g., <code class="language-plaintext highlighter-rouge">orders_cdc_slot</code>, <code class="language-plaintext highlighter-rouge">analytics_cdc_slot</code>) so they are easy to identify when monitoring. The default Debezium-generated name (<code class="language-plaintext highlighter-rouge">debezium</code>) is ambiguous if multiple connectors run against the same database.</p>

<h2 id="schema-evolution">Schema Evolution</h2>

<p>What happens when a table’s schema changes while Debezium is streaming?</p>

<h3 id="adding-a-column">Adding a Column</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="k">ALTER</span> <span class="k">TABLE</span> <span class="n">orders</span> <span class="k">ADD</span> <span class="k">COLUMN</span> <span class="n">notes</span> <span class="nb">TEXT</span><span class="p">;</span>

<span class="k">INSERT</span> <span class="k">INTO</span> <span class="n">orders</span> <span class="p">(</span><span class="n">customer</span><span class="p">,</span> <span class="n">product</span><span class="p">,</span> <span class="n">amount</span><span class="p">,</span> <span class="n">notes</span><span class="p">)</span>
<span class="k">VALUES</span> <span class="p">(</span><span class="s1">'grace'</span><span class="p">,</span> <span class="s1">'Widget G'</span><span class="p">,</span> <span class="mi">24</span><span class="p">.</span><span class="mi">99</span><span class="p">,</span> <span class="s1">'Rush delivery'</span><span class="p">);</span>

<span class="k">INSERT</span> <span class="k">INTO</span> <span class="n">orders</span> <span class="p">(</span><span class="n">customer</span><span class="p">,</span> <span class="n">product</span><span class="p">,</span> <span class="n">amount</span><span class="p">)</span>
<span class="k">VALUES</span> <span class="p">(</span><span class="s1">'henry'</span><span class="p">,</span> <span class="s1">'Widget H'</span><span class="p">,</span> <span class="mi">34</span><span class="p">.</span><span class="mi">99</span><span class="p">);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Both events include the <code class="language-plaintext highlighter-rouge">notes</code> field — one with the value, one with <code class="language-plaintext highlighter-rouge">null</code>. Debezium picks up the schema change automatically. No connector restart is needed.</p>

<h3 id="renaming-a-column">Renaming a Column</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="k">ALTER</span> <span class="k">TABLE</span> <span class="n">orders</span> <span class="k">RENAME</span> <span class="k">COLUMN</span> <span class="n">notes</span> <span class="k">TO</span> <span class="n">remarks</span><span class="p">;</span>

<span class="k">INSERT</span> <span class="k">INTO</span> <span class="n">orders</span> <span class="p">(</span><span class="n">customer</span><span class="p">,</span> <span class="n">product</span><span class="p">,</span> <span class="n">amount</span><span class="p">,</span> <span class="n">remarks</span><span class="p">)</span>
<span class="k">VALUES</span> <span class="p">(</span><span class="s1">'iris'</span><span class="p">,</span> <span class="s1">'Widget I'</span><span class="p">,</span> <span class="mi">44</span><span class="p">.</span><span class="mi">99</span><span class="p">,</span> <span class="s1">'After rename'</span><span class="p">);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The event has <code class="language-plaintext highlighter-rouge">remarks</code> instead of <code class="language-plaintext highlighter-rouge">notes</code>. Debezium reflects the current schema.</p>

<h3 id="dropping-a-column">Dropping a Column</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="k">ALTER</span> <span class="k">TABLE</span> <span class="n">orders</span> <span class="k">DROP</span> <span class="k">COLUMN</span> <span class="n">remarks</span><span class="p">;</span>

<span class="k">INSERT</span> <span class="k">INTO</span> <span class="n">orders</span> <span class="p">(</span><span class="n">customer</span><span class="p">,</span> <span class="n">product</span><span class="p">,</span> <span class="n">amount</span><span class="p">)</span>
<span class="k">VALUES</span> <span class="p">(</span><span class="s1">'jack'</span><span class="p">,</span> <span class="s1">'Widget J'</span><span class="p">,</span> <span class="mi">54</span><span class="p">.</span><span class="mi">99</span><span class="p">);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The event no longer has the <code class="language-plaintext highlighter-rouge">remarks</code> field.</p>

<h3 id="what-this-means">What This Means</h3>

<p>Debezium does not care about schema evolution. It forwards whatever the WAL gives it as JSON. Column added, renamed, dropped — Debezium serializes the current row state and produces it to Kafka. No restart, no config change.</p>

<p>The schema evolution problem moves entirely downstream — the consumer of the Kafka events must handle the changing JSON shape. Common approaches:</p>

<ul>
  <li><strong>Store events as JSON blobs</strong> — use a schemaless column type (e.g., <code class="language-plaintext highlighter-rouge">JSONB</code> in PostgreSQL, <code class="language-plaintext highlighter-rouge">VARIANT</code> in Snowflake, <code class="language-plaintext highlighter-rouge">STRING</code> in BigQuery) so the varying shapes do not cause failures. The JSON blob just has different keys over time.</li>
  <li><strong>Schema registry</strong> — use Avro or Protobuf with a schema registry that tracks schema versions and handles compatibility. Consumers deserialize using the schema version embedded in each message.</li>
  <li><strong>Flatten on ingest</strong> — use ExtractNewRecordState to flatten the envelope, then let the sink connector handle schema mapping. Some sink connectors (e.g., JDBC sink) can auto-create and alter target table columns.</li>
</ul>

<p>The <code class="language-plaintext highlighter-rouge">before</code>/<code class="language-plaintext highlighter-rouge">after</code> fields in the event envelope contain the raw row data. If you store those as schemaless JSON, adding or dropping columns in the source table has no effect on the landing table — the JSON blob absorbs the change. Schema evolution on the event envelope itself (e.g., a Debezium version upgrade adding a new metadata field to <code class="language-plaintext highlighter-rouge">source</code>) is a separate concern handled by the sink connector or schema registry.</p>

<h2 id="monitoring-checklist">Monitoring Checklist</h2>

<p>A minimal monitoring setup for Debezium in production:</p>

<h3 id="postgresql-side">PostgreSQL Side</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="rouge-code"><pre><span class="c1">-- Replication slot health</span>
<span class="k">SELECT</span> <span class="n">slot_name</span><span class="p">,</span> <span class="n">active</span><span class="p">,</span>
       <span class="n">pg_size_pretty</span><span class="p">(</span><span class="n">pg_current_wal_lsn</span><span class="p">()</span> <span class="o">-</span> <span class="n">restart_lsn</span><span class="p">)</span> <span class="k">AS</span> <span class="n">wal_retained</span><span class="p">,</span>
       <span class="n">pg_size_pretty</span><span class="p">(</span><span class="n">pg_current_wal_lsn</span><span class="p">()</span> <span class="o">-</span> <span class="n">confirmed_flush_lsn</span><span class="p">)</span> <span class="k">AS</span> <span class="n">consumer_lag</span>
<span class="k">FROM</span> <span class="n">pg_replication_slots</span><span class="p">;</span>

<span class="c1">-- Publication tables</span>
<span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">pg_publication_tables</span><span class="p">;</span>

<span class="c1">-- Replica identity per table</span>
<span class="k">SELECT</span> <span class="n">relname</span><span class="p">,</span> <span class="n">relreplident</span>
<span class="k">FROM</span> <span class="n">pg_class</span>
<span class="k">WHERE</span> <span class="n">relname</span> <span class="k">IN</span> <span class="p">(</span><span class="s1">'orders'</span><span class="p">,</span> <span class="s1">'customers'</span><span class="p">);</span>
<span class="c1">-- 'f' = FULL, 'd' = DEFAULT, 'n' = NOTHING, 'i' = INDEX</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="kafka-connect-side">Kafka Connect Side</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="c"># Connector status</span>
curl http://localhost:8083/connectors/orders-cdc-connector/status | jq

<span class="c"># List all connectors</span>
curl http://localhost:8083/connectors | jq

<span class="c"># Task-level status (a connector can have multiple tasks)</span>
curl http://localhost:8083/connectors/orders-cdc-connector/tasks/0/status | jq
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="what-to-alert-on">What to Alert On</h3>

<table>
  <thead>
    <tr>
      <th>Signal</th>
      <th>Meaning</th>
      <th>Action</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Slot <code class="language-plaintext highlighter-rouge">active = false</code> for &gt; 5 min</td>
      <td>Connector is down</td>
      <td>Investigate connector status, restart if needed</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">wal_retained</code> &gt; threshold</td>
      <td>WAL accumulating</td>
      <td>Either the connector is behind or the slot is leaked</td>
    </tr>
    <tr>
      <td>Connector state <code class="language-plaintext highlighter-rouge">FAILED</code></td>
      <td>Connector crashed</td>
      <td>Check task error in status endpoint, fix config or data issue, restart</td>
    </tr>
    <tr>
      <td>Consumer lag growing</td>
      <td>Connector falling behind</td>
      <td>Check for large transactions, schema changes, or resource constraints</td>
    </tr>
    <tr>
      <td>No new messages on CDC topic</td>
      <td>Either no writes or connector stalled</td>
      <td>Cross-reference with database write activity</td>
    </tr>
  </tbody>
</table>]]></content><author><name>kimserey</name></author><category term="postgres" /><summary type="html"><![CDATA[Debezium’s reliability comes from PostgreSQL’s replication slots — they guarantee that no WAL is discarded before the connector has consumed it, so a crashed connector can catch up without data loss. But that same guarantee creates the biggest operational risk: an abandoned slot tells PostgreSQL to hold WAL indefinitely, and on a busy database the WAL can fill the disk. This post covers connector failure and recovery, leaked slots, and how Debezium handles schema evolution.]]></summary></entry><entry><title type="html">Debezium PostgreSQL Connector — Configuration That Matters</title><link href="https://www.kimsereylam.com/postgres/2026/08/12/debezium-postgresql-connector-configuration.html" rel="alternate" type="text/html" title="Debezium PostgreSQL Connector — Configuration That Matters" /><published>2026-08-12T00:00:00-05:00</published><updated>2026-08-12T00:00:00-05:00</updated><id>https://www.kimsereylam.com/postgres/2026/08/12/debezium-postgresql-connector-configuration</id><content type="html" xml:base="https://www.kimsereylam.com/postgres/2026/08/12/debezium-postgresql-connector-configuration.html"><![CDATA[<p>The default Debezium connector config produces usable events, but the defaults leave important gaps — <code class="language-plaintext highlighter-rouge">before</code> values are missing on UPDATEs and DELETEs, all tables are captured including system tables, and each table gets its own Kafka topic. This post covers the configuration knobs that close those gaps: replica identity, table filtering, publication modes, and single-topic routing with the ByLogicalTableRouter transform.</p>

<!--more-->

<h2 id="replica-identity--getting-the-before-value">Replica Identity — Getting the “before” Value</h2>

<p>With the default connector from the previous post, UPDATE and DELETE events have <code class="language-plaintext highlighter-rouge">before: null</code>. The previous row state is missing because PostgreSQL’s default replica identity only writes the primary key to the WAL on UPDATEs and DELETEs — not the full row. Debezium can only produce what the WAL gives it.</p>

<h3 id="seeing-the-problem">Seeing the Problem</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="k">UPDATE</span> <span class="n">orders</span> <span class="k">SET</span> <span class="n">status</span> <span class="o">=</span> <span class="s1">'shipped'</span> <span class="k">WHERE</span> <span class="n">id</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The resulting event:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre><span class="p">{</span><span class="w">
  </span><span class="nl">"before"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
  </span><span class="nl">"after"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
    </span><span class="nl">"customer"</span><span class="p">:</span><span class="w"> </span><span class="s2">"alice"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"product"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Widget A"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"amount"</span><span class="p">:</span><span class="w"> </span><span class="mf">29.99</span><span class="p">,</span><span class="w">
    </span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"shipped"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"op"</span><span class="p">:</span><span class="w"> </span><span class="s2">"u"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></pre></td></tr></tbody></table></code></pre></div></div>

<p>There is no way to tell what <code class="language-plaintext highlighter-rouge">status</code> was before the update. For audit trails, sync pipelines, or any use case that needs to know what changed (not just what the current state is), this is a problem.</p>

<h3 id="fixing-it">Fixing It</h3>

<p>Set the table’s replica identity to FULL so PostgreSQL writes the entire old row to the WAL:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="k">ALTER</span> <span class="k">TABLE</span> <span class="n">orders</span> <span class="n">REPLICA</span> <span class="k">IDENTITY</span> <span class="k">FULL</span><span class="p">;</span>

<span class="k">UPDATE</span> <span class="n">orders</span> <span class="k">SET</span> <span class="n">status</span> <span class="o">=</span> <span class="s1">'cancelled'</span> <span class="k">WHERE</span> <span class="n">id</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Now the event has <code class="language-plaintext highlighter-rouge">before</code> populated:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
</pre></td><td class="rouge-code"><pre><span class="p">{</span><span class="w">
  </span><span class="nl">"before"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
    </span><span class="nl">"customer"</span><span class="p">:</span><span class="w"> </span><span class="s2">"alice"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"product"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Widget A"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"amount"</span><span class="p">:</span><span class="w"> </span><span class="mf">29.99</span><span class="p">,</span><span class="w">
    </span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"shipped"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"after"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
    </span><span class="nl">"customer"</span><span class="p">:</span><span class="w"> </span><span class="s2">"alice"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"product"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Widget A"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"amount"</span><span class="p">:</span><span class="w"> </span><span class="mf">29.99</span><span class="p">,</span><span class="w">
    </span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"cancelled"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"op"</span><span class="p">:</span><span class="w"> </span><span class="s2">"u"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></pre></td></tr></tbody></table></code></pre></div></div>

<p>DELETEs also benefit — a DELETE on a FULL table produces <code class="language-plaintext highlighter-rouge">before</code> with the deleted row’s data and <code class="language-plaintext highlighter-rouge">after: null</code>.</p>

<h3 id="automating-it">Automating It</h3>

<p>Instead of manually running <code class="language-plaintext highlighter-rouge">ALTER TABLE ... REPLICA IDENTITY FULL</code> on each table, Debezium can do it automatically:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nl">"replica.identity.autoset.values"</span><span class="p">:</span><span class="w"> </span><span class="s2">"public.*:FULL"</span><span class="w">
</span></pre></td></tr></tbody></table></code></pre></div></div>

<p>This tells the connector to set <code class="language-plaintext highlighter-rouge">REPLICA IDENTITY FULL</code> on all tables in the <code class="language-plaintext highlighter-rouge">public</code> schema matching the pattern. You can verify it took effect:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="k">SELECT</span> <span class="n">relname</span><span class="p">,</span> <span class="n">relreplident</span>
<span class="k">FROM</span> <span class="n">pg_class</span>
<span class="k">WHERE</span> <span class="n">relname</span> <span class="k">IN</span> <span class="p">(</span><span class="s1">'orders'</span><span class="p">,</span> <span class="s1">'customers'</span><span class="p">);</span>
<span class="c1">-- 'f' = FULL, 'd' = DEFAULT</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="when-you-dont-need-full">When You Don’t Need FULL</h3>

<p>If you only care about the current state of a row — for example, replicating a table to another database where you overwrite the target row with the latest <code class="language-plaintext highlighter-rouge">after</code> — DEFAULT replica identity is fine. FULL adds overhead because PostgreSQL writes more data to the WAL on every UPDATE and DELETE.</p>

<h2 id="table-filtering">Table Filtering</h2>

<p>The default connector captures all tables in the database. In practice, you want to exclude system tables (migration tracking, job queues, framework-internal tables) and capture only domain tables.</p>

<h3 id="tableincludelist">table.include.list</h3>

<p>An allowlist of fully-qualified table names:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nl">"table.include.list"</span><span class="p">:</span><span class="w"> </span><span class="s2">"public.orders,public.customers"</span><span class="w">
</span></pre></td></tr></tbody></table></code></pre></div></div>

<p>Any table not in the list is ignored. With this config, inserts into other tables produce no CDC events and no Kafka topics are created for them.</p>

<h3 id="tableexcludelist">table.exclude.list</h3>

<p>A denylist — capture everything except these tables:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nl">"table.exclude.list"</span><span class="p">:</span><span class="w"> </span><span class="s2">"public.flyway_schema_history,public.schema_migrations"</span><span class="w">
</span></pre></td></tr></tbody></table></code></pre></div></div>

<p>You can use one or the other, not both.</p>

<h3 id="the-alternative-publication-filtering">The Alternative: Publication Filtering</h3>

<p>Instead of filtering at the Debezium level, you can control it at the PostgreSQL level through publications. The <code class="language-plaintext highlighter-rouge">publication.autocreate.mode</code> setting controls how Debezium manages publications:</p>

<table>
  <thead>
    <tr>
      <th>Mode</th>
      <th>What it does</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">all_tables</code></td>
      <td>Auto-creates a publication for all tables. Simple, good for development</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">filtered</code></td>
      <td>Auto-creates a publication only for tables in <code class="language-plaintext highlighter-rouge">table.include.list</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">disabled</code></td>
      <td>Expects a publication to already exist — you create it yourself via a migration</td>
    </tr>
  </tbody>
</table>

<p>For production, <code class="language-plaintext highlighter-rouge">disabled</code> with a manually-created publication gives the most explicit control. The publication is created in a database migration, not by the connector:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="c1">-- In a migration script</span>
<span class="k">CREATE</span> <span class="n">PUBLICATION</span> <span class="n">cdc_publication</span> <span class="k">FOR</span> <span class="k">TABLE</span> <span class="n">orders</span><span class="p">,</span> <span class="n">customers</span><span class="p">,</span> <span class="n">products</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Then the connector references it:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="nl">"publication.autocreate.mode"</span><span class="p">:</span><span class="w"> </span><span class="s2">"disabled"</span><span class="err">,</span><span class="w">
</span><span class="nl">"publication.name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"cdc_publication"</span><span class="w">
</span></pre></td></tr></tbody></table></code></pre></div></div>

<p>This avoids surprises when new tables are added to the schema — they are only captured when explicitly added to the publication.</p>

<h2 id="single-topic-routing-with-bylogicaltablerouter">Single-Topic Routing with ByLogicalTableRouter</h2>

<p>By default, Debezium creates one Kafka topic per table (<code class="language-plaintext highlighter-rouge">learn.public.orders</code>, <code class="language-plaintext highlighter-rouge">learn.public.customers</code>, etc.). The ByLogicalTableRouter transform routes events from multiple tables into a single topic.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="nl">"transforms"</span><span class="p">:</span><span class="w"> </span><span class="s2">"route-to-single-topic"</span><span class="err">,</span><span class="w">
</span><span class="nl">"transforms.route-to-single-topic.type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"io.debezium.transforms.ByLogicalTableRouter"</span><span class="err">,</span><span class="w">
</span><span class="nl">"transforms.route-to-single-topic.topic.regex"</span><span class="p">:</span><span class="w"> </span><span class="s2">".*"</span><span class="err">,</span><span class="w">
</span><span class="nl">"transforms.route-to-single-topic.topic.replacement"</span><span class="p">:</span><span class="w"> </span><span class="s2">"learn.v0.cdc"</span><span class="err">,</span><span class="w">
</span><span class="nl">"transforms.route-to-single-topic.key.enforce.uniqueness"</span><span class="p">:</span><span class="w"> </span><span class="s2">"false"</span><span class="w">
</span></pre></td></tr></tbody></table></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">route-to-single-topic</code> name is an alias you choose — it is just a label to reference the transform in the config. The <code class="language-plaintext highlighter-rouge">.type</code> is the actual Java class. Everything else under that name is config passed to that class.</p>

<p>After applying this, all events from all captured tables land on <code class="language-plaintext highlighter-rouge">learn.v0.cdc</code>. The consumer distinguishes them by the <code class="language-plaintext highlighter-rouge">source.table</code> field in the event envelope.</p>

<h3 id="why-one-topic">Why One Topic</h3>

<ul>
  <li><strong>Simpler topic management</strong> — one topic to configure, monitor, and set retention on</li>
  <li><strong>Downstream sinks</strong> — an S3 sink or data warehouse connector only needs to subscribe to one topic</li>
  <li><strong>Ordering across tables</strong> — events from the same transaction across different tables land on the same topic (though not guaranteed to be on the same partition)</li>
</ul>

<p>The trade-off: consumers need to handle mixed event types and filter by <code class="language-plaintext highlighter-rouge">source.table</code>.</p>

<h3 id="chaining-transforms">Chaining Transforms</h3>

<p>You can chain multiple transforms — they run in order on each event:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="nl">"transforms"</span><span class="p">:</span><span class="w"> </span><span class="s2">"filter,route"</span><span class="err">,</span><span class="w">
</span><span class="nl">"transforms.filter.type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"..."</span><span class="err">,</span><span class="w">
</span><span class="nl">"transforms.route.type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"..."</span><span class="w">
</span></pre></td></tr></tbody></table></code></pre></div></div>

<h2 id="snapshot-modes">Snapshot Modes</h2>

<p>The <code class="language-plaintext highlighter-rouge">snapshot.mode</code> setting controls what happens when the connector starts:</p>

<table>
  <thead>
    <tr>
      <th>Mode</th>
      <th>Behavior</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">initial</code></td>
      <td>Snapshot all existing rows on first start, then stream. On subsequent starts, just stream from where the slot left off</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">never</code></td>
      <td>No snapshot — only stream changes from the WAL. Existing rows are not captured</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">when_needed</code></td>
      <td>Snapshot if the replication slot does not exist or the slot’s position is no longer available in the WAL</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">initial_only</code></td>
      <td>Snapshot existing rows and then stop — no streaming</td>
    </tr>
  </tbody>
</table>

<p><code class="language-plaintext highlighter-rouge">initial</code> is the most common choice. It gives you the full current state of the database as <code class="language-plaintext highlighter-rouge">op: "r"</code> events, followed by real-time changes as <code class="language-plaintext highlighter-rouge">op: "c"/"u"/"d"</code>.</p>

<p><code class="language-plaintext highlighter-rouge">never</code> is useful when you only care about changes going forward — for example, if the historical data has already been loaded by another mechanism.</p>

<h2 id="tombstone-records">Tombstone Records</h2>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nl">"tombstones.on.delete"</span><span class="p">:</span><span class="w"> </span><span class="s2">"false"</span><span class="w">
</span></pre></td></tr></tbody></table></code></pre></div></div>

<p>When set to <code class="language-plaintext highlighter-rouge">true</code> (the default), Debezium produces a second event after every DELETE with a <code class="language-plaintext highlighter-rouge">null</code> value (a Kafka tombstone). Tombstones tell Kafka’s log compaction to remove the key entirely. If your downstream consumers do not rely on log compaction, setting this to <code class="language-plaintext highlighter-rouge">false</code> removes the extra event.</p>

<h2 id="a-full-production-oriented-config">A Full Production-Oriented Config</h2>

<p>Putting it all together — a connector config that filters tables, sets replica identity, routes to a single topic, and uses explicit publication management:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
</pre></td><td class="rouge-code"><pre><span class="p">{</span><span class="w">
  </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"orders-cdc-connector"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"config"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"connector.class"</span><span class="p">:</span><span class="w"> </span><span class="s2">"io.debezium.connector.postgresql.PostgresConnector"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"database.hostname"</span><span class="p">:</span><span class="w"> </span><span class="s2">"postgres"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"database.port"</span><span class="p">:</span><span class="w"> </span><span class="s2">"5432"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"database.user"</span><span class="p">:</span><span class="w"> </span><span class="s2">"cdc_reader"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"database.password"</span><span class="p">:</span><span class="w"> </span><span class="s2">"${file:/secrets/db-password}"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"database.dbname"</span><span class="p">:</span><span class="w"> </span><span class="s2">"orders_db"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"topic.prefix"</span><span class="p">:</span><span class="w"> </span><span class="s2">"orders"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"plugin.name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"pgoutput"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"slot.name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"orders_cdc_slot"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"publication.name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"orders_publication"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"publication.autocreate.mode"</span><span class="p">:</span><span class="w"> </span><span class="s2">"disabled"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"table.include.list"</span><span class="p">:</span><span class="w"> </span><span class="s2">"public.orders,public.customers,public.products"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"snapshot.mode"</span><span class="p">:</span><span class="w"> </span><span class="s2">"initial"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"tombstones.on.delete"</span><span class="p">:</span><span class="w"> </span><span class="s2">"false"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"replica.identity.autoset.values"</span><span class="p">:</span><span class="w"> </span><span class="s2">"public.*:FULL"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"transforms"</span><span class="p">:</span><span class="w"> </span><span class="s2">"route"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"transforms.route.type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"io.debezium.transforms.ByLogicalTableRouter"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"transforms.route.topic.regex"</span><span class="p">:</span><span class="w"> </span><span class="s2">".*"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"transforms.route.topic.replacement"</span><span class="p">:</span><span class="w"> </span><span class="s2">"orders.v0.cdc"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"transforms.route.key.enforce.uniqueness"</span><span class="p">:</span><span class="w"> </span><span class="s2">"false"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"key.converter"</span><span class="p">:</span><span class="w"> </span><span class="s2">"org.apache.kafka.connect.storage.StringConverter"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"value.converter"</span><span class="p">:</span><span class="w"> </span><span class="s2">"org.apache.kafka.connect.json.JsonConverter"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"value.converter.schemas.enable"</span><span class="p">:</span><span class="w"> </span><span class="s2">"false"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"database.ssl.mode"</span><span class="p">:</span><span class="w"> </span><span class="s2">"require"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"errors.retry.timeout"</span><span class="p">:</span><span class="w"> </span><span class="s2">"600000"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"producer.override.max.request.size"</span><span class="p">:</span><span class="w"> </span><span class="s2">"20971520"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></pre></td></tr></tbody></table></code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>Config</th>
      <th>Why</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">publication.autocreate.mode: disabled</code></td>
      <td>Explicit control — publication created via migration, not by Debezium</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">replica.identity.autoset.values</code></td>
      <td>Ensures before-values on UPDATE/DELETE for audit history</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">database.ssl.mode: require</code></td>
      <td>Encrypted connections</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">errors.retry.timeout: 600000</code></td>
      <td>Retry transient errors for 10 minutes before failing</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">producer.override.max.request.size: 20971520</code></td>
      <td>Handles large payloads (big text fields, JSONB columns) up to 20 MB</td>
    </tr>
  </tbody>
</table>

<h2 id="available-debezium-transforms">Available Debezium Transforms</h2>

<p>The transforms that come up most often in practice:</p>

<table>
  <thead>
    <tr>
      <th>Transform</th>
      <th>Type class</th>
      <th>What it does</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>ByLogicalTableRouter</strong></td>
      <td><code class="language-plaintext highlighter-rouge">io.debezium.transforms.ByLogicalTableRouter</code></td>
      <td>Routes multiple tables to a single topic via regex</td>
    </tr>
    <tr>
      <td><strong>EventRouter</strong></td>
      <td><code class="language-plaintext highlighter-rouge">io.debezium.transforms.outbox.EventRouter</code></td>
      <td>Routes outbox table rows to dynamic topics based on a field — used with the transactional outbox pattern</td>
    </tr>
    <tr>
      <td><strong>ExtractNewRecordState</strong></td>
      <td><code class="language-plaintext highlighter-rouge">io.debezium.transforms.ExtractNewRecordState</code></td>
      <td>Flattens the envelope — strips <code class="language-plaintext highlighter-rouge">before</code>/<code class="language-plaintext highlighter-rouge">source</code>/<code class="language-plaintext highlighter-rouge">op</code>, emits just the <code class="language-plaintext highlighter-rouge">after</code> payload</td>
    </tr>
    <tr>
      <td><strong>Filter</strong></td>
      <td><code class="language-plaintext highlighter-rouge">io.debezium.transforms.Filter</code></td>
      <td>Drops events based on scripting expressions</td>
    </tr>
    <tr>
      <td><strong>ContentBasedRouter</strong></td>
      <td><code class="language-plaintext highlighter-rouge">io.debezium.transforms.ContentBasedRouter</code></td>
      <td>Routes to different topics based on expressions evaluated against event content</td>
    </tr>
    <tr>
      <td><strong>TimezoneConverter</strong></td>
      <td><code class="language-plaintext highlighter-rouge">io.debezium.transforms.TimezoneConverter</code></td>
      <td>Converts timestamps between timezones</td>
    </tr>
  </tbody>
</table>

<p>ByLogicalTableRouter and EventRouter cover the majority of production use cases. ExtractNewRecordState is useful when the downstream consumer wants a flat record instead of the full envelope — for example, when sinking directly into a relational table where each column maps to a field in <code class="language-plaintext highlighter-rouge">after</code>.</p>]]></content><author><name>kimserey</name></author><category term="postgres" /><summary type="html"><![CDATA[The default Debezium connector config produces usable events, but the defaults leave important gaps — before values are missing on UPDATEs and DELETEs, all tables are captured including system tables, and each table gets its own Kafka topic. This post covers the configuration knobs that close those gaps: replica identity, table filtering, publication modes, and single-topic routing with the ByLogicalTableRouter transform.]]></summary></entry></feed>