<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Embed on PHP Boy Scout</title><link>https://blog-570662.gitlab.io/tags/embed/</link><description>Recent content in Embed on PHP Boy Scout</description><generator>Hugo -- gohugo.io</generator><language>en-gb</language><copyright>Matt Cockayne</copyright><lastBuildDate>Sat, 21 Mar 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://blog-570662.gitlab.io/tags/embed/index.xml" rel="self" type="application/rss+xml"/><item><title>Many embedded filesystems, one merged view</title><link>https://blog-570662.gitlab.io/many-embedded-filesystems-one-merged-view/</link><pubDate>Sat, 21 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/many-embedded-filesystems-one-merged-view/</guid><description>&lt;img src="https://blog-570662.gitlab.io/many-embedded-filesystems-one-merged-view/cover-many-embedded-filesystems-one-merged-view.png" alt="Featured image of post Many embedded filesystems, one merged view" /&gt;&lt;p&gt;Go&amp;rsquo;s &lt;code&gt;embed&lt;/code&gt; package is one of those features that makes you slightly giddy the first time you use it. One &lt;code&gt;//go:embed&lt;/code&gt; directive and your default config, your templates, your docs are all baked into the binary. The tool just works the moment it&amp;rsquo;s installed, with nothing external to lose or forget to ship.&lt;/p&gt;
&lt;p&gt;And then you go and build something modular on top of it, and you discover the catch nobody warned you about.&lt;/p&gt;
&lt;h2 id="embedfs-is-an-island"&gt;&lt;code&gt;embed.FS&lt;/code&gt; is an island
&lt;/h2&gt;&lt;p&gt;An &lt;code&gt;embed.FS&lt;/code&gt; has a property that&amp;rsquo;s easy to miss until it bites: it&amp;rsquo;s local to the package that declared it. The &lt;code&gt;//go:embed&lt;/code&gt; directive can only see files at or below its own source file. So in any project bigger than a toy, you don&amp;rsquo;t have &lt;em&gt;an&lt;/em&gt; embedded filesystem. You have many. The root package embeds one. Each feature, each subcommand that ships its own templates or defaults, embeds another. They&amp;rsquo;re islands, one per package, and Go gives you no native way to make them behave as a whole.&lt;/p&gt;
&lt;p&gt;For most files that&amp;rsquo;s perfectly fine. A feature&amp;rsquo;s templates can stay on the feature&amp;rsquo;s island; nothing else needs them.&lt;/p&gt;
&lt;p&gt;It stops being fine the moment features need to contribute to something shared.&lt;/p&gt;
&lt;h2 id="the-shared-config-problem"&gt;The shared-config problem
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the case that forces the issue. A go-tool-base tool has a global &lt;code&gt;config.yaml&lt;/code&gt; of defaults, embedded at the root. Now you add a feature, and that feature has its own configuration keys, with their own sensible defaults.&lt;/p&gt;
&lt;p&gt;Where do those defaults go?&lt;/p&gt;
&lt;p&gt;The naive answer is: edit the root &lt;code&gt;config.yaml&lt;/code&gt; and add the feature&amp;rsquo;s section. And that&amp;rsquo;s a genuinely bad answer, because it inverts the dependency. The root config now has to know about every feature. Add a feature, edit the centre. Remove one, edit the centre again. The central file becomes a pinch point that every feature has to reach into, and a modular architecture where you can&amp;rsquo;t add a module without editing the core isn&amp;rsquo;t really modular at all&amp;hellip; it just has more files.&lt;/p&gt;
&lt;p&gt;What you actually want is for the feature to ship its own slice of default config, on its own island, and for the global config the tool reads to somehow already contain it. The feature contributes; the centre doesn&amp;rsquo;t budge.&lt;/p&gt;
&lt;h2 id="propsassets-merge-the-islands"&gt;&lt;code&gt;props.Assets&lt;/code&gt;: merge the islands
&lt;/h2&gt;&lt;p&gt;That&amp;rsquo;s the job of &lt;code&gt;props.Assets&lt;/code&gt;. (Yes, it lives on &lt;a class="link" href="https://blog-570662.gitlab.io/props-the-container-that-does-the-heavy-lifting/" &gt;Props&lt;/a&gt;, the load-bearing container I keep going on about. Most of the good stuff does.) It&amp;rsquo;s a layer that implements the standard &lt;code&gt;fs.FS&lt;/code&gt; interface, and into it you &lt;code&gt;Register&lt;/code&gt; each &lt;code&gt;embed.FS&lt;/code&gt; under a name:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// root main.go&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;Assets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewAssets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AssetMap&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;root&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// a feature&amp;#39;s command constructor&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;//go:embed assets/*&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;assets&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FS&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;NewCmdFeature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;cobra&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Assets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;feature&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now &lt;code&gt;Props&lt;/code&gt; carries one &lt;code&gt;Assets&lt;/code&gt; value that represents all the islands as a single filesystem. The root&amp;rsquo;s files and every registered feature&amp;rsquo;s files, addressable through one &lt;code&gt;fs.FS&lt;/code&gt;. Each registration is named, so the islands stay individually identifiable, but they read as one.&lt;/p&gt;
&lt;p&gt;That alone solves the addressing problem. The genuinely clever part is what happens for structured files.&lt;/p&gt;
&lt;h2 id="opening-a-file-that-exists-in-several-places"&gt;Opening a file that exists in several places
&lt;/h2&gt;&lt;p&gt;When you &lt;code&gt;Open&lt;/code&gt; a path through &lt;code&gt;props.Assets&lt;/code&gt; and that path has a structured extension (&lt;code&gt;.yaml&lt;/code&gt;, &lt;code&gt;.yml&lt;/code&gt;, &lt;code&gt;.json&lt;/code&gt;, &lt;code&gt;.csv&lt;/code&gt;) it doesn&amp;rsquo;t simply return the first match it stumbles across. It does this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Discovery.&lt;/strong&gt; It finds every instance of that path, across every registered filesystem.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Parsing.&lt;/strong&gt; It unmarshals each one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Merging.&lt;/strong&gt; It deep-merges the parsed data, using &lt;code&gt;mergo&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Re-serialisation.&lt;/strong&gt; It hands you back a single &lt;code&gt;fs.File&lt;/code&gt; whose contents are the combined, merged result.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So picture the shared-config problem again, only solved this time. The root ships a &lt;code&gt;config.yaml&lt;/code&gt; with the base defaults. Each feature ships a &lt;code&gt;config.yaml&lt;/code&gt; on its own island carrying only its own keys. Nobody edits anybody else&amp;rsquo;s file. When the &lt;code&gt;init&lt;/code&gt; command opens &lt;code&gt;config.yaml&lt;/code&gt; through &lt;code&gt;props.Assets&lt;/code&gt;, it doesn&amp;rsquo;t get the root&amp;rsquo;s copy. It gets the deep-merge of the root&amp;rsquo;s copy and every registered feature&amp;rsquo;s copy: one &lt;code&gt;config.yaml&lt;/code&gt; that contains every default in the tool, assembled at runtime from contributions that never knew about each other.&lt;/p&gt;
&lt;p&gt;A feature contributes its defaults simply by existing and registering. The centre never changes. That&amp;rsquo;s the modular property the naive approach couldn&amp;rsquo;t give you, and it generalises well beyond config&amp;hellip; the same merge applies to a shared &lt;code&gt;commands.csv&lt;/code&gt;, or any structured file features want to add rows or keys to.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s also a &lt;code&gt;Mount&lt;/code&gt; method for attaching an arbitrary &lt;code&gt;fs.FS&lt;/code&gt; at a virtual path, which is handy for surfacing something external (a temp directory, say) as part of the same tree. But the structured merge is the feature that really earns &lt;code&gt;Assets&lt;/code&gt; its place.&lt;/p&gt;
&lt;h2 id="boiling-it-down"&gt;Boiling it down
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;embed.FS&lt;/code&gt; is per-package by design, so a modular CLI ends up with many embedded filesystems, one island per feature. Most of the time that&amp;rsquo;s fine. It fails specifically when features need to contribute to a shared resource like the global &lt;code&gt;config.yaml&lt;/code&gt;, because the naive fix forces every feature to reach in and edit a central file.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;props.Assets&lt;/code&gt; merges all the registered islands into a single &lt;code&gt;fs.FS&lt;/code&gt;, and for structured files it goes further: opening a &lt;code&gt;.yaml&lt;/code&gt;, &lt;code&gt;.json&lt;/code&gt; or &lt;code&gt;.csv&lt;/code&gt; discovers every copy across every island, deep-merges them, and returns the combined whole. A feature drops its own defaults onto its own island, registers, and the merged config the tool reads already includes them. Contribution without coupling, which is rather the whole point of being modular in the first place.&lt;/p&gt;</description></item></channel></rss>