<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://blog.pierluc-codes.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://blog.pierluc-codes.com/" rel="alternate" type="text/html" /><updated>2026-07-13T03:05:25+00:00</updated><id>https://blog.pierluc-codes.com/feed.xml</id><title type="html">Pier-Luc writes</title><subtitle>Notes on software design and the craft of programming, mostly in Go and Rust, with runnable, reproducible code behind every claim.</subtitle><entry><title type="html">Newtype for better communication</title><link href="https://blog.pierluc-codes.com/go/rust/2026/07/12/newtype-for-better-communication.html" rel="alternate" type="text/html" title="Newtype for better communication" /><published>2026-07-12T14:16:58+00:00</published><updated>2026-07-12T14:16:58+00:00</updated><id>https://blog.pierluc-codes.com/go/rust/2026/07/12/newtype-for-better-communication</id><content type="html" xml:base="https://blog.pierluc-codes.com/go/rust/2026/07/12/newtype-for-better-communication.html"><![CDATA[<h2 id="day-at-the-office">Day at the office</h2>

<p>Coding is mostly about communication.
We communicate with the machine when we give it instructions.
But we also communicate with our colleagues (and our future selves) through design and comments.</p>

<p>I was refactoring some code at work.
The interface is a Categorizer that categorizes information according to a specified version.</p>

<p>I stumbled on a struct like the following.</p>

<figure class="highlight"><pre><code class="language-go" data-lang="go"><span class="k">type</span> <span class="n">Request</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">CategoryVersion</span> <span class="kt">string</span> <span class="c">// one of {"1.1", "2.1", "2.2", "3.1"}</span>
<span class="p">}</span>

<span class="k">type</span> <span class="n">Response</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">CategoryVersion</span> <span class="kt">string</span> <span class="c">// one of {"1.1", "2.1", "2.2", "3.1"}</span>
    <span class="n">CategoryCode</span>    <span class="kt">string</span> <span class="c">// one of {"123", "456"}</span>
<span class="p">}</span></code></pre></figure>

<p>The code was working well, no doubt.
The problem is that while refactoring, it is extremely easy to mix up Version and Code.
Both are strings, so the compiler happily lets you pass one where the other is expected.
This mix-up is caused by the heavy use of <code class="language-plaintext highlighter-rouge">string</code> to represent different concepts.</p>

<p>That reminded me of an interesting pattern I saw in Rust that the current situation could leverage.</p>

<h2 id="newtype">Newtype</h2>

<p>The idea of newtype is to create a custom type to wrap those stronger identifiers that are backed by a simple type.</p>

<p>For instance, in our current scenario we can refactor in the following manner.</p>

<figure class="highlight"><pre><code class="language-go" data-lang="go"><span class="k">type</span> <span class="n">CategoryVersion</span> <span class="kt">string</span>

<span class="k">func</span> <span class="n">NewCategoryVersion</span><span class="p">(</span><span class="n">version</span> <span class="kt">string</span><span class="p">)</span> <span class="n">CategoryVersion</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">CategoryVersion</span><span class="p">(</span><span class="n">version</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">type</span> <span class="n">CategoryCode</span> <span class="kt">string</span>

<span class="k">func</span> <span class="n">NewCategoryCode</span><span class="p">(</span><span class="n">code</span> <span class="kt">string</span><span class="p">)</span> <span class="n">CategoryCode</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">CategoryCode</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">Request</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">CategoryVersion</span> <span class="n">CategoryVersion</span>
<span class="p">}</span>

<span class="k">type</span> <span class="n">Response</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">CategoryVersion</span> <span class="n">CategoryVersion</span>
    <span class="n">CategoryCode</span>    <span class="n">CategoryCode</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">Categorize</span><span class="p">(</span><span class="n">v</span> <span class="n">CategoryVersion</span><span class="p">,</span> <span class="n">c</span> <span class="n">CategoryCode</span><span class="p">)</span> <span class="n">Response</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">Response</span><span class="p">{</span><span class="n">CategoryVersion</span><span class="o">:</span> <span class="n">v</span><span class="p">,</span> <span class="n">CategoryCode</span><span class="o">:</span> <span class="n">c</span><span class="p">}</span>
<span class="p">}</span>

<span class="c">// Categorize(c, v) does NOT compile:</span>
<span class="c">//   cannot use c (variable of string type CategoryCode) as CategoryVersion value in argument to Categorize</span>
<span class="c">//   cannot use v (variable of string type CategoryVersion) as CategoryCode value in argument to Categorize</span></code></pre></figure>

<p><a href="https://go.dev/play/p/8YiSI-Mhp_4">Run it on the Go Playground</a></p>

<p>This gives us stronger types to play with those identifiers.
Now <code class="language-plaintext highlighter-rouge">func Categorize(v CategoryVersion, c CategoryCode)</code> will not compile if you swap the two arguments, and the mistake is caught at build time instead of in production.</p>

<p>The constructors do not do much yet: right now they are just conversions.
They become useful once we hang validation off them, which is where we are headed.</p>

<h3 id="closed-sets-reach-for-constants-first">Closed sets: reach for constants first</h3>

<p>Our versions are a small, known set: <code class="language-plaintext highlighter-rouge">{"1.1", "2.1", "2.2", "3.1"}</code>.
When the set of valid values is closed and lives in your code, typed constants are the simplest strong option.</p>

<figure class="highlight"><pre><code class="language-go" data-lang="go"><span class="k">type</span> <span class="n">CategoryVersion</span> <span class="kt">string</span>

<span class="k">const</span> <span class="p">(</span>
    <span class="n">V11</span> <span class="n">CategoryVersion</span> <span class="o">=</span> <span class="s">"1.1"</span>
    <span class="n">V21</span> <span class="n">CategoryVersion</span> <span class="o">=</span> <span class="s">"2.1"</span>
    <span class="n">V22</span> <span class="n">CategoryVersion</span> <span class="o">=</span> <span class="s">"2.2"</span>
    <span class="n">V31</span> <span class="n">CategoryVersion</span> <span class="o">=</span> <span class="s">"3.1"</span>
<span class="p">)</span></code></pre></figure>

<p><a href="https://go.dev/play/p/Eik0aKhuqDz">Run it on the Go Playground</a></p>

<p>Now the rest of the code refers to <code class="language-plaintext highlighter-rouge">V21</code> instead of a bare <code class="language-plaintext highlighter-rouge">"2.1"</code>, so a typo becomes a compile error and your editor can autocomplete the valid options.</p>

<h3 id="values-from-outside-validate-at-construction">Values from outside: validate at construction</h3>

<p>Constants only help for values you write yourself.
The moment a version arrives from user input, a database, or an API, you are back to an arbitrary string, and you want to check it once, at the boundary.</p>

<figure class="highlight"><pre><code class="language-go" data-lang="go"><span class="k">import</span> <span class="s">"fmt"</span>

<span class="k">type</span> <span class="n">CategoryVersion</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">version</span> <span class="kt">string</span>
<span class="p">}</span>

<span class="k">var</span> <span class="n">validVersions</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">bool</span><span class="p">{</span>
    <span class="s">"1.1"</span><span class="o">:</span> <span class="no">true</span><span class="p">,</span>
    <span class="s">"2.1"</span><span class="o">:</span> <span class="no">true</span><span class="p">,</span>
    <span class="s">"2.2"</span><span class="o">:</span> <span class="no">true</span><span class="p">,</span>
    <span class="s">"3.1"</span><span class="o">:</span> <span class="no">true</span><span class="p">,</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">NewCategoryVersion</span><span class="p">(</span><span class="n">version</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="n">CategoryVersion</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="o">!</span><span class="n">validVersions</span><span class="p">[</span><span class="n">version</span><span class="p">]</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">CategoryVersion</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">"unknown category version %q"</span><span class="p">,</span> <span class="n">version</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="n">CategoryVersion</span><span class="p">{</span><span class="n">version</span><span class="o">:</span> <span class="n">version</span><span class="p">},</span> <span class="no">nil</span>
<span class="p">}</span></code></pre></figure>

<p><a href="https://go.dev/play/p/nAaeVApBrPa">Run it on the Go Playground</a></p>

<p>Because <code class="language-plaintext highlighter-rouge">version</code> is unexported, code outside this package cannot set it directly.
It has to go through <code class="language-plaintext highlighter-rouge">NewCategoryVersion</code>, and that is exactly where our validation lives.
So once you hold a <code class="language-plaintext highlighter-rouge">CategoryVersion</code>, you know it was checked.</p>

<h2 id="rust">Rust</h2>

<p>In Rust, you can attach methods to a type directly, which makes it a little more convenient.</p>

<figure class="highlight"><pre><code class="language-rust" data-lang="rust"><span class="k">pub</span> <span class="k">struct</span> <span class="nf">CategoryVersion</span><span class="p">(</span><span class="nb">String</span><span class="p">);</span>

<span class="nd">#[derive(Debug)]</span>
<span class="k">pub</span> <span class="k">enum</span> <span class="n">CategoryError</span> <span class="p">{</span>
    <span class="n">Empty</span><span class="p">,</span>
<span class="p">}</span>

<span class="k">impl</span> <span class="n">CategoryVersion</span> <span class="p">{</span>
    <span class="k">pub</span> <span class="k">fn</span> <span class="nf">new</span><span class="p">(</span><span class="n">raw</span><span class="p">:</span> <span class="k">impl</span> <span class="nb">Into</span><span class="o">&lt;</span><span class="nb">String</span><span class="o">&gt;</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="k">Self</span><span class="p">,</span> <span class="n">CategoryError</span><span class="o">&gt;</span> <span class="p">{</span>
        <span class="k">let</span> <span class="n">raw</span> <span class="o">=</span> <span class="n">raw</span><span class="nf">.into</span><span class="p">();</span>
        <span class="k">if</span> <span class="n">raw</span><span class="nf">.is_empty</span><span class="p">()</span> <span class="p">{</span>
            <span class="k">return</span> <span class="nf">Err</span><span class="p">(</span><span class="nn">CategoryError</span><span class="p">::</span><span class="n">Empty</span><span class="p">);</span>
        <span class="p">}</span>
        <span class="nf">Ok</span><span class="p">(</span><span class="nf">CategoryVersion</span><span class="p">(</span><span class="n">raw</span><span class="p">))</span>
    <span class="p">}</span>

    <span class="k">pub</span> <span class="k">fn</span> <span class="nf">as_str</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="o">&amp;</span><span class="nb">str</span> <span class="p">{</span>
        <span class="o">&amp;</span><span class="k">self</span><span class="na">.0</span>
    <span class="p">}</span>
<span class="p">}</span></code></pre></figure>

<p>The field inside the tuple struct is private by default, so, just like the Go example, the only way to build a <code class="language-plaintext highlighter-rouge">CategoryVersion</code> from outside the module is through <code class="language-plaintext highlighter-rouge">new</code>, which validates.</p>

<p>Usage looks like this.</p>

<figure class="highlight"><pre><code class="language-rust" data-lang="rust"><span class="k">let</span> <span class="n">version</span> <span class="o">=</span> <span class="nn">CategoryVersion</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="s">"2.1"</span><span class="p">)</span><span class="o">?</span><span class="p">;</span>

<span class="c1">// This does not compile: a CategoryCode is not a CategoryVersion.</span>
<span class="c1">// categorize(code, version);</span>

<span class="nf">categorize</span><span class="p">(</span><span class="n">version</span><span class="p">,</span> <span class="n">code</span><span class="p">);</span></code></pre></figure>

<p>And you get nice conversion code by implementing a couple of standard traits.</p>

<figure class="highlight"><pre><code class="language-rust" data-lang="rust"><span class="k">use</span> <span class="nn">std</span><span class="p">::</span><span class="n">fmt</span><span class="p">;</span>

<span class="c1">// Print it like a plain string.</span>
<span class="k">impl</span> <span class="nn">fmt</span><span class="p">::</span><span class="n">Display</span> <span class="k">for</span> <span class="n">CategoryVersion</span> <span class="p">{</span>
    <span class="k">fn</span> <span class="nf">fmt</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">,</span> <span class="n">f</span><span class="p">:</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="nn">fmt</span><span class="p">::</span><span class="n">Formatter</span><span class="o">&lt;</span><span class="nv">'_</span><span class="o">&gt;</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nn">fmt</span><span class="p">::</span><span class="nb">Result</span> <span class="p">{</span>
        <span class="nd">write!</span><span class="p">(</span><span class="n">f</span><span class="p">,</span> <span class="s">"{}"</span><span class="p">,</span> <span class="k">self</span><span class="na">.0</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// Fallible conversion straight from &amp;str.</span>
<span class="k">impl</span> <span class="n">TryFrom</span><span class="o">&lt;&amp;</span><span class="nb">str</span><span class="o">&gt;</span> <span class="k">for</span> <span class="n">CategoryVersion</span> <span class="p">{</span>
    <span class="k">type</span> <span class="n">Error</span> <span class="o">=</span> <span class="n">CategoryError</span><span class="p">;</span>

    <span class="k">fn</span> <span class="nf">try_from</span><span class="p">(</span><span class="n">raw</span><span class="p">:</span> <span class="o">&amp;</span><span class="nb">str</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="k">Self</span><span class="p">,</span> <span class="k">Self</span><span class="p">::</span><span class="n">Error</span><span class="o">&gt;</span> <span class="p">{</span>
        <span class="nn">CategoryVersion</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="n">raw</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span></code></pre></figure>

<p><a href="https://play.rust-lang.org/?version=stable&amp;edition=2021&amp;gist=8da69ca1d318c949e687ae4e0ef46480">Run it on the Rust Playground</a></p>

<h2 id="go-trap">Go trap</h2>

<p>Be careful when implementing newtype in Go. You can easily bypass the validation.</p>

<p>Within the same package, nothing stops you from building the value directly and skipping the constructor entirely.</p>

<figure class="highlight"><pre><code class="language-go" data-lang="go"><span class="c">// Skips NewCategoryVersion, so the validation never runs.</span>
<span class="n">bad</span> <span class="o">:=</span> <span class="n">CategoryVersion</span><span class="p">{}</span>

<span class="c">// For the `type CategoryVersion string` variant, a plain</span>
<span class="c">// conversion bypasses validation too.</span>
<span class="n">alsoBad</span> <span class="o">:=</span> <span class="n">CategoryVersion</span><span class="p">(</span><span class="s">"literally anything"</span><span class="p">)</span></code></pre></figure>

<p>The unexported field only protects you across package boundaries.
So if you rely on validation, put the newtype in a package of its own.
That way the constructor is the single door in, and the rest of your codebase physically cannot construct an invalid value.
You do not need one package per type, though: a small internal package that groups related domain types is usually enough.</p>

<h2 id="cost">Cost</h2>

<p>Does this abstraction cost anything at runtime? In short: no. And rather than take my word for it, we can measure it.</p>

<p>The tool output below is from Go 1.26 and rustc 1.92 on amd64. Exact registers and byte counts vary across compiler versions and architectures, but the conclusion (identical layout, wrapper folded away) does not.</p>

<h3 id="go-same-size">Go: same size</h3>

<p>A defined type shares the memory layout of its underlying type. In the words of the <a href="https://go.dev/ref/spec#Types">Go spec</a>, “each type T has an underlying type,” and a value can be <a href="https://go.dev/ref/spec#Conversions">converted</a> between a type and its underlying type precisely because the two share the same representation. We can confirm with <code class="language-plaintext highlighter-rouge">unsafe.Sizeof</code>.</p>

<figure class="highlight"><pre><code class="language-go" data-lang="go"><span class="k">var</span> <span class="n">s</span> <span class="kt">string</span>                    <span class="c">// size=16 align=8</span>
<span class="k">var</span> <span class="n">a</span> <span class="n">CategoryVersion</span>           <span class="c">// type CategoryVersion string</span>
<span class="k">var</span> <span class="n">st</span> <span class="k">struct</span><span class="p">{</span> <span class="n">version</span> <span class="kt">string</span> <span class="p">}</span> <span class="c">// the struct variant</span>

<span class="c">// unsafe.Sizeof reports:</span>
<span class="c">// string:                size=16 align=8</span>
<span class="c">// type X string:         size=16 align=8</span>
<span class="c">// struct{ version str }: size=16 align=8</span></code></pre></figure>

<p>All three are 16 bytes: the two words of a string header (a data pointer and a length). Wrapping adds no header and no allocation.</p>

<h3 id="go-the-accessor-disappears">Go: the accessor disappears</h3>

<p>Small accessor methods get inlined. Compile with <code class="language-plaintext highlighter-rouge">-gcflags=-m</code> and the compiler tells you so:</p>

<figure class="highlight"><pre><code class="language-text" data-lang="text">cost/asm/lib.go:21:57: inlining call to CategoryVersion.Value</code></pre></figure>

<p>Better still, compare the generated code directly. Take a function that goes through the newtype and its accessor, and one that touches the raw string, and force both to stay real functions with <code class="language-plaintext highlighter-rouge">//go:noinline</code>:</p>

<figure class="highlight"><pre><code class="language-go" data-lang="go"><span class="c">//go:noinline</span>
<span class="k">func</span> <span class="n">Wrapped</span><span class="p">(</span><span class="n">c</span> <span class="n">CategoryVersion</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">c</span><span class="o">.</span><span class="n">Value</span><span class="p">())</span> <span class="p">}</span>

<span class="c">//go:noinline</span>
<span class="k">func</span> <span class="n">Raw</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></code></pre></figure>

<p><code class="language-plaintext highlighter-rouge">go tool objdump</code> gives byte-for-byte identical bodies:</p>

<figure class="highlight"><pre><code class="language-text" data-lang="text">TEXT main.Wrapped(SB)          TEXT main.Raw(SB)
  MOVQ AX, 0x8(SP)               MOVQ AX, 0x8(SP)
  MOVQ BX, AX                    MOVQ BX, AX
  RET                           RET</code></pre></figure>

<p>The wrapper leaves no trace in the machine code.</p>

<h3 id="rust-same-story-proven-by-the-linker">Rust: same story, proven by the linker</h3>

<p>A Rust newtype is a zero-cost abstraction. <code class="language-plaintext highlighter-rouge">struct CategoryVersion(String)</code> has the same size and alignment as the <code class="language-plaintext highlighter-rouge">String</code> it wraps, and <code class="language-plaintext highlighter-rouge">size_of</code> confirms it:</p>

<figure class="highlight"><pre><code class="language-text" data-lang="text">String:                    size=24 align=8
CategoryVersion(String):   size=24 align=8
repr(transparent) wrapper: size=24 align=8</code></pre></figure>

<p>If you want that identical layout <em>guaranteed</em> (rather than merely observed), add <a href="https://doc.rust-lang.org/reference/type-layout.html#the-transparent-representation"><code class="language-plaintext highlighter-rouge">#[repr(transparent)]</code></a>, which promises the wrapper is laid out exactly like its single field.</p>

<p>The accessor gets the same treatment. Compile a wrapped length accessor and a raw one to assembly with <code class="language-plaintext highlighter-rouge">rustc -O --emit asm</code>, and LLVM does not just produce equivalent code, it recognises the two functions are identical and makes one an alias of the other:</p>

<figure class="highlight"><pre><code class="language-text" data-lang="text">raw_len:
    movq    16(%rdi), %rax
    retq
wrapped_len = raw_len            # same symbol, folded by the optimiser</code></pre></figure>

<p>The wrapper exists only to satisfy the type checker and is gone by the time the machine runs your code.</p>

<p>So you get the safety for free.</p>

<h2 id="conclusion">Conclusion</h2>

<p>Newtypes are pretty cool.
They cost you almost nothing at runtime, they turn a whole class of mix-ups into compile errors, and they give validation a single place to live.
But the real win is communication: a parameter typed <code class="language-plaintext highlighter-rouge">CategoryVersion</code> tells the next reader what that string means, where a bare <code class="language-plaintext highlighter-rouge">string</code> tells them nothing.
Your future self will thank you.</p>

<h2 id="references">References</h2>

<ul>
  <li><a href="https://www.howtocodeit.com/guides/ultimate-guide-rust-newtypes">The Ultimate Guide to Rust Newtypes</a></li>
  <li><a href="https://doc.rust-lang.org/rust-by-example/generics/new_types.html">Rust by Example: New Type Idiom</a></li>
  <li><a href="https://rust-unofficial.github.io/patterns/patterns/behavioural/newtype.html">Rust Design Patterns: Newtype</a></li>
  <li><a href="https://go.dev/ref/spec#Types">Go spec: Types and underlying types</a></li>
  <li><a href="https://doc.rust-lang.org/reference/type-layout.html#the-transparent-representation">Rust Reference: the <code class="language-plaintext highlighter-rouge">transparent</code> representation</a></li>
</ul>]]></content><author><name></name></author><category term="go" /><category term="rust" /><summary type="html"><![CDATA[Day at the office]]></summary></entry></feed>